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
|