MokaByte 89 - 8bre 2004 
Pratiche di sviluppo del software
IV parte -
Refactoring
di
Strefano Rossini

Dopo avere affrontato l'importante pratica dei test (vedere [MOKA_TDD]) e della Conti-nuous Integration (vedere[MOKA_CI_1] e [MOKA_CI_2]), in questo articolo ci concen-tremo su un'altra importante pratica software: il Refactoring

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


MokaByte® è un marchio registrato da MokaByte s.r.l. 
Java®, Jini® e tutti i nomi derivati sono marchi registrati da Sun Microsystems.
Tutti i diritti riservati. E' vietata la riproduzione anche parziale.
Per comunicazioni inviare una mail a info@mokabyte.it