Refactoring:
Cos'è ?
Il Refactoring è una pratica che prevede la modifica
di un sistema software per migliorare la sua struttura interna
senza alterarne le funzionalità.
Di fatto è un modo disciplinato di "ripulire"
il codice esistente (clean-up code) minimizzando le possibilità
di introdurre nuovi bug rendendo così il codice piu'
facile da comprendere e conseguentemente da gestire (manutenzione,
bug fixing o aggiunta di nuove funzionalità).
Il Refactoring è quindi un cambiamento della struttura
interna di un software dopo che è già stato
realizzato ma il cui codice deve essere migliorato per risultare
più comprensibile e gestibile.
Figura 1: Refactoring
Refactoring:
Perché ?
Un
sistema software può degenerare per svariate ragioni.
Nei
casi peggiori un sistema può degenerare al punto da
diventare difficilmente manutenibile e soprattutto difficile
da estendere se non a fronte di interventi "costosi"
in termini di risorse e/o di tempo.
Senza
volere entrare nel merito dei motivi (esula dallo scopo dell'articolo)
il Refactoring è una pratica che permette di fronteggiare
il decadimento del software (software decay) rispetto ai requisiti
attuali e futuri cercando di tenere il codice il più
lineare possibile.
Il
Refactoring permette di effettuare modifiche al codice di
tipo evolutive e/o incrementali e/o migliorative a parità
di funzionalità del prodotto stesso.
Refactoring:
Come ?
Il processo di Refactoring è composto da passi minimali
di modifica del codice.
L'effetto cumulativo di piccole e semplici modifiche di refactoring
può migliorare drasticamente il progetto. Ogni passo
deve essere semplice e chiaro come, ad esempio, lo spostamento
di un metodo da una classe all'altra.La
precondizione fondamentale per potere effettuare il Refactoring
è di avere i test (vedere [Moka_TDD]) che permettano
di rilevare eventuali regressioni del software a fronte di
operazioni di refactoring. Ogni
operazione di Refactoring deve essere associato ad un test
ad hoc in grado di verificare la correttezza dell'operazione
effettuata e rilevare eventuali regressioni del software
Grazie
ai test è possibile controllare la bontà delle
modifiche, minimizzando la possibilità di introdurre
bug. Il
Refactoring è quindi in netto contrasto con la (vecchia!?)
filosofia del "finchè funziona lasciamolo stare",
bensì prevede la modalità di migliorare un qualcosa
che già funziona ma che potrebbe/dovrebbe essere migliorato.
I
test sono lo strumento principe che aiuta a contrastare la
"paura" di effettuare cambiamenti su un qualcosa
che, bene o male (generalmente più male che bene),
"funziona".
Figura 2: Un "piccolo passo" di Refactoring
Avere dei buoni test che permettano di avere confidenza sulla
correttezza e robustezza del proprio codice è quindi
una condizione fondamentale e inprescindibile per potere pensare
di effettuare refactoring su un qualsiasi sistema, semplice
o complicato che sia.
Figura 3: Test, Refactoring e
Test!
Martin Fowler ha organizzato un catalogo che raccoglie le
tipiche operazioni di Refactoring (vedere[MFCAT]). Per ogni
operazione si descrivono le motivazioni e i dettagli implementativi
della trasformazione del codice. Dal catalogo si vede come
il target del refactoring sono spesso i Design Pattern, ovvero
soluzioni riusabili (e consolidate) per risolvere problemi
ricorrenti (vedere [MOKA_PATTERN]).
Esempi
di possibili operazioni di refactoring sono ad esempio lo
spostamento di un campo (move field) o di un metodo (move
method) da una classe all'altra, l'incapsulamento di un campo
(encapsulate fiedl) mediante opportuni metodi accessor e/o
mutators, la sostituzione di condizionali mediante polimorfismo
(replace conditional with polimorphism), l'estrazione di un
blocco di codice in un nuovo metodo (extract method), ecc
Refactoring:
Quando ?
Continuamente.
L'importante è pianificare piccole sessioni dedicate
(e disciplinate!).
Ad
esempio un buon momento in cui effettuare Refactoring è
al termine di ogni funzionalità implementata e prima
di passare alla successiva. Sicuramente
è importante fare refactoring laddove si evidenzia
un'oggettiva difficoltà di comprensione e/o manutenzione
del prodotto software.
Refactoring:
un esempio pratico
Esemplificando
quanto detto fino ad ora riprendiamo in esame l'esempio presentato
in [MOKA_TDD], un po' "triviale" ma (spero!) efficace.
Supponiamo
di dovere aggiungere alla classe Account il calcolo di una
commissione sull'operazione (versamento e prelievo) che dipende
da tipo di Conto (conto corrente Famiglia o conto corrente
Azienda).
Supponiamo
di trovarci davanti ad una classe che risponda a questo nuovo
requisito con uno switch sul tipo del conto corrente duplicato
sia nel metodo versamento che nel metodo prelievo come riportato
di seguito:
public
class Account implements java.io.Serializable {
public
static final int CONTO_CORRENTE_FAMIGLIA=0;
public
static final int CONTO_CORRENTE_AZIENDA=1;
//
... << altri tipi di Conto Corrente >> ...
private
int tipoConto;
private
float saldo;
public
Account(int tipoConto){
this.saldo=0;
this.tipoConto=tipoConto;
}
public
float getSaldo(){
return
this.saldo;
}
public
void versamento(float somma) throws Exception{
if
(somma <= 0){
throw
new Exception("Somma rifiutata: " + somma);
}
this.saldo
= this.saldo + somma;
//
calcola gli interessi di commissione sull'operazione
float
commissioneOperazione = 0;
switch(tipoConto){
case
Account.CONTO_CORRENTE_FAMIGLIA:
commissioneOperazione
= 10;
break;
case
Account.CONTO_CORRENTE_AZIENDA:
commissioneOperazione
= 5;
break;
//
case << altri tipi di Conto Corrente >> : . .
. break;
}
//
sottraggo la commisione calcolata al saldo
this.saldo
= this.saldo - ((this.saldo * commissioneOperazione)/100);
}
public
void prelievo(float somma) throws Exception{
if
(somma < 0){
throw
new Exception("Somma negativa: " + somma);
}
if
(this.saldo < somma){
throw
new Exception("Saldo insufficiente : " + somma);
}
this.saldo
= this.saldo - somma;
//
calcola gli interessi di commissione sull'operazione
float
commissioneOperazione = 0;
switch(tipoConto){
case
Account.CONTO_CORRENTE_FAMIGLIA:
commissioneOperazione
= 10;
break;
case
Account.CONTO_CORRENTE_AZIENDA:
commissioneOperazione
= 5;
break;
//
case << altri tipi di Conto Corrente >> : . .
. break;
}
//
sottraggo la commiszione calcolata al saldo
this.saldo
= this.saldo - ((this.saldo * commissioneOperazione)/100);
}
}
Il
codice è sicuramente migliorabile
iniziamo con
il Refactoring? No!
Per prima cosa si deve approntare un test che verifichi il
corretto funzionamento della classe.
Aggiungiamo quindi al metodo di test della classe AccountTest
(vedere [MOKA_TDD ]) la seguente verifica:
public
void testVersamento(){
. . . .
try
{
account.versamento(1000);
assertEquals("Check
saldo:", new Float(900.0),new Float(account.getSaldo()));
}
catch(Exception e){
fail("Versamento
fallito : " + e.getMessage());
}
}
Solo
e solamente se il test darà verde potremo procedere
all'operazione di Refactoring (se invece il test segnala rosso
bisogna ritornare sulle modifiche fatte per eliminare il bug
introdotto dall'operazione di refactoring).
Verde! Ok, procediamo
Come primo passo si può prevedere di incapsulare la
logica del calcolo della commissione in un unico metodo che
sarà poi invocato dai metodi versamento() e prelievo().
Tale operazione si chiama "extract method" (vedere
[MFCAT_EM]).Creiamo
quindi un nuovo metodo (con un bel nome "parlante"
calcolaCommissioneOperazione) che centralizza il calcolo della
commissione dell'operazione:
private
float calcolaCommissioneOperazione(float somma){
//
calcola gli interessi di commissione sull'operazione
float
commissioneOperazione = 0;
switch(tipoConto){
case
Account.CONTO_CORRENTE_FAMGLIA:
commissioneOperazione
= 10;
break;
case
Account.CONTO_CORRENTE_AZIENDA:
commissioneOperazione
= 5;
break;
//
case << altri tipi di Conto Corrente >> : XXX
break;
}
return
commissioneOperazione;
}
All'interno
dei metodi versamento() e prelievo() togliamo lo switch e
inseriamo l'invocazione al metodo calcolaCommissioneOperazione():
//
calcola gli interessi di commissione sull'operazione
float commissioneOperazione = this.calcolaCommissioneOperazione(somma);
// sottraggo gli interessi calcolati al saldo
this.saldo = this.saldo - ((this.saldo * commissioneOperazione)/100);
Passo
obbligatorio e doveroso è rieseguire i test. Se è
verde si prosegue, se è rosso bisogna ritornare sulle
modifiche fatte per eliminare il bug introdotto dall'operazione
di refactoring.
Verde! Proseguiamo
Vediamo
come sostituire lo switch "old fashion" nel metodo
calcolaCommissioneOperazione() con una soluzione piu' object
oriented.
Applichiamo quindi il replace conditional with polimorphism
(vedere [MFCAT_RCWP]) dove al posto dello switch introduciamo
il polimorfismo basato sulla classe Account.
Nella
classe Account "svuotiamo" il metdo calcolaCommissioneOperazione()
e lo rendiamo abstract
public abstract float calcolaCommissioneOperazione(float somma);
e
creiamo due sottoclassi che estendono da Account e ridefiniscono
il metodo calcolaCommissioneOperazione()
class
AccountFamiglia extends Account {
public
float calcolaCommissioneOperazione(float somma){
return
10;
}
}
class
AccountAzienda extends Account {
public float calcolaCommissioneOperazione(float
somma){
return 5;
}
}
Ovviamente
bisogna rieseguire i test per verificare che tutto funzioni
correttamente e che non sono state introdotte delle regressioni.
Rispetto
alla precedente versione del test, bisogna apportare la seguente
modifica nel metodo setUp(): da
this.account
= new Account(Account.CONTO_CORRENTE_FAMIGLIA);
a
this.account
= new AccountFamiglia();
Se
è verde si può proseguire con un'eventuale nuova
operazione di Refactoring come ad esempio rimpiazzare i valori
numerici (Magic Number) 5 e 10 con costanti simboliche dichiarate
static e final (Replace Magic Number with Simbolic Constant
- vedere [MFCAT_RMCWSC]).
Figura 4: Un esempio di Refacroring
Da notare come alcuni ambienti di sviluppo stanno già
integrando direttamente le principali operazioni di refactoring
con ad esempio avviene in Eclispe (vedere [ECLIPSE]).
Figura
5: Refactoring da Eclipse
Conclusioni
Il
Refactoring è la pratica che prevede di modificare
il codice di un sistema software (senza alterarne le funzionalità)
per contrastare il "Software Decay".
Elemento
fondamentale per potere applicare la pratica di Refactoring
è di sviluppare dei test ad hoc che permettano di verificare
la bontà e la correttezza dell'operazione effettuata.
Il
Refactoring è un momento di riflessione a posteriori
dello sviluppo che permette di analizzare e verificare se
l'applicazione necessita di modifiche per migliorare il codice
o la sua struttura.
Di
fatto il Refactoring aiuta a bilanciare a posteriori eventuali
applicazioni che potrebbero risultare sotto-ingegnerizzate
(come nell'esempio mostrato in precedenza) o sovra-ingegnerizzate
(ad esempio quando si applicano in modo "scriteriato"
i Design Pattern).
XP
annovera il Refactoring come "core practice" (vedere
[WSXP]) mentre l'Agile Modeling la indirizza come pratica
complementare (vedere [PAM]).
Figura 6: Refactoring in XP e AM
E'
bene comunque evidenziare come l'importanza e l'utilità
del Refactoring ne fanno una pratica applicabile in modo trasversale
a qualsiasi metodologia. Attenzione che non sempre si riesce
a trasformare un codice scadente in uno equivalente scritto
bene
a volte conviene riscriverlo da capo (from scratch).
Bibliografia
[MOKA_TDD] S. Rossini, Pratiche di sviluppo del software (I):
Test Driven Development, MokaByte N.86 - Giugno 2004
[MOKA_CI_1] S. Rossini, A. D'Angeli: Pratiche di sviluppo
del software (II)
Continuous Integration: la teoria - Mokabyte N.87 Luglio Agosto
2004
[MOKA_CI_2] S. Rossini, A. D'Angeli: Pratiche di sviluppo
del software (II)
Continuous Integration: la pratica - Mokabyte N.88 Settembre
2004
[MOKA_PATTERN] S. Rossini: J2EE Patterns - Mokabyte N. 62
e successivi
[RHP] Refactoring Home Page: http://www.refactoring.com/
[MFCAT] Refactorings in Alphabetical Order: http://www.refactoring.com/catalog/index.html
[MFCAT_EM] Extract Method - http://www.refactoring.com/catalog/extractMethod.html
[MFCAT_RCWP] Replace Conditional with Polymorphism -
http://www.refactoring.com/catalog/replaceConditionalWithPolymorphism.html
[MFCAT_RMCWSC] Replace Magic Number with Symbolic Constant
-
http://www.refactoring.com/catalog/replaceMagicNumberWithSymbolicConstant.html
[MFIDD] Martin Fowler: Is Design Dead?
http://www.martinfowler.com/articles/designDead.html
[YAGNI] You Arent Gonna Need It - http://xp.c2.com/YouArentGonnaNeedIt.html
[PAM] Scott W. Ambler: The Practices of Agile Modeling (AM)
-
http://www.agilemodeling.com/practices.htm
[WSXP] Ron Jeffries: What is Extreme Programming? -
http://www.xprogramming.com/xpmag/whatisxp.htm
[VCUR3] V. Crescenzi: Refactoring
http://www.dia.uniroma3.it/~pizzonia/swe/slides/ES_03_refactoring.pdf
[ECLIPSE] www.eclipse.org
|