Refactoritzar és modificar el codi perquè tingui una altre estructura, però que segueixi fent el mateix.
Introducció
Però per qué necessitem refactoritzar un codi si aquest funciona? Després de tot al compilador i a l'ordinador no li importa si el codi costa d’entendre o no està ben estructurat. Però quan una persona ha de modificar un codi, primer ha d’entendre el codi, i molts cops és difícil d'entendre el que està escrit encara que ho haguem escrit nosaltres fa unes setmanes.
Exemple
Per desenvolupar aquesta activitat farem servir un exemple, però aquest exemple és de poques línies de codi perquè sigui fàcil d'entendre. Per tant, en una situació real tindria poca utilitat refactoritzar el codi perquè segur que la majoria el podem entendre i podem tolerar que el codi no s’adapti al nostre estil, però en molts casos no és així.
Farem servir d’exemple una empresa que proporciona màquines virtuals al núvol, i factura mensualment als clients a partir del total d’hores que ha utilitzat el recurs virtual que s’ha creat sota demanda.
Per modelitzar les dades farem servir llistes, tuples i diccionaris tal com pots veure a continuació:
# recursos (màquines virtuals)
resources = {
# ResourceId: (Processor, price per hour, cores, GB of memory)
'CAX11': ('Ampere', 0.0072, 2, 4),
'CAX21': ('Ampere', 0.0124, 4, 8),
'CX11': ('Intel', 0.0081, 1, 2),
'CPX11': ('AMD', 0.0084, 2, 2),
}
# recursos utilitzats per un client
invoice = {
'client': 'ACME',
# (ResourceId, hours)
'rentals': [('CAX21', 6), ('CPX11', 58), ('CAX21', 376)]
}
El codi que haurem de refactoritzar construeix la factura mensual en format text del client:
Rental Record for ACME
CAX21 0.0744
CPX11 0.4872
CAX21 2.3312
Amount owed is 2.89
A continuació tens el codi d’exemple main.py
:
# funció que genera el document de text d'una factura
def statement(invoice, resources):
totalAmount = 0
result = f"Rental Record for {invoice['client']}\n"
for rental in invoice['rentals']:
amount = 0
resourceId = rental[0]
resource = resources[resourceId]
cpu = resource[0]
price = resource[1]
hours = rental[1]
# determine amount for each resource rental
match cpu:
case 'Ampere':
amount = hours * price
if hours > 240:
amount = amount / 2
case 'AMD':
amount = hours * price
if hours > 360:
amount = amount / 2
case 'Intel':
amount = hours * price
if hours > 360:
amount = amount / 2
case _:
msg = f"Unknow cpu: {cpu}"
raise Exception(msg)
# print figures for this rental
result += f'\t{resourceId}\t{amount:.4f}\n'
totalAmount += amount
# add footer lines
result += f'Amount owed is {totalAmount:.2f}\n'
return result
# recursos (màquines virtuals)
resources = {
# ResourceId: (Processor, price per hour, cores, GB of memory)
'CAX11': ('Ampere', 0.0072, 2, 4),
'CAX21': ('Ampere', 0.0124, 4, 8),
'CX11': ('Intel', 0.0081, 1, 2),
'CPX11': ('AMD', 0.0084, 2, 2),
}
# recursos utilitzats per un client
invoice = {
'client': 'ACME',
# (ResourceId, hours)
'rentals': [('CAX21', 6), ('CPX11', 58), ('CAX21', 376)]
}
result = statement(invoice, resources)
print(result)
El resultat que obtenim quan executem python3 main.py
és l’esperat:
Rental Record for ACME
CAX21 0.0744
CPX11 0.4872
CAX21 2.3312
Amount owed is 2.89
Aquesta funció és un exemple d’una funció que potser és massa llarga, però que funciona. Per tant cal un bon motiu per dedicar-li el temps necessari per refactoritzar-la perquè
-
Si ningú ha de llegir i entendre el codi, no és un problema que el codi no estigui ben estructurat.
-
Si no s’han d’afegir noves característiques o depurar el codi, no és un problema que el codi sigui dificil d’entendre.
En el nostre cas tenim un motiu per refactoritzar el codi: A més de crear una factura en format text volem que es pugui crear una factura en format HTML
<h1>Rental Record for <em>ACME</em></h1>
<table>
<tr><td>CAX21</td><td>0.0744</td></tr>
<tr><td>CPX11</td><td>0.4872</td></tr>
<tr><td>CAX21</td><td>2.3312</td></tr>
</table>
<p>Amount owed is <em>2.89</em></p>
Refactoritzar
El primer que hem de fer es refactoritzar la funció perquè puguem afegir la nova funcionalitat.
Descomposar la funció
Quan treballes en una funció molt llarga, el primer que has de fer és buscar blocs lògics de codi per convertirlos en funcions, i el primer bloc de codi que crida fàcilment l’atenció és la sentència match
, que la podem convertir en la funció amountFor
:
def statement(invoice, resources):
totalAmount = 0
result = f"Rental Record for {invoice['client']}\n"
for rental in invoice['rentals']:
amount = 0
resourceId = rental[0]
resource = resources[resourceId]
cpu = resource[0]
price = resource[1]
hours = rental[1]
# determine amount for each resource rental
amount = __amountFor(rental,resources)
# print figures for this rental
result += f'\t{resourceId}\t{amount:.4f}\n'
totalAmount += amount
# add footer lines
result += f'Amount owed is {totalAmount:.2f}\n'
return result
def __amountFor(rental,resources):
amount = 0
resourceId = rental[0]
resource = resources[resourceId]
cpu = resource[0]
price = resource[1]
hours = rental[1]
# determine amount for each resource rental
match cpu:
case 'Ampere':
amount = hours * price
if hours > 240:
amount = amount / 2
case 'AMD':
amount = hours * price
if hours > 360:
amount = amount / 2
case 'Intel':
amount = hours * price
if hours > 360:
amount = amount / 2
case _:
msg = f"Unknow cpu: {cpu}"
raise Exception(msg)
return amount
Com pots veure en el codi, tambè hem de copiar les variable temporals que hi ha abans de la sentència match
.
Pots verificar que fa el mateix que abans executant el programa main.py
.
A continuació hem d’eliminar les variables temporal que ja no fem servir:
def statement(invoice, resources):
totalAmount = 0
result = f"Rental Record for {invoice['client']}\n"
for rental in invoice['rentals']:
resourceId = rental[0]
amount = __amountFor(rental,resources)
# print figures for this rental
result += f'\t{resourceId}\t{amount:.4f}\n'
totalAmount += amount
# add footer lines
result += f'Amount owed is {totalAmount:.2f}\n'
return result
def __amountFor(rental,resources):
amount = 0
resourceId = rental[0]
resource = resources[resourceId]
cpu = resource[0]
price = resource[1]
hours = rental[1]
# determine amount for each resource rental
match cpu:
case 'Ampere':
amount = hours * price
if hours > 240:
amount = amount / 2
case 'AMD':
amount = hours * price
if hours > 360:
amount = amount / 2
case 'Intel':
amount = hours * price
if hours > 360:
amount = amount / 2
case _:
msg = f"Unknow cpu: {cpu}"
raise Exception(msg)
return amount
Eliminar variables temporals
A continuació hem d’eliminar les variables temporals dins del bucle for
:
def statement(invoice, resources):
totalAmount = 0
result = f"Rental Record for {invoice['client']}\n"
for rental in invoice['rentals']:
# print figures for this rental
result += f'\t{rental[0]}\t{__amountFor(rental,resources):.4f}\n'
totalAmount += __amountFor(rental,resources)
# add footer lines
result += f'Amount owed is {totalAmount:.2f}\n'
return result
D’aquesta manera podem dividir el bucle perquè s’estan executant dos lògiques diferents: generar el text de la factura i computar el cost total.
def statement(invoice, resources):
totalAmount = 0
result = f"Rental Record for {invoice['client']}\n"
for rental in invoice['rentals']:
result += f'\t{rental[0]}\t{__amountFor(rental,resources):.4f}\n'
for rental in invoice['rentals']:
totalAmount += __amountFor(rental,resources)
# add footer lines
result += f'Amount owed is {totalAmount:.2f}\n'
return result
Alguns programadors poden estar molt preocupats pel rendiment del programa si refactoritzem d’aquesta manera, i si és el teu cas, pots llegir aquest article que parla del rendiment dels programes: Yet Another Optimization Article
Com que hem dividit el bucle ara podem crear la funció totalAmount
:
def statement(invoice, resources):
totalAmount = __totalAmount(invoice,resources)
result = f"Rental Record for {invoice['client']}\n"
for rental in invoice['rentals']:
result += f'\t{rental[0]}\t{__amountFor(rental,resources):.4f}\n'
# add footer lines
result += f'Amount owed is {totalAmount:.2f}\n'
return result
def __totalAmount(invoice, resources):
totalAmount = 0
for rental in invoice['rentals']:
totalAmount += __amountFor(rental,resources)
return totalAmount
I a continuació eliminar la variable temporal totalAmount:
def statement(invoice, resources):
result = f"Rental Record for {invoice['client']}\n"
for rental in invoice['rentals']:
result += f'\t{rental[0]}\t{__amountFor(rental,resources):.4f}\n'
result += f'Amount owed is {__totalAmount(invoice,resources):.2f}\n'
return result
D’aquesta manera tenim un codi refactoritzat en què la funció statement només s’encarrega de generar la factura en format text, i podem començar a afegir la nova funcionalitat: que també generi la factura en format html.
def statement(invoice, resources):
result = f"Rental Record for {invoice['client']}\n"
for rental in invoice['rentals']:
result += f'\t{rental[0]}\t{__amountFor(rental,resources):.4f}\n'
result += f'Amount owed is {__totalAmount(invoice,resources):.2f}\n'
return result
def __totalAmount(invoice, resources):
result = 0
for rental in invoice['rentals']:
result += __amountFor(rental,resources)
return result
def __amountFor(rental,resources):
result = 0
resourceId = rental[0]
resource = resources[resourceId]
cpu = resource[0]
price = resource[1]
hours = rental[1]
# determine amount for each resource rental
match cpu:
case 'Ampere':
result = hours * price
if hours > 240:
result = result / 2
case 'AMD':
result = hours * price
if hours > 360:
result = result / 2
case 'Intel':
result = hours * price
if hours > 360:
result = result / 2
case _:
msg = f"Unknow cpu: {cpu}"
raise Exception(msg)
return result
Nova funcionalitat
Un cop que hem refactoritzat el codi és molt fàcil afegit la nova funcionalitat:
def htmlStatement(invoice, resources):
result = f"<h1>Rental Record for <em>{invoice['client']}<em></h1>\n"
result += "<table>\n"
for rental in invoice['rentals']:
result += f' <tr><td>{rental[0]}</td><td>{__amountFor(rental,resources):.4f}</td></tr>\n'
result += "</table>\n"
result += f'<p>Amount owed is <em>{__totalAmount(invoice,resources):.2f}</em></p>\n'
return result
Com pots observar, si el codi hagués estat ben format la nova funcionalitat es podia implementar en cinc minuts.
Però tenir un codi ben format també necessita temps.
Perdre el temps
La funció privada __amountFor
es pot refactoritzar, però que estaria bé refactoritzar no vol dir que s’hagi de refactoritzar. Si ningú l’ha de tornar a llegir o modificar és perdre el temps. Funciona, i punt.
Però com exercici educatiu la podem refactorizar.
El primer pas és eliminar les variables temporals fent servir funcions internes:
def __amountFor(rental,resources):
result = 0
def hours(): return rental[1]
def cpu():
resourceId = rental[0]
resource = resources[resourceId]
return resource[0]
def price():
resourceId = rental[0]
resource = resources[resourceId]
return resource[1]
match cpu():
case 'Ampere':
result = hours() * price()
if hours() > 240:
result = result / 2
case 'AMD':
result = hours() * price()
if hours() > 360:
result = result / 2
case 'Intel':
result = hours() * price()
if hours() > 360:
result = result / 2
case _:
msg = f"Unknow cpu: {cpu()}"
raise Exception(msg)
return result
I podem eliminar les variable temporals de les funcions internes:
def __amountFor(rental,resources):
result = 0
def hours(): return rental[1]
def resource(): return resources[rental[0]]
def cpu(): return resource()[0]
def price():return resource()[1]
match cpu():
case 'Ampere':
result = hours() * price()
if hours() > 240:
result = result / 2
case 'AMD':
result = hours() * price()
if hours() > 360:
result = result / 2
case 'Intel':
result = hours() * price()
if hours() > 360:
result = result / 2
case _:
msg = f"Unknow cpu: {cpu()}"
raise Exception(msg)
return result
Ha valgut la pena? Si no fem res més la veritat és que no.