Refactoring significa modificare la struttura interna del codice senza cambiarne il comportamento esterno. Lo scopo è rendere il codice più facile da capire e da modificare: rinominare, spostare, dividere, semplificare. Il rischio è evidente: ogni modifica può introdurre un errore. Per questo il refactoring ben fatto non è un grande intervento eroico, ma una sequenza di piccoli passi verificati, ciascuno dei quali lascia il programma funzionante.
Refactoring non è riscrittura
La distinzione è importante. Riscrivere un modulo da zero significa sostituire codice funzionante, con tutti i suoi casi particolari accumulati negli anni, con codice nuovo e non collaudato. Il refactoring, invece, trasforma il codice esistente gradualmente, mantenendolo sempre in uno stato funzionante.
Un segnale pratico: se durante un "refactoring" il programma resta rotto per giorni, non si sta facendo refactoring, si sta riscrivendo.
È altrettanto importante non mescolare refactoring e nuove funzioni nello stesso passaggio. Prima si ristruttura, verificando che il comportamento non cambi; poi si aggiunge la nuova funzione sul codice ormai più pulito. Due modifiche separate sono più facili da verificare e da annullare.
La rete di sicurezza: i test
Senza test, il refactoring è un atto di fede. Prima di toccare una porzione di codice, verifica che sia coperta da test che controllano il comportamento attuale. Se non ci sono, la prima mossa è scriverli.
Con il codice legacy, spesso non è chiaro quale sia il comportamento "corretto". In questi casi si usano i test di caratterizzazione: test che fotografano ciò che il codice fa oggi, anche se sembra strano.
def test_caratterizzazione_calcolo_spedizione():
# Registriamo il comportamento attuale, non quello ideale
assert calcola_spedizione(peso=0, zona="nord") == 4.90
assert calcola_spedizione(peso=2.5, zona="isole") == 12.40
assert calcola_spedizione(peso=30, zona="sud") == 18.00
Se durante il refactoring uno di questi test fallisce, sai di aver cambiato il comportamento. Se scopri che il comportamento attuale è un bug, lo correggi in un passaggio separato e consapevole.
Il ritmo dei piccoli passi
Un ciclo di lavoro efficace:
- esegui i test e verifica che passino tutti;
- fai una singola trasformazione piccola;
- esegui di nuovo i test;
- se passano, fai un commit; se falliscono, annulla e prova un passo più piccolo.
Commit frequenti significano che, se qualcosa va storto, si torna indietro di pochi minuti di lavoro, non di un pomeriggio intero.
Le trasformazioni più utili
Estrarre una funzione
Il refactoring più comune. Un blocco di codice con uno scopo riconoscibile diventa una funzione con un nome che descrive quello scopo.
# Prima
def stampa_fattura(ordine):
totale = 0
for riga in ordine.righe:
totale += riga.prezzo * riga.quantita
if ordine.cliente.e_premium:
totale *= 0.9
...
# Dopo
def totale_ordine(ordine):
totale = sum(r.prezzo * r.quantita for r in ordine.righe)
return totale * 0.9 if ordine.cliente.e_premium else totale
def stampa_fattura(ordine):
totale = totale_ordine(ordine)
...
Ora il calcolo del totale si può testare da solo e riutilizzare altrove.
Altre trasformazioni frequenti
| Trasformazione | Quando applicarla |
|---|---|
| Rinominare | Un nome non descrive più ciò che rappresenta |
| Sostituire numeri magici con costanti | Valori come 0.9 o 86400 compaiono senza spiegazione |
| Introdurre un oggetto parametro | Una funzione riceve sei o sette argomenti correlati |
| Semplificare condizioni | if annidati su più livelli o condizioni lunghe e ripetute |
| Usare clausole di guardia | Uscire subito dai casi particolari invece di annidare tutto in un else |
| Spostare una funzione | Una funzione usa più dati di un altro modulo che del proprio |
Quando fare refactoring
Il momento migliore non è un "mese del refactoring" pianificato in astratto, ma il lavoro di tutti i giorni. Alcune occasioni naturali:
- prima di aggiungere una funzione: se il codice attuale rende difficile la modifica, prima lo si ristruttura per renderla facile, poi si fa la modifica;
- quando si corregge un bug: capire il bug spesso rivela una struttura confusa che lo ha favorito;
- durante la revisione del codice: un secondo sguardo individua duplicazioni e nomi poco chiari.
Una regola semplice, spesso associata al mondo degli scout, riassume l'atteggiamento: lascia il codice un po' più pulito di come l'hai trovato. Piccoli miglioramenti continui, sommati nel tempo, valgono più di grandi interventi rari.
Gli strumenti aiutano
Gli editor moderni offrono refactoring automatici: rinomina, estrazione di funzioni e variabili, spostamento di metodi. Sono più sicuri delle modifiche a mano, perché analizzano il codice e aggiornano tutti i riferimenti. Nei linguaggi dinamici come Python l'analisi è meno precisa che in quelli tipizzati staticamente, e i test restano indispensabili. Aggiungere annotazioni di tipo e un controllore statico rende questi strumenti molto più affidabili.
In sintesi
- Il refactoring cambia la struttura del codice senza cambiarne il comportamento.
- I test sono la rete di sicurezza: con il codice legacy, inizia con test di caratterizzazione.
- Procedi a piccoli passi, eseguendo i test e facendo commit dopo ogni trasformazione.
- Non mescolare refactoring e nuove funzioni nella stessa modifica.
- Integra il refactoring nel lavoro quotidiano, prima di modifiche e correzioni.