MokaByte 48 - Gennaio 2001
Foto dell'autore
di
Andrea Gini
Refactoring
La qualità del software
La manutenzione del codice e' forse l'attivita' piu' importante per un programmatore professionista. Ogni sistema software, grande o piccolo che sia, richiede un enorme sforzo di revisione, sia nella fase di collaudo, sia nel periodo sucessivo alla consegna, volta ad eliminare bugs o imperfezioni. Non appena un sistema viene messo in funzione, emergono nuove esigenze alle quali si chiede di rispondere: tutte queste attivita' comportano il dover riadattare del codice scritto settimane o mesi prima al fine di piegarlo alle nuove esigenze. Se il sistema e' stato sviluppato da un team, capita che la revisione venga eseguita da una persona diversa da chi ha scritto il codice inizialmente: in simili circostanze al problema di soddisfare una nuova esigenza, si sovrappone quello di riuscire a comprendere il funzionamento di un frammento di codice scritto da altri.
Per molti anni gli esperti di programmazione ad oggetti hanno sviluppato una collezione di tecniche volte a migliorare l'integrita' strutturale di programmi gia' in funzione; tale pratica e' nota con il nome di Refactoring. 
In questo articolo introdurremo i concetti che stanno alla base di queste tecniche.


Ogni programmatore desidera produrre codice di qualita', sebbene il significato di "qualita' del codice" non sia un concetto facile da definire. I criteri in gioco sono molti, e spesso hanno una valenza soggettiva; per semplicita' restringeremo la nostra attenzione a tre fattori: la struttura, la robustezza e l'eleganza stilistica.

Il primo fattore riveste un'importanza cruciale nel campo dei linguaggi ad oggetti, dove la suddivisione dei compiti tra oggetti specializzati porta alla definizione di architetture software di una certa complessita'. Quando affrontiamo lo studio di un programma ad oggetti ci poniamo domande del tipo: a cosa serve questa classe? In che relazione e' con quest'altra? Quali informazioni racchiude? Nella programmazione ad oggetti l'aspetto strutturale puo' essere trattato ad un livello di astrazione superiore ricorrendo ai diagrammi di classe UML. Il Design by Pattern e' la disciplina che si occupa di definire architetture efficienti ed eleganti per risolvere i problemi piu' comuni. Il libro piu' conosciuto su questo argomento e' "Design Pattern: Elements of Reusable Object Oriented Software" della Gang Of Four. 

La robustezza e' un fattore piu' difficile da valutare: il codice robusto e' quello che non entri in crisi nel momento in cui viene utilizzato in un contesto differente da quello di collaudo. Ad esempio la seguente funzione calcola correttamente il fattoriale di un numero intero positivo:

  public static int fact1(int x) {
    int returnVal = 1;
    int cont = x;
    while(cont!=0) {
      returnVal = returnVal*cont;
      cont--;
    }
    return returnVal;
  }

Sebbene l'algoritmo sia corretto, il metodo non e' robusto: e' sufficiente passare come parametro un numero negativo per metterelo in crisi e farlo entrare in un loop infinito. Chi lo ha scritto non ha preso in considerazione questa possibilita', dando per scontato che chi lo usera' sia a conoscenza del fatto che la funzione fattoriale non e' definita per numeri negativi. La seguente implementazione e' piu' robusta, dal momento che identifica e segnala la condizione di errore:

  public static int fact2(int x) {
    if(x<0)      //parametro non valido
      return -1; 

    int returnVal = 1;
    int cont = x;
    while(cont!=0) {
      returnVal = returnVal*cont;
      cont--;
    }
    return returnVal;
  } 

L'ultimo dei tre fattori ha una valenza soprattutto estetica:  non bisogna dimenticare che il codice sorgente deve essere interpretato dalle persone che ci lavorano sopra, e non solo dalle macchine che lo devono eseguire. Sottostimare questo fattore puo' portare in breve a lavorare su codice indecifrabile.
 
 
 

Limiti del Design nel controllo della qualita'
I processi di upfront design, ovvero quell'insieme di tecniche che aiutano a descrivere la struttura di un sistema prima di entrare in fase di codifica, danno un contributo enorme nella definizione di sistemi di buona qualita' dal punto di vista strutturale. Nel momento in cui si passa alla fase implementativa il loro aiuto viene meno, dal momento che la produzione di codice e' un'attivita' difficile da automatizzare, e che non esistono metriche soddisfacenti per misurare la correttezza, la robustezza e l'eleganza del codice.

