Introduzione
Chi ha coniato il termine "spaghetti code" è
riuscito a esprimere, in modo molto efficace, la sensazione
che si prova quando si ha a che fare con problematiche di
reingegnerizzazione o anche di semplice manutenzione di una
applicazione: centinaia di file con il codice letteralmente
"arrotolato" su sè stesso, proprio come accade
in una matassa di spaghetti. Lavorare con il codice scritto
da altri può costituire un impegno estremamente gravoso,
anche in virtù di fattori quali la carenza di documentazione
aggiornata (che a volte manca del tutto), così come
l'assenza nel progetto - allo stato odierno dell'arte - di
quelle persone che a suo tempo sono state i principali protagonisti
dello sviluppo. L'unica fonte di informazioni disponibile
sembra essere il codice stesso, ma da dove incominciare? L'obiettivo
del presente articolo è proprio questo, e cioè
porre l'accento sull'importanza di principi quali quelli del
Packaging e del Layering, introdurre il concetto di Metriche
e presentare, in un esempio concreto, cosa significa applicare
tutto questo ad una applicazione reale arrivando ad effettuarne
il cosiddetto Refactoring.
Modularizzare
il codice Java: i Package
Impiegare in modo corretto e appropriato l'astrazione è
un concetto centrale nell'ambito dell' Object-Orientation.
Tuttavia, troppe volte si trascura che ugualmente importante
è la decomposizione del codice in moduli: una struttura
modulare si rivela essenziale quando si affronta lo sviluppo
e la manutenzione di applicazioni business-critical.
Naturalmente non si tratta di un concetto nuovo, già
negli anni '70 principi quali flessibilità, comprensibilità
e riduzione del tempo necessario allo sviluppo sono stati
messi in evidenza (rif.[1]).
In
ambito strettamente Java, modularizzare il codice significa
innanzitutto basarsi sull'individuazione e l'uso dei cosiddetti
Package, come descritto in rif.[2]: in particolare, viene
evidenziato che ogni classe Java è parte di un Package,
e che i diversi Package sono gerarchicamente strutturati ad
albero.
Abbiamo
appena parlato di Package senza darne una definizione: perchè?
Perchè
in Java i Package vengono definiti in modo implicito, non
in quel modo rigoroso in cui sono definite classi e metodi.
In Java una classe (o più precisamente quella che si
definisce una compilation unit) dichiara semplicemente di
trovarsi in un certo Package, e questo fatto è sufficiente
ad indicare che quel determinato Package esiste. Tutto qui.
In sostanza un Package altro non è se non un raggruppamento
(un "impacchettamento", appunto) di classi con i
rispettivi metodi.
Anche
i concetti di dipendenza e interfaccia, propri dei Package
Java, sono definiti implicitamente, e cioè:
- Dati
i Package A e B, le dipendenze che esistono tra di essi
sono costituite dall'insieme delle dipendenze che vanno
dai tipi che appartengono ad A verso i tipi che appartengono
a B. Pertanto, le dipendenze tra Package sono implicitamente
dichiarate nelle definizioni di quei tipi - in A - che referenziano
altri tipi - in B.
- L'interfaccia
di un Package è definita dall' insieme delle interfacce
di tutti i tipi public (classi o interfacce) presenti nel
Package. I tipi sono parte di un Package se le compilation
unit cui appartengono contengono un "package statement"
che si riferisce al Package medesimo.
Naturalmente,
come è logico aspettarsi, una dipendenza verso un Package
A implica anche una dipendenza verso il Package "padre
di A". Ad esempio, una classe che dipende da java.util,
dipende automaticamente anche da java.
Per illustrare più chiaramente la cosa, si veda in
fig.1 lo schema UML delle dipendenze individuabili all'interno
della libreria standard delle classi Java. E' interessante
notare come, ad esempio, java.lang dipenda da java.util,java.io,
java.nio e java.security.
Figura 1 - Dipendenze all'interno della Java Class Library
Applicare
in pratica e in modo efficace il cosiddetto Package Design
però è tutt'altro che banale, ed è per
questo che esiste a tale scopo un insieme di linee guida.
Robert C. Martin, in un testo fondamentale (rif.[3]), enuncia
una serie di principi, quelli dell'Agile Development, secondo
i quali "l'obiettivo di un progetto basato sull'uso dei
Package porta ad una struttura, anche essa basata su Package,
facile da comprendere, collaudare, manutenere ed estendere".
Tra
questi principi possiamo ricordare ad esempio:
- Principio
di Dipendenza Aciclica (ADP), secondo cui le dipendenze
nel grafo dei Package non devono costituire cicli
- Principio
di Dipendenza Stabile (SDP), secondo cui i Package Stabili
sono quelli più difficili da modificare
- Principio
delle Astrazioni Stabili (SAP), riferito al numero di classi
astratte e interfacce presenti
e
diversi altri (si veda rif.[3] per una trattazione completa).
Migliorare
la comprensione della struttura: il Layering
Quando si affronta il disegno della architettura di un Sistema
e si inizia a definirne la struttura e a suddividerla in Package,
un approccio collaudato e molto efficace consiste nel progettare
in modo cosiddetto "layered" (cioè a "strati").
I vantaggi di tale approccio consistono nell' ottenere un
disegno estremamente più facile da comprendere e quindi
da manutenere.
Senza addentrarci ulteriormente nelle metodologie algoritmiche
di Layering esistenti in letteratura (si consulti rif.[4]
per maggiori informazioni) proponiamo, a titolo di esempio,
la fig.2, che rappresenta il risultato della applicazione
di un algoritmo di layering alla libreria standard java
Figura 2 - Layering applicato a java
Sempre
secondo rif.[3], se nell'ambito di una architettura complessa
si ha cura di rendere ciascun Package conforme al principio
ADP, si può essere sicuri di aver ottenuto un disegno
"layered".
Qualità
nello sviluppo: le Metriche
Progettare o analizzare secondo i principi del Package Design
e tenendo presenti i dettami del Layering. Siamo arrivati
fin qui. Ma
come facciamo a sapere fino a che punto il nostro disegno
progettuale - il modello grafico di una applicazione nuova
ovvero quello che ci siamo costruiti a partire da una applicazione
preesistente per analizzarla - è coerente con Packaging
e Layering?
E "quanto" lo è, ossia in che misura percentuale?
E'
proprio a questo punto che interviene la necessità
di poter disporre delle cosiddette Metriche. Esse sono sostanzialmente
un insieme di indicatori che permettono di verificare quanto
la struttura del sistema in esame è percentualmente
aderente a determinate regole, ad esempio quelle del Packaging
e Layering.
Esistono
diversi tipi di Metriche (Metriche di Base, Metriche di Stabilità,
Metriche di Qualità), ciascuna dedicata ad aspetti
diversi della struttura di un Sistema.
Le
Metriche di Base, ad esempio, ci permettono di evidenziare
la quantità con cui determinati elementi (subpackages,
classi astratte, interfacce) compaiono nel nostro disegno
progettuale.
Nella pratica tuttavia, le Metriche di Qualità si rivelano
di gran lunga più interessanti, proprio in quanto basate
sui Principi del Packaging descritti più sopra.
E' proprio a queste ultime che dedicheremo ora un pò
più di attenzione.
Un
esempio concreto: JMeter
Prendiamo in considerazione, tra le Metriche di Qualità,
quella derivata direttamente dal Principio ADP sopra descritto,
denominata Metrica di Dipendenza Aciclica (ADP/ADPR), una
delle più diffuse e apprezzate.
Vogliamo ora applicare tale Metrica a un'applicazione reale
e ai suoi Package.
Il nostro obiettivo, secondo quanto prescritto da essa, consiste
nell'individuare ed eliminare i cicli formati dalle dipendenze
indesiderate.
Come prima cosa, calcoliamo la percentuale di quelle dipendenze
che, al contrario, non determinano cicli, e che quindi non
necessitano di essere rimosse.
Se ad esempio troveremo un valore pari a 100%, potremo concludere
che non ci sono cicli nella nostra struttura, essa cioè
è completamente conforme al principio ADP.
Nel caso in cui il valore raggiunto sia ad esempio 90%, occorrerà
cercare quali sono le dipendenze da rimuovere e poi applicare
tali modifiche al codice della applicazione complessiva. L'applicazione
scelta per illustrare un esempio concreto di applicazione
delle Metriche è Jmeter (rif.[5]), che nasce nella
community Apache come applicazione destinata alla effettuazione
di test statici e dinamici su diverse configurazioni di software,
permettendo anche di simulare carichi su rete.
Esaminiamo Jmeter dal punto di vista della Metrica ADP, con
l'intento di vedere se è possibile migliorarne la struttura.
Servendoci del tool Compuware OptimalAdvisor [rif.(6)], specifico
per questo genere di problematiche, costruiamo innanzitutto
un modello grafico UML di Jmeter (fig.3).
Figura 3 - Modello Grafico di JMeter
Il
modello grafico ottenuto in tal modo mostra le dipendenze
tra Package, raffigurate con linee tratteggiate, e il valore
percentuale di compatibilità di Jmeter stessa con la
Metrica prescelta. Nel nostro caso, come si nota nell'angolo
in alto a destra, il valore percentuale rilevato è
dell' 88% rispetto alla Metrica ADP. Questo significa che
occorre effettuare un certo lavoro di analisi per individuare
tutte quelle dipendenze che determinano la presenza di cicli
indesiderati.
Come trovarle? Come rimuoverle?
Sempre guardando la figura 3, si può constatare come
a ciascun Package sia stato assegnato un colore differente,
che va dal rosso (Package contenente un elevato numero di
dipendenze indesiderate) al verde (Package sul quale non è
più necessario intervenire). Le sfumature di colore
intermedie tra questi due estremi evidenziano in modo molto
preciso il grado di criticità del Package.
Un'altra importantissima informazione che viene fornita riguarda
le dipendenze su cui occorre lavorare. Esse vengono rappresentate
con una linea tratteggiata in grigio o in rosso, indicando
in quest'ultimo caso che si tratta di dipendenze critiche,
che andrebbero rimosse. A ciascuna di esse inoltre è
associato un coefficiente numerico, detto "peso",
che indica il numero di "referenze" che esistono
tra i Package che "corrono" su quella linea tratteggiata:
in pratica, corrisponde alla quantità di lavoro che
occorre spendere su quella determinata dipendenza per rimuoverla.
Vogliamo avere maggiori informazioni su di essa prima di procedere
alla rimozione? E' sufficiente selezionarla, e ciò
permette di visualizzare la lista completa delle dipendenze
che sussistono tra le classi nei rispettivi Package (fig.
4).
Figura 4 - Visualizzazione Dipendenze
Accanto
a ciascuna dipendenza in rosso, viene posta anche una icona
a forma di lampadina che fornisce una serie di suggerimento
pratico (tramite menu pop-up) sulle azioni da svolgere materialmente
per eliminare una determinata dipendenza. Selezionando una
delle possibilità offerte, viene attuata automaticamente
l'azione prescelta.
Procedendo in tal modo, è possibile pervenire rapidamente
ad una struttura con un valore percentuale sempre più
alto, rispetto alla Metrica scelta (ADP). In figura 5 un esempio
di come Jmeter evolve man mano che si eliminano le dipendenze
indesiderate.
Figura 5 - Evoluzione della struttura di JMeter
Procedendo
per analisi e approssimazioni successive è possibile
pervenire ad un modello che sia completamente aderente alla
Metrica ADP prescelta, che presenti cioè un valore
percentuale pari a 100% (fig. 6).
Figura 6 - Jmeter completamente ADP compliant
E'
importante ricordare che tutte le azioni di modifica effettuate
finora (rimozione dipendenze, spostamento di una classe da
un Package ad un altro e così via) sono state effettuate
sul modello UML che di Jmeter è stato costruito quando
abbiamo iniziato l'analisi. Il passo finale consisterà
nel riflettere tali modifiche sul codice sottostante, reingegnerizzando
completamente l'applicazione di partenza. Procediamo quindi
con una ricapitolazione finale complessiva di tutte le modifiche
sin qui effettuate sul modello (fig.7).
Figura 7 - Analisi riassuntiva delle modifiche effettuate
Ora
esaminiamo se le diverse componenti di codice (classi, interfacce)
su cui effettuare materialmente le modifiche esistono, sono
scrivibili e non sono state nel frattempo modificate da altri
(fig.8).
Figura 8 - Verifica ultima della attuazione del Refactoring
L'ultimo
passo consiste semplicemente nel "cliccare" il bottone
"Refactor!" (in basso a sinistra in figura), per
attuare materialmente sul codice tutte le azioni prescelte
e il gioco è fatto!
Abbiamo reingegnerizzato la struttura attuando il cosiddetto
"Refactoring", quello che viene definito come un
processo di "Miglioramento a livello strutturale dei
componenti di una applicazione".
Conclusioni
Abbiamo percorso passo dopo passo un processo di analisi progressivo,
attraverso Packaging, Layering, Metriche, fino ad arrivare
al Refactoring. Conoscere e saper applicare questi concetti
è qualcosa che va ben oltre la pura ricerca metodologica
di tipo accademico. Nella vita informatica reale, capita di
doversi confrontare tanto con problematiche di sviluppo ex-novo,
quanto con questioni di analisi e manutenzione su applicazioni
preesistenti, quindi con codice scritto da altri.
Ecco quindi che paradigmi quali Packaging, Layering, Metriche
possono rivelarsi vitali, ma occorre anche tenere presente
che questo può non bastare.
Come evidenziato attraverso l'esempio di Jmeter e osservato
anche in rif.[7] infatti, è di estrema importanza poter
disporre anche di adeguati strumenti in grado di venire in
nostro aiuto anche e soprattutto quando non siamo stati noi
i principali attori durante le fasi iniziali di sviluppo.
Strumenti che ci permettano di affrontare la analisi completa
di una applicazione preesistente offrendoci la possibilità
di applicare Metriche basate sui principi del Packaging e
del Layering per poi supportarci durante tutte le fasi del
processo di Refactoring, e in modo quanto più possibile
automatizzato.
Bibliografia
[1] D.Parnas, Carnegie-Mellon University, "On the criteria
to be used in decomposing systems into modules", Communications
of the ACM, Vol. 15, No. 12, 1972, pp. 1053 - 1058 http://www.acm.org/classics/may96/
[2]
J. Gosling, B. Joy, G. Steele, G. Bracha, "The Java Language
Specification", Chapter 7 Packages. Online version at
http://java.sun.com/docs/books/jls/second_edition/html/packages.doc.html#60384
[3]
R.C Martin, "Agile Software Development: Principles,
Patterns, and Practices", Prentice Hall 2002.
[4]
Compuware Lab "Layering", http://javacentral.compuware.com/products/optimaladvisor/documentation/v3.1/2095-21-10-57-59.html
[5]
Apache Community "Jmeter" http://jakarta.apache.org/jmeter/
[6]
Compuware Lab "OptimalAdvisor" - http://javacentral.compuware.com/products/optimaladvisor/
[7]
E. Hautus, "Improving Java Software through Package Structure
Analysis" The 6th IASTED International Conference Software
Engineering and Applications, 2002, online copy at http://www.xs4all.nl/~ehautus/papers/PASTA.pdf
Doriano
Gallozzi è nato a Roma nel 1964 e si è laureato
in Ingegneria Elettronica (indirizzo Informatica) nel 1989.
Da sempre interessato a problematiche di analisi, progettazione
e sviluppo di Sistemi Informativi, ha collaborato per diversi
anni con aziende del settore informatico italiano (gruppo
ENI, gruppo Telecom Italia), dove ha acquisito diverse esperienze
tanto nel campo della progettazione e sviluppo del software
(in ambiente M/F come in ambiente distribuito) quanto nel
campo dei RDBMS (DBA su diversi progetti per clienti finali
quali TELECOM Italia). Da gennaio 2000 collabora con la Divisione
Prevendita di Compuware Italia. La sua attività verte
principalmente sulla piattaforma J2EE e tecnologie correlate,
ma anche su ambiti tecnologici quali l'Enterprise Application
Integration, i Portali Web, gli strumenti di Business Process
Modeling.
|