In altre branche dell'ingegneria questi problemi non esistono. Nell'ingegneria civile, per fare un esempio, un progetto descrive in maniera rigorosa le specifiche relative agli elementi di cui e' composta un'opera. Un sistema di equazioni matematiche permette di descrivere la struttura di un ponte e le forze ad esso applicate quando ancora e'  sulla carta, e garantire che se viene costruito secondo determinate specifiche non crollera'. Chi costruisce si assume l'impegno di aderire alle specifiche fornite dal progettista; al termine dei lavori, e' possibile sottoporre il ponte ad una serie di verifiche strutturali, e garantirne l'agibilita'. Se il ponte venisse dichiarato inagibile, il finanziatore del progetto potrebbe richiedere una perizia e stabilire con certezza se attribuire la responsabilita' al progettista o all'impresa di costruzione.

Purtroppo nell'industria del software mancano delle tecniche di verifica della qualita' soddisfacenti;  quelle esistenti non sono automatizzabili, sono applicabili ad una casistica molto ristretta e richiedono delle competenze che vanno al di la' di quelle del programmatore medio.

Per questa ragione non si puo' considerare i programmatori alla stregua di semplici manovali: essi sono coinvolti attivamente nel processo di produzione del software, e a loro spetta  il compito di verificare la struttura del sistema e di realizzare codice robusto ed elegante.
 
 
 

Come il caos prende il sopravvento
La natura immateriale del Software e' cio' che rende la programmazione un'attivita' cosi' diversa da qualunque altra disciplina artigianale o ingengneristica. L'assenza di vincoli stretti consente una flessibilita' tale da permettere un accrescimento delle funzionalita' di un sistema software anche dopo la sua messa in opera attraverso la revisione del codice sorgente. Purtroppo la flessibilita', alla lunga, porta al caos: a furia di revisioni, la qualita' del codice sorgente tende a deteriorare, la struttura diviene piu' complessa in seguito all'aggiunta di estensioni e la robustezza delle procedure viene messa alla prova da nuove modalita' di utilizzo.
 
 
 

Il Refactoring come rimedio al caos
Il Refactoring e' una disciplina tesa a migliorare la qualita' del codice gia' scritto, allo scopo di minimizzare le occasioni di introdurre bugs. E' un processo che si propone di migliorare la struttura interna di un sistema software, lasciandone inalterato il comportamento esteriore. Il Refactoring, insomma, e' una pratica di ottimizzazione del codice, laddove con il termine "ottimizzazione" non si intende "rendere il codice piu' veloce" ma piuttosto "rendere il codice piu' bello" e "piu' facile da mantenere". 
In conclusione, il refactoring e' un insieme di tecniche che permette di invertire il processo entropico che domina la vita di un software, riportando ordine nella struttura e  rinforzando la robustezza. E' anche un insieme di strategie "cosmetiche" finalizzate a rendere il codice piu' chiaro e semplice da interpretare.
 
 
 

Refactoring strutturale
Per quanto accurata sia l'analisi, un progetto contiene sempre qualche errore di valutazione: classi troppo generiche o troppo specializzate, metodi che vengono definiti in una classe ma che dovrebbero trovarsi in un'altra e cosi' via. Vediamo alcuni esempi di manipolazione delle gerarchie di classi. Nel primo esempio ci troviamo di fronte ad una superclasse e ad una sottoclasse che non presentano grosse differenze. Questo problema emerge con frequenza quando, nel tentativo di specializzare le classi il piu' possibile, finiamo con il creare sottoclassi inutili. Il rimedio consigliato e' quello di collassare la gerarchia (Collapse Hierarchy) fondendo le due classi.
 
 

Figura 1: Collapse Hierarchy

 

Il caso opposto e' quello in cui abbiamo una classe che possiede una funzione che viene usata solo da alcune istanze. In questi casi possiamo estrarre una sottoclasse (Extract Subclass) in modo da distinguere il caso generale dal particolare
 
 

Figura 2: Extract Subclass

Se scopriamo di avere due classi non imparentate che contengono funzionalita' simili, possiamo creare una superclasse che contenga le funzionalita' comuni (Extract Superclass)
 
 

Figura 3: Extract Superclass

 

Il Refactoring strutturale e' una delle colonne portanti della tecnica di Lightweight Design nota con il nome di eXtreme Programming (XP). Chi pratica questa tecnica sviluppa il design di un sistema parallelamente al codice, e lo revisiona periodicamente attraverso tecniche di refactoring. Chi fosse interessato a conoscere XP puo' consultare la bibliografia.
 
 
 

Come il Refactoring puo' rendere il codice piu' robusto
Prevenire e' meglio di curare. A volte basta un po' di attenzione per evitare di incorrere in fastidiosi bugs. Alcune tecniche di Refactoring hanno proprio questo obbiettivo: negli esempi seguenti ne vedremo alcune.

Il primo suggerimento prende il nome di "Hide Method", ovvero rendere privati i metodi che vengono usati solo all'interno della classe. In questo modo si riduce la dimensione dell'interfaccia di programmazione, e di conseguenza le possibilita' di utilizzarla in modo scorretto.

Gli algoritmi di ricerca lineare mal congegnati sono un terreno fertile per i bugs, specialmente se implementati attraverso un ciclo while con piu' di una condizione di uscita. "Remove Control Flag" consiglia di rimuovere le variabili flag dal corpo del ciclo e di rimpiazzarle con return o break. Il seguente frammento di codice presenta una versione particolarmente criptica di una funzione che verifica la presenza di numeri negativi in un vettore di interi:

....
public boolean checkNegative(int[] numbers) {
  int foundNegativeInIndex = -1;
  for(int i=0;i<numbers;i++) {
    if(numbers[i]<0)
      foundNegativeInIndex =i;
  }
  if(foundNegativeInIndex!=-1)
    return true;

  return false;
}
....

Con una piccola revisione si puo' rendere il codice piu' snello ed elegante: 

....
public boolean checkNegative(int[] numbers) {
  for(int i=0;i<numbers;i++) {
    if(numbers[i]<0)
      return true;
  }

  return false;
}
....

Un altro punto critico sono i metodi che verificano i parametri e restituiscono un codice di errore. Il linguaggio Java fornisce il meccanismo delle eccezioni proprio per rispondere a queste esigenze. Proviamo ad applicare "Replace Error Code with Exception" alla funzione fattoriale vista all'inizio di questo articolo:

  public static int fact3(int x) throws IllegalArgumentException {
    if(x<0)
      throw new IllegalArgumentException();

    int returnVal = 1;
    int cont = x;
    while(cont!=0) {
      returnVal = returnVal*cont;
      cont--;
    }
    return returnVal;
  } 

L'ultima  esempio che vorrei segnalare e' "Encapsulate Downcast". Una delle prerogative piu' importanti di java e' la verifica statica del tipo di una variabile. A volte siamo costretti nostro malgrado a ricorrere ad un downcast, ad esempio quando estraiamo oggetti da un Vector: 

....
Student lastStudent = (Student)students.lastElement();
....

E' sufficiente isolare all'interno di un metodo l'operazione di downcast per eliminare una grossa fonte di errori:

public Student getLastStudent() {
 return (Student)students.lastElement();
}
....
Student lastStudent = getLastStudent();
 
 

Refactoring cosmetico
L'essenza del Refactoring consiste nel riconoscere che spesso la prima stesura di un sorgente possa contenere dei passaggi poco eleganti. Questo fenomeno ha due radici: in primo luogo chi codifica e' talmente concentrato sulla soluzione del particolare problema da non essere in grado di cogliere le implicazioni stilistiche del proprio lavoro; in secondo luogo lo stile e' frutto dell'esperienza, e l'esperienza si matura solo con il tempo. 

L'esperienza permette di individuare all'interno di un frammento di codice una sequenza di istruzioni farraginosa, che potrebbe essere male interpretata da un collega o da noi stessi dopo un certo lasso di tempo. Il caso piu' comune sono i metodi troppo lunghi, che eseguono piu' operazioni di quelle che dovrebbero. In questi casi si consiglia di raggruppare le istruzioni in sottosequenze, e quindi creare per ognuna di esse un metodo, il cui nome chiarisca l'operazione svolta (Estract Method). Ad esempio, il frammento di codice 

  public void printOwing() {
    printBanner();
    //print details
    System.out.println ("name: " + _name);
    System.out.println ("amount " + getOutstanding());
  }

puo' essere riscritto cosi': 

  public void printOwing() {
    printBanner();
    printDetails(getOutstanding());
  }
  protected void printDetails (double outstanding) {
    System.out.println ("name: " + _name);
    System.out.println ("amount " + outstanding);
  }

In questo esempio la creazione del metodo printDetails(double outstanding) ha reso il codice piu' chiaro, rendendo inutile la riga di commento che compare nel primo frammento di codice: l'eliminazione dei commenti inutili e' uno degli scopi del Refactoring . L'estrazione di metodo e' una tecnica che puo' aiutare a rendere piu' semplici i costrutti condizionali: vediamo quest'esempio in cui abbiamo una sequenza di test condizionali che restituiscono lo stesso risultato

  public double disabilityAmount() {
    if (_seniority < 2) return 0;
    if (_monthsDisabled > 12) return 0;
    if (_isPartTime) return 0;
    // compute the disability amount
    ....
  }

I tre condizionali possono essere combinati in un unica espressione condizionale (Consolidate Conditional Expression), il cui test e' contenuto in un metodo separato:

  public double disabilityAmount() {
    if (isNotEligableForDisability()) return 0;
    // compute the disability amount
    ....
  }
  protected boolean isNotEligableForDisability() {
    if (_seniority < 2 || _monthsDisabled > 12 || _isPartTime) 
      return false;
    else
      return true;
  }

Le espressioni condizionali complesse sono oggettivamente difficili da interpretare. Ecco un altro esempio di semplificazione di espressioni condizionali (Decompose Conditional)

   if (date.before (SUMMER_START) || date.after(SUMMER_END))
    charge = quantity * _winterRate + _winterServiceCharge;
  else
    charge = quantity * _summerRate;

Per semplificare possiamo compattare la condizione booleana, il then e l'else ricorrendo nuovamente all'estrazione di metodo:

  if (notSummer(date))
    charge = winterCharge(quantity);
  else
    charge = summerCharge(quantity);
 
 

Mettere in pratica il Refactoring
Dopo aver visto alcuni esempi di refactoring, resta da illustrare come metterlo in pratica. Una vecchia massima cara ai programmatori di applicazioni bancarie dice piu' o meno cosi': "finche' funziona, lascialo stare". I sostenitori del refactoring predicano qualcosa di diverso, tipo "se hai messo le mani da qualche parte, verifica che tutto funzioni come prima". In effetti se vogliamo essere sicuri che il nuovo codice funzioni come quello vecchio, dobbiamo disporre di una modalita' di verifica. Il suggerimento e' quello di predisporre, durante tutto il processo di sviluppo, una serie di test che verifichino il funzionamento di ogni singola classe del programma (pratica nota con il nome di "Self testing code"). Dopo aver modificato una parte del sistema, lanciando i test, sara'  possibile verificare che le cose vadano come ci si aspetterebbe. Uno strumento caro agli amanti del refactoring e' la test suite JUnit (http://www.junit.org/), di cui ci occuperemo prossimamente 
 
 
 

Conclusioni
Questo articolo ha presentato alcune delle tematiche che stanno alla base di una filosofia di manutenzione del codice nata all'interno della comunita' dei programmatori di linguaggi ad oggetti (in particolare della comunita' Smalltalk). L'elenco delle tecniche di refactoring e' lungo, e non e' questa la sede per visionarli tutti. Chi scrive codice per molte ore al giorno trovera' familiare alcune di queste tecniche: lo stesso Martin Fowler, autore del piu' importante libro su questo tema, si e' limitato a catalogare le tecniche piu' usate, a dare loro un nome e a presentare alcuni esempi d'uso. Potete trovare al sito http://www.refactoring.com/catalog/index.html  un elenco di piu' di settanta esempi, tra i quali quelli presentati. A chi volesse approfondire l'argomento consiglio comunque di dare un'occhiata al libro di Fowler "Refactoring - Improving the Design of Existing Code", un testo che ha il merito di trattare con chiarezza, umilta' ed un pizzico di ironia un tema di straordinaria importanza per tutti i programmatori professionisti.
 
 

Bibliografia
Refactoring : Improving the Design of Existing Code 
di  Martin Fowler, Kent Beck (Contributor), John Brant (Contributor), William Opdyke, don Roberts  - Ed. Addison-Wesley Object Technology Series
http://www.refactoring.com/ 

Design Patterns: Elements of Reusable Object Oriented Software
di Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides, Grady Booch 
Ed. Addison-Wesley Pub Co

Extreme Programming Explained: Embrace Change di Kent Beck
Ed. Addison-Wesley Pub Co

Extreme Programming -  http://www.extremeprogramming.org/ 

La cattedrale e il bazaar  di Eric S. Raymond
 (22/11/1998 ore 04:01:20)
  http://www.apogeonline.com/openpress/doc/cathedral.html
 

Vai alla Home Page di MokaByte
Vai alla prima pagina di questo mese


MokaByte®  è un marchio registrato da MokaByte s.r.l.
Java® è un marchio registrato da Sun Microsystems; tutti i diritti riservati
E' vietata la riproduzione anche parziale
Per comunicazioni inviare una mail a
mokainfo@mokabyte.it