Sei sulla pagina 1di 368

Luca Vetti Tagliati

lava Best Practice


I migliori consigli
per scrivere codice di qualità

tecniche nuove
Sommario

Introduzione vii
Capitolo 1. Elementi base
Introduzione 1
Obiettivi 2
Direttive 2
1.1 Selezionare nomi significativi per le classi 2
1.2 Selezionare nomi significativi per attributi/variabili e parametri 6
1.3 Selezionare nomi significativi per i metodi 10
1.4 Implementare classi orientate agli oggetti 11
1.5 Porre attenzione alla scrittura dei metodi 14
1.6 Utilizzare correttamente l'ereditarietà 19

Capitolo 2. Programmazione Java


Introduzione 21
Obiettivi 21
Direttive 22
2.1 Investire nello stile 22
2.2 Utilizzare accuratamente le "costanti" 22
2.3 Concludere correttamente i programmi Java 24
2.4 Scrivere correttamente i metodi 26
2.5 Implementare attentamente i metodi "accessori" e "modificatori" (get/set) 32
2.6 Utilizzare con oculatezza la classe java.lang.Runtime 36
2.7 Implementare i metodi Object 36
2.8 Porre attenzione alla chiusura degli stream 40
2.9 Tipi numerici 43
2.10 Selezionare attentamente le collezioni 49
2.11 Lavorare con le date 50
2.12 Problemi con il riutilizzo dei nomi 55

Capitolo 3. Approfondimenti
Introduzione 59
Obiettivi 59
Direttive 60
3.1 I Generics 60
3.2 Static import 71
3.3 Auto-Boxing / Unboxing 73
091 ßU|ßÖO| |3U 3J|JS3AU| 1'9
091 aA|UaJ|a
09i ¿9jezz|||jn 6uj66o| jp |OOi a|en{)
8Si (Df) 6u;66oi eABf aipedv
9si ßu|ßßon!jneABf
ojudujeuojzunj 3 Ejnuiuis
8H ffrßOT
m euojs |p,od up
9fr L |A|U3|qO
sn auo|znpojju|
6u;66o| h '9 0|0)jde)
ffrL E3|is|ßßess3Ui |p jujdisjs jp oßdjduiyie ¡a¡ib|3j juia|qojd pissep \ ajejapjsuo)
OH |U0|Z3»3 3||3p BjniBU B| 3JBJ3p|SU0) 9S
OH ¡UOJZ3333 3||3p BJ|A jp 0|3)J |l 3JU3U1BJU3UB 3JBjn|B/\ S S
8£L |UOjZ3333 3||3p BJ^JOU jp B}||BpOUJ B||B dUOjZUdUB 3JBJ frS
SE L ¡IKHZ3M3 3||3p 3SBq ¡d|l | 3JBZZ|||in UON £S
2£l A||eu[| 0»0|q |¡ ajuaiueyajjOD ajBzzjUifl TS
(¿II ¡uojzajja 3| 3 J B n | | | i n L S

m aA|U3J!0
821. OSnjIB BA|JB|3J BISJ3A0JIU03 B"|
921. pd)|)dip 3 3UI|lUnj ¡UOjZd})]
Sil B¡l|3JBJ39
Sil EACf Uj |U0|Z3W8 d||dp P|ipjeja6 en
Kl e|Joaj|p,od un
Kl |A|ua¡qo
£ZL auo|znpoJiu|
m o r o n a d||dp d u o j ) S d 6 ;p e i ß a i e j i s ' S o | o i ; d e 3
81L 3U0|ZB}U3lU3|dlU|,| 3}U3UJB1U3UB 3JBJU3UJUI0) S'fr
£11 jSSEp 3| 3JU3UJBH3JJ03 SJBJUaUJUJO)
601 3ßB)|DBd !P 0||3A{| |B BUOjZBJUaUimOp B| 3JjJ3SUj |p BJ|||q|SSOd B| 3JBjn|BA
SOI DOQBABf IJU3UIUI03 | 3J3AU3S l fr
201 aUOJZBJUBUinJOp B||3U 3Jj)S3AU| ffr
Í01 aA.ma.na
?0l jAjUdjqo
loi auojznpojiui
ÜU8UJUJ03 I 't7 0|01jdP3

¡JU3JJ03U03 ¡SS333B l|iqiSSOd 3)U3U1BU3JJ0} 3JJJS39 / £


68 S BABf p3pB3Ji)j-|i|nuj !U0jZB}¡|ddv 9'£
8L ||BUO|Z¡pBJJ p3pB3il)J-|J|nUI |UO|ZB3||ddV S'£
9i ¡IjqCMCA ¡)U3UJ0ßjB :sßiBJBA fr£

AI
6.2 Porre attenzione al contenuto del log 162
6.3 Utilizzare correttamente i livelli di log 163
6.4 Valutare l'impatto sulle prestazioni 164
6.5 Implementare un corretto logging delle eccezioni 166

Capitolo 7. Test di unità


Introduzione 169
Obiettivi 170
Un po'di teoria 170
I vantaggi dei test di unità 171
Processi iterativi e incrementali 172
La copertura dei test di unità 173
JUnit 178
JUnit prima della versione 4 178
JUnit versione 4 181
Direttive 182
7.1 Investire nei test di unità 182
7.2 Utilizzare JUnit 185
7.3 Test di unità e mock objects 187
7.4 Utilizzare tool di analisi della copertura 190
7.5 Scrivere test chiari ed efficaci 192

Capitolo 8. Test di integrazione


Introduzione 197
Obiettivi 198
Catalogazione dei test di integrazione 199
Sanity Check e Smoke Test 200
Alcuni tool 200
Direttive 201
8.1 Investire negli use case e test case 201
8.2 Gestire correttamente i vari progetti di test 202
8.3 Gestire correttamente i vari ambienti 204
8.4 Pianificare correttamente i test di integrazione 204
8.5 Verificare gli elementi principali 206
8.6 Test di applicazioni Java EE 207
8.7 Investire sui test della GUI 208
8.8 Test di integrazione delle performance 210

Capitolo 9. Organizzazione di un progetto, build e deploy


Introduzione 213
Obiettivi 214
I tool: filosofia e uso di Ant e Maven 216
Ant 216
Struttura 217
Maven 221
Breve storia di Maven 222
Obiettivi di Maven 223
Caratteristiche principali di Maven 224
Vantaggi di Maven 224
Componenti principali di Maven 225
Archetipo 226
POM 227
I repository di Maven 232
I comandi di Maven 236

Confronto tra Ant e Maven 237


Struttura di progetti medio-grandi 239
Direttive 240
9.1 Utilizzare strumenti moderni per il build 241
9.2 Organizzare correttamente il proprio progetto 242
9.3 Automatizzare il processo di build 243
9.5 Utilizzare i repository Maven 245
9.6 Distinguere i processi build e di personalizzazione 245

Appendice A. JavaDoc
Introduzione 247
L'utility JavaDoc 247

Appendice B. Tag HTML 255


Appendice C. Hashing
Introduzione 261
Collisioni 264
Gestione interna alla tabella 265
Gestione esterna alla tabella 267
Funzioni hash e numeri primi 271
Alcuni esempi di algoritmi di hash 271
Un test informale 277

Appendice D. Multi-threading
Introduzione 283
Obiettivi 284
Nozioni di base 284
Programmazione MT Java originaria 295
Problemi di programmazione MT in Java 321
Innovazioni del JDK 5 per il MT 324

Appendice E. Riferimenti bibliografici 349


Introduzione
Prefazione
La maggior parte dei lettori di questo libro, quasi sicuramente, avrà partecipato allo sviluppo
di progetti software scritti in Java, taluni in ambiti più professionali, altri in ambienti squisita-
mente accademici. Altrettanto probabilmente, durante l'attività di codifica, molti si saranno
trovati alle prese con una serie ricorrente di problemi, dubbi e necessità di comprendere, tra le
diverse soluzioni implementative, quale sia quella in grado di fornire migliori prestazioni, sia in
termini di computazione, sia di occupazione di memoria, sia di migliore qualità del codice, sia
di maggiore coerenza (o, come si usa dire con termine adattato dall'inglese, maggiore "consi-
stenza"), e così via. Infine, un gruppo più limitato di lettori, verosimilmente, si sarà trovato a
dover redigere standard aziendali relativi all'attività di programmazione. Ciò sia al fine di ga-
rantire che il software prodotto dalla specifica azienda rispetti prestabiliti livelli di qualità, sia
per assicurare che questo presenti un prefissato livello di consistenza, indipendentemente dallo
specifico progetto e/o team di sviluppo.
L'idea alla base di questo libro è proprio questa: fornire un compendio di risposte univoche,
operative e concise ai succitati problemi e non solo. Pertanto questo libro si propone come un
vademecum per i programmatori Java: una guida volta a fornire best practice, direttive e quan-
t'altro è necessario sia per migliorare la qualità del software prodotto sia per assicurare un
elevato livello di consistenza, non solo all'interno del medesimo gruppo di lavoro, ma anche tra
diversi team della stessa organizzazione.
La lettura di questo libro, inoltre, dovrebbe aiutare i programmatori meno esperti sia a rive-
dere una serie di errori che spesso finiscono involontariamente con il commettere in modo
sistematico, sia a renderli consapevoli di una serie di inesattezze e di insidie che ogni linguaggio
di programmazione pone, più o meno direttamente.
Questo libro è dunque un utile prontuario sia per l'attività di codifica, sia per quella, spesso
trascurata, di revisione del codice. Come tale, non è assolutamente un'alternativa ai libri di
testo che costituiscono il background fondamentale e irrinunciabile di ciascun programmatore
(cfr. la bibliografia); piuttosto ne è un'utile integrazione, una guida rapida e concisa che ogni
sviluppatore Java dovrebbe tenere a portata di mano.
Come ogni vademecum che si rispetti, anche questo intende fornire una serie di norme,
corredate da una breve spiegazione e da eventuali esempi pratici, senza però dilungarsi eccessi-
vamente sulla rispettiva teoria, per la quale si rimanda alla lettura di testi specifici. Tuttavia,
alcune tematiche particolarmente importanti e complesse, per esempio la programmazione multi-
threading, richiedono una trattazione più approfondita: la sola enunciazione delle linee guida
sicuramente non risulterebbe sufficiente alla comprensione da parte del pubblico ampio cui ci
si rivolge. Pertanto, al fine di mantenere coerente lo stile del libro, si è deciso di includere temi
complessi, ma sicuramente interessanti e utili a determinate categorie di lettori, in specifiche
appendici il cui obiettivo è proprio quello di approfondire le tematiche più "particolari".
Per quanto la programmazione OO e il linguaggio Java siano il fulcro del testo, molta impor-
tanza è attribuita anche a tematiche quali i test di unità e di integrazione, l'organizzazione dei
progetti in termini di file system, il processo di build e di deploy, che svolgono un ruolo fonda-
mentale per la produzione di software di elevata qualità. Sebbene l'elevata qualità del codice sia
una caratteristica fondamentale di ogni sistema, questa non è assolutamente sufficiente per
garantire un'altrettanta elevata qualità dei sistemi prodotti nel loro complesso: parafrasando
una frase accademica fin troppo inflazionata, si tratta della famosa CNMNS (Condizione Neces-
saria Ma Non Sufficiente).
Al momento in cui viene scritto questo libro, non esiste una definizione comune universal-
mente accettata di cosa si intenda con il termine di "qualità" del software, sebbene la quasi
totalità della comunità informatica concordi nell'asserire che realizzare software di elevata qua-
lità è un requisito irrinunciabile per l'ingegneria del software. Quello che è certo, tuttavia, è che
la qualità del codice è la punta dell'iceberg e che quindi è strettamente correlata alla modalità
con cui si svolgono tutta una serie di attività, come il disegno del sistema, la definizione dell'ar-
chitettura, l'analisi dei requisiti e così via. Non è infrequente infatti il rilascio di sistemi tecnica-
mente all'avanguardia, con codice di "elevata qualità", che però semplicemente non risolvono
le necessità degli utenti.
La situazione è decisamente diversa per il software di bassa lega. Non sembri un atteggiaménto
sprezzante definire certo software come "di bassa lega": la sua diffusione è un dato di fatto. In
questi casi, è possibile diagnosticare la cattiva qualità dall'analisi di alcuni semplici sintomi, quali:

• sviluppatori che si oppongono con ogni mezzo a richieste di variazioni del codice;
• tempificazioni esagerate anche a fronte di richieste di variazioni minime;
• sistemi che permangono nella fase di test oltremisura; o peggio, che, una volta messi in
esercizio, frequentemente terminano inaspettatamente, oppure che impiegano un tem-
po prolungato per svolgere singoli servizi;
• sistemi che modificati da una parte, presentano comportamenti anomali in altre parti del
sistema apparentemente non relazionate, e così via.

Produrre codice di qualità, pertanto, è il prerequisito per assicurare l'evoluzione del sistema
e 0 relativo adeguamento al perenne cambiamento dei requisiti, senza dover rinunciare a carat-
teristiche chiavi come la robustezza, la consistenza e le buone prestazioni.
Obiettivi
L'obiettivo fondamentale di questo libro è di fornire una guida operativa, concisa e di rapida
consultazione per l'implementazione di sistemi Java (e non solo) di elevata qualità. Pertanto, il
fine ultimo è presentare succintamente linee guida e best practice ma anche le trappole in cui i
programmatori Java potrebbero cadere, illustrando soluzioni e alternative, tutte in un quadro
molto pragmatico, dove la teoria sia ridotta al minimo indispensabile.
Le direttive presentate si fondano su un impianto logico il cui obiettivo consiste nel far ac-
quisire al software realizzato 8 principali caratteristiche che riportiamo di seguito.

Correttezza
La correttezza è il grado di aderenza del software ai requisiti che lo ispirano, e quindi la capa-
cità del sistema di implementare esattamente quanto inizialmente previsto.

Robustezza
La robustezza è intesa coma capacità del sistema sia di rilevare situazioni anomale di funziona-
mento, sia di eseguire le previste procedure di gestione.

Efficienza
Efficienza è la capacità del sistema di produrre gli effetti desiderati con un utilizzo limitato di
risorse, sia temporali, sia fisiche.

Semplicità
Codice semplice è quello che permette di comprendere immediatamente gli algoritmi alla base
del software e di individuare semplicemente la corrispondenza dei vari elementi con lo spazio
del problema automatizzato.

Leggibilità
La leggibilità va intesa come la capacità di far comprendere allo stesso sviluppatore, e a perso-
ne estranee all'implementazione, il fondamento logico dell'implementazione. Ciò include i
pattern e gli algoritmi utilizzati con eventuali varianti, le scelte operate (incluse le relative giu-
stificazioni) e così via. Questa caratteristica è propedeutica ad altre proprietà fondamentali del
software, quali ad esempio la manutenibilità.

Manutenibilità
La manutenibilità è il grado di difficoltà (o semplicità, se si è ottimisti) di correzione e trasfor-
mazione del sistema al fine di adeguarlo alla perenne variazione dei relativi requisiti.

Trasportabilità
La trasportabilità è intesa come la capacità del sistema di funzionare correttamente anche in
ambiti diversi da quelli originariamente considerati.

Generalizzabilità
La generalizzabilità rappresenta il grado di generalità del software. Più è elevata e, chiaramen-
te, maggiore è la classe di problemi in grado di risolvere e quindi maggiore è il risultato dell'in-
vestimento (ROI, Return On Investment) dell'azienda produttrice.
Molta enfasi è poi stata conferita ad aspetti di importanza fondamentale per la produzione di
sistemi software di qualità come la gestione delle eccezioni, il logging, i test di unità e di integra-
zione e il processo di build. Pertanto, non solo è importante produrre un sistema di elevata
qualità, ma è anche fondamentale utilizzare una serie di best practice, come, per esempio, orga-
nizzare correttamente il progetto e utilizzare un sistema per il build continuo del sistema, al fine
di raggiungere questo risultato nella maniera più efficiente possibile e minimizzando i rischi.
Questo volume non intende illustrare né i principi fondamentali delTObject Oriented, né
insegnare il linguaggio di programmazione Java, per quanto diverse digressioni e riferimenti
facciano capolino qua e là in molti capitoli. Si tratta di tematiche imprescindibili che, in questo
contesto, assumono il ruolo di prerequisiti.

Genesi
L'idea di scrivere questo libro è nata come risposta a una necessità pratica. In molte occasioni
l'autore si è trovato a dover istruire personale junior, a dover fornire delle specifiche circa la
creazione dell'ambiente di sviluppo, a dover eseguire la revisione del codice prodotto da altri
membri del team, e così via. In tutti questi casi, di fronte alle richieste dei collaboratori di fornire
delucidazioni relative alle regole da seguire per produrre software di maggiore qualità, o sempli-
cemente di fronte a richieste di suggerimenti relativi alla documentazione da consultare, era
naturale suggerire una serie di libri, spesso voluminosi (molti dei quali inclusi nella bibliografia).
Questi libri, anche se rappresentano il bagaglio culturale fondamentale di ogni programmatore
Java, non costituivano però una risposta soddisfacente per coloro che, già a conoscenza del
linguaggio Java e delle fondamentali leggi dell'OO, necessitavano di reperire una sorta di
vademecum che, riducendo al minimo l'illustrazione della teoria, proponesse una serie di rapide
linee guide, supportate da esempi pratici, atte a migliorare la qualità del software prodotto.
Pertanto, l'idea base di questo libro è proprio quella di dar vita ad una sorta di vademecum di
carattere operativo per i programmatori Java, volto a fornire best practice, direttive e quant'altro,
al fine di supportare il miglioramento della qualità del software, senza dover necessariamente
ripetere tutta la teoria di base. L'impronta operativa del testo si riscontra sia dagli innumerevoli
esempi riportati, sia dal dominio di appartenenza: la maggior parte degli esempi, infatti, è stata
prelevata direttamente dai sorgenti delJDK 1.5. Questa decisione ha permesso sia di minimizza-
re presentazione e spiegazione dei vari esempi (le principali API Java, infatti, dovrebbero appar-
tenere al dominio di conoscenza della quasi totalità dei lettori), sia di fornire esempi reali.
Il libro si basa sulla versione JDK 1.5.

A chi è rivolto
Questo libro è rivolto alla comunità dei programmatori Java. In particolare, si tratta di un
supporto per tutti coloro che sviluppano quotidianamente codice scritto in Java, indipenden-
temente dalla loro esperienza. Questo libro, inoltre, è una guida per tutti coloro che hanno il
compito di revisionare il codice prodotto nella propria azienda, sia al fine di assicurare il rispet-
to dello standard qualitativo aziendale, sia per fornire un fondamentale feedback agli autori del
codice stesso. Infine, questo libro dovrebbe risultare molto utile a tutti coloro che sono chia-
mati a scrivere gli standard qualitativi aziendali.
Struttura
La struttura di questo libro è organizzata nei capitoli seguenti.

Presentazione
Questa sezione, come suggerisce il nome, è dedicata alla presentazione del libro e pertanto il
lettore ne ha già letto circa la metà. In particolare, gli argomenti trattati in questa sezione sono:
gli obiettivi, il potenziale pubblico dei lettori, la struttura, e, come per qualsiasi libro che si
rispetti, l'immancabile sezione dedicata ai ringraziamenti.

Capitolo 1. Elementi base


Questo capitolo rappresenta l'avvio della trattazione e pertanto presenta una serie di linee
guida di carattere generale relative alla programmazione. La maggior parte di queste regole
hanno una validità indipendente dagli specifici linguaggi di programmazione e dal particolare
paradigma, sebbene particolare attenzione sia rivolta ai linguaggi basati sul paradigma OO e a
Java in particolare.
Le direttive presenti in questo capitolo hanno un carattere generale e pertanto introducono
argomenti relativi a come implementare classi ben disegnate, alla scelta dei nomi per le varie
entità di un programma, alla strutturazione dei metodi, all'implementazione di classi a elevata
coesione e minimo accoppiamento, e così via.

Capitolo 2. Programmazione Java


In questo capitolo si assiste alla transizione verso tematiche a maggiore carattere tecnico. Ciò si
riflette anche sul linguaggio di programmazione: Java assume il ruolo di fulcro, sebbene molte
regole continuino ad avere una valenza molto generale. In questo capitolo si affrontano tematiche
come l'utilizzo delle aree static, la verifica dei parametri, l'utilizzo delle interfacce, i down-
casting, problemi spessi ignorati relativi al trattamento di numeri reali, e così via.

Capitolo 3. Approfondimenti
Questo capitolo è dedicato ad argomenti di livello tecnico molto avanzato come il multi-tbreading
e a specifici package/costrutti introdotti con il JDK 1.5, quali per esempio i generics e il nuovo
package della concorrenza. Gli argomenti proposti, gioco forza, sono completamente incentra-
ti su Java. In questo capitolo, Java sale in cattedra per assumere il ruolo centrale che gli spetta.
Molti degli argomenti trattati presentano un elevato livello di difficoltà. E il caso del multi-
threading per il quale è stata realizzata un'apposita appendice che probabilmente vale la pena
leggere prima di avventurarsi in questo capitolo.

Capitolo 4. I commenti
In questo capitolo sono presentate una serie di direttive atte a migliorare l'efficacia dei com-
menti del codice. Sebbene la quasi totalità della comunità informatica sia concorde nel ricono-
scere l'importanza di un codice ben documentato e del fatto che i commenti siano un requisito
fondamentale per la qualità del software, ci si imbatte spesso in applicazioni mal commentate
0 addirittura non commentate in alcun modo. Il problema fondamentale è che raramente una
determinata porzione di codice, per tutto il suo ciclo di vita, sarà mantenuta da chi l'ha scritta
per primo. Molto più frequente è il caso che diverse persone si avvicendino alla sua manuten-
zione. Pertanto, codici che presentano problemi per quanto riguarda chiarezza, comprensibilità
e facilità di manutenzione corrono il serio rischio di essere buttati via e riscritti. Cosa che,
ovviamente, non dovrebbe generare il compiacimento di nessun autore di codice.

Capitolo 5. Strategia di gestione delle eccezioni


Indipendentemente dal livello di qualità del codice prodotto, le eccezioni si verificano: e questo
è un dato di fatto. Ciò nonostante, la loro strategia di gestione può fare la differenza tra un
sistema robusto e uno instabile, fragile. Pertanto, è necessario che tutti i membri del team di
sviluppo concordino e applichino una strategia efficace e consistente di gestione delle eccezioni.
Obiettivo di questo capitolo è proprio questo: fornire una strategia consolidata ed efficace
per la gestione delle eccezioni.

Capitolo 6. Il logging
Una strategia di logging ben congeniata è un altro elemento di importanza fondamentale per
garantire una elevata qualità ai sistemi. Spesso anche questa attività è trascurata e/o realizzata
di fretta nei giorni precedenti al rilascio del sistema e/o eseguita in maniera casuale senza segui-
re specifiche linee guide. Gli inconvenienti generati da questi approcci emergono in tutta la
loro drammaticità quando, una volta installato il sistema in ambiente UAT o, peggio ancora, in
produzione, si ha la necessità di correggere i primi malfunzionamenti. Solo a questo punto,
venendo meno la possibilità di poter utilizzare sofisticati strumenti di debug, ci si rende conto
di quanto sia difficile analizzare anomalie senza un logging chiaro e consistente...
Obiettivo di questo capitolo è fornire un insieme di best practice e linee guide affinché la
strategia di logging sia presente nel sistema fin dalle primissime fasi e affinché questa si evolva
di pari passo con il sistema stesso.

Capitolo 7. Test di unità


Una delle poche aree in cui tutti i processi di sviluppo del software concordano è l'importanza
dei test. In questo capitolo, in particolare, si focalizza l'attenzione sui test di unità {unti test)
implementati per mezzo dell'ormai famoso framework JUnit.
Questi test, come suggerisce il nome, sono disegnati per verificare il corretto funzionamento
di singole unità di codice, ossia classi/componenti, considerate isolatamente.
In modo analogo ai capitoli precedenti, il capitolo contiene una serie di linee guida su come
utilizzare efficacemente questo strumento e su come redigere efficaci test di unità.

Capitolo 8. Test di integrazione


1 test di integrazione (integration test) rappresentano un'altra importantissima fase di test an-
cora a carico del team di sviluppo. Mentre i test di unità servono per verificare che tutti i
moduli, considerati singolarmente, presentino il funzionamento atteso, in questa fase si verifica
che anche una volta assemblati tra loro in unità più complesse fino a raggiungere l'intero siste-
ma, questi moduli continuino a esibire il funzionamento previsto.
In questo capitolo si espongono una serie di consigli, relativi alla realizzazione dei test di
integrazione, ai tool disponibili, alle strategia da utilizzare e così via.

Capitolo 9. Organizzazione di un progetto,


build e deploy
In questo capitolo l'attenzione è focalizzata su due argomenti molto rilevanti: struttura del
progetto dal punto di vista del file system e processi di build e deploy. Sebbene coerentemente
con l'impostazione di questo libro, la presentazione dei tool utilizzabili per l'automazione del
processo di build non sia l'obiettivo principale del testo, tali sono l'importanza e il successo di
Ant e Maven che almeno le nozioni base vengono illustrate dettagliatamente. Sebbene per
molte persone il processo di build equivalga a un click del mouse su un apposito bottone del
proprio ambiente di sviluppo, in questo capitolo si vedrà quali rischi possono derivare da que-
sto approccio semplicistico e quali strategie adottare per far in modo che questi elementi con-
tribuiscano effettivamente al successo dell'intero progetto.

Appendice A. JavaDoc
Questa appendice è dedicata alla presentazione dell'applicazione di utilità Java per la produ-
zione automatica della documentazione: JavaDoc. In particolare, molta attenzione è assegnata
ai diversi tag.

Appendice B. Tag HTML


Questa appendice è dedicata alla presentazione di una serie di tag HTML estremamente utili
per la scrittura di commenti doc.

Appendice C. Hashing
Questa terza appendice è dedicata a un concetto molto interessante: Vhashing. Si tratta di un
teoria impiegata in diversi ambiti dell'informatica: dalla crittografia alle strutture dati.
In questa appendice, però, l'attenzione è focalizzata sul secondo aspetto. Ciò perché i pro-
grammatori Java, sia direttamente (estensione del metodo hashCode della classe java.lang.Object),
sia indirettamente (utilizzo delle collezioni java.util.Hashtable e java.util.HashMap), utilizzano fre-
quentemente il concetto di hashing con una frustrazione abbastanza ricorrente: interpellando
diversi programmatori, infatti, è facile riscontrare come questo concetto sia spesso avvolto da
una "sacra inibizione".

Appendice D. Multi-threading
Questa corposa appendice è dedicata alla programmazione multi-threading (MT). In particola-
re, si affrontano sia i concetti base della programmazione concorrente in MT, sia le tematiche
più avanzate legate al linguaggio Java. Poiché il MT è una tecnica di programmazione, il lin-
guaggio selezionato finisce gioco forza per offrire una serie di opportunità peculiari e porre
immancabili vincoli la cui intima comprensione è propedeutica alla produzione di sistemi MT
efficaci e che funzionano.
Le principali tematiche affrontate sono le tipiche problematiche della programmazione MT
in Java, i costrutti fondamentali dedicati alla programmazione concorrente e anche 0 nuovo
package Java specificamente dedicato alla concorrenza: java.util.concurrent. Riteniamo che que-
sta appendice rappresenti una risorsa molto utile, poiché riassume in un unico testo aspetti
molteplici e aggiornati.

Appendice E. Riferimenti bibliografici


Una breve ma significativa raccolta con le indicazioni di libri, articoli e siti di riferimento.

Ringraziamenti
Il primo ringraziamento d'obbligo nonché la personale riconoscenza dell'autore va agli altri due
membri dell'ormai consolidata "banda dei tre", che, come al solito, lo hanno assistito e consiglia-
to nel faticoso onere di scrivere un altro libro... Sarà l'ultimo... Fino a quando non si inizierà il
prossimo! In particolare si ringraziano gli amici Roberto Virgili, team leader/project manager,
nonché architetto e sviluppatore di sistemi distribuiti, e Antonio Rotondi, project manager di
notevole caratura ed esperienza tecnica, che negli ultimi anni si è speso per la messa in opera di
progetti globali per banche di investimento di grandi dimensioni. Tecnici informatici di profonda
levatura ed esperienza, con il vizio di realizzare sistemi informatici che funzionano veramente.
Altri ringraziamenti spettano a Giovanni Puliti di Imola Informatica (gpuliti@mokabyte.it, Java
enthusiast, project manager esperto di Java e tecnologie annesse, autore, nonché creatore e
direttore della rivista web www.mokabyte.it) che si è dimostrato, da subito, entusiasta all'idea di
questo libro. Un particolare ringraziamento va poi a Francesco Saliola che, come al solito, ha
curato la redazione e l'impaginazione del libro: ormai pensa in Java e scrive log JIRA per richie-
dere correzioni/miglioramenti.
Sentiti ringraziamenti spettano di diritto alla casa editrice Tecniche Nuove per il coraggio di
continuare nella pubblicazione di libri tecnici scritti nella meravigliosa lingua di Dante, e per la
disponibilità e la comprensione sempre dimostrate.
Dulcis in fundo, la gratitudine dell'autore va ai familiari, a Vera per la tanta pazienza dimo-
strata e a tutti coloro che sono stati presenti nei momenti di bisogno.

Breve biografia dell'autore


Luca Vetti Tagliati è nato a Roma il 03/12/1972. Ha studiato IT da sempre. Ha iniziato a
lavorare professionalmente nel 1991 occupandosi di sistemi firmware e assembler per micro-
processori della famiglia iAPx286. Nel 1994/95 ha intrapreso il lungo cammino nel mondo OO
grazie al meraviglioso linguaggio C++. Nel 1996 si è trovato catapultato nel mondo Java.
Negli ultimi anni ha applicato la progettazione/programmazione Component Based e SOA
in settori che variano dal mondo delle smart card (collaborazione con la Datacard,
www.datacard.com) a quello del trading bancario (www.hsbc.com, www.ubs.co.uk e www.lehman.com).
Attualmente lavora, di giorno, come Senior Architect/Development Manager presso la sede
londinese della banca Lehman Brothers e, nottetempo, si occupa di ricerca di processi di svi-
luppo del software alla Birkbeck University of London dove sta conseguendo un PhD. Ha
scritto il libro UML e l'ingegneria del software, pubblicato da Tecniche Nuove, e ha collaborato
attivamente con esperti del calibro di John Daniels ([UMLCOM]) e Frank Armour
([ADVAUC]).
È rintracciabile presso l'indirizzo lvettitagliati@mokabyte.it

Convenzioni grafiche
Questo carattere senza grazie è utilizzato per i termini appartenenti al linguaggio di programma-
zione, come ad esempio class, HashTable, e così via. Pertanto una loro non corretta digitazione
genera un errore di compilazione

Il carattere più piccolo, di corpo ridotto, in paragrafi rientranti come questo è utilizzato per parti
di testo introduttive o note a margine la cui lettura e comprensione non sono strettamente necessarie
per l'apprendimento di quanto riportato successivamente.

Non è infrequente il caso in cui per illustrare al meglio una best practice sia più facile partire
da una worst practice. Spesso, enfatizzando gli errori, si riesce a fornire un sistema più immedia-
to e intuitivo per comprendere approfonditamente le motivazioni intrinseche di una serie di
suggerimenti, idee e regole. Inoltre, spesso è necessario evidenziare alcune trappole e insidie
per evitare che queste finiscano per avere la meglio sugli sviluppatori.

L'icona presentata a fianco, con il chicco di caffè Java "bollito", serve proprio a
questo: enfatizzare chiaramente porzioni di codice utilizzate per mostrare errori,
problemi e trappole, ossia "worst practice". Come tali, ovviamente, non devono
assolutamente essere prese ad esempio positivo.
Ed è poesia
La realizzazione di codice di programmazione può apparire, a chi non vi si addentri, come un
mondo distante alieno, un deserto privo di umanità.
Non è così. Nella volta celeste dei byte luminosi c'è spazio anche per ben altro, anche per
momenti di riflessione, di arte e di poesia.
Di seguito sono riportate tre brevi composizioni inedite di Leonello Tatti (leonellotatti@libero.it),
affermato poeta, che il buon Dio mi ha donato come zio.

Lampi
Distanti le mura fanno da cornice ad uno sfondo sublime
e le luci che vi si riflettono sembrano apparire fugaci
per poi diventare aggressive e senza confini.
Attimi di pura fantasia, immagine infinita, essenza
stravagante, misura di tempo.
Oltre il pensiero la figura emerge e si insinua timida fra le pieghe
di sommessi tratti e successive definizioni
Fino ad interrompere le visioni.
Ombra sottile, generosa amante, pensiero invadente, struttura
appagante, sentiero nascosto, vita che traspare.

Evasione
Conosco bene le mura del mio paradiso...
mi destano... e fra di esse ogni giorno riscopro il mio sole la mia luna.
Solo ad esse posso parlare se voglio essere ascoltato.
Non c'è nulla al di là del mio paradiso... solo, la mia illusione
di poter toccare respirando, un'aria a me nuova
pensando, a come potrebbe essere la mia vita
se solo ci fosse al posto di questa apatia,
di questa solitudine, un uragano dentro di me
che mi scuotesse e mi portasse via.
Ma io non ho altro che questo paradiso.

Rifugio sicuro
Rifugio sicuro più non sei...
quell'abete candido ed inerme che dinnanzi
la mia strada trovai, ora vibra d'immortale possenza
e se pur tanto è il dominio suo nel tollerare
qualsivoglia gesto, a nulla può valere quella linfatica scintilla
di cui donata la mia essenza si priva.
Rifugio sicuro più non sei...
quel mio attingere dannato, volle far sì che impotente subissi
e quell'abete candido ed inerme, tu scordato hai.
Capitolo
Elementi base
Introduzione
Questo capitolo è dedicato all'illustrazione di un insieme iniziale di linee guida di carattere
generale relativo alla programmazione. L'obiettivo fondamentale è favorire la realizzazione di
programmi di migliore qualità. In particolare, in questo primo capitolo l'attenzione è principal-
mente focalizzata sulla produzione di codici più facilmente leggibili e quindi più facilmente
comprensibili, mantenibili e riusabili. L'enfasi, pertanto, è quasi interamente conferita a carat-
teristiche "stilistiche" della programmazione, mentre considerazioni relative al miglioramento
delle performance, a un più efficiente utilizzo della memoria e al miglioramento della
trasportabilità sono rimandate ai capitoli successivi. Tuttavia, compaiono anche tematiche più
complesse, come per esempio l'intelligente ricorso alla relazione di ereditarietà la riduzione
dell'accoppiamento tra classi.
Benché questa trattazione sia esplicitamente orientata a linguaggi di programmazione basati
sul paradigma Object Oriented (OO), e in particolare al linguaggio Java, le direttive proposte si
prestano a essere facilmente adattate ad altri linguaggi di programmazione anche non necessa-
riamente basati sul paradigma OO.
Considerata la caratteristica di generalità di questo primo capitolo, vi compaiono sia linee
guida relative a concetti basilari come le dimensioni ottimali di classi e dei metodi, ad una miglio-
re selezione dei nomi dei vari elementi del linguaggio di programmazione (attributi, metodi,
interfacce e classi), sia altre di carattere più squisitamente OO, come coesione, accoppiamento,
etc. Benché queste ultime regole presentino un livello concettuale molto diverso da quelle più
immediate presenti nel primo gruppo, si è comunque deciso di illustrarle in quanto rappresenta-
no concetti fondamentali della programmazione, frequentemente nominati e discussi.
Pertanto, benché l'obiettivo principale del libro sia quello di presentare aspetti molto pratici
riducendo al minimo la teoria, non è infrequente il caso in cui la trattazione si orienti verso
tematiche più astratte. Ciò è necessario sia per evitare un'eccessiva costrizione della trattazione
che finirebbe per porre un eccessivo limite, sia per fornire indicazioni a tutti colori che intenda-
no approfondire tematiche meno pratiche a maggior grado di astrazione.
La piena comprensione di quanto riportato in questo capitolo non richiede particolari
prerequisiti, sebbene una minima esperienza di programmazione Java sia sicuramente di aiuto.
Per quanto la maggior parte delle direttive presentate in questo capitolo possano, a tratti,
sembrare basilari e quasi scontate, nella pratica lavorativa esse vengono non di rado trascurate.

Obiettivi
L'obiettivo principale di questo capitolo consiste nel supportare la produzione di codice più
facilmente comprensibile. Si tratta di una caratteristica fondamentale del software ed è pre-
requisito irrinunciabile per un'altra caratteristica fondamentale: la manutenibilità. Quest'ulti-
ma ha assunto un ruolo fondamentale nei moderni processi di sviluppo del software essenzial-
mente per due motivi. In primo luogo perché è ormai universalmente accettato il fatto che i
requisiti del sistema siano un'entità dinamica e quindi in continua evoluzione. Ciò rende im-
possibile e/o non conveniente "congelare i requisiti". La logica conseguenza è che ogni software
di successo debba essere continuamente rivisto al fine di adeguarlo al perenne cambiamento
dei requisiti. In secondo luogo perché la quasi totalità dei moderni processi di sviluppo del
software include approcci di carattere iterativo e incrementale al fine di controllare più effica-
cemente i rischi progettuali. Ciò fa sì che la versione "finale" del sistema sia ottenuta attraverso
una serie di iterazioni, ognuna delle quali aggiunge un determinato incremento alla versione
precedente che, tipicamente, implica l'aggiornamento del codice prodotto precedentemente.
In ogni modo, produrre codici facilmente leggibili è importante per i seguenti motivi:

• permette di capire più velocemente, e spesso in maniera più completa, il problema che il
codice risolve: quindi semplifica l'attività di test e di individuazione di eventuali errori;
• semplifica l'utilizzo di approcci iterativi e incrementali che spesso richiedono, a persone
diverse, di modificare parti di programma realizzate in precedenza;
• facilita la comunicazione in termini degli algoritmi selezionati, delle scelte operate, e
così via: ciò è particolarmente importante sia per l'attività di revisione del codice, sia in
contesti di progetti di media-grande difficoltà;
• semplifica l'adeguamento del codice al perenne cambiamento dei requisiti.

Daremo pertanto di seguito delle indicazioni, delle direttive, che in questo caso saranno
numerate per poter fare riferimento ad esse nel corso di tutto il libro.

Direttive
1.1 Selezionare nomi significativi per le classi
La selezione dei nomi delle classi dovrebbe avvenire principalmente durante la fase di disegno
del sistema. Tuttavia, in alcuni contesti molto ben limitati e definiti, come per esempio circoscritte
investigazioni (pratica c o m u n e m e n t e nota con il nome di speak programmimi, può risultare
opportuno procedere con la codifica a partire da un documento di disegno molto essenziale. In
scenari di questo tipo, il programmatore si trova nella situazione di dover disegnare parte del
software direttamente codificando. Inoltre, anche in scenari più formali, l'attività di disegno non
dovrebbe mai giungere a un eccessivo livello di dettaglio. Ciò, probabilmente, risulterebbe in un
cattivo utilizzo del tempo a disposizione e c o n d u r r e b b e alla mortificazione del team di sviluppo.
Comunque sia, è abbastanza frequente la creazione di classi, inizialmente non previste, direttamente
nella fase di sviluppo e vi è quindi la necessità di includere tale serie di regole in questo volume
fortemente orientato alla programmazione.

1.1.1 Selezionare nomi (relativamente) brevi


La prima regola per la selezione di un nome opportuno da assegnare a una classe consiste nello
scegliere un nome breve ma significativo. Si faccia attenzione che in questo contesto con il
termine "nome" si intende proprio l'elemento grammaticale: la particella linguistica che indica
esseri viventi, oggetti, idee, fatti o sentimenti. Pertanto, è necessario scegliere una stringa breve,
in grado di indicare le responsabilità principali della classe. Tipicamente, i nomi più efficaci
sono quelli attinti dal dominio che il programma intende automatizzare.

Alcuni testi, non eccessivamente moderni, indicano in 15 lettere il valore ideale per la lunghezza
dei nomi di classi. Probabilmente, si tratta di un'indicazione eccessivamente restrittiva visti i
servizi esposti dai moderni I D E (Integrated Development Environment, Ambienti di Sviluppo
Integrato), ma che c o m u n q u e fornisce un'idea dell'ordine di grandezza.

Alcuni esempi di validi nomi di classe sono riportati nella tabella 1.1.

C o m e si può notare, i programmi dovrebbero essere scritti utilizzando la lingua Inglese. Q u e s t o


per garantirne la massima audience possibile e per evitare brutture del tipo getEta, g e t A n n o N a s c i t a .
S e c o n d o le direttive I j a v a C C ] il n o m e delle classi deve essere scritto con la prima lettera in
maiuscolo e le rimanenti in minuscolo. Qualora, il nome della classe sia composto da più termini,
la prima lettera di ciascuna parola c o m p o n e n t e deve essere scritta in maiuscolo.

1.1.2 Valutare attentamente il ricorso alle abbreviazioni


Qualora la selezione di un nome breve sia ottenibile solo introducendo improbabili abbrevia-
zioni, è consigliato rilassare la regola sul contenimento della lunghezza di un nome e quindi

Dominio Esempi
A g e n z i a di viaggi Flight, Travel, Booking, Customer, Itinerary, Geographicllnit, Nation, City,
TimeZone, ShoppingCart, etc.
I s t i t u z i o n e di i n v e s t i m e n t o Currency, Price, StreamPrice, Quote Trade, Counterparty, Settlement,
HolidayCalendar, Transfer, Instrument, etc.
Biblioteca Book, Paper, Article, Author, Picture, Editor, etc.
Università Lecture, Topic, Teacher, Student, Thesis, Department, etc.

Tabella 1.1- Esempi di nomi di classi derivanti dal dominio di riferimento.


abbandonarsi a nomi più lunghi. In alcune sporadiche occasioni, tuttavia, il ricorso ad abbre-
viazioni è assolutamente necessario per evitare di introdurre nomi troppo lunghi. In questo
caso, occorre utilizzare abbreviazioni in modo oculato, e soprattutto coerente, al fine di evitare
confusione.
Per esempio, per il calcolo del fattore di rischio di diversi trade è necessario consultare spe-
cifici valori di rischio denominati Future Fluctuatìon Risk Factors. Questi elementi sono indicati
con l'acronimo FFR dagli stessi operatori del business. Quindi, per la relativa implementazione,
è possibile e conveniente dar luogo a classi denominate FFR: FfrVO, FfrBS, etc.

1.1.3 Porre attenzione all'utilizzo di "verbi" per i nomi di classi


I nomi delle classi dovrebbero essere costituiti da nomi e non da verbi. Questo perché le classi
dovrebbero rappresentare entità, oggetti del dominio e non azioni. Pertanto, ad eccezione di
alcuni casi molto limitati, qualora si abbia la necessità di denominare una classe con un verbo,
bisognerebbe interrogarsi circa le responsabilità della classe, se abbia veramente senso creare
una classe, e su quali siano proprietà le fondamentali, come coesione e accoppiamento. Nel
caso in cui questo controllo abbia esito positivo, è importante ricordare che quasi sempre è
possibile passare da un verbo ad un nome. Come esempio si consideri l'applicazione del pattern
Command.

Il Command, molto brevemente, è un pattern che permette di richiedere a un oggetto l'esecuzione


di determinate azioni, appositamente incapsulate in istanze di una predefinita classe, senza che
l'istanza richiedente sia al corrente dei dettagli dell'operazione da eseguire né del concreto oggetto
che la eseguirà. L'elemento fondamentale di questo pattern è la classe astratta, tipicamente
denominata Command, che astrae il comportamento comune di tutte le classi che implementano
comandi concreti. Queste, frequentemente, sono denominate con il nome del comando, che
spesso è un verbo, come per esempio Save, Read, Update, etc. Anche in questo contesto, tuttavia,
il nome può essere, frequentemente, ottenuto da una composizione, come per esempio FileSaver,
FileReader, etc. Ed ecco che il nome della classe torna a essere un nome.

1.1.4 Non utilizzare i nomi delle classi delle librerie Java


In alcune occasioni è possibile trovarsi di fronte a classi predefinite che non implementino
esattamente i requisiti richiesti o che necessitano di essere ulteriormente estese per potervi
aggiungere l'ulteriore comportamento necessario. Alcuni sviluppatori risolvono questo scena-
rio implementando delle classi con lo stesso nome di quelle base. Questa stessa tecnica, ahimè,
è stata utilizzata dai disegnatori del linguaggio Java in diversi casi come con le classi java.Util.Date
e java.sql.Date. Questa è una pratica assolutamente sconsigliata, soprattutto qualora si intenda
riutilizzare il nome di classi appartenenti alla piattaforma Java (come per esempio quello delle
classi del package java.lang." automaticamente importato in tutte le classi).
Per quanto Java fornisca una serie di meccanismi per poter distinguere le varie classi grazie al
fully qualìfied name, il codice divine confuso, diviene sempre necessario o opportuno ripetere il
nome completo della classe, il codice è meno elegante, poco leggibile, etc.
Pertanto è sempre consigliabile evitare di riusare il nome delle classi, soprattutto di quelle
appartenenti alla piattaforma Java. E sempre possibile e opportuno definirsi nomi simili ma
non uguali, come per esempio MySystem, BasicString, Enhancedlnteger, etc.
1.1.5 Aggiungere al nome della classe un suffisso
relativo allo strato di appartenenza
Sebbene al momento in cui viene scritto questo libro, non ci sia un accordo nella comunità
informatica circa il significato della parola "architettura", una sua caratteristica su cui è possi-
bile individuare un accordo unanime è che le architetture dovrebbero essere multi-strato (mul-
ti-layered o multi-tiered)-, organizzate in una serie di strati (layer o tier) adiacenti e comunicanti.
Pertanto, in queste architetture è frequente che diverse versioni di una stessa classe esistano in
diversi strati con diverse responsabilità. In questi circostanze è conveniente aggiungere al nome
delle classi, in maniera coerente, opportuni suffissi atti a identificarne lo strato di appartenenza
(tabella 1.2).
Altri utili suffissi sono VO e DTO (TrolleyVO, TrolleyDTO) relativi, rispettivamente, a Value Object
e Data Transfer Object; questi non appartengono necessariamente a un solo strato ma, anzi,
vengono tipicamente utilizzati per trasportare informazioni tra strati differenti.
Questa convenzione offre il grande vantaggio di poter derivare molte informazioni relative a
una specifica entità già dalla semplice lettura del nome. Qualora si decida di utilizzare una
simile convenzione, tuttavia, è di fondamentale importanza utilizzarla con tassativa coerenza
onde evitare inutili confusioni.

1.1.6 Valutare l'utilizzo di appositi prefissi per evidenziare


interfacce e classi astratte
Per quanto non si tratti di una strategia utilizzata dalla Sun, spesso risulta conveniente aggiun-
gere, al nome delle classi, un prefisso, composto da un solo carattere, per evidenziare se la
classe sia astratta o concreta e si tratti o meno di un'interfaccia. Pertanto la convenzione consi-
ste nell'utilizzare:

• prefisso I per le interfacce. Per esempio IRepository, IConfigurationManager, etc.


• prefisso A per denotare classi astratte. Per esempio AltemsRepository, ADoubleLinkedUst.

Il vantaggio offerto da questa nomenclatura consiste nella possibilità di risalire ad alcune


informazioni utili circa una determinata classe o interfaccia già dalla sola lettura del nome.
Qualora si decidesse di utilizzare questa convenzione, è fondamentale applicarla consisten-
temente altrimenti si correrebbe il rischio di generare inutile confusione. Per esempio, la lettu-
ra di un nome di classe non prefissato dalla lettera I o A, porterebbe alla legittima conclusione
che si tratti di una classe concreta e quindi il programmatore sarebbe portato a trattarla di

Strato Dizione inglese Esempi


strato di presentazione presentation layer TrolleyPage, TrolleyHandler
strato di business Business Service layer TrolleyBS
strato di business object Business Object layer TrolleyBO
strato di integrazione integration layer TrolleyDAO

Tabella 1.2 - Strati di una classica architettura multi-tiered.


conseguenza, mentre, invece potrebbe trattarsi di una classe astratta o di un'interfaccia deno-
minata in maniera incorretta.

1.1.7 Utilizzare il suffisso "Exception" per indicare le classi eccezione


L'utilizzo del termine Exception per indicare classi atte a incapsulare eccezione è un'utile con-
venzione utilizzata della Sun che dovrebbe essere sempre rispettata. Per esempio:
NulIPointerException, HlegalArgumentException, etc.

1.2 Selezionare nomi significativi


per attributi/variabili e parametri
Variabili e attributi sono elementi fondamentali della programmazione, tanto che Dijkstra sosteneva
che "una volta che un programmatore ha compreso l'utilizzo delle variabili, ha compreso l'essenza
della programmazione". Gli attributi, poi, sono a tutti gli effetti variabili incapsulate in oggetti,
utilizzate per mantenere lo stato degli stessi. Pertanto, una corretta ed efficace definizione dei
loro nomi costituisce una condizione irrinunciabile per ottenere software di qualità.

1.2.1 Selezionare nomi (relativamente) brevi


Coerentemente a quanto specificato per i nomi delle classi, anche i nomi di attributi/variabili e
parametri devono essere mnemonici e brevi (anche in questo caso diverse letterature, non ec-
cessivamente moderne, identificano in 15 lettere il valore della lunghezza massima del nome di
un attributo). Coerentemente con l'analoga direttiva valida per le classi, anche per i nomi di
attributi, variabili e parametri è necessario cercare di utilizzare sostantivi (nomi). Inoltre, anche
per gli attributi, quando possibile, è utile ricorre a nomi attinti dal dominio di riferimento.
Per esempio, una classe Currency potrebbe disporre dei seguenti attributi: mainllnitName (per
e s e m p i o Euro), secondaryllnitName (per e s e m p i o Cent), symbol, currencySign, decimalPositions, etc.
Se usassimo l'italiano, una classe Libro potrebbe disporre dei seguenti attributi: titolo, autori,
n u m e r o P a g i n e , caseEditrici, prezzoConsigliato, etc.

Da notare che mentre gli attributi come titolo. numeroPagine e prezzoConsigliato, sono veri e
propri attributi, caseEditrici, autori sono relazioni con altri oggetti. Queste distinzioni però, sebbene
siano fondamentali in termini di disegno, lo sono molto meno in termini implementativi.

Per quanto concerne il nome delle variabili, esso dovrebbe ricordarne l'utilizzo. Alcuni esem-
p i s o n o : b o o l e a n s k i p W h i t e S p a c e , int l e n g t h , c h a r c u r r e n t C h a r , b y t e [ ] b u f f e r , e t c .
Una convenzione efficace per la dichiarazione dei parametri, molto utile per i metodi costruttori
e modificatori, consiste nell'aggiungere al nome un prefisso formato dall'articolo indeterminati-
vo. Ciò evita eventuali conflitti con gli attributi di classe, che comunque in Java si risolvono
inserendo la parola chiave this per identificare gli attributi di classe. Ecco un esempio di un
costruttore della classe Byte. (L'implementazione Sun utilizza la convenzione this.value = value).

! "

' C o n s t r u c t s a newly allocateti < c o d e > B y t e < / c o d e > object that


* represents the specifled < c o d e > b y t e < / c o d e > value.
* @param value the value to be represented by the
<code>Byte</code>.
7
public Byte(byte aValue) I
value = aValue;
I

Un'altra tecnica molto interessante consiste nell'aggiungere al nome della proprietà il suffis-
so new. Per esempio si consideri il metodo riportato di seguito atto a impostare il limite del
Buffer, nella classe java.nio.Buffer. Ecco il metodo limit della classe java.nio.Buffer.

public linai Buffer limit(inl newLimit) I


it ((newLimit > capacity) || (newLimit < 0))
throw new HlegalArgumentException();

limit = newLimit;
if (position > limit) position = limit;
if (mark > limit) mark = -1;

return this;
)

1.2.2. Valutare attentamente il ricorso alle abbreviazioni


I nomi di variabili spesso richiedono la combinazione di due o più termini. Le abbreviazioni, in
linea di principio, tendono a ridurre il grado di leggibilità del codice. Tuttavia, qualora il nome
di un attribuito tenda a divenire eccessivamente lungo, è consigliabile valutare opportune ab-
breviazioni: l'importante però è che queste siano comprensibili e soprattutto utilizzate coeren-
temente. Per esempio:

/" ' If the next character is a line feed, skip it ' /


privale boolean skipLF = false;

/ * * default char buffer size */


private sialic ini defaultCharBufSize = 8192;

1.2.3 Considerare l'utilizzo delle lettere i, j, k per le variabili di ciclo


Le lettere i, j, k, grazie anche ad un retaggio matematico, risultano ottimi nomi per le variabili che
regolano cicli come for e while. Inoltre, per cicli annidati è consigliabile seguire l'ordine alfabetico.
Qualora, il ciclo sia semplice, è possibile utilizzare anche nomi come counter. Infine, qualora si
abbia la necessità di eseguire computazioni su matrici bidimensionali, potrebbe risultare conve-
niente utilizzare i seguenti indici: row, col. Ecco un esempio di uso delle variabili contatore.

lor (ini row=0; row < MAX_R0WS; row++) (

lor (ini col=0; col < MAX_C0LS; col++) I


<istruction>
<instruction>
I

1.2.4 Evitare di utilizzare nomi simili nell'ambito dello stesso scope


Molti linguaggi di programmazione, tra cui Java, prevedono sintassi case-sensitive-, sensibili al
carattere. Pertanto, caratteri maiuscoli e minuscoli sono considerati diversi (per esempio: Count
è diverso da count). In questo caso, è necessario evitare nomi simili che differiscano semplice-
mente per la modalità di scrittura, per esempio: currencylsoCode, currencylSOCode. In generale è
sempre opportuno evitare nomi simili nell'ambito del medesimo scope (campo d'azione).
Nel seguente, su due colonne, sono mostrate due implementazione del metodo rehash della
classe java.util.Hashtable. L'implementazione di sinistra è stata volutamente resa meno leggibile
utilizzando nomi di variabile molto simili tra loro.

Il metodo rehash serve per incrementare la capacità di un oggetto Hashtable e quindi per meglio
organizzare gli elementi riducendo il numero di conflitti e aumentandone l'efficienza.

Codice sconsigliato Codice più leggibile


protected void rehashf) { protected void rehash() I
int cap = table.length; int oldCapacity = table.length;
Entry[] map = table; Entry[] oldMap = table;

int capacity = cap * 2 + 1; int newCapacity = oldCapacity * 2 + 1 ;


Entry[] m a p p = new Entryfcapacity]; Entry[] n e w M a p = new Entry[newCapacity];

modCount++; modCount++;
threshold = (int) (capacity * loadFactor); threshold = (int) (newCapacity * loadFactor);
table = mapp; table = newMap;

for (int i = cap ; i - > 0 ;) I for (int i = oldCapacity ; i - > 0 ;) I

tor (Entry<K,V> mapn = map[i]; m a p n ! = n u l l ; ) { for (Entry<K,V> old = oldMap[i] ; old != null ; ) (

Entry<K,V> e = mapn; Entry<K,V> e = old;

mapn = mapn.next; old = old.next;

int ¡1 = (e.hash & 0x7FFFFFFF) % int index= (e.hash & 0x7FFFFFFF) %

capacity; newCapacity,

e.next = mapp[i1]; e.next = newMap(index);

mapp[i1] = e; newMap[index] = e;

I I
1.2.5 Evitare di utilizzare la stessa variabile per scopi diversi
Non è infrequente analizzare implementazioni di metodi in cui una stessa variabile sia riutilizzata,
all'interno di uno stesso metodo, per scopi completamente diversi. Questa pratica dovrebbe
essere evitata in quanto tende a ridurre il grado di leggibilità del codice e, frequentemente, a
confondere il lettore. Inoltre, "pseudo-ottimizzazioni" di questo tipo raramente portano un
vantaggio reale e anzi finiscono per degradare la leggibilità del codice. Chiaramente, un discor-
so completamente diverso vale per tentativi di "riciclare" oggetti, soprattutto in contesti di
programmazione concorrente.

1.2.6 Valutare attentamente il ricorso al carattere sottolineato


come prefisso delle variabili di classe
La convenzione di utilizzare il carattere sottolineato basso (underscore, il segno _) per indicare
attributi di classe è una prassi molto utilizzata nella programmazione C++. In Java ciò non è
assolutamente necessario, giacché il linguaggio dispone della parola chiave this. Si osservi que-
sto costruttore della classe java.lang.Boolean.

public Boolean(boolean valué) I


thisvalue = value;
I

Inoltre, come visto in precedenza, in questi casi è sufficiente assegnare al parametro un pre-
fisso costituito dall'articolo indeterminativo: aValue.

1.2.7 Qualora si decida di utilizzare la notazione ungherese,


la si utilizzi coerentemente

La notazione ungherese (liunganan Notation in Inglese, il cui nome è un omaggio al suo ideatore
Charles Simonyi di origine appunto ungherese) è una convenzione nata in casa Microsoft per
l'attribuzione di nomi di parametri, variabili e attributi particolarmente popolare nella comunità
di programmatori C e C++. L'idea base consiste nell'assegnare un prefisso ai nomi al fine di
specificarne il tipo. L'obiettivo consiste nel permettere agli sviluppatori di capire il tipo di una
variabile semplicemente dal relativo nome. Si tratta di una convenzione spesso utile, in quanto
evita di dover effettuare continui salti di pagina dovuti al fatto che l'utilizzo di variabili,
frequentemente, avviene in punti distanti dalla relativa dichiarazione. Per esempio: iCOUilter
rappresenta un contatore intero, bFOUnd rappresenta una variabile di tipo booleana, OACCOUflt
rappresenta il riferimento a un oggetto di tipo account, e così via.

La notazione ungherese è oggetto di diversi dibattiti tra chi la considera molto utile e altri
che invece le addebitano di portare alla generazione di codice meno leggibile.
In questo contesto non viene presa alcuna posizione, ad eccezione del fatto che è fondamen-
tale preservare la coerenza. Pertanto, qualora si decidesse di usare la notazione ungherese, è
necessario utilizzarla in maniera costante e consistente. Nella tabella 1.3 viene proposto un
elenco standard di prefissi.
Infine, qualora si debba intervenire per modificare del codice scritto da altri, è consigliabile
mantenere la convenzione utilizzata dall'autore del codice onde evitare confusione.
Prefisso Tipo
b Boolean
c Char
by Byte
s Short
i Int
I Long
f Float
d Double
0 Object
e Exception

Tabella 1.3 - Prefissi standard da utilizzarsi per la notazione ungherese.

1.3 Selezionare nomi significativi per i metodi


1.3.1 Selezionare nomi brevi
I nomi dei metodi devono essere sufficientemente brevi ma significativi, in modo da riuscire a
illustrare chiaramente il servizio che forniscono. I metodi rappresentano azioni e pertanto i
loro nomi dovrebbero includere un verbo. Qualora poi non si tratti di metodi di servizio, i nomi
dovrebbero essere facilmente riconducibili al linguaggio business.
Da tener presente che i vari metodi, tipicamente, eseguono qualche azione relativa alla classe
cui appartengono. Pertanto, il nome della classe dovrebbe essere omesso soprattutto per i me-
todi non privati.
Per esempio, qualora si consideri una classe carrello della spesa (Trolley) per il nome del
metodo atto a verificarne la consistenza è sufficiente considerare check invece che checkTrolley.
Altri esempi di metodi della classe "carrello della spesa" sono: addltem, deleteltem, isEmpty, etc.

1.3.2 Valutare attentamente il ricorso alle abbreviazioni


Utilizzare abbreviazioni per nomi dei metodi tende a diminuirne il grado di comprensione e a
creare confusione. Pertanto il loro utilizzo dovrebbe essere sempre limitato a casi in cui non
ricorrere alle abbreviazione genererebbe nomi di metodi troppo lunghi. Qualora si decida di
introdurre delle abbreviazioni, come di consueto, è opportuno utilizzarle in modo standard.
Per esempio, una delle operazioni necessarie per eseguire il settlement (gestione del paga-
mento) di un trade consiste nell'individuare tra le istruzioni di pagamento (Standard Settlement
Instructions) predefinite dalla controparte quella più opportuna. In questo caso, un metodo
potrebbe essere getMatchingSsi. Da notare che l'acronimo SSI è di uso comune anche tra gli
operatori business.

1.3.3 Qualora un metodo svolga più azioni logicamente distinte,


aggiungere le particelle "and" o "or" nel nome
Spesso l'implementazione di un metodo richiede di includere diverse azioni di alto livello che
è opportuno evidenziare chiaramente già dal nome. Non è infrequente il caso in cui sia neces-
sario includere due azioni distinte nello stesso metodo, perché, per esempio debbano condivi-
dere una stessa transazione o perché è conveniente avere una sezione protetta in comune.
Qualora queste azioni siano eseguite sequenzialmente, allora è necessario specificare la con-
giunzione and tra i nomi di queste azioni, mentre qualora le azioni siano eseguite in alternativa,
allora è necessario utilizzare la congiunzione or. Per esempio: updateOrlnsertltem,
saveAndPublishMessage,

1.3.4 Non ripetere il nome della classe nei nomi dei metodi
Come visto in precedenza, per la selezione dei nomi dei metodi si dovrebbe limitare l'attenzio-
ne ai soli verbi, sebbene sia frequentemente necessario ricorrere a nomi composti. Spesso però
si compone il nome dei metodi utilizzando anche il nome della classe di appartenenza. Sebbe-
ne ciò non sia un problema serio, si tratta comunque di una ripetizione inutile.
Si consideri per esempio la classe java.util.Vector. Alcuni dei suoi metodi sono: addElement,
i n s e r t E l e m e n t A t , e l e m e n t A t , size, f i r s t E l e m e n t , l a s t E l e m e n t , i n d e x O f , e t c .

1.3.5 Utilizzare una convenzione chiara nella selezione dei nomi dei metodi
Nel disegno di classi succede spesso di dover realizzare metodi ricorrenti come per esempio
"inserimento", "eliminazione", "ricerca" di elementi e così via. In questo caso si consiglia di
selezionar coerentemente i nomi per questi metodi. Per esempio, analizzando le classi Java è
possibile evidenziare le convenzioni riportate nella tabella 1.4.

1.4 Implementare classi orientate agli oggetti


1.4.1 Evitare l'implementazione di classi di grandi dimensioni
Classi di grandi dimensioni sono sempre da evitare in quanto tendono a diventare difficilmente
comprensibili e quindi manutenibili, a complicare i test, a rendere problematico Io sviluppo in
parallelo da parte di team di diverse persone e a ridurre il riutilizzo.
Classi di grandi dimensioni, spesso, sono il risultato dell'inglobamento di diverse classi in
una e pertanto si prestano a dare luogo a implementazioni a bassa coesione che, come i principi
dell'OO insegnano, andrebbero evitati. La tendenza naturale di sistemi OO è quella di genera-
re molte classi di dimensioni medio-piccole, facili da comprendere, manutenere, verificare e
riusare. Ciò, tra l'altro, favorisce il naturale processo di apprendimento della mente umana che
richiede, in ogni momento, di concentrasi su un insieme limitato di aspetti. Un numero ecces-

Nome metodo Utilizzo


add Inserimento di un elemento in una lista.
addAII Inserimento di una collezione in una lista.
clear Rimuove tutti gli elementi di una lista.
get Reperimento di un elemento specifico di una lista.
remove Rimozione di un elemento.
size Restituzione del numero degli elementi di una lista.

Tabella 1.4 - Tabella con la convenzione java per alcuni nomi di metodi ricorrenti.
sivo di classi, chiaramente, è altrettanto sbagliato e tende a creare una serie di problemi legati a
un elevato accoppiamento.
Ogni qualvolta la dimensione di una classe cominci a passare i limiti, dovrebbe essere natu-
rale interrogarsi se si stiano inglobando più concetti distinti in una sola classe; e, se non è così,
è comunque bene chiedersi se l'introduzione di opportune classi "helper" possano facilitarne
la comprensione.

1.4.2 Utilizzare il livello di accesso più ristretto possibile


Il livello di accesso di un elemento (classe, interfaccia, metodo e attributo) determina quali
altre classi appartenenti allo stesso sistema possono accedere all'elemento in questione.
Una delle leggi fondamentali dell'OO, l'incapsulamento (nota anche come principio
dell'information hiding), prescrive che le classi celino al mondo esterno la propria organizzazio-
ne in termini di struttura e logica interna. Il principio fondamentale è che nessuna parte di un
sistema debba dipendere dai dettagli interni di una sua parte. Questo, come è lecito attendersi,
rende possibile modificare la struttura interna delle classi di un sistema evitando che si generi-
no ripercussioni su altre parti dello stesso.
Per esempio, succede varie volte di accorgersi, durante la fase di test, che determinati oggetti
danno luogo a un eccessivo consumo di memoria oppure eseguono specifiche operazioni con
performance insoddisfacenti. In questi casi, qualora si sia utilizzato con intelligenza il principio
dell'incapsulamento, è possibile reingegnerizzare le classi da cui derivano tali oggetti, senza
dover modificare altre parti del sistema.
I linguaggi di programmazione OO tendono a prevedere i seguenti quattro livelli di accesso,
riportati in ordine di accesso crescente: privato, amichevole o di default, protetto e pubblico. Al
fine di evitare qualsiasi confusione, in tabella 1.5 è riportato uno schema dei livelli di accesso Java.

Da notare che per quanto concerne il livello di accesso "amichevole", in Java questo è dichiarato
omettendo il livello di accesso di un elemento. Inoltre, un elemento il cui livello di accesso è
amichevole è inaccessibile a tutte le classi che non si trovino nello stesso package, anche qualora
queste siano classi figlie.

L'applicazione della legge dell'incapsulamento, per quanto concerne gli attributi, implica di
utilizzare prevalentemente un livello di accesso privato. Qualora una classe possa prevedere

Livello di accesso Stessa Stesso Classe Tutte le


classe package ereditante altre
Privato
• X X X
private
Amichevole
• •/ X X
non specificato

Protetto
s V V X
protected
Pubblico
• •/ •
public

Tabella 1.5 - Livelli di accesso in Java.


delle specializzazioni (classi ereditanti) che, per loro natura, sono fortemente legate alla classe
antenata (come per esempio la classe java.util.AbstractCollection e le varie collezioni concrete),
allora potrebbe risultare molto utile dichiarare alcuni attributi con un livello di accesso protet-
to. Il livello di accesso di default, invece potrebbe risultare utile nella realizzazione di package
grafici. Infine il livello di accesso pubblico dovrebbe essere riservato esclusivamente alla di-
chiarazione di costanti.
Dato che una buona implementazione O O prevede che tutti gli attributi siano privati, ne
segue che l'interfaccia propria di una classe sia data dai suoi metodi, la cui invocazione rappre-
senta l'unico modo con cui i vari oggetti possano interagire tra loro. Pertanto, i metodi dovreb-
bero essere dichiarati privati (meglio se protetti, al fine di consentirne l'accesso a possibili classi
ereditanti), a meno che non si tratti di metodi appartenenti all'interfaccia della classe (metodi
che possano essere invocati da altre classi). In questo caso, il livello di accesso da scegliere è
pubblico (se invocabile da qualsiasi classe) o di default (se invocabile da classi appartenenti allo
stesso package).

1.4.3 Massimizzare la coesione interna delle classi


"Un modulo presenta un'elevata coesione quando tutti i componenti collaborano fra loro per
fornire un ben preciso comportamento" (Booch). Dal punto di vista delle classi, la coesione è la
misura della correlazione delle proprietà strutturali (attributi e relazioni con le altre classi) e
comportamentali di una classe (metodi). Nella letteratura informatica è possibile individuare diverse
formule matematiche atte a determinare il grado di coesione di una classe. Una delle più note è:

(m - I ( n r ) / a )
c=
( m -1)

dove

m è il numero di metodi della classe


3 è il numero di attributi della classe.
mj è il numero di metodi che accedono all'attributo i mo.
Urr^) è la sommatoria di tutti i contributi rn calcolati per tutti gli attributi della classe.

Da notare che minore è il valore di C e maggiore è il grado di coesione della classe. In sostanza,
maggiore è il numero di metodi che accedono a diversi attributi della classe, e superiore è il grado
di coesione. Questa formula considera anche casi in cui più classi siano state inglobate in una
sola, caratterizzati dall'esistenza di diversi gruppi (logici) di attributi, acceduti ciascuno da un
insieme distinto e ben definito di metodi. Pertanto la classe espone servizi che eseguono compiti
non relazionati.

I problemi tipici generati da classi a bassa coesione sono relativi alla difficoltà di compren-
sione del codice e quindi di manutenzione, di laboriosità nell'eseguire i vari test, impossibilità
nel riutilizzo, difficoltà di isolare elementi soggetti a variazioni, ecc. In casi estremi si giunge
alla perdita di controllo dell'intera applicazione caratterizzata da servizi sparpagliati in classi in
cui non dovrebbero appartenere e conseguente incremento del grado di accoppiamento.
Un esempio di classe a bassa coesione è relativa alla rappresentazione di un utente del siste-
ma (User) ove attributi del tipo name, surname, dateOfBirth, gender, etc. siano combinati con altri
del tipo currency, value, etc. Lo stesso vale nel caso in cui in una classe di tipo carrello della spesa
(Trolley), si trovassero dei metodi del tipo empty, addltem, verifyContent, etc. e altri del tipo placeOrder,
findllsers, etc.
Per valutare il grado di coesione di una determinata classe, non sempre è necessario applica-
re un approccio formale basato sulle formule. Infatti, problemi di scarsa coesione possono
essere facilmente rilevati da una serie di segnali di allarme. Il più evidente è connesso alle
dimensioni delle classi. Qualora una classe contenga troppi attributi, oppure troppe relazioni
con altre classi, oppure un numero eccessivo di metodi, molto probabilmente il livello di coe-
sione di questa non è molto elevato. Pertanto è probabile si abbia a che fare con una classe che
ne ingloba altre. Un altro segnale è rappresentato dalla presenza, nella medesima classe, di
attributi partizionabili in insiemi distinti e non relazionati, e dall'avere metodi che non accedo-
no mai ad attributi appartenenti a diversi insiemi. Un altro indicatore di scarsa coesione è dato
da situazioni in cui non si riesca ad identificare un nome preciso per una classe oppure questo
risulta troppo generico.

1.4.4 Minimizzare l'accoppiamento tra le classi


Un principio di importanza fondamentale nel disegno e nell'implementazione delle classi è rela-
tivo al grado di accoppiamento che, chiaramente, deve essere minimizzato. Massima coesione e
minimo accoppiamento sono due proprietà imprescindibili e fortemente relazionate dell'OO. Il
minimo accoppiamento è particolarmente ricercato in quanto capace di generare tutta una serie
di vantaggi (del tutto equivalenti a quelli generati dalla massima coesione), quali maggiore com-
prensione e facilità di manutenzione del codice, aumento della probabilità di riutilizzo, sempli-
ficazione delle attività di manutenzione, ecc. Il livello di accoppiamento delle classi deve essere
minimizzato ma, ovviamente, non eliminato: non esiste un reale disegno OO senza accoppia-
mento. Ogni volta che un oggetto interagisce con un altro si ha una manifestazione di accoppia-
mento. La mancanza totale di accoppiamento porterebbe generare l'effetto contrario, ossia la
produzione di codice poco leggibile dovuto a classi (scarsamente coese) omnicomprensive.
L'accoppiamento è definita coma la "misura della dipendenza tra componenti software (clas-
si, package, componenti veri e propri, ecc.) di cui è composto il sistema". Sia ha una dipenden-
za tra due classi, quando un elemento (client) per espletare le proprie responsabilità accede alle
proprietà comportamentali (metodi) e/o strutturali (attributi) di un altro (supplier, fornitore).
Ciò implica che il funzionamento del componente stesso dipende dal corretto funzionamento
(e quindi dall'implementazione) degli altri componenti "acceduti"; pertanto cambiamenti in
questi ultimi generano ripercussioni sul componente client. Come scritto poco sopra, il livello
di accoppiamento deve essere ridotto al minimo, ma non eliminato.

1.5 Porre attenzione alla scrittura dei metodi


1.5.1 Implementare metodi che eseguono un solo compito
Come principio di carattere generale bisognerebbe implementare metodi che svolgano una
sola funzione ben definita. Ciò permette di realizzare codice di maggiore leggibilità, quindi più
facilmente comprensibile, manutenibile, riusabile, e così via. Ciò, inoltre, concorre ad aumen-
tarne il livello di coesione. Ecco codice con metodi che eseguono un solo compito ben definito.
' expands the capacity of an A b s t r a c t S t r m g B u i l d e r obejct
' è ' p a r a m m i n i m u m C a p a c i t y m i n i m u n capacity requested to this object.
• t
void expandCapacity(int minimumCapacity) I
int newCapacity = (value.length + 1) * 2;

il (newCapacity < 0) (
newCapacity = lnteger.MAX_VALUE;
I else il (minimumCapacity > newCapacity) I
newCapacity = minimumCapacity;
I

char newValue[] = new char[newCapacity];


System.arraycopyfvalue, 0, newValue, 0, count);
value = newValue;
I

1.5.2 Evitare l'implementazione di metodi lunghi


Metodi la cui implementazione risulti maggiore di circa 15 righe risultano di difficile compren-
sione e quindi di difficile test, manutenzione e riutilizzo. Una tecnica decisamente più conve-
niente consiste nello scrivere metodi la cui intera implementazione sia visualizzabile in una
pagina di un normale monitor evitando così di dover eseguire continui scroll della pagina.
Questo approccio è poi confacente alla caratteristica tipica della mente umana di concentrasi,
in ogni momento, su un insieme ristretto di concetti.
Ogni qualvolta l'implementazione di un metodo ecceda le dimensioni suddette, è meglio
tentare di ridurlo nella composizione di metodi più piccoli di più facile comprensione.

1.5.3 Non scrivere metodi con molti parametri


Ogni qualvolta la firma di un metodo contenga troppi parametri (tipicamente più di quattro o
cinque), è necessario verificare se sia il caso di incapsulare questi parametri in un'apposita
classe o permetterne l'impostazione attraverso opportuni metodi. Questa regola può essere
rilassata per i metodi costruttori di oggetti disegnati puramente per trasportare valori, per esempio
Value Object (VO) e Data Transfer Object (DTO). Anche in questi casi però, disegnare metodi
costruttori con diversi parametri può creare non pochi problemi in presenza di gerarchie di
oggetti. Infatti, la variazione del costruttore di una classe antenata (per esempio l'aggiunta di un
nuovo parametro) finisce con il ripercuotersi in tutte le classi ereditanti.
Il problema è che metodi la cui firma contiene una lunga lista di parametri sono difficili da
utilizzare, il loro utilizzo richiede di controllare continuamente il significato e la posizione dei
parametri, etc. Pertanto, metodi di questo tipo possono essere causa di frequenti errori.
Si consideri come esempio di metodo con molti parametri, il costruttore della classe
SimpleTimeZone: riportato nel listato seguente.

public SimpleTimeZone(int rawOffset, String ID,


int startMonth, int startDay, int startDayOfWeek, _ int startTime,
int endMonth, int endDay, int endDayOfWeek, int endTime)
1.5.4 Eseguire il controllo del valore dei parametri appena possibile
E opportuno far sì che le prime linee di codice dell'implementazione di ogni metodo siano
relative al controllo della validità del valore dei parametri forniti. Se il metodo è destinato a
fallire è opportuno che fallisca il prima possibile. Questa regola è particolarmente importante
per i metodi costruttori. Le tipiche eccezioni (Java standard) considerate per comunicare que-
sto tipo di errore sono NulIPointerException e HlegalArgumentException. Si consideri l'implementazione
di uno dei costruttori della classe java.util.Vector.

public Vector(inl initlalCapacity, int capacitylncrement) I


super();

il (InitialCapacity < 0)
throw new IHegalArgumentExceptionf'lllegal Capacity: "+initialCapacity);

this.elementData = new Object[initialCapacity];


this.capacitylncrement = capacitylncrement;
I

1.5.5 Estrarre il comportamento comune


Nell'implementare metodi di una classe, non è infrequente il caso in cui una determinata sezio-
ne di codice sia ripetuta in diversi metodi, magari con minime differenze. Questa evenienza,
tipicamente, porta ad implementare metodi lunghi e quindi meno leggibili. Inoltre, è naturale
che una stessa porzione di codice sia ripetuta in diverse parti. Ciò fa sì che eventuali modifiche
relative a tale sezione debbano essere ripetute diverse volte con il concreto rischio di tralasciar-
ne qualcuna. In questi casi è opportuno verificare la possibilità di raggruppare il comporta-
mento comune, eventualmente parametrizzandolo, in appositi metodi privati. Si consideri il
codice del listato seguente, con un'improbabile implementazione del metodo setLength della
classe java.lang.AbstractStringBuilder.

public void setLength(int newLength) I


if (newLength < 0)
throw new StringlndexOutOfBoundsException(newLength);
if (newLength > value.length) {

int newCapacity = (value.length + 1) * 2;

ensureCapacity
if (newCapacity < 0) (
newCapacity = lnteger.MAX_VALUE
) else if (newLength > newCapacity) {
newCapacity = minimumCapacity;

char newValue[] = new char[newCapacity];


System.arraycopy(value, 0, newValue, 0, count);
value = newValue
if (count < newLength) I
for (; count < newLength; count++)
resetArray
value[count] = '\0';
I else {
count = newLength;
)

1.5.6 Intervallare il codice con apposite righe vuote


Dall'analisi del codice dei metodi è possibile notare come alcuni gruppi di istruzioni eseguano
compiti ben definiti e quindi risultino fortemente interconnesse. Questi gruppi formano le parti-
celle dell'implementazione del metodo. Pertanto costituisce una buona norma evidenziare questi
gruppi a maggiore coesione separandoli dagli altri tramite righe vuote. Codici organizzati in questo
modo sono più facili da leggere e da comprendere. Vediamo nel listato successivo, su due colonne,
due versioni del metodo equals presente nella classe java.util.AbstractList. In quella di sinistra (sconsi-
gliata) le istruzioni sono scritte in sequenza senza separare i diversi blocchi. In quella di destra,
invece, sono state aggiunte delle righe vuote per assegnare più enfasi a gruppi logici di istruzioni.

Codice sconsigliato Codice consigliato

public boolean equals(Object o) {

Z/ I A\ if (o == this)
return true;

public boolean equals(0bject o) I if (!(o Instanceof List))


If (o == this) return false;
return true;
if (!(o instanceof List)) Listlterator<E> e1 = listlterator();
return false; Listlterator e2 = ((List) o).listlterator();
Listlterator<E> e1 = listlterator();
Listlterator e2 = ((List) o).listlterator(); while (e1.hasNext() && e2.hasNext()) I
while (e1 hasNext() && e2.hasNext()) { Eo1 =e1.next();
E o1 = e1.next(); Object o2 = e2.next();
Object o2 = e2.next();
If (!(o1==null ? o2==null : o1.equals(o2))) If (!(o1==null ? o2==null : o1.equals(o2)))
return false; return false;
} }
return !(e1.hasNext() || e2.hasNext());
I return !(e1.hasNext() || e2.hasNext());

1.5.7 Valutare l'utilizzo delle parentesi graffe


anche quando non è strettamente necessario
L'utilizzo delle parentesi graffe anche quando non strettamente necessario (per esempio nel
caso di cicli con una sola istruzione) è una buona tecnica per migliorare lo stile del proprio
codice. In particolare, semplifica la lettura e la manutenzione del codice, specie qualora il
codice contenga una serie di costrutti annidati, e agevola la manutenzione del codice qualora
sia necessario aggiungere ulteriori istruzioni in un costrutto che originariamente ne conteneva
una sola. Nel listato seguente, su due colonne, due versioni di un metodo costruttore della
classe java.io.File. Quello di destra (corretto) utilizza le parentesi graffe anche in situazioni in cui
non è strettamente necessario al fine di favorire la leggibilità del codice.

Codice sconsigliato Codice consigliato

public File(String parent, String child) I


¡((child == null) {
throw newNullPointerExceptionO;
public File(String parent, String child) { I
it (child == null) ¡»(parent != null) I
throw new NullPointerException(); if(parent.equals("")) I
it (parent != null) this.path =fs.resolve(fs.getDefaultParent(),
if (parent.equalsf")) fs.normalize(child));
this.path = fs.resolve(fs.getDefaultParent(), I elsel
fs.normalize(child)); this.path = fs.resolve(fs.normalize(parent),
else fs.normalize(child));
this.path = fs.resolve(fs.normalize(parent), I
fs.normalize(child)); ) elsel
else this.path = fs.normalize(child);
this.path = fs.normalize(child); 1
this.prefixLength = fs.prefixLength(this.path); this.prefixLength = fs.prefixLength(this.path);

1.5.8 Implementare metodi a minimo accoppiamento


In prima analisi, i metodi di ciascuna classe possono essere suddivisi in tre macro-categorie:

1. metodi che forniscono un servizio semplicemente elaborando i dati di input senza ricor-
rere all'utilizzo di altri dati e senza utilizzare lo stato dell'oggetto (per esempio in Java i
metodi della classe java.lang.Math, come Matti.absQ);
2. metodi che comunicano una porzione dello stato interno di un oggetto, oppure elabora-
no risultati dipendenti da esso, senza modificarlo (per esempio i famosi metodi getXQ);
3. metodi che aggiornano lo stato interno di un oggetto (per esempio i metodi setXQ).

Per quanto concerne la prima tipologia di metodi, essi presentano un accoppiamento nullo
quando risultano privi di effetti collaterali (i famosi side effects), ottenuti mutando lo stato di un
oggetto, e operano dunque esclusivamente sui parametri di input. Qualora questi metodi utiliz-
zino altri dati, magari privati all'oggetto, di cui però si abbia strettamente bisogno, si ha ancora
un accoppiamento minimo. Per quanto concerne i risultati generati, il metodo deve produrre
unicamente un dato atomico o eventualmente un altro oggetto di cui è fornito il riferimento in
memoria. Per mantenere un accoppiamento nullo, il metodo, durante la propria esecuzione,
non deve poi delegare ad altri parte del proprio processo (non deve invocare altri metodi). Da
quanto riportato, è evidente che non sempre un accoppiamento nullo è assolutamente indi-
spensabile e desiderabile. Anzi, spesso sono accettabilissimi alcuni compromessi, purché l'ac-
coppiamento resti minimo, magari al fine di soddisfare altre proprietà di qualità del software,
come per esempio rendere i metodi più leggibili, manutenibili, riusabili, etc. magari derogando
parte del proprio lavoro ad altri metodi.
Nel caso di metodi del secondo tipo, si ha un accoppiamento minimo quando il metodo, per
generare i risultati della propria elaborazione, utilizza i parametri di input e accede ai soli attri-
buti e metodi della classe (sia statici che non). Ancora una volta restituisce un valore atomico o
un riferimento a un apposito grafo di oggetti o, eventualmente, genera un'eccezione per comu-
nicare uno stato di errore. Metodi di questo tipo, pertanto, accedono allo stato dell'oggetto
senza però modificarlo e utilizzano esclusivamente proprietà (metodi e attributi) della classe o
dell'oggetto stesso.
Per i metodi dell'ultimo tipo, la materia non varia di molto. La differenza è che la propria
esecuzione altera lo stato dell'oggetto. Chiaramente un accoppiamento minimo non prevede la
variazione dello stato di altri oggetti.

1.6 Utilizzare correttamente l'ereditarietà


L'ereditarietà è probabilmente la legge più nota dell'OO e, verosimilmente, anche quella più
abusata. Brevemente, l'ereditarietà è un meccanismo attraverso il quale un'entità più specifica
incorpora struttura e comportamento definiti da entità più generali. Se da una parte è vero che,
qualora utilizzata correttamente, essa permette una migliore ristrutturazione gerarchica
(raggruppare il comportamento condiviso da più classi in una versione generalizzata incapsulata
in un'apposita classe antenata), dall'altro bisogna tenere in mente che l'ereditarietà non è esente
da controindicazioni come per esempio il forte legame di dipendenza che si instaura tra classe
antenata e quelle discendenti (modifiche eseguite su una classe antenata tendono a ripercuotersi
su tutte le classi discendenti) e la staticità e rigidità della struttura gerarchica. In particolare, una
volta che una classe sia incastonata in questo tipo di organizzazione, non ne potrà più uscire.

1.6.1 Ereditare il tipo di una classe e non solo gli attributi/metodi


Utilizzare la relazione di ereditarietà per dar luogo ad una "specializzazione" dipendente dal
tipo di una classe e non semplicemente per condividere pochi attributi e/o metodi. Con il tipo
di una classe, brevemente, si intende la sua "interfaccia implicita" (elenco delle proprietà strut-
turali e comportamentali esposte ad altre classi). Il problema è che non è raro il caso di relazioni
di ereditarietà realizzate semplicemente per "ereditare" opportune porzioni di implementazione.
Ciò dovrebbe costituire una conseguenza e non un principio.

1.6.2 Non utilizzare l'ereditarietà per oggetti che possono "cambiare tipo"
L'ereditarietà presenta una serie di problemi qualora un'istanza di una determinata classe ab-
bia necessità, in qualche modo, di "trasmutare tipo" durante il proprio ciclo di vita. In questi
casi, un'alternativa migliore consiste nel rappresentare questo comportamento per mezzo di
opportune versioni della relazione di associazione (composizione) e non con legami di
generalizzazione. Per chiarire quanto espresso, si consideri l'esempio classico relativo alla clas-
sificazione dei ruoli degli "attori" di una compagnia area. In prima analisi alcuni programmato-
ri sarebbero portati a definire una classe base, probabilmente astratta denominata Person, e
quindi a specializzarla con classi del tipo Pilot, Crew, Passenger, etc. Questo è un chiaro esempio
di errato utilizzo della relazione di ereditarietà: alcune entità possono cambiare il loro "tipo"
durante il relativo ciclo di vita. Per esempio, un pilota frequentemente è anche un passeggero.
Pertanto, una soluzione migliore consiste nel realizzare comunque la classe Person, questa volta
concreta e associarla, attraverso apposita associazione, con una classe astratta denominata Role,
le cui specializzazioni sono appunto le classi Pilot, Crew, Passenger, etc.

Figura 1.1 - Esempio di utilizzo errato della relazione di ereditarietà.


Capitolo 2
Programmazione Java
Introduzione
Dopo aver investigato nel corso del capitolo precedente le nozioni di carattere generale relative
alla programmazione, con particolare riferimento ai linguaggi basati sul paradigma OO, in
questo capitolo l'attenzione è completamente focalizzata sul linguaggio Java. Pertanto, sebbene
alcune considerazioni continuano a mantenere una valenza generale, molte regole qui presenta-
te hanno un dominio di applicazione strettamente limitato al linguaggio Java.
Le nozioni presenti in questo capitolo, come lecito attendersi, presentano un elevato grado
di operatività, e includono indicazioni relative a istruzioni il cui utilizzo dovrebbe essere evitato
(per esempio System.exit), a suggerimenti atti ad utilizzare i formati numerici correttamente
evitando classiche trappole, a spiegazioni relative al sempre enigmatico hash (spiegato in detta-
glio nell'Appendice C), a consigli utili per gestire gli stream, etc.
Alcuni insidie riportate in questo capitolo tendono a essere regolarmente trascurate da molti
programmatori. Il caso più evidente è quello legato ai tipi reali. In questo caso, tutto procede
regolarmente finché, eseguendo particolari calcoli scientifici, o importanti computazioni mo-
netarie, ci si trova nella situazione di dover individuare le ragioni per cui alcune computazioni
generino risultati diversi da quelli attesi. In questo caso, l'esperienza insegna che spesso si fini-
sce con il dissipare moltissimo tempo prima di comprendere che il problema sta proprio alla
radice: i tipi reali base sono intrinsecamente imprecisi. Si tratta di problemi subdoli da risolvere
poiché i relativi effetti si manifestano solo in alcuni casi ed con modalità diverse.

Obiettivi
L'obiettivo di questo capitolo è fornire una serie di direttive operative, best practice e quant'al-
tro al fine di supportare il miglioramento della qualità del software prodotto, Java e non. In
particolare, in questo contesto sono analizzate anche caratteristiche quali performance e
portabilità e così via. La lettura di questo capitolo dovrebbe permettere di considerare e di
identificare tutta una serie di imprecisioni sistematicamente commesse da diversi sviluppatori
ed eventualmente di apprendere proficue tecniche di programmazione Java. In questo capitolo
sono affrontate diverse tematiche abbastanza complesse, come la manipolazione dei campi
date, i campi numerici, l'utilizzo degli stream, la corretta conclusione dei programmi Java, etc.

Direttive
2.1 Investire nello stile
Uno stile di programmazione lineare, razionale e consistente è requisito fondamentale per la
produzione di codice più facilmente leggibile e comprensibile. Logica conseguenza di queste
due caratteristiche è un codice più facilmente testabile, mantenibile e perfino riusabile. Ciò è
particolarmente importante considerando che:

• una buona percentuale del ciclo di vita del software è utilizzato dal processo di manutenzione
(a seconda dello studio considerato, questo fattore può variare da un minimo di 4 0 % ad un
massimo di 9 0 % ) ;
• lo stesso codice, durante l'intero ciclo di vita, tende a essere mantenuto da diverse persone;
è raro che il codice sia scritto e mantenuto dalla stessa persona;

Pertanto è opportuno investire nello stile del codice partendo dall'utilizzo di una convenzione
largamente condivisa. Ciò permette di ridurre i tempi di apprendimento del codice e ne favorisce
una comprensione più approfondita, che quindi ne consente un maggiore riuso, una più semplice
manutenzione, e cosi via.

2.1.1 Adottare lo stile di programmazione standard Java


La Sun Microsystem, fin dalle primissime versioni del linguaggio di programmazione Java, ha
pubblicato un documento relativo allo standard di programmazione Java (cfr. [SJAVCC]). Questo
include direttive relative al nome delle classi Java, all'organizzazione dei file, all'indentazione
del codice sorgente, all'organizzazione delle dichiarazioni, dei costrutti e dei cicli, alle conven-
zioni sui nomi, etc.
Disponendo di una convenzione standard ben collaudata, documentata accuratamente, con-
divisa da una larghissima comunità di programmatori distribuiti su l'intero globo terrestre,
perché tentare di inventarne un'altra?
Inoltre, utilizzando questa notazione, si evitano una serie di problematiche, come per esem-
pio Xhiding dei tipi, descritto a fine capitolo.

2.2 Utilizzare accuratamente le "costanti"


Nel linguaggio Java non esistono costanti nel senso tradizionale del termine. Questo è dovuto al
fatto che i progettisti del linguaggio Java hanno deciso di non dotarlo di un precompilatore onde
evitare la proliferazione di alcuni tipici abusi presenti nei linguaggi C e C + + (vedere kilometriche
introduzioni di istruzioni #lfdef). Pertanto in Java non esiste un meccanismo di sostituzione, a
"tempo di compilazione", tra etichette e corrispondenti valori. Come alternativa è possibile definire
delle variabili condivise il cui valore non può essere modificato (final) che, pertanto, conviene
dichiarare statiche (Static).

2.2.1 Non inserire valori hard-coded nel codice


Una direttiva base della programmazione, indipendentemente dal linguaggio considerato, sta-
bilisce di non includere valori invariabili direttamente nelle istruzioni del programma. Tale
pratica dannosa, indicata tipicamente con i termini di hard-coding, è in grado di generare una
serie di problemi, ad esempio l'aumento della resilienza alle modifiche, una minore consisten-
za, e così via.
Pertanto, ogniqualvolta si abbia la necessità di inserire un valore non variabile direttamente
nel codice, è necessario valutare se si tratti di un valore che:

1. non cambierà pressoché mai. In questo caso è sufficiente utilizzare una "costante"
Java. Per esempio:

public static final String N E W _ U N E = System.getPropertyfline.separator");

2. ha buone probabilità di venir aggiornato. In questo caso il ricorso a una variabile statica
non costituisce una buona pratica poiché variazioni del valore richiedono di ricompilare
il codice, distribuirlo, etc. In questo caso è più opportuno inserire tale valore in un
opportuno file di configurazione (property file, file XML, etc.).

3. seppur con scarsa probabilità, potrebbe comunque cambiare. In questo caso, una vali-
da strategia consiste nell'incapsulare i valori in opportuni metodi, affinché un eventuale
cambio di strategia risulti assolutamente trasparente. Per esempio: getTextFileExtension().

2.2.2 Valutare il caso di inserire valori costanti


in un'opportuna interfaccia Java
Una buona pratica di programmazione consiste nell'includere dentro un'opportuna interfaccia
Java le costanti largamente utilizzate in un framework, package o sistema.
Per esempio, si analizzi il caso del listato seguente.

public AppIConstants {

// C o m m o n Strings
public static final String N E W _ U N E = System.getPropertyf'line.separator");
public static final String FILEJ3EPARAT0R = System.getPropertyffile.separator");
public static final String PATH_SEPARATOR = System.getProperty("path.separator");
1

2.2.3 Non eseguire l'hard-coding dei nomi di file


Questa regola rappresenta una ripetizione di quanto esposto nei precedenti punti; ciò nono-
stante, si è deciso di enfatizzarla nuovamente per via dell'elevata frequenza con cui viene disattesa.
Eseguire l'inglobamento dei percorsi direttamente nel codice può porre seri problemi sia rela-
tivi all'istallazione dei sistemi e al riutilizzo del codice, sia alla portabilità del codice. Un caso
frequente è relativo all'inizializzazione di stringhe contenenti la concatenazione del percorso
del file, incluso il relativo nome. Per esempio: private String configFile = "C:\mysys\config\config.xml".
Ciò crea non solo i tipici problemi dovuti aü'hard-codtMg, ma anche di portabilità. Per esem-
pio, nel sistema operativo Unix i percorsi assoluti iniziano con il carattere "/", mentre in Windows,
questi iniziano con una lettera identificante l'unità dati.
Per sottrarsi a questi problemi è necessario:

1. evitare Yhard-coding dei percorsi ricorrendo alle tecniche illustrate precedentemente;


2. costruire i percorsi utilizzando:
a. la classe File(File, String) per costruire il percorso del fine;
b. le proprietà di sistema per costruire i percorsi. In particolare:
System.getPropertyffile.separator") e System.getPropertyfpath.separator")

2.2.4 Evitare Yhard-coding dei caratteri utilizzati per terminare una linea
Anche questa regola rappresenta un'ulteriore enfatizzazione di quanto enunciato in preceden-
za ma si tratta di un aspetto troppe volte trascurato.
Il problema consiste nel fatto che il terminatore di linea nei file di testo varia a seconda della
piattaforma di riferimento. In particolare, è possibile avere tre differenti convenzioni: "\n"
(Windows), "\r" (Unix) e "\r\n" (Apple). Per esempio, la seguente istruzione:

System.out.print(i+") \n user=currentUser.getLogin() \n attempts="+failedAttempts);

verrebbe mostrata correttamente solo in ambiente Windows

1)
user=vettitagl
attempts=2

Mentre in ambiente Unix produrrebbe il seguente output:

1) \n user= vettitagl \n attempts=2

Per evitare problemi di questo tipo è sufficiente utilizzare come sequenza di nuova riga, il
risultato della seguente istruzione

System.getPropertyfline.separator").

2.3 Concludere correttamente i programmi Java


Quando si scrivono applicazioni Java è necessario tenere a mente le regole di terminazione. In
particolare, un'applicazione Java termina quando terminano tutti i thread non daemon (detti
anche user thread) attivi nella medesima J V M . I tbread creati dal programmatore, tipicamente,
sono non daemon. Qualora l'esecuzione dell'applicazione preveda un solo thread, demandato
all'esecuzione del metodo il main, l'applicazione termina alla terminazione di tale thread.
2.3.1 Non terminare l'esecuzione del programma
con l'istruzione System.exit()
L'esecuzione dell'istruzione System.exit() forza l'immediata terminazione di tutti i thread in ese-
cuzione nella sottostante J V M e quindi termina anche quest'ultima. Pertanto l'utilizzo di que-
sta istruzione dovrebbe essere evitato.
Uno dei problemi centrali è che l'invocazione di System.exit() non permette ai vari thread di
terminare in maniera pulita (gracefully) e non dà l'opportunità di salvare eventuali dati tempo-
ranei, contesti, etc. Tale istruzione, inoltre, limita il riutilizzo del codice e pone seri vincoli
all'integrazione del codice in opportuni sottosistemi. Si consideri per esempio il caso in cui un
codice sia integrato in un sistema più complesso, oppure in un framework di integrazione. In
questo caso l'esecuzione dell'istruzione System.exit() provocherebbe la chiusura di tutte le ap-
plicazioni in esecuzione sulla medesima JVM. Situazione non sempre auspicabile.
Le direttive della Sun stabiliscono "Si tratta di un comportamento troppo drastico. Potreb-
be, per esempio, distruggere tutte le finestre create daH'in terpreter senza dare all'utente la pos-
sibilità di registrarne o addirittura di leggerne il contenuto [...] I programmi dovrebbero ter-
minare, di solito, arrestando tutti i thread di tipo non daemon; nel caso più semplice di un
programma da linea di comando, si tratta semplicemente di un return dal metodo main. System.exit
dovrebbe essere riservato solo all'uscita da un errore catastrofico, o ai casi in cui un prgramma
sia pensato per l'uso come utilità in uno script che possa avere dipendenza da sul codice di
uscita dal programma".
Un'applicazione dovrebbe terminare sempre in modo controllato (gracefully), dando il ne-
cessario tempo ai vari thread di rilasciare le eventuali risorse bloccate, di registrare la persistenza
per eventuali dati temporanei, etc.

2.3.2 Non utilizzare le istruzioni System.runFinalizerOnExit


e Runtime.runFinalizerOnExit
Molti sviluppatori probabilmente, e per loro fortuna, non avranno mai sentito parlare di questi
metodi (se questo è il caso, benissimo: passare alla regola successiva). In effetti sono stati de-
precati fin dalla versione 1.2 per motivi decisamente validi. In effetti sono intrinsecamente
insicuri (unsafe). Ciò perché possono facilmente generare scenari di dead-lock o comunque di
comportamento randomico, dovuti all'esecuzione del dei finalizer su oggetti ancora validi men-
tre altri thread potrebbero trovarsi ad utilizzarli o potrebbero richiederne la manipolazione.

2.3.3 Evitare uscite brusche dal costrutto finally


Ogni qualvolta un costrutto o un blocco di codice lancia un'eccezione o esegue una delle se-
guenti parole chiavi return, break e continue, questo termina bruscamente (abruptly in inglese):
interrompe il proprio flusso di esecuzione senza eseguire la restante parte del codice.
Interrompere bruscamente un costrutto finally non è mai una buona prassi. In primo luogo,
programmi che fanno uso di uscite brusche tendono a essere confusi e quindi noti fà®ìj£pjl
comprendere. Inoltre, poiché istruzioni presenti nella parte finally sono eseguite quasi nella
totalità dei casi (unica eccezione è la presenta dell'uscita dal programma: System.exit), indipen-
dentemente da quello che accade nel costrutto try, eventuali uscite brusche nel costrutto try
verrebbero perse e sostituite da quella presente nel finally. Ciò raramente è un comportamento
desiderato. Un esempio di un pessimo codice è mostrato nel listato successivo. In questo caso,
sebbene il costrutto try tenti di ritornare un valore false, l'uscita brusca presente nel blocco
finally, l'istruzione return, forza sempre a ritornare un valore true.
try I

// some code

return talse;

I tinally {
return true;
)

2.3.4 Valutare il ricorso allo shutdown hook


Lo shutdown hook (gancio di chiusura) è un meccanismo, introdotto con la versione Java 1.3,
che permette di intercettare e quindi gestire in qualche modo lo shutdown della macchina
virtuale Java. Ciò è possibile grazie al metodo addShutdownHook() della classe java.lang.Runtime, il
cui solo parametro è un oggetto di tipo Thread, che, come lecito attendersi, viene invocato du-
rante la terminazione della JVM. L'unico modo per evitare che questo modo sia chiamato con-
siste nell'eseguire il metodo Runtime.halt(). Questo meccanismo è molto utile per intercettare e
gestire chiusure brusche di JVM, inclusi application server, etc. Si tratta di un meccanismo
fondamentale in tutti quei casi in cui l'applicazione gestisca delle risorse da rilasciare corretta-
mente, come per esempio oggetti presenti solo in memoria da persistere sul database (meccani-
smi di write-behind), presenza di file temporanei, etc.

;V add the shutdown thread


Runtime.0efflunf/'me().addShutdownHook(new Thread() {

7 if the shutdown is in progress


public void run() |
// clean resources ...
I
I);

Chiaramente questo metodo poco potrebbe in caso di chiusura dell'applicazione dovuta a


mancanza di alimentazione elettrica: ma questo scenario in ambienti professionali è di fatto
impossibile.

2.4 Scrivere correttamente i metodi


Gli oggetti sono entità in grado di eseguire un insieme ben definito di attività che ne rappresen-
tano il comportamento. Queste attività, in termini implementativi, sono rappresentate dai metodi.
Poiché le leggi dell'OO, specie l'incapsulamento, prescrivono che le classi debbano celare al
mondo esterno la propria logica interna e in particolare le proprietà strutturali (attributi e
relazioni), ne segue che i metodi assumono un ruolo fondamentale: rappresentano l'interfaccia
propria delle classi, ossia gli elementi di un oggetto accessibile da parte di altri, il contratto tra
la classe che fornisce i servizi e le classi che li utilizzano. Pertanto, ogni oggetto possiede una
propria interfaccia (per così dire intrinseca) costituita da un insieme di comandi (i metodi),
ognuno dei quali esegue un'azione specifica. Un oggetto può richiedere a un altro di eseguire
una determinata azione inviandogli un "messaggio".

2.4.1 Assegnare ai parametri il tipo più generico possibile


Nel definire la firma dei metodi è consigliabile assegnare ai parametri formali un tipo che sia il
più generico possibile. Questa tecnica permette di creare un livello di astrazione tra metodi
invocanti e quelli invocati, schermandoli da eventuali variazioni di implementazione dei para-
metri attuali. In questo modo qualora vari il tipo di un oggetto passato come parametro, le
ripercussioni di tale variazione resteranno limitate grazie all'esistenza di metodi che si riferisco-
no al parametro per mezzo di una conveniente astrazione (tipicamente un'interfaccia). Chiara-
mente, il tipo deve essere il più generico tra quelli possibili. Come spiegato nella regola succes-
siva, è una pratica sconsigliata definire un tipo generico per poi doverne effettuare il down-cast
nell'implementazione del metodo per via della necessità di utilizzare metodi specifici di un tipo
discendente. Per esempio, è da evitare il caso in cui un oggetto java.util.Stack sia passato come
parametro di tipo java.util.List, e poi aver necessità di utilizzare il metodo pop, e quindi dover
eseguire un down-cast (Stack givenStack = (Stack) aList; ), per prelevare l'ultimo elemento inserito
e quindi il primo a dover essere reperito. Utilizzare un tipo generico è un principio è necessario
soprattutto nella realizzazione di metodi non privati.
Qualora un metodo manipoli una collezione, l'applicazione di questo principio equivale ad
asserire di riferirsi a tale oggetto per mezzo della relative interfaccia: List, Set e Map. Da notare
che liste e insiemi dispongono di un'interfaccia di livello di astrazione ancora superiore: Collection.
Anche il ricorso ad array (si ricordi che in Java gli array sono oggetti) può risultare utile soprat-
tutto per classi di utilizzo generico.
Per esempio, dovendo implementare un metodo della classe Portfolio atto ad aggiungere i
relativi strumenti finanziari, potrebbe aver senso implementare una versione che preveda come
parametro di input un array di strumenti ed eventualmente eseguirne X overloading utilizzando
l'intefaccia List, piuttosto di implementare una versione che preveda un ArrayList.
La firma dei metodi potrebbe assumere le seguenti forme:

public void addlnstruments(lnstrument[] instruments)

public void addlnstruments(List instruments)

2.4.2 Nell'assegnare il tipo a un parametro,


evitare di dover eseguire il down-casting
Si effettua un'operazione down-cast ogni qualvolta un oggetto trattato come un tipo antenato è
forzato a un tipo discendente. Pertanto si passa da un tipo più generale a uno più specializzato.
Giacché solo a tempo di esecuzione è possibile esaminare il tipo dell'oggetto fornito come
parametro al metodo, ne segue che il controllo di tipo non può essere effettuato a tempo di
compilazione ma solo in esecuzione, quindi il codice è meno type-safe. Ciò implica la possibili-
tà di generare fastidiosi errori a runtime. Si consideri per esempio l'improbabile implementazione
di un metodo atto a memorizzare un oggetto properties in uno stream di output.

public synchronized BooleanstoreProperties (OutputStream out, Dictionary properties)


throws lOException I
BufferedWriter awriter =
new BufferedWriter(
new OutputStreamWriter(out, "8859_1")
);

il (properties != null) I

if (properties linstanceof Properties) I


throw new HlegalParameterExceptionf'par. Properties not valid");
)

prop = (Properties) properties;

for (Enumeration e = prop.keys(); e.hasMoreElements();) I


String key = (String) e.nextElement();
String val = (String) get(key);
key = saveConvert(key, true);

val = saveConvert(val, false);


writeln(awriter, key + "=" + val);
I
)
awriter.close();
I

Come si può notare, dopo aver eseguito i controlli di rito, si esegue il cast (prop = (Properties)
properties;). Quello che si deve evitare, a meno che non sia strettamente necessario, è esattamen-
te quello che accade nel codice presentato: accettare un parametro generico e poi eseguirne il
down-casting. Questa pratica, come regola generale, è sconsigliabile poiché la firma del metodo
permette di dedurre, legittimamente, che il metodo preveda un tipo generico, mentre poi
l'implementazione ne richiede uno più specifico. Pertanto, l'implementazione del metodo ri-
chiede vincoli più stringenti di quelli sanciti dalla relativa firma, e quindi, ne viola il contratto.
Il down-casting però non è sempre evitabile. Per esempio è frequentissimo nell'im-
plementazione dei metodi equals. A un certo punto, infatti, è necessario passare dal tipo genera-
le Object a quello specifico per poter valutare l'uguaglianza dei vari elementi specifici. Da notare
che nel caso dell'equals però, questo è sia necessario per implementare una serie di metodi
generici (come per esempio indexOf, lastlndexOf, remove, etc. nelle liste), sia è consentito: non
avrebbe senso cercare di confrontare oggetti diversi e qualora si cercasse di fare ciò, il metodo
risponderebbe correttamente con un valore false.

public boolean equals(Object o) (


if (o == this)
return trae;

if (!(o instanceof List))


return false;

Listlterator<E> e1 = listlterator();
Listlterator e2 = ((List) o).listlterator();

while (e1 ,hasNext() && e2.hasNext()) I


Eoi = e1.next();
Object o2 = e2.next();
if ( !(o1 ==null ? o2==null : o1.equals(o2)) )
return false;
)
return !(e1.hasNext() || e2.hasNext());
)

Questa regola, in prima analisi, potrebbe risultare in contraddizione con quella precedente.
Da un'analisi più attenta si capisce che non lo è, giacché la regola precedente asserisce di sele-
zionare il tipo più generico tra quelli possibili.

2.4.3 Valutare l'opportunità di restituire un valore nuli per collezioni


Dovendo implementare un metodo che restituisca una collezione (array, Iterator, List, etc.), nel
caso in cui tale collezione sia vuota è spesso conveniente non restituire un valore nuli, ma un
oggetto vuoto. Questa tecnica permette di rendere più agevole il codice del metodo chiamante,
eliminando la necessità di effettuare la verifica esplicita per determinare l'eventuale presenza di
un valore nuli.
Le implementazioni delle collezioni Java, per esempio, nel caso in cui le relative istanze siano
vuote, restituiscono comunque un oggetto Iterator.
Nel codice, mostrato al punto precedente, relativo al metodo equals, si può notare che una
volta restituiti gli oggetti Listlterator, non è stato necessario eseguire il test per verificare se siano
o meno nulli; si può passare direttamente al ciclo while, e quindi il codice risulta più lineare.

2.4.4 Fare attenzione al passaggio delle collezioni


Dovendo implementare metodi generici che eseguono delle operazioni su oggetti di tipo colle-
zione, spesso viene la tentazione di inserire del codice in grado di inizializzare una collezione
qualora la relativa variabile sia nulla. Sebbene, in linea di principio ciò potrebbe avere senso, in
pratica non è possibile. Ciò per via del meccanismo Java di passaggio dei parametri per valore.
Nel caso delle collezioni, viene passata una copia dell'indirizzo (il riferimento originale è copia-
to in apposito e disgiunto elemento nello stack); pertanto, qualora questo sia variato, tale varia-
zione andrebbe persa: il nuovo valore non sarebbe copiato nella variabile iniziale.
Si consideri il seguente frammento di codice.

public class Test I

public statlc boolean validateUserData(User aUser, List<String> errors) I

boolean vaIResult = true;


if ( (allser.getNameO == null) || (aUser.getName().trim().length() == 0) ) f
errors = addError{"Hame not set", errors);

// all other validations: middlename. surname, date ot birth, etc

System.ouf.printlnf error size:"+errors.size());

return valResult;

public static List<String> addError(String error, List<String> errors) I

if (errors == null) {
errors = new ArrayList<String>();

errors, add(error);

return errors;
I

public static void main(String[] args) I

List<String> errors = null;


User userl = new User();

it ( ! validateUserData(userl, errors)) I
lor (int i = 0; i < errors.size(); i++) I
System.ouf.println(i+") error"+errors.get(i));
I
I else I
System. ouf.printlnfValidated!");

Il frammento di codice ha lo scopo di validare la corretta impostazione di alcuni elementi e di


memorizzare gli errori trovati (nel caso in questione un oggetto di tipo User). Un frammento del
genere (ma corretto) potrebbe risultare utile qualora sia necessario validare una serie di oggetti.
Si consideri per esempio il caso in cui questi oggetti siano impostati attraverso un'interfaccia
utente di un servizio internet: invece di comunicare all'utente un problema alla volta, evidente-
mente è più opportuno comunicare l'intera lista degli errori. Ciò servirebbe ad evitare all'uten-
te frustrazione e perdita di tempo dovuti a continue comunicazione avanti ed indietro con il
server. Al fine di ottenere ciè è necessario memorizzare i vari errori (sicuramente in una struttu-
ra un po' più complessa della semplice stringa) e proseguire con l'analisi del contenuto invece
di fermarsi al primo errore.
Eseguendo il programma, tuttavia, si assiste a un comportamento apparentemente "strano".
Infatti, da una parte viene stampato il messaggio: errar size:1 e dall'altro vi è una nuli point
exception. Quindi, da un lato la lista degli errori ha una stringa, dall'altra però non vi è alcun
elemento! Il problema è che, quando si crea la lista all'interno di un metodo, come aggiornan-
do un parametro, non si fa altro che variare il riferimento di una copia della variabile originale
che quindi viene persa al ritorno dall'invocazione.

2.4.5 Cercare di implementare metodi con un solo punto di ingresso


e un solo punto di uscita
Un valido principio di programmazione strutturata, incluso nella programmazione OO, affer-
ma che ogni metodo dovrebbe essere dotato di un solo punto di entrata e un solo punto di
uscita. Si tratta di una buona regola che facilita la comprensione e la manutenibilità del codice.
In effetti, tipicamente, non è sempre agevole aggiornare un metodo dotato di vari punti di
uscita (return), diversi da quelli semplici posti all'inizio del metodo come controllo dei parame-
tri, soprattutto quando la sezione da modificare è quella inclusa tra diversi punti di uscita.
Una legittima eccezione a questa regola è l'implementazione della serie iniziale di test dei
parametri di un metodo. In questo caso, eventuali ritorni anticipati sono permessi e anzi tendo-
no a semplificare l'implementazione e la lettura del metodo.

2.4.6 Considerare l'utilizzo di una variabile result per i metodi


che restituiscono un valore
Molti tecnici, specialmente quelli provenienti dal linguaggio C, trovano molto leggibile l'utilizzo
del nome convenzionale result per l'eventuale variabile che memorizza il risultato di ritorno dei
metodi. Ecco un frammento di codice atto a verificare l'uguaglianza di due array di oggetti.

public static boolean equals(Object[] o1, Object[] o2) I


boolean result = true;

,7 tlie same obiect or bolli nuli?


it (o1==o2) Il ( (o1==null) && (o2==null) )
return true;

intlength = o1 .length;
result = (o2.length == length);

int i=0;
while ( (result) && (i clength) ) [
result = ( (o1 == nuli) ? (o2 == nuli) : ( o[i].equals(o2[i]) );
i++;
I

return result;

2.4.7 Non utilizzare metodi deprecati


I metodi deprecati sono metodi ancora presenti nella versione del JDK che si sta utilizzando,
ma dei quali è stata pianificata l'eliminazione nelle versioni future. Pertanto è probabile che il
loro utilizzo crei seri problemi di portabilità verso nuove versioni del JDK.
2.4.8 Evitare ineleganti e inefficienti sequenze di instanceof
istanceof è un operatore Java utilizzato per verificare se un determinato oggetto sia o meno del
tipo atteso a tempo di esecuzione. La sintassi è molto semplice: if (objectReference instanceof type).
Sebbene il suo utilizzo sia strettamente necessario in una serie di situazioni, come per esempio
nell'implementazione di metodi generici come equals, in generale deve essere utilizzato con
parsimonia e possibilmente evitato.
Scott Mayers, nel suo libro Effective C++, si esprimeva, in maniera fin troppo severa, come
segue: "Ogni volta che ti trovi a scrivere del codice nella forma 'se questo oggetto è di tipo Ti,
allora fai qualcosa, se il suo tipo è T2 allora fai qualcos'altro', prenditi a schiaffi". Sebbene
Mayers sia un po' duro, in effetti molto spesso ciò può essere tranquillamente evitato ricorren-
do alle leggi del polimorfismo e dell'overriding.
In ogni modo, qualora sia assolutamente necessario eseguire un down cast (da un tipo più
generale a uno più specifico, per esempio da List ad ArrayList) è sempre opportuno schermarlo
con un test di tipo utilizzando appunto istanceof... Sempre che poi si sappia come gestire il caso
in cui questo test restituisca un valore falso.

2.5 Implementare attentamente i metodi "accessori"


e "modificatori" (get/set)
I metodi accessori (getXXX(), ÌsXXX(), hasXXX()) sono funzioni implementate per accedere al valore
degli attributi di un oggetto, mentre i metodi modificatori (SetXXX()) sono implementati per
modificare il valore di questi attributi e quindi variano lo stato del relativo oggetto.
L'utilizzo dei metodi accessori e modificatori è stato, soprattutto in passato, oggetto di appassionati
dibattiti. In particolare, vi sono tecnici contrari al loro utilizzo motivato dal fatto che renderebbe il
codice meno efficiente e che la loro codifica non sia un brillante investimento del tempo a
disposizione. Si tratta di argomentazioni la cui validità è abbastanza opinabile. In effetti, si constata
che per asserire che questi metodi generano colli di bottiglia bisognerebbe riuscire a produrre
codice estremamente efficiente, che non acceda a risorse I/O, in cui ogni dettaglio presenti un
elevato grado di ottimizzazione, etc. Cosa, ovviamente, raramente possibile. Per quanto riguarda
un eventuale migliore l'utilizzo del tempo a disposizione, è sufficiente notare che gli I D E moderni
sono in grado di generare automaticamente questi metodi corredati da opportuni commenti JavaDoc.
In questo testo, questi metodi sono consigliati perché aumentano la leggibilità del codice,
concorrono a renderlo più robusto, ne semplificano la manutenibilità, e cosi via.

2.5.1 Insistere sull'utilizzo di metodi accessori/modificatori


Come riportato sopra, l'utilizzo dei metodi accessori e modificatori aumenta la leggibilità del
codice, concorre a renderlo più robusto (ogni attributo ha pochi e ben definiti punti di acces-
so) e ne semplifica la manutenibilità. Pertanto, è sempre opportuno incapsulare gli attributi di
una classe assegnando loro visibilità privata e implementando opportuni metodi di accesso e
modifica.

2.5.2 Utilizzare i metodi accessori per campi soggetti a validazione


L'utilizzo dei metodi modificatori è necessario in tutti quei casi in cui l'insieme dei valori che
un campo può assumere è vincolato. Il ricorso a tali metodi serve per evitare di dover ripetere
la stessa sequenza di validazione in diverse parti del codice e per semplificare la manutenzione
del codice, qualora si renda necessario dover variare la procedura di validazione. Ecco un
esempio del metodo setGender di una classe Person.

public void setGender(char aGender) |


il (aGender == nuli)
thorw new IHegalArgumentExceptlonfGender = nuli");

aGender = Character.toUpperCase(aGender);
If (aGender != 'M') && (aGender != 'F') (
thorw new NlegalArgumentExceptionfGender = "'+aGender+ );
)

gener = aGender;
)

2.5.3 Utilizzare i metodi modificatori per campi la cui variazione


può influenzare il valore di altri
L'utilizzo dei metodi accessori è assolutamente necessario qualora l'aggiornamento del valore
di un campo influenzi il valore di altri campi o richieda l'esecuzione di determinate azioni. In
questo caso il ricorso a metodi accessori permette di realizzare un codice più chiaro, più robu-
sto e permette di circoscrivere eventuali effetti "ondulatori" sul valore dei campi. Nel listato
seguente c'è l'implementazione del metodo setValue del componente swing JProgressiveBar. Questo
metodo permette di impostare il valore attuale della barra di progressione e tipicamente richie-
de il ridisegno del componente.

public void setValue(int n) {


// updates the underlying model
BoundedRangeModel brm = getModelf);
Int oldValue = brm.getValue();
brm.setValue(n);

Il (accessibleContext != nuli) (
// forces a repaint
accessibleContext.flrePropertyChange(
AccessibleContext.ACCESSIBLE_VALUE_PROPERTY,
new Integer(oldValue),
new lnteger(brm.getValue())
);
)
I

Un altro interessante esempio è fornito dalla classe astratta Buffer del package NIO, con 1'
implementazione del metodo limit della classe java.nio.Buffer.
" Sets this buffer's limit. If the position is larger lhan the new limit
* then It Is set to the new limit. If the mark is defined and larger than
' the new limit then It Is discarded. </p>

" @param n e w L l m i t The new l l m l l value: m u s t be non-negative


and no larger than this buffer's capacity
* ©return This buller
" ©throws NlegalArgumentException If the p r e c o n d i t i o n s on
< t t > n e w L l m i t < / t t > do not hold
* /

public final Buffer limit(int newLlmit) I


if ((newLimit > capacity) || (newLimit < 0))
throw n e w NlegalArgumentException();

limit = newLimit;
if (position > limit)
position = limit;
if (mark > limit)
mark = -1;
return this;

2.5.4 Implementare metodi accessori/modificatori multipli per collezioni


Il ricorso all'utilizzo dei metodi accessori/modificatori, come illustrato precedentemente, pre-
senta una serie di importanti vantaggi, come aumento del livello di incapsulamento, maggiore
leggibilità del codice, semplificazione della manutenibilità, e così via.
Tuttavia, nel caso di collezioni, quali array, array dinamici, vettori, tavole hash etc., il ricorso
a questi metodi richiede l'implementazione di un insieme più articolato di metodi accessori/
modificatori.
Da tener presente che metodi di questo tipo sono inclusi in classi diverse da quelle degli
elementi che rappresentano. Per esempio il metodo di addOrderLine, atto ad inserire istanze di
tipo OrderLine nella classe Order, è, ovviamente, un metodo di quest'ultima. Pertanto nel nome
dei metodi get/set è necessario ripetere il soggetto della collezione.
Come esempio, si consideri il caso del pattern Observer. In particolare, si consideri un ogget-
to "osservato" che accetti una lista di oggetti di tipo Listener a cui inviare le segnalazioni di
cambiamento del proprio stato.
Vediamo ora una serie di pattern per l'implementazione dei metodi accessori/modificatori di
collezioni: al nome del metodo seguono alcuni esempi e una descrizione dello stesso.

setCollection()

Esempi:

setObservers(Observer[] observers)
setLineOrders(LineOrder[] lineOrders)
Questo metodo esegue due attività molto importanti: azzera la collezione e vi imposta i
valori specificati. Questi dovrebbero essere specificati utilizzando il tipo di dati più semplice
possibile: array o apposita interfaccia della collezione usata.

getCollectionf)

Esempi:

getObserversQ
getLineOrders()

Questo metodo restituisce la specifica collezione. In modo equivalente a quanto detto per il
corrispondente metodo set, anche in questo caso è opportuno riportare il tipo di dati più sem-
plice possibile. Ottimi canditati sono Iterator, un apposito array, l'interfaccia Collection, e così via.

addCollectionElement()

Esempio:

getObserverElement(Observer aObserver)
getLineOrderElement(LineOrde aLineOrder)

oppure:

getObserverElement(String observerld)
getLineOrderElement(String lineOrderld)

Questo metodo ha l'obiettivo di restituire uno specifico elemento presente nella collezione.
Si sarebbero potute utilizzare forme più contratte, tipo getLineOrder, ma queste si prestano a
generare confusione con il metodo get della collezione.

removeColleclionElementO

Esempio:

removeObserverElement(Observer aObserver)
removeLineOrderElementfLineOrde aLineOrder)

oppure:

removeObserverElement(String observerld)
removeLineOrderElement(String lineOrderld)

Questo metodo ha l'obiettivo di rimuovere uno specifico elemento dalla collezione. Anche
in questo caso si sarebbero potute utilizzare forme più contratte, tipo removeLineOrder, ma si è
preferita la forma più lunga onde evitare confusione.
setCollection()

Esempi:

clearObservers()
clearLineOrders()

Compito di questo metodo è rimuovere tutti gli elementi della Collection.

2.6 Utilizzare con oculatezza la classe


java.lang.Runtime
Ogni applicazione Java è fornita di una sola istanza di questa classe i cui metodi le permettono di
accedere all'ambiente di esecuzione dell'applicazione. Per quanto questi metodi possano fornire
una serie di utili servizi, il relativo utilizzo pone seri problemi di portabilità.

2.6.1 Valutare attentamente l'utilizzo dell'istruzione Runtime.exec


L'istruzione Runtime.exec permette di richiedere al sistema operativo di eseguire in un apposito
processo il commando specificato nell'argomento. Sebbene si tratti di un'istruzione indubbia-
mente potente, il suo utilizzo può porre una serie di problemi di portabilità. Per esempio, si
consideri il caso in cui la si utilizzi per eseguire un'applicazione specifica Windows, come per
esempio Cale, non disponibile in altri ambienti. Tale utilizzo, chiaramente, limita la portabilità
dell'applicazione in ambienti come Unix. Le direttive Sun raccomandano l'utilizzo di Runtime.exec
solo qualora i seguenti criteri siano soddisfatti:

• l'invocazione deve essere un risultato diretto di un'azione specifica dell'utente e pertan-


to deve essere a conoscenza del fatto che si sta eseguendo un programma diverso, come
per esempio un browser Internet;
• l'utente deve essere messo in grado di selezionare, a tempo di esecuzione o direttamente
nel processo di invocazione dell'istruzione, il programma da eseguire;
• eventuali problemi nell'esecuzione dell'istruzione devono essere gestiti chiaramente e
limpidamente, specialmente se dovuti all'assenza dell'applicazione di cui si è tentato di
invocare l'esecuzione;
• l'utilizzo dell'istruzione è indipendente dalla piattaforma e perciò l'applicazione invocata
è disponibile per le diverse piattaforme, come l'invocazione del compilatore Java: javac.

2.7 Implementare i metodi Object


La classe java.lang.Object, antenata di tutte le classi Java, definisce una serie di metodi di servizio
(per esempio toString, equals, hasheode, clone, etc.) che, per quanto sia noioso, in molti contesti è
necessario implementare al fine di assicurare il corretto funzionamento delle API Java. Per esem-
pio, il metodo equals è utilizzato dalle collezioni Java per verificare l'uguaglianza di due oggetti,
per rimuovere oggetti dalla collezione, etc. Pertanto, qualora si vogliano utilizzare propriamente
le collezioni Java è necessario ridefinire il comportamento definito da questo metodo.
2.7.1 Implementare il metodo toString
Ogni classe dovrebbe implementare la propria versione del metodo toString (effettuarne
l'overriding). Questo è definito nella classe java.lang.Object e quindi è ereditato da tutte le classi
Java. Il suo compito è di riprodurre in una stringa lo stato dell'oggetto. Questa stringa dovreb-
be essere sintetica ma descrittiva.
L'implementazione di base del metodo toString è riportata nel frammento di codice del listato
seguente.

public String toString() I


return getClass().getName() + + Integer.toHexString(hashCodeO);
I

Il metodo toString è molto utile per una serie di motivi, come eseguire il debug delle applica-
zioni, specie di applicazioni multi-threading, scrivere su opportuni file di log lo stato dei vari
oggetti, etc.
Tipicamente, l'esecuzione di questo metodo non è vincolata da forti requisiti di performan-
ce, però in alcuni contesti (per esempio applicazioni multi-threading che sfruttano il metodo
per effettuare il log dell'applicazione) potrebbe diventarlo. In questi casi è opportuno ricorrere
alla classe java.util.StringBuffer o lang.StringBuilder.

2.7.2 Implementare il metodo equals pet le classi "dati"


Analogamente al metodo toString, anche la versione embrionale del metodo equals è definita
nella classe java.lang.Object. Il suo compito è quello di verificare se l'oggetto in questione è, o
meno, uguale a quello fornito.

public boolean equals(Object obj) I


return (this == obj);

L'implementazione del metodo deve soddisfare le seguenti proprietà:

• riflessiva: x.equals(x) = true;


• simmetrica: x.equals(y) = true <=> y.equals(x) = true;
• transitiva: x.equals(y) = true and y.equals(z) = true => x.equals(z) = true;
• di consistenza: se X.equals(y) = true allora questo deve essere vero per un numero qualsivoglia
di invocazioni di tale metodo (a condizione che le due istanze non siano soggette a modifiche).

L'implementazione di questo metodo è fondamentale per tutte quelle classi disegnate quasi
esclusivamente per incapsulare dati, come per esempio Value Object e Data Transfer Object. Questo
perché il metodo è fortemente utilizzato nelle collezioni standard. Per esempio in
Collection.contains, Map.containsKey, Vector.indexOf, e t c . etc.
Secondo le direttive Java, se una classe ridefinisce il metodo equals, allora deve ridefinire
anche il metodo hashCode. Vediamo un esempio di implementazione del metodo equals nella
classe Array.

public static boolean equals(long[] a, long[] a2) {


if (a == a2)
return true;

if ( a == nuli || a2 == nuli)
return false;

int length = a.length;


if (a2.length != length)
return false;

for (int i=0; klength; i++)


if (a[i) != a2[i])
return false;

return true;

2.7.3 Implementare il metodo hashCode per le classi "dati"


La piena comprensione del metodo hashCode e della sua implementazione richiede la conoscenza
di alcune nozioni base della teoria dell'hashing. Pertanto, si consiglia di leggere l'Appendice (. ad
esso dedicata al fine di approfondire quanto riportato di seguito.

Quanto detto per il metodo equals in merito al corretto utilizzo delle collezioni Java è valido
per il metodo hashCode. Per esempio gli oggetti inseriti in un hashtable devono necessariamente
ridefinire il metodo hashCode perché questo è utilizzato dai vari metodi della collezione, come
per esempio get. Pertanto, qualora questo metodo non fosse definito per una determinata clas-
se, questa potrebbe creare problemi se utilizzata come chiave in collezioni come Hashtable e
HashMap.
Per capire appieno l'importanza del metodo hashCode è necessario ricordare che il valore
hash di un oggetto, utilizzato come chiave in una collezione Hashtable o HashMap, serve per
determinarne la posizione dei relativi elementi all'interno di tali collezioni. Poiché però esiste
anche il problema delle collisioni (elementi diversi possono generare uno stesso valore hash),
ne segue che il valore hash dell'elemento chiave è utilizzato per accedere alla posizione delle
liste di collisione (liste in cui sono memorizzati elementi diversi le cui chiavi danno luogo allo
stesso valore hash). In effetti, una collezione hash non è altro che un array di liste di collisione.
Una volta determinata la lista di collisione di un elemento, l'elemento stesso è individuato
utilizzando il metodo equals. Ma vediamo l'implementazione del metodo get della classe
java.util.HashMap.

public V get(Object key) I

Object k = maskNull(key);
ini hash = hash(k);

int i = indexForfhash, table.length);


Entry<K,V> e = table[i];

while (true) I
il (e == null)
return null;
il (e.hash == hash & & eq(k, e.key))
return e.value;
e = e.next;
I

static int indexFor(int h, int length) I


return h & (length-1);
]

static int hash(Object x) I


int h = x.hashCode();

h += ~(h « 9);
h ( h » > 14);
h += (h « 4);
h A = (h » > 10);

return h;

Come si può notare il valore hash della chiave è utilizzato per accedere alla lista dei conflitti (i =
indexFor(hash, table.length)) che viene scorsa finché o la lista termina e quindi l'elemento non è
presente, oppure finché l'elemento considerato e quello passato come parametro hanno lo stesso
valore di hash della chiave e questa è esattamente quella cercata ((e.hash == hash && eq(k, e.key))).
Come si può notare, la classe java.util.HashMap implementa un metodo hash (hash(Object x)) da
utilizzarsi per irrobustire il valore di hash restituito dagli elementi forniti. Si tratta di una tecnica
atta a sopperire a problemi generati da eventuali metodi hashCode non particolarmente brillanti.

Le proprietà dell'implementazione del metodo hashCode sono:

• consistenza; l'invocazione del metodo, per un dato oggetto, deve ritornare lo stesso valore
intero a meno che l'oggetto stesso non sia modificato (in particolare non siano modificati gli
attributi utilizzati per la generazione del valore). Chiaramente, se così non fosse, sorgerebbero
dei problemi per l'individuazione degli elementi memorizzati in strutture hash;
• uguaglianza; se x.equals(y) = true <=> X.hashCodeO = y.hashCodef). Questo implica, tra l'altro,
che gli attributi utilizzati nel m e t o d o equals devono essere utilizzati anche
nell'implementazione del metodo hashCode.

Da notare che non è richiesto che x.equals(y) = false <=> x.hashCode() != y.hashCode(). Questa
proprietà sarebbe molto utile da ottenere; purtroppo il calcolo delle probabilità insegna che
funzioni hash in grado di non generare conflitti, il cui dominio è il tipo int, sono semplicemente
non fattibili!

2.7.4 Implementare il metodo clone


Il metodo clone è utilizzato per restituire una copia (un clone appunto) dell'oggetto in cui è
definito. L'implementazione di questo metodo è più complessa di quanto possa sembrare. In-
fatti, se l'oggetto ha attributi che sono oggetti a loro volta, anche questi dovrebbero essere
clonati (in questo caso si parla di "copia profonda", deep copy). Questa regola, ovviamente,
non è valida per gli oggetti collezione, di cui si accetta la generazione di una copia dell'oggetto
collezione che però si riferisca alle medesime istanze degli oggetti memorizzati nella collezione
di partenza i quali, quindi, non vengono copiati (in questo caso si parla di "copia superficiale",
shallow copy). Ecco l'implementazione del metodo clone nella classe java.util.ArrayList.

public Object clone() (


try<
ArrayList<E> v = (ArrayList<E>) super.clone();

v.elementData = (E[])new Object[size];


System.arraycopy(elementData, 0, v.elementData, 0, size);

v.modCount = 0;

r a t u m v;

) catch (CloneNotSupportedException e) I
// this shouldn't happen, since we are Cloneable
throw new lnternalError();
I
I

2.8 Porre attenzione alla chiusura degli stream


La libreria Java per l'Input e Output (I/O, java.io) è basata sul concetto di flussi {¡(reami. Si tratta
di un'astrazione utilissima che permette di acquisire e di scrìvere informazioni indipendentemente
dalla sorgente dati, che può essere un file, una connessione remota, la console, e così via. Inoltre,
gli stream possono essere tutilizzati congiuntamente a servizi aggiuntivi, come la compressione, la
crittografazione, la traslazione, e così via.

2.8.1 Chiudere sempre gli stream


Gli stream rappresentano risorse la cui corretta cessazione richiede l'esplicita chiusura invo-
cando il corrispondente metodo dose. Nelle classi di output l'esecuzione di questa funzione
include anche l'esecuzione del metodo di flush.
Se un determinato codice usa diversi oggetti stream (p.e.: uno di input, uno di output e uno per
la notifica di particolari scenari di errore), è necessario che la sezione dedicata alla chiusura gesti-
sca la cessazione dei tre gestendo opportunamente possibili eccezioni. I listati seguenti mostrano
due versioni di codice per la corretta chiusura degli stream. La prima è la versione classica mentre
la seconda è possibile dalla versione Java 5.0 grazie all'introduzione dell'interfaccia Closeable, che
definisce un solo metodo: close, la cui semantica prevede la chiusura dello stream e il rilascio di
tutte le risorse ad esso associate; se lo stream è già chiuso, l'invocazione del metodo non sortisce
alcun effetto. Ecco un esempio di chiusura di stream classica con codice delle versioni precedenti.

InputStream in = nuli;
OutputStream out = null;
OutputStream err = null;

try {
in = new FilelnputStream(source);
out = new FileOutputStream(dest);
err = new FileOutputStream(errStr);

// do some work

I finally {

If (in 1= null) {
try (
in.close();
} catch (lOException ex) I
// log this exception
I
1

If (out != null) I
try!
in.close();
) catch (lOException ex) {
// log this exception
I
I

II (err != null) I
try {
in.close();
I catch (lOException ex) {
// log this exception

}
Ed ecco un esempio di chiusura di stream con con la nuova versione Java 5.

try I

in = n e w FilelnputStream(source);
out = new FileOutputStream(dest);
err = new FileOutputStream(errStr);

// do s o m e w o r k

) finally I
closeAndLogException(in);
closeAndLogException(out);
closeAnd LogException(err) ;

' close the given s t r e a m


' If the given s t r e a m is already closed than it does not do a n y t h i n g
' © p a r a m s t r e a m s t r e a m to close
' * /

private static void closeAndLogException(Closeable stream) I


if (stream 1= null) I
try!
stream.close();
) calch (lOException ioe) I
LOGGER.warning(ioe);
I
I
I

2.8.2 Non fare affidamento sul metodo finalize


Ogni classe Java eredita il metodo finalize dalla classe antenata di tutte: java.lang.ObjeCt. Tale
metodo è usato a volte erroneamente per rilasciare le risorse utilizzate dall'oggetto. Secondo le
specifiche Sun, il metodo finalize di un oggetto è invocato dal garbagc collcctor nel momento in
cui libera la memoria occupata. Pertanto non vi è alcuna garanzia di quando e in che ordine il
metodo finalize sarà invocato. Inoltre, questo potrebbe non essere mai invocato fino alla chiusura
dell'intera applicazione, evitando quindi che altri oggetti possano accedere alle risorse bloccate
da un oggetto dereferenziato. Infine, affidarsi all'esecuzione del metodo finalize potrebbe creare
problemi di portabilità. In effetti, la politica utilizzata dal garbagc collcctor dipende dal particolare
algoritmo adottato per la relativa implementazione, che quindi dipendende, in ultima analisi,
dalla specifica implementazione J V M in cui l'applicazione è in esecuzione.

Data l'imprevedibilità intrinseca del metodo, questo non è idoneo per implementare proce-
dure di "pulizia" delle risorse allocate da un oggetto: queste dovrebbero essere rilasciate espli-
citamente dopo l'utilizzo o, eventualmente, riconsegnate ad un apposito pool. L'utilizzo del
metodo finalize, in questo senso, potrebbe essere utilizzato solo come precauzione qualora si
implementi un framework estendibile. La procedura di rilascio delle risorse deve essere affida-
ta a un apposito metodo: dose, dispose, etc. che il client dell'oggetto deve chiamare esplicita-
mente per assicurare il corretto rilascio delle risorse.

2.8.3 Valutare l'utilizzo di oggetti Buffer


Per ogni tipologia di flusso (stream), le API Java offrono la possibilità di utilizzare una versione
dotata di buffer. Alcuni esempi di questa tipologia di classi sono: BufferedlnputStream,
BufferedOutputStream, BufferedReader, BufferedWriter e StringBufferlnputStream. P e r t a n t o , ogniqualvolta
si abbia la necessità di lavorare con gli stream, soprattutto per trasferimenti di dati con file, reti
etc., è consigliabile ricorrere all'utilizzo delle versioni dotate di buffer al fine di migliorare le
performance.
Da notare che con la versione JDK 1.4, Java è stato dotato di un nuovo package di I/O denomi-
nato NIO (New Input Output). In particolare, questo package fornisce una serie di classi per la
gestione delle operazione di I/O basate sul concetto di buffer modellato dalla classe astratta
java.nio.Buffer e specializzato in diverse forme. In base alle specifiche Sun, si tratta di
un'implementazione più efficiente, più scalabile e in grado di gestire l'intero set di caratteri. Per-
tanto, qualora sia necessario utilizzare oggetti buffer, è consigliato valutare il ricorso alla API NIO.

2.9 Tipi numerici


Tipi numerici: tutti i programmi ne fanno uso e tutti sembrano avere le idee piuttosto chiare...
Eppure, eppure analizzando applicazioni in campo finanziario, è possibile individuare una
serie di problemi ricorrenti legati al trattamento di informazioni numeriche. Il problema di
fondo, spesso ignorato, è relativo al fatto che la rappresentazione in virgola mobile è intrinseca-
mente imprecisa. Pertanto, qualora sia necessaria una certa precisione e/o quando anche una
piccola imprecisione possa generare grosse inconsistenze passando attraverso una serie di for-
mule (si immagini il valore di un'azione moltiplicata per milioni), allora è necessario ricorrere a
rappresentazioni assolutamente più precise.
Come se non bastasse, il problema dei campi numerici e della precisione è abbastanza sub-
dolo: l'errore potrebbe non verificarsi per lungo tempo fino poi a presentarsi al momento in cui
sia necessario dover spiegare dei risultati numerici inattesi, magari una riconciliazione fallita
con un cliente istituzionale che abbia investito miliardi di dollari. In questo caso, l'esperienza
insegna, che solo dopo aver investito moltissimo tempo in investigazioni, analisi e studi, si
riesce a capire che il problema è proprio intrinseco alla rappresentazione in virgola mobile.
Si tratta di problemi relativi ai numeri reali presenti in quasi la totalità dei linguaggi di pro-
grammazione moderni (più precisamente in tutti i linguaggi che implementano lo standard
IEEE754). Ma vediamo qual è la questione e come si riflette sul linguaggio Java. Java dispone
di due tipi base per il trattamento dei tipi reali: float e doublé (con le relative classi di wrapping:
java.lang.Float e java.lang.Doublé), la cui semantica è sancita dallo standard IEEE 754 - Binary
Floating-Point Arithmetic (Aritmetica binaria in virgola mobile). La tabella 2.1 mostra le diffe-
renze principali tra questi due tipi.
La parte dedicata alla mantissa è tipicamente indicata come precisione: questo per il sempli-
ce motivo che, disponendo di un numero finito di cifre nella mantissa, è possibile rappresenta-
re esattamente solo un numero finito di valori frazionari.
Float Double
Dimensione in byte 4 8
Bit segno 1 1
Bit esponente 8 11
Bit mantissa 32 52
Max numero positivo 3.40282347e+38 1.79769313486231570e+308
Min numero positivo 1.40239846e-45 4.94065645841246544e-324

Tabella 2.1 - Le differenze principali tra il tipi base float e il doublé.

2.9.1 Valutare attentamente la precisione richiesta


Come visto, la rappresentazione in virgola mobile standard dispone di un numero finito di
cifre, organizzate secondo una ben definita struttura (segno, mantissa ed esponente normaliz-
zato): ciò fa sì che non tutti i numeri reali decimali siano rappresentabili esattamente con la
notazione della virgola mobile binaria. Spesso diversi sviluppatori pensano erroneamente che
questa insidia si manifesti solo per numeri particolarmente grandi e/o con molte cifre decimali.
Per comprendere quanto ciò sia errato, basti considerare il listato riportato poco sotto. Dalla
relativa analisi ci si aspetterebbe che l'esecuzione generi in uscita il valore 1.0; in effetti il codice
non fa altro che sommare 10 volte il valore 0.1. Purtroppo non va a finire così: il risultato
restituito è 1.0000001. Si noti che qualora realNum sia dichiarato doublé, la situazione migliore-
rebbe notevolmente, si passerebbe da un errore dell'ordine IO7 ad uno dell'ordine di 10 16;
tuttavia ciò non risolverebbe il problema, e infatti il risultato prodotto sarebbe
0.9999999999999999. Quindi, sebbene il tipo doublé offra un'approssimazione decisamente mi-
gliore, presenta comunque dei margini di imprecisione. Ecco un semplice frammento di codice
atto a mostrare le insidie del tipo float. Infatti, il risultato prodotto è 1.0000001.

float realNum = 0.0F;

for (int i=1; i <= 10; i++) (


realNum += 0.1;
)

System.ouf.printlnfResult : "trealNum);

La domanda da porsi è se ciò rappresenti un vero problema. La ovvia risposta è: dipende dal
tipo di applicazione. Per esempio, qualora il programma debba calcolare la distanza tra due
città, verosimilmente la probabilità di commettere un errore dell'ordine di qualche centimetro
o anche decimetro, raramente risulterebbe problematica. Si consideri invece il caso di applica-
zioni finanziarie in cui determinati valori siano utilizzati in calcoli abbastanza complessi da
ripetersi per milioni di transazioni giornaliere. In questi contesti, l'errore introdotto dalla per-
dita di precisione tenderebbe ad amplificarsi fino a poter generare problemi.
Nei casi in cui sia molto importante gestire cifre a elevata precisione, non resta altro da fare
che utilizzare classi come java.math.BigDecimal. Questi oggetti, come suggerisce il nome, utilizza-
no direttamente rappresentazioni decimali (array di cifre decimali), e pertanto offrono una
serie di vantaggi, come la possibilità di rappresentare numeri di lunghezza variabile (virtual-
mente infinita), l'esatta rappresentazione (tutta la precisione richiesta), etc. Gli inevitabili svan-
taggi sono "grammatica" più complessa e perdita di prestazioni (ciò principalmente perché
richiedono maggiore occupazione di memoria; le operazioni non possono avvenire tra registri
della CPU e questi oggetti sono immutabili). Vediamo un frammento di codice equivalente a
quello precedente, in cui il tipo float è sostituito da BigDecimal.

BigDecimal realNum = new BigDecimalf'O");

for (ini i=1; i <= 10; i++) (


realNum = realNum.add(new BigDecimal("0.1 "));

System.ouf.printlnfResult : "+reall\lum);

Per quanto concerne la complessità della notazione, tutto sarebbe più facile se in Java venisse
introdotta la possibilità di eseguire 1 'overloading degli operatori, come in C++.

2.9.2 Valutare attentamente il ricorso a strategie diverse


Una strategia spesso utilizzata per aggirare il problema della limitata precisione dei tipi float e
doublé consiste nell'utilizzare tipi long, spostando accuratamente la virgola. L'espediente consiste
nel moltiplicare per 10 elevato al numero di posti decimali necessari durante la memorizzazione
del numero per poi dividere per lo stesso fattore durante il reperimento. Così per esempio il
numero 875000.45, moltiplicato per IO2 verrebbe rappresentato come il long 87500045. Per
quanto questo espediente offra diversi vantaggi in alcuni scenari, crea seri problemi fino a giun-
gere, in casi estremi, a porre seri limiti all'espandibilità/correttezza del sistema. Si consideri il
caso in cui sia necessario considerare diverse cifre decimali. In questi casi sarebbe necessario
implementare movimenti di 6, 7 o anche più posti decimali. Ora considerando che il tipo long è
in grado di rappresentare numeri molto grandi (8 byte con segno che permette di rappresentare
numeri da -9,223,372,036,854,775,808 a +9,223,372,036,854,775,807), una precisione di 7 ci-
fre decimali, ridurrebbe la capacità del tipo long al seguente intervallo: da circa -922,337,203,685
a +922,337,203,685. Sebbene ciò a prima vista, potrebbe sembrare ancora conveniente, in alcu-
ni ambienti (come per esempio applicazioni finanziarie) in cui sia necessario manipolare diverse
monete e grandi numeri, potrebbe esserlo molto meno. Basti prendere in considerazione il cam-
bio Euro e Yen (nel momento in cui viene redatto questo paragrafo): 100 EUR = 15,570.60 JPY.
Il problema in questi casi è che l'errore potrebbe emergerebbe dopo alcuni mesi/anni del siste-
ma in esercizio e dopo aver memorizzato molti dati in database, fogli di calcolo, e così via.

2.9.3 Considerare la presenza di numeri speciali in virgola mobile


Lo standard IEEE 754 oltre a stabilire la struttura e semantica dei numeri a virgola mobile, si
occupa di definire delle particolari entità, come per esempio: NaN (Not a Number), -0, +infinity
(infinito positivo) e -infinity (infinito negativo). Questi elementi, la cui rappresentazione è affi-
data a configurazioni riservate, presentano delle peculiarità, alcune delle quali sono illustrate di
seguito. Nella tabella 2.2 sono riportate alcune proprietà di questi speciali numeri in virgola
mobile.
La situazione è complicata dal fatto che alcune regole cambiano a seconda se si utilizzi i tipi
base o i corrispondenti oggetti di wrapping. Per esempio, qualora si utilizzino due float, allora
Espressione Risultato
Math.sqrt(-I.O) NaN
0.0 / 0.0 NaN
1.0 / 0.0 Infinity
-1.0 / 0.0 -Infinity
NaN + 1.0 NaN
Infinity + 1.0 Infinity
Infinity + Infinity Infinity
NaN > 1.0 false
NaN == 1.0 false
NaN < 1.0 false
NaN == NaN false

Tabella 2.2- Alcune particolari espressioni relative ai numeri speciali in virgola mobile. Secondo le
specifiche standard, NaN non è uguale a nessun altro numero in virgola mobile, incluso sé stesso.

la comparazione tra due variabili impostate a NaN produce un valore false, mentre la stessa
comparazione (metodo equals) tra due oggetti Float produce un risultato true. Ancora, mentre 0
e -0 sono considerati uguali in termini di tipi base, lo stesso non vale per gli oggetti. In questo
caso è necessario eseguire le comparazioni utilizzando il metodo compareTo.
La presenza di questi elementi impone diverse cautele durante l'implementazione di metodi
che manipolano tipi reali. Per esempio:

• Se (realel < reale2) allora !(reale1 >= reale2) è vero solo quando entrambi i numeri non
hanno valore NaN
• reale == reale è vero (true) solo quando reale non ha un valore NaN
• realel + reale2 - reale2 = realel solo quando reale2 non è né un infinity né assume il valore NaN

2.9.4 Porre attenzione alle comparazioni con i numeri reali


Dopo avere esaminato il comportamento dell'entità NaN nelle comparazioni, i vari
arrotondamenti e gli errori di precisione, dovrebbe essere ormai chiaro che eseguire la compa-
razione tra numeri reali nasconde diverse insidie, tanto che alcuni autori giungono a suggerire
di evitare completamente le comparazioni tra numeri reali! Questo chiaramente è un suggeri-
mento un po' estremo e non sempre possibile.
Tuttavia, date le limitazioni delle comparazioni dei numeri in virgola mobile, è possibile
ottenere implementazioni robuste solo ricorrendo a opportune strategie. Per esempio, una buona
tecnica per valutare se due numeri reali siano uguali o meno consiste nel verificare che la loro
distanza sia prossima allo zero, ossia che la differenza appartenga a un intervallo molto piccolo.
Un'altra strategia necessaria per eseguire il confronto di numeri reali, dovuta soprattutto per
via del fatto che la configurazione NaN non ha proprietà di ordinamento, consiste nell'assicurar-
si che i valori appartengano all'insieme di validità desiderato, piuttosto che cercare di escludere
valori non validi (il frammento successivo mostra un esempio da evitare e l'equivalente consi-
gliato). Come di consueto, ecco su due colonne, degli esempi di codice atti ad evitare
l'impostazione di valori reali indesiderati.
Codice sconsigliato Codice consigliato

public void setDepth(float aDepth) {


public void setDepth(lloat aDepth) I It ( (aDepth >= 0) && (aDepth <= MAX_VAL) )
if (aDepth < 0) depth = aDepth;
throw new IHegalArgumentException(...); else
depth = aDepth; throw new HlegalArgumentException(...);

2.9.5 Non utilizzare float e doublé con BigDecimal


Come visto nei paragrafi precedenti, le rappresentazioni float e doublé sono intrinsecamente im-
precise. Pertanto qualora sia necessario disporre di un'elevata precisione è necessario ricorrere a
classi quali BigDecimal. Ciò rappresenta una condizione necessaria ma non sufficiente. In effetti,
qualora si ricorra a queste classi è importante evitare ogni passaggio per i tipi base float e doublé,
incluso la restituzione dei valori finali: si correrebbe il rischio di non risolvere il problema. Per
esempio utilizzando i tipi float e doublé nei costruttori degli oggetti BigDecimal (BigDecimal(Biglnteger
vai), BigDecimal(double vai), BigDecimal(String vai), etc) si correrebbe il rischio di introdurre un vizio
di precisione all'inizio della computazione (cfr. listato seguente), mentre utilizzando tali tipi a
risultato generato, si correrebbe il rischio di invalidare gran parte delle cautele adottate. Ecco un
frammento di codice atto ad illustrare la necessità di non passare per i tipi float e doublé. Infatti
l'output di questo frammento di codice è: 0.01 e 0.00999999977648258209228515625.

BigDecimal realNuml = new BigDecimal("0.01");


BigDecimal realNum2 = new BigDecimal(0.01F);

System.ouf.printlnfResult : "+realNum1);
System.ouf.prinllnfResult : "+realNum2);

2.9.6 Evitare l'utilizzo del metodo BigDecimal#equals


Qualora si decida di utilizzare le rappresentazioni BigDecimal, bisogna porre attenzione all'uti-
lizzo del metodo equals. In effetti, poiché questi oggetti utilizzano una rappresentazione simile
a quella delle stringhe, i numeri 10.00 e 10 finiscono per essere interpretati come diversi dal
metodo equals. Il problema è facilmente risolto utilizzando il metodo compareTo.
Quindi, il suggerimento consiste nel ricorrere al metodo compareTo per comparazioni aritme-
tiche. Di seguito, le insidie del metodo BigDecimal equals. Questo frammento restituisce i valori
false e 0.

BigDecimal realNuml = new BigDecimal("10.00");


BigDecimal realNum2 = new BigDecimal("10");

System. ouf.println(realNum1.equals(realNum2));
System. ouf.pnntln(realNum1.compareTo(reall\lum2));
2.9.7 Ricordarsi che i BigDecimal sono oggetti immutabili
Gli oggetti BigDecimal, come gli oggetti String, sono immutabili. Ciò significa che una volta im-
postato il relativo valore, questo non è più modificabile. Fin qui tutto bene se non fosse per
la presenza di alcuni metodi particolarmente ingannevoli, come per esempio add. In particola-
re, un'istruzione del tipo subTotal.add(taxAmount) lascerebbe presupporre che il valore dell'istan-
za taxAmount venga aggiunta a quella dell'oggetto subTotal aggiornandolo. Questo però non è il
caso, visto che la somma avviene correttamente, ma il risultato è restituito incapsulato da un
nuovo oggetto. Pertanto, il frammento di codice corretto è:

BigDecimal total = BigDecimal.ZERO,


total = total.add(base);
total = total.add(subTotal);
total = total.add(taxAmount);

2.9.8 Verificare attentamente la possibilità di overflow


Java rappresenta il tipo long per mezzo di 8 byte permettendo di rappresentare un intervallo
numerico molo ampio (-9,223,372,036,854,775,808 a +9,223,372,036,854,775,807). Tuttavia, spesso
alcuni utilizzi tendono ad accedere questa capacità senza che ciò sia immediatamente chiaro.
Spesso problemi di overflow tendono a generare problemi difficilmente scovabili.
Inoltre, alcune convenzioni Java non sempre aiutano. Si consideri la seguente dichiarazione:

public linai long TM_MICROS_PER_DAY = 24 * 60 * 60 * 1000 * 1000;

Per quanto tutto possa sembrare corretto, la precedente dichiarazione genera un overflow: il
valore impostato è infatti -194313216. Questo perché le varie moltiplicazioni sono eseguite su
tipi interi e solo al termine il risultato è impostato in una variabile di tipo long. Per evitare ogni
problema, è sufficiente indicare esplicitamente che la prima cifra (24) di tipi long come segue:

public final long TM_MICROS_PER_DAY = 24L * 60 * 60 * 1000 * 1000;

2.9.10 Verificare attentamente le assegnazioni composte


Le assegnazioni composte sono espressioni del tipo a += b. Per molti, espressioni di questo tipo
equivalgono alla seguente versione espansa: a = a + b. Ciò non è del tutto corretto e in effetti la
grammatica relativa prevede il cast automatico al tipo oggetto dell'assegnamento. Pertanto, qua-
lora gli elementi partecipanti siano dello stesso tipo, il cast praticamente non sortisce alcun
effetto, mentre nei casi in cui questi siano di tipo a maggiore precisione, allora si corre il rischio
di generare risultati imprevisti senza che il compilatore emetta alcuna segnalazione (listato se-
guente). Cosa che invece avverrebbe senza assegnazione composta. Questo frammento di codi-
ce invece di stampare il valore 100001 stampa -31071, per via del cast implicito. Da notare che
l'istruzione count =count + ine avrebbe generato un errore di compilazione senza cast esplicito.

short count = 1;
int ine = 100000;
2.10 Selezionare attentamente le collezioni
Nell'implementazione di classi che manipolano liste di dati è particolarmente importante selezionare
correttamente la classe Java delegata a memorizzare e gestire tali collezioni.
Questa selezione, anche per motivi "storici", non è sempre agevole. Ciò essenzialmente per il fatto
che le versioni iniziali del linguaggio furono dotate di collezioni thread-safe, come Vector e Hashtable,
che per questioni di retrocompatibilità, sono state mantenute nelle versioni più recenti. Queste classi,
sebbene permettano di realizzare più agevolmente programmi multi-threading (la quasi totalità dei
metodi sono sincronizzati), fanno pagare, soprattutto in termini di efficienza, la gestione di dell'accesso
concorrente ( t h r e a d - s a f e t y ) anche nei casi in cui ciò non sia richiesto. Si pensi per esempio
all'implementazione degli E J B (Enterprise JavaBeans), in questo caso il multi-threading è gestito
dall'application server e quindi ogni esecuzione è eseguita da un opportuno thread. Quindi, ulteriori
non necessarie sincronizzazioni finiscono per ridurre le performance.
Con la versione Java 2 questo problema è stato risolto introducendo un nuovo insieme di classi
collezione, che diventano thread-safe solo su richiesta.

2.10.1 Valutare l'utilizzo delle classi ArrayList e HashTable


al posto di Vector e Hashtable quando possibile
Le iniziali classi per la gestione delle collezioni come Vector e Hashtable sono state disegnate e
implementate includendo la gestione del multi-threading.
Pertanto, questa caratteristica (thread-safety) finisce per degradare le performance in tutti i
casi in cui questa caratteristica non sia necessaria. Pertanto, si consiglia sempre di ricorrere
all'utilizzo delle corrispondenti classi introdotte con la versione Java 2 (ArrayList, HashMap e HashSet)
in cui la gestione del multi-threading è opzionale. In questi casi, qualora, sia richiesta la sincro-
nizzazione dei metodi che modificano le collezioni, è possibile ricorrere a due alternative:

1. sincronizzare i metodi delle classi che incapsulano le collezioni;


2. utilizzare le classi standard disegnate per incapsulare le collezioni dotandole di sincro-
nizzazione. Per esempio, per ArrayList è necessario utilizzare il costrutto List list =
Collections.synchronizedList(new ArrayList(...));

2.10.2 Scegliere accuratamente la classe da utilizzare


La tabella 2.3 fornisce una sintesi atta a semplificare la scelta della struttura da utilizzare per
manipolare una collezione di oggetti secondo i propri requisiti.
Da notare che in tutte le strutture hash l'ordine fa riferimento alla sequenza con cui gli
elementi sono accessibili nel corrispondente oggetto Iterator.

2.10.3 Valutare il ricorso alle nuovi collezioni


Nell'implementazione di applicazioni multi-threading è opportuno considerare le collezioni
introdotte con il nuovo package della concorrenza (cfr. Appendice D). Queste sono state appo-
sitamente disegnate per funzionare in ambienti fortemente concorrenti e quindi tendono a
presentare performance decisamente migliori in applicazioni multi-threading.

2.10.4 Valutare l'utilizzo di StringBuffer o StringBuilder


In alcuni contesti in cui è necessario costruire opportune stringhe in diverse fasi dell'esecuzione di
un metodo, invece di utilizzare un oggetto stringa, è consigliabile utilizzare la classe java.util .StringBuffer
Interfaccia D u p l i c a t i Classi JDK
1.0
LinkedHashSet TreeSet
Set No HashSet (ordinata - (ordinata

veloce) - lenta )

Vector,
List Sì ArrayList LinkedList x
Stack
LinkedHashMap TreeMap
N o chiavi Hashtable,
Map HashMap (ordinala - (ordinata
Sì o o c i t i Properties
veloce) - lenta)

Tabella 2.3 - Corrispondenza tra le varie strutture dati.

o, meglio ancora, la versione non sincronizzata introdotta con il JDK 1.5: java.lang.StringBuilder. Il
problema della classe String è dovuto al fatto che è immutabile. Pertanto la concatenazione di più
stringhe è ottenuta attraverso la creazione di una serie di oggetti stringa intermedi.
Sebbene i compilatori moderni siano in grado di effettuare una serie di ottimizzazioni (so-
prattutto quando si tenta di costruire una stringa con un solo comando), evitando la generazio-
ne di una serie di oggetti intermedi, è sempre opportuno verificare l'utilizzo delle classi StringButfer
o StringBuilder qualora la costruzione richieda diversi concatenamenti eseguiti in tempi successi-
vi. Ecco il m etodo toString della classe java.util.Array.

public static String toString(long[] a) I


if (a == nuli)
return "nuli";
it (a.length == 0
return "[]";

StringBuilder buf = new StringBuilder();


buf.append(T);
buf.append(a[0]);

tor (int i = 1; i < a.length; i++) {


buf.appendf, ");
buf.append(a[i]);

buf.appendf]");

return buf.toString();
)

2.11 Lavorare con le date


Esaminando la classa java.util.Date si nota subito che la quasi totalità dei metodi è stata, giusta-
mente, deprecata (di 28 metodi solo 8 sono ancora disponibili e 5 sono la ridefinizione dei
metodi della classe Object) a tal punto che verrebbe quasi da chiedersi se essa abbia ancora
senso. Chiaramente la risposta è affermativa in quanto, come minimo, rappresenta un conteni-
tore, un wrapper di campi data. Quindi ha lo stesso ruolo che classi come Integer, Long, etc.
hanno nei confronti dei corrispondenti tipi base: int, long, etc.
In ogni modo, la manipolazione delle informazioni di tipo data tende a generare non pochi
problemi e pertanto è consigliato seguire le semplici regole riportate di seguito.

2.11.1 Consultare la documentazione...


Questa area delle API Java verosimilmente non è una delle migliori in assoluto, sia per gli
iniziali errori di disegno della classe Date, sia per alcune strane decisioni, come si vedrà di segui-
to. Per risolvere alcuni grattacapi, già dalla versione Java 1.1 sono state introdotte le classi dei
calendari. Queste permettono di risolvere una serie di problemi della classe Date, come la man-
canza di supporto per le diverse rappresentazioni (internationalization), il disegno non basato
su istanze immutabili, etc.
Per questi motivi la manipolazione delle informazioni di tipo data può presentare alcuni
problemi, spesso abbastanza subdoli. Pertanto, il consiglio non può che essere quello di con-
sultare sempre attentamente la documentazione delle API qualora sia necessario manipolare
variabili di tipo data.

2.11.2 Creare correttamente oggetti di tipo Calendar


Come visto, la quasi totalità dei metodi della classe Date sono deprecati, pertanto il trattamento
delle informazioni di tipo data deve passare attraverso l'utilizzo degli oggetti di tipo Calendar. In
particolare, Java fornisce una sola specializzazione di questa classe astratta: la classe
GregorianCalendar. Il nome di questa classe ricorda il calendario in uso in gran parte del mondo
ed è un omaggio al Papa Gregorio Vili che lo introdusse nel 1582 con apposita bolla papale
{Inter Gravissima.s). L'idea moderna che ne è derivata è che ogni istante di tempo possa essere
formulato come un valore espresso in millisecondi che definisce la distanza dal 1 Gennaio 1970
a mezzanotte precisa (00:00:00.000 GMT).
Per quanto sia possibile creare direttamente un'istanza di tipo GregorianCalendar
(GregorianCalendar now = new GregorianCalendar() ), questa non è la procedure più corretta. Le
direttive Java suggeriscono di utilizzare la seguente procedura: Calendar now = Calendar.getlnstance().

2.11.3 Porre attenzione alla rappresentazione dei mesi


L'utilizzo dei mesi in formato numerico della classe Calendar presenta alcune peculiarità a cui è
necessario porre attenzione. Si consideri il seguente frammento di codice, con un esempio di
problemi con il trattamento del campo mese:

Calendar endCentury = Calendar.gef/nsfance();

endCentury.set(1999, 12, 31);

System.ouf.println( "Date ->"+ DateFormat.geiDa/er/me//?sfa/7ce().format(endCentury.getTime()));

A prima analisi si sarebbe tenuti a pensare che questo produca in output un qualcosa del
genere: Date ->31-Dec-1999 00:00:00, mentre così non è. In particolare l'anno stampato è "magi-
camente" il 2000. Questo perché:
• i mesi vengono rappresentati a partire da 0, che significa Gennaio; pertanto Dicembre è
rappresentato dal numero 11 anziché 12;
• per qualche strana decisione, un valore fuori scala (come per esempio 12 del listato),
invece di generare un'eccezione, sposta il calendario in avanti!

Pertanto... come sempre, occorre porre la massima attenzione!

2.11.4 Utilizzare DateFormat e SimpleDateFormat


La classe astratta java.text.DateFormat e la sua unica specializzazione java.text.SimpleDateFormat, for-
niscono una serie di metodi standard per la conversione di date e time incapsulate in oggetti di
tipo Date. Pertanto, invece di implementare proprie utility di conversione è molto più facile e
immediato utilizzare le librerie predefinite. Nella tabella 2.4 sono mostrati alcuni metodi utili.
Qualora, questi formati non siano sufficienti, è possibile definirsene propri customizzati uti-
lizzando la classe java.text.SimpleDateFormat. La tabella 2.5 ne descrive la convenzione.

2.11.5 Attenzione all'utilizzo di SimpleDateFormat


Questo è un altro esempio di quanto sia valida la regola sulla lettura approfondita della docu-
mentazione. Infatti, spesso nell'utilizzare questa classe sfuggono alcuni interessanti informa-
zioni che possono portare alla generazione di errori difficili da individuare. In particolare, il
metodo parse, per default, non genera eccezioni. Inoltre, non è thread-safe.
Si inizi con il considerare il seguente frammento di codice: attenzione al fatto che il metodo
parse di SimpleDateFormat, di default, non lancia eccezioni:

String fmt = "yyyyMMdd";

String testDate = "20080231";

Date dt = nuli;
tryl
dt = (new SimpleDateFormat(fmt)).parse(testDate);
] catch (ParseException e) {

Metodo Tipo di output


DateFormat.getlnstance().format(now) 23/02/08 16:17
DateFormat.getTimelnstance().format(now) 16:17:56
DateFormat.getDateTimelnstance().lormat(now) 23-Feb-200816:17:56
DateFormat.getTimelnslance(DaleFormat.SHORT).format(now) 16:17
DateFormat. getTimelnstance(DateFormat.MEDIUM).format(now) 16:17:56
DateFormat.getTimelnstance(DateFormat.LONG).format(now) 16:17:56 GMT
DateFormat.getDateTimelnslancef
23/02/08 16:17
DateFormat.SHORT, DateFormat.SHORT).format(now)
DaleFormat.getDateTimelnstancef
23-Feb-2008 16:17
DaleFormal.MEDIUM, DateFormat.SHORT).format(now)
DateFormat.getDateTimelnslancef
23 February 2008 16:17:56 GMT
DateFormat.LONG, DateFormat.LONG).lormat(now)

Tabella 2.4. Metodi standard del DateFormat.


Simbolo Significato Tipo Esempio Risultato
G Era Testo "GG" "AD"

"yy" "08"
y Year (anno) Numero "yyyy"
"2008"
"M" "7"
"MM" "07"
M Month (mese) Testo/Numero
"MMM" "Jul"
"MMMM" "July"
Day in month "d" "3"
d Numero
(giorno del mese) "dd" "03"
"h" "5"
h Hour (ora) 1 - 1 2 . A M / P M Numero
"hh" "05"
"H" "17"
H Hour (ora) 0 - 2 3 Numero
"HH" "17"
"k" "5"
k Hour (ora) 0 - 1 1 . A M / P M Numero
"kk" "05"
"K" "17"
K Hour (ora) 1-24 Numero
"KK" "17"
"m" "7"
m Minute (minuti) Numero
"mm" "07"
"s" "3"
s Second (secondi) Numero
"ss" "03"
Millisecond
s Numero "SSS" "003"
(millisecondi) 0-999
Day in week "EEE" "Mon"
E Testo
(Giorno della settimana) "EEEE" "Monday"
Day in year "D" "42"
D Number
(Giorno dell'anno) "DDD" "042"
Day of week in month
F (settimana del mese a cui Number "1"
il giorno appartiene) ( 1 - 5 )
Week in year
w Number "w" "7"
(Settimana dell'anno) 1 - 5 3
Week in month 1 - 5
W Number "W" "3"
(Settimana del mese)
"a" "AM"
a AM/PM Testo
"aa" "AM"
"z" "EST"
z Time zone (fuso orario) Testo "zzz" "EST"
"zzzz" "Eastern Standard time"
Escape for text Delimitatore "'hour' h " "hour 9 "
" Single quote Testo "ss"SSS" "42'576"

Tabella 2.5. Grammatica del tipo SimpleDateFormat.


System.ouf.println(e);
I

System. ou/.println(dt.toString());

Come si può notare, il codice tenta di impostare come data il 31 Febbraio del 2008. Come
risposta ci si attenderebbe una sonora eccezione... Invece, ahimè, questo non è il caso e infatti,
il risultato è il seguente output: Sun Mar 02 00:00:00 GMT 2008. Per avere il comportamento desi-
derato è necessario inibire il comportamento "silente" di default. Ciò si ottiene inserendo le
seguenti istruzioni al listato precedente: SimpleDateFormat sdì = new SimpleDateFormat(fmt);
sdf.setLenient(false); ossia si deve esplicitare la variabile di tipo SimpleDataFromat, e quindi impor-
re a false il comportamento silente.
Il secondo problema è relativo al fatto che la classe SimpleDateFormat non è thread-safe. Quin-
di una mancata considerazione di questa decisione di disegno può generare seri problemi. Per
esempio, non è infrequente visionare frammenti di codice, soprattutto in ambienti real-time, in
cui si tenti di evitare la ripetuta inizializzazione di oggetti di questa classe al fine di aumentare le
performance, magari ricorrendo a una dichiarazione static. Sebbene l'idea sia corretta soprat-
tutto considerando che tale inizializzazione non è sempre velocissima, è necessario implemen-
tare soluzioni più sofisticate per evitare problemi di raise condition. Un buon esempio consiste
nel memorizzare l'oggetto di tipo SimpleDateFormat in un'apposita istanza ThreadLocal.

2.11.6 Porre attenzione all'informazione tempo dei campi data


Si consideri il frammento di codice mostrato di seguito. Si tratta di un semplice frammento atto
a impostare una data. Tuttavia, è importante notare che, volenti o nolenti, in questi casi il
campo data memorizza anche il contenuto temporale dell'informazione. Infatti, eseguendo questo
frammento si produce il seguente output: Date ->23-Feb-2008 18:17:08. Nella maggior parte dei
casi ciò dovrebbe essere innocuo, tuttavia vi sono alcuni scenari, come per esempio qualora la
data debba poi essere memorizzata nella base di dati, in cui ciò potrebbe creare dei problemi.
Basti immaginare date inizializzate a cavallo del momento in cui avviene il cambio di orario
dovuto al passaggio da ora legale estiva a ora solare invernale o viceversa. Qualora non si voglia
correre rischi, è opportuno ricorrere al seguente metodo di set: cal.set(2008, 1, 23, 0,0,0).

Calendar cai = Calendar ,getlnstance()\

cal.set(2008,1, 23);

System.ouf.println( "Date ->"+ DateFormat.gefDafer/me//7sfance().format(cal.getTime()));

2.11.7 Utilizzare UTC internamente


Quale formato e fuso utilizzare per i campi data? Si tratta di un interrogativo che pur molto
semplice, risulta particolarmente ricorrente tanto che tutti i programmatori prima o poi si tro-
vano a doverlo affrontare. Tipicamente la prima occasione è fornita dal primo sistema fruibile
a utenti dislocati in diverse località geografiche. Il problema è dovuto al fatto che le necessità
tipiche degli utenti si scontrano con quelle dei sistemi. Da un lato vi sono i primi, i quali legit-
timamente desiderano vedere le date conformate alla nazione di appartenenza, sia in termini di
fuso orario, sia di formattazione. Dall'altra vi è il sistema che, per una serie di motivi, per
esempio possibilità di confrontare date relative a dati inseriti da utenti in diverse parti del
mondo, ordinamenti consistenti sul database, etc. necessita di trattare tutte le date nella stessa
maniera, evitando, possibilmente, il problema del cambiamento di orario. La soluzione standard
a questo quesito consiste nel distinguere problemi di memorizzazione e trattamento delle date
dai requisiti relativi alla relativa presentazione. In particolare, è sempre opportuno far in modo
che il sistema tratti internamente le date come oggetti i cui valori siano espressi in UTC. Ciò
include la memorizzazione sul database e lo scambio di messaggi. Questi campi data, tuttavia,
vanno mostrati all'utente nel formato più desiderato. Ciò è ottenibile utilizzando diverse strate-
gie, per esempio gestire un profilo utente in cui, tra le varie informazioni, memorizzare il fuso
orario e il formato delle date gradito dall'utente, oppure prevedere in ogni interfaccia grafica
un meccanismo (una form di impostazione) atta ad impostare questi valori, oppure una solu-
zione ibrida che includa entrambe, etc. In ogni modo è fondamentale riportare a fianco di
ciascuna data il fuso orario di riferimento.
E consigliabile seguire questo consiglio anche qualora si realizzino applicazioni pensate ini-
zialmente per funzionare in un'unica specifica nazione. Ciò per tutta una serie di motivi: è più
facile implementare il sistema, è possibile implementare e riutilizzare proprie librerie, perché
requisiti di questo tipo tendono inevitabilmente a emergere, e così via.

2.12 Problemi con il riutilizzo dei nomi


Riutilizzare i nomi è una pratica spesso azzardata che per qualche motivo sembra popolare
presso molti sviluppatori e non solo. Per esempio, anche i disegnatori Java probabilmente han-
no commesso qualche leggerezza in questo ambito, come si vedrà di seguito.
Nello scrivere parti di codice, è opportuno valutare attentamente il riutilizzo dei nomi al fine
di evitare problemi del tipo: hiding (nascondimento), shadowing (porre in ombra) e obscuring
(oscurare), descritti di seguito.

2.12.1 Non nascondere gli attributi


Ogni qualvolta in una classe figlia si dichiara un attributo istanza con lo stesso nome (anche di
tipo diverso) di quello della classe genitore, si genera quello che tecnicamente viene chiamato
hiding (nascondimento) dell'attributo. Si noti che questo non ha nulla a che vedere con il
validissimo principio dell'information hiding (incapsulamento).
Per quanto il meccanismo delhiding degli attributi sia presente in Java, il suo utilizzo è
fortemente sconsigliato soprattutto perché si finisce col rendere il codice oscuro e se ne riduce
il controllo. Per esempio, è possibile effettuare un hiding variando il tipo dell'attributo, nel
listato seguente, ciò si otterrebbe dichiarando private int classLevel = 2 nella classe TestB, finendo
per diminuire il livello di comprensione del codice e creando problemi ad altre classi che po-
trebbero utilizzare queste.

Inoltre, qualora si esegua Xhiding di un attributo diminuendone il livello di accesso (come nel
listato che segue poco sotto), si finisce per violare l'importantissimo principio di sostituzione di
Liskov (se S è una classe che eredita dalla classe T, ne è un sottotipo, allora istanze di tipo T
possono essere sostituite con oggetti di tipo 5, senza alterare il corretto funzionamento del codice.
Quindi le classi ereditanti devono, almeno, mantenere le proprietà della classe da cui eredi-
tano). Si consideri il seguente listato relativo alle tre classi TestA, TestB e TestC, con un esempio di
hiding di un attributo.

public class TestA I


public String classLevel = "Parent";

public class TestB extends TestA I


private String classLevel = "Child";
I

public class TestC I

public static void main(String[] args) {

TestA testClassA = new TestA();


System.out.println( testClassA.classLevel );

TestA testClassB = new TestB();


System.out.println( testClassB.classLevel );

Eseguendo il metodo main del precedente listato si ottiene la stampa ripetuta della stringa
"Parent". Mentre qualora la classe TestC cercasse di accedere all'attributo classLevel della classe
TestB (basterebbe variare la dichiarazione di testClassB come segue: TestB testClassB = new TestB() )
si otterrebbe un errore di compilazione.
Come si nota hiding è ben diverso dal meccanismo òétYoverridde, ottenibile introducendo
il metodo getClassLevel, prassi decisamente consigliata. In tal caso le classi ereditanti sarebbero
state vincolate dal dover dichiarare lo stesso tipo di ritorno e un compatibile livello di accesso.
In particolare, il modificatore di accesso di un metodo overridden deve garantire almeno lo
stesso livello di accesso del corrispondente metodo nella classe genitore. Inoltre, una volta che
un metodo subisce Yoverridde, questo non può essere eseguito nella classe ereditante, a meno
di esplicita invocazione per mezzo della parola chiave parent, ed inoltre diviene inaccessibile a
classi discendenti oltre la classe figlia.
Si ricordi quindi di evitare sempre l'hiding degli attributi, e in caso sia necessaria una situa-
zione come quella descritta dal listato, utilizzare la tecnica dell'overridde attraverso l'introdu-
zione di appositi metodi.

2.12.2 Evitare l'oscuramento dei tipi


Utilizzando correttamente la convenzione Java relativa al codice (cfr. [SJAVCC]), e in partico-
lare le regole relative al nome degli attributi, questo problema non si verifica mai. Pertanto è
possibile che molti sviluppatori anche esperti non abbiamo mai assistito a questa strana mani-
festazione. Tuttavia è opportuno esserne a conoscenza.
Il fenomeno dell'oscuramento si genera qualora si dichiari una variabile con lo stesso nome
di un tipo nello stesso ambito (scope). In questo scenario, l'utilizzo del nome nello spazio in cui
sia la variabile sia il tipo siano utilizzabili, fa sì che la variabile prenda la precedenza [cfr. seguen-
te listato) e che quindi il tipo non sia utilizzabile. Da tener presente che l'oscuramento può
avere effetto anche sui package (N.B. Il codice non compila!).

public class TestObscuring I

private String System;

public static void main(String[] args) I

// System siring obscures System class


/'/ therefore it does not compile!
System.out.println("Compile error!");

I
I

2.12.3 Porre attenzione allo shadowing


Il fenomeno dello shadowing (porre in ombra) avviene quando una variabile o un metodo o
una classe condividano lo stesso nome di corrispondenti elementi nello stesso ambito d'azione.
Quando ciò avviene, ne segue che l'elemento posto in ombra non è più accessibile con lo stesso
nome e, a volte, non è accessibile del tutto. Quando si scrivono i programmi Java è opportuno
evitare questo fenomeno. L'unica eccezione è rappresentata dall'implementazione dei metodi
setXXX. In questo caso è prassi dichiarare i parametri con lo stesso nome degli attributi istanza
da assegnare. In questo contesto, la confusione viene eliminata attraverso l'utilizzo della parola
chiave this.

public class TestShadowing I

static String comment^ "This is a shadowing test!";

public static void main(String[] args) I

String comment = "This is the shadowing";

System.ouf.println(comment);

2.12.4 Fare attenzione al modificatore final


Non è infrequente il caso in cui programmatori Java fraintendano la funzione del modificatore
final. Questo perché il suo comportamento varia a seconda che venga utilizzato con attributi o
con metodi, tanto che si può parlare di tentativo di riutilizzo di una parola chiave.
Per gli attributi, questo significa semplicemente che il valore dell'attributo può essere asse-
gnato una sola volta, e quindi di fatto trasforma l'attributo in una constante. Mentre nel caso
dei metodi significa che questo non può essere né ridefinito (caso dei metodi istanza) né nasco-
sto (metodi statici). Si consideri il seguente frammento di codice relativo al modificatore final:

public class TestA 1


public static final String BASIC_VALUE = "one mlllon";

public class TestB extends TestA I

public static final String BASIC_VALUE = "1 thousand";

public static void main(String[] args) I

System.out.println(BASIC_VALUE);

Come si può notare è stato possibile nascondere e quindi ridefinire il valore della "costante"
BASIC_VALUE. Per evitare problemi di questo tipo è conveniente affidarsi ad opportuni metodi
e v e n t u a l m e n t e p r o t e t t i dal m o d i f i c a t o r e final (public static final String getBasicValue()). In q u e s t o
caso è veramente impossibile cambiare il comportamento dello stesso.
Capitolo 3
Approfondimenti
Introduzione
In questo capitolo si trattano in dettaglio sia alcune specifiche features introdotte con la versio-
ne Java 5, come per esempio i Generics, le nuove classi della concorrenza, l'autobox, e lo static
import, sia argomenti tradizionalmente molto complessi come la programmazione multi-
threading (MT), per approfondire la quale si rimanda anche all'Appendice D.
Java 5 è stata senza ombra di dubbio una versione fortemente innovativa. In particolare,
sono stati introdotti una serie di nuovi meccanismi, alcuni dei quali a lungo richiesti dalla co-
munità dei programmatori: per esempio i Generics, che hanno finito per cambiare drasticamente
la programmazione in Java.
Vedremo in dettaglio una di direttive e di trucchi molto utili per il lavoro quotidiano del
programmatore. Per quanto concerne la programmazione MT sono illustrati una serie di con-
cetti che anche programmatori esperti tendono a dimenticare, come per esempio il fatto che le
specifiche Java non prescrivano alcun modello MT di riferimento.

Obiettivi
Dalla lettura di questo capitolo, i programmatori dovrebbero approfondire la conoscenza delle
nuove feature introdotte con la versione Java5, inclusi alcuni aspetti spesso trascurati dalle
trattazioni ufficiali, e diverse strategie molto utili nella pratica quotidiana.
Per quanto riguarda la programmazione multi-threading, è sempre opportuno averne una
buona conoscenza, non solo perché prima o poi tutti si trovano a dover scrivere almeno piccole
utility MT, ma anche perché essere a conoscenza di quanto accade dietro le quinte aiuta a
implementare codici di migliore qualità.
Direttive
3.1 I Generics
I Generics rappresentano indubbiamente una delle caratteristiche più importanti introdotte con
il JDK5. A lungo richiesti dalla comunità degli sviluppatori, sono la versione Java del concetto dei
templates presenti in C++ con profondissime differenze. Queste sono principalmente dovute al
fatto che i Generics sono "risolti" completamente dal compilatore che si occupa di generare un
bytecode assolutamente compatibile con le precedenti versioni di Java. Questo meccanismo è
chiamato cancellazione (erasure). In questo contesto, questa tecnica è utilizzata per "ridurre" i
tipi parametrizzati nei corrispondenti raw type (tipi grezzi). Questa scelta risolve alcuni impor-
tanti problemi, come la compatibilità con le versioni precedenti (la policy di back-compatibility
ha da sempre ha contraddistinto la Sun), ma ne introduce di nuovi dovuti essenzialmente al fatto
che a run-time vengono perse diverse informazioni relative al tipo dei Generics. Al fine di apprez-
zare il meccanismo della cancellazione, si consideri il seguente frammento di codice:

ArrayList<IOException> exceptionList = new ArrayList<IOException>();


ArrayList<lnteger> integerList = new ArrayList<lnteger>();

System.ouf.printlnf'Exception list type: \t"+ exceptionList.getClass().getName()):

System, ouf.pnntlnflnteger list type: \t"+ integerList.getClass().getName());

L'output prodotto dalla sua esecuzione è:

Exception list type: java.util.ArrayList


Integer list type: java.util.ArrayList

Come si può notare, il compilatore si occupa di "ridurre" i tipi ArrayLÌSt<IOException> e


ArrayList<Integer> a un semplice ArrayList. Non ci sono dubbi che l'introduzione dei Generics
abbia prodotto un significativo miglioramento della qualità del codice scritto Java e un signifi-
cativo incremento di produttività; tuttavia, i Generics nascondono più di qualche insidia, come
mostrato successivamente. Il problema di fondo è che il disegno dei Generics rappresenta un
buon compromesso tra l'esigenza di far evolvere il linguaggio Java e la necessità di mantenere la
compatibilità con le librerie esistenti. Comunque di compromesso si tratta e, come tale, finisce
per presentare diversi lati deboli. Sebbene l'introduzione dei Generics abbia richiesto di riscrivere
la quasi totalità della libreria del JDK1.4, che è stata estesa appunto per usare questa nuova
feature in JDK5, il codice Java compilato (bytecode) per la versione 1.4 può girare senza pro-
blemi e senza dover essere ricompilato anche su 1.5. Il che indubbiamente è un grandissimo
successo tecnico. A fronte di questo vantaggio, c'è però il limite che i Generics , presentano
alcuni lati oscuri, e non sono così eleganti, efficienti e lineari come i corrispondenti template del
C++. Per ulteriori dettagli sui Generics si consiglia di leggere il testo [JAVAGEN].

3.1.1 Utilizzare i Generics


Viste le loro caratteristiche, si consiglia di utilizzare i Generics. I principali vantaggi sono:

• eliminazione di molti down-cast richiesti dalle iniziali collezioni Java. Il codice è maggior-
mente type-safe, spostando il controllo del tipo dall'esecuzione (runtime) alla compilazione;
• codice più elegante e leggibile. Spariscono molti casting, è sempre possibile capire che
tipo di dati gestisce una particolare collezione, si genera codice più succinto, e così via

Inoltre, le collezioni base Java 2 (presenti prima dell'arrivo dei Generics) sono tollerate solo
per compatibilità con le passate versioni di Java, e, stando a quanto sancito dal documento
delle specifiche ufficiali, potrebbero essere completamente rimosse in future versioni! Infine,
l'utilizzo dei Generics non genera alcun impatto sulle performance. Questo perché iJ compila-
tore fa in modo che i Generics a run-time siano rappresentati dai rispettivi tipi grezzi. Quindi
non ci sono variazioni di sorta sulle performance, cosa che invece avviene in C++, dove i template,
tipicamente, permettono di ottenere prestazioni migliori. Per comprendere alcuni vantaggi si
considerino i due frammenti di codice presentati di seguito. A sinistra, una versione pre-Generics;
a destra la versione che fa uso dei Generics.

import java.util.ArrayList; import java.util.ArrayList;


import java.util.Collection; import java.util.Collection;
import java.util.lterator; import java.util.Iterator;
import java.util.List; import java.util.List;

public class Test I public class Test {

private void testCollection() I private void testCollection() {


List list = new ArrayList();
List<String> list = new ArrayList<String>();
list.add(new StringfJava Best Practise"));
list.add(new String("Luca V.T.")); list.addfJava Best Practise");
Iist.add(new String("2008")); list.addf'Luca V.T.");
Iist.add("2008");
printCollection(list);
printCollection(list);

private void printCollection(Collection c) I


private void printCollection(Collection c) I
Iterator i = c.iterator();
lterator<String> items = c.iteratorQ;
while (i.hasNext()) I
String item = (String) i.next(); while (items.hasNext()) I
System.ouf.println(item);
System. ouf.println(items.next());

public static void main(String argv[]) (


Test myTest = new TestQ; public static void main(String argv[]) {
myTest.testCollection(); Test myTest = new Test();
myTest.testCollection();
Come si può notare, la versione con i Generics è più concisa, elegante: elimina la necessità di
eseguire una serie di down-cast (per esempio String item = (String) i.next()) e cosa più importante
fa sì che molti controlli di tipo avvengano al tempo di compilazione. Per comprendere l'impor-
tanza di ciò è sufficiente aggiungere la seguente linea nel metodo testCollection: list.add(new
lnteger(400)). Nel primo caso, versione pre-Generics, il codice compila "regolarmente" e genera
l'eccezione di ClassCastException (dovuta al tentativo di effettuare il cast a String) solo a tempo di
esecuzione, e più precisante al momento di stampare i valori. Nel caso della versione con i
Generics invece, poiché il controllo avviene a tempo di compilazione, ne segue che il codice
semplicemente non compila.
Da notare, che nel listato con i Generics si è utilizzata il meccanismo, sempre introdotto in
Java 5, dell'auto-boxing (list.add("Java Best Practise"))

3.1.2 Evitare mix tra Generics e tipi grezzi


Le motivazioni alla base dell'utilizzo dei Generics dovrebbero essere ormai chiare. Tuttavia
alcuni programmatori potrebbero essere portati a scrivere del codice contenente sia i Generics
sia i tipi raw. Un esempio tipico si ha con l'utilizzo di oggetti di tipo Iterator non tipizzati. Si
tratta di una pessima scelta implementativa. Ciò sia perché si generano gli stessi inconvenienti
dovuti al non utilizzo dei Generics (codice meno chiaro, necessità di down-cast, etc.), sia perché
si dà luogo ad uno stile decisamente poco professionale.
Inoltre, un utilizzo non accorto di entrambi i tipi può dar luogo ad un problema noto con il
nome di heap pollution (inquinamento dell'heap). Con questo nome ci si riferisce al caso in cui
una variabile di un tipo parametrizzato si riferisca a un oggetto che non è di quel tipo. Questo
scenario si ottiene mischiando i tipi grezzi (raw) con quelli parametrizzati, e con l'aggiunta di
cast non opportuni. In queste situazioni il compilatore emette opportuni warmng, ma il codice
viene regolarmente compilato.
Si consideri il listato riportato poco sotto che dimostra una heap pollution. La relativa esecu-
zione genera il seguente output: "->[10, Luca, Vetti Tagliati]". Come si può notare, per via del principio
della cancellazione si riesce "tranquillamente" ad utilizzare un ArrayLiSt<Number> come un LiSt<String>.
Ciò perché il compilatore esegue il mapping di ArrayList<Number> a ArrayList e di List<String> a
List, e quindi si finisce per generare la paradossale situazione in cui sia assolutamente legittimo
utilizzare un ArrayList come un oggetto di tipo List.
Chiaramente si tratta di un codice assolutamente sconsigliato, in cui il compilatore si limita
ad emettere un warning relativo all'istruzione che genera l'inquinamento dell'heap.

public static void test(ArrayList<Number> listi) {

List<String> list = nuli;

list = genericListl; / / h e a p pollution!!!


list.addfLuca");
list.addf'Vetti Tagliati");

System.out.prlntln("->"+llst);
public static void main(String args[]) {
ArrayList<Number> al = new ArrayList<l\lumber>();
al.add(10);
Test.test(al);
I

3.1.3 I Generics non implementano la proprietà della covarianza


Un'altra caratteristica a cui bisogna porre attenzione con i Generics è dovuta al fatto che questi
non implementano un'importante caratteristica chiamata tecnicamente "covarianza". Per esem-
pio, si immagini di realizzare un oggetto Java "cestino della frutta" e di volerlo utilizzare come
"cestino delle arance", dove, come lecito attendersi, la classe "arancia" estende quella "frutta".
Ora, se si implementasse ciò attraverso array, tutto funzionerebbe correttamente (gli array im-
plementano la proprietà della covarianza), mentre ciò non è possibile con i Generics. Badare
bene però a non far confusione: con i Generics è tuttavia consentito inserire un'arancia nel
cestino della frutta! Per comprendere quanto riportato si analizza il tutto in termini di codice.
Si assuma di aver implementato un metodo, f i n d M i n i m u m ( l \ l u m b e r [ ] n u m b e r s ) , in grado di trova-
re il numero più piccolo di un dato insieme. Questo potrebbe essere correttamente invocato
come segue:

lnteger[] intNumbers = 12, 4 5 , 1 , 1 0 , 45, 56, 34, 22, 3 3 , 1 2 , 78


Integer min = findMinimum(intNumbers)

Ciò dimostra che un array di tipi più specifici possa sempre essere sostituito a un array di tipi
più generali. In effetti, gli array a run-time continuano a mantenere informazioni relative al
proprio tipo. Ciò permette alla JVM di poter eseguire i controlli relativi ai tipi di dati gestiti.
Mentre il tentativo di invocare il seguente metodo:

public int findMinimum(List<Number> numbers) {

con il seguente oggetto

List<lnteger> intNumber = new ArrayList<lnteger>()

! i some initialisation

findMinimum(intNumbers)

genera un errore di compilazione, mentre la seguente istruzione è assolutamente valida:


List<Number> numbers = new ArrayList<Number>()
number.addfnew lnteger(3));

Quindi sebbene sia possibile inserire un'arancia in un cestino della frutta, non è possibile
utilizzare un cestino di arance come un cesto della frutta.
Da notare che per via del fatto che gli array implementano la proprietà della covarianza, non
è possibile creare array parametrizzati il cui tipo sia un tipo concreto. Per esempio la seguente
dichiarazione non è valida:

HashMap<String,String>[] intPairArr = new HashMap<String,String>[10]; // ERRORI

3.1.4 Non tutti i tipi possono essere parametrizzati


Quando si disegnano classi parametrizzate è necessario considerare il fatto che sebbene la
quasi totalità dei tipi Java possano essere parametrizzati attraverso il meccanismo dei Generics,
esistono le seguenti eccezioni:

• tipi enumerati. Questi, semplificando, rappresentano una lista di valori statici come tali
avrebbe poco senso cercare di parametrizzarne il tipo;
• eccezioni. Le eccezioni rappresentano il meccanismo utilizzato a run-time dalla JVM
per segnalare e gestire situazioni di errore. Poiché la stessa JVM non ha conoscenza del
meccanismo dei Generics (vengono cancellati attraverso il meccanismo dell'erasure),
questa non sarebbe capace di distinguere il tipo Generics in un costrutto catch (si consi-
deri l'errato listato poco sotto). Pertanto dichiarazioni come le seguenti non sono con-
sentite: public class MyException<T> extends Exception. Tuttavia le eccezioni possono essere
il tipo di una collezione;
• classi anonime annidate. Sebbene queste classi possano utilizzare al loro interno i Generics,
le stesse non posso essere tipizzate: avrebbe decisamente poco senso per via del sempli-
ce fatto che non dispongono di un nome.

void NlegalExceptionTypeO I

lry(
executeAMethod();

I catch (IHegalArgumentException<String> iaeS) { // ERRORI

/ / . . . do something

I catch (NlegalArgumentException<Long> iaeL) I // ERRORI

/ / . . . do something
I
)

3.1.5 Prendere spunto dalle classi della libreria Java


Molto spesso durante l'implementazione di codice basato sui Generics, soprattutto in utilizzi
avanzati, è possibile trovarsi nella condizione di non riuscire a individuare la strategia più effi-
cace, o di essere perplessi circa alcune costrizioni imposte dal compilatore. In questi casi, oltre
al sempre valido suggerimento di ricercare nel mondo della conoscenza su Internet, è
consigliabile dare un'occhiata ai sorgenti delle classi della libreria standard Java. Alcune parti-
colarmente interessanti, da questo punto di vista, sono quelle del package java.util.
3.1.6 Porre attenzione agli overloading e overriding
La tecnica della cancellazione genera una serie di conseguenze anche significative a diversi mec-
canismi della programmazione Java. In particolare, poiché i tipi Generics a run-time perdono
l'informazione circa il tipo, automaticamente si finisce con l'influenzare il comportamento di
gran parte dei meccanismi sensibili al tipo. Tra questi figurano anche Voverloading e l'overriding.
Si consideri il listato riportato poco sotto con alcune peculiarità dell'overloading con tipi Generics:
tale listato stampa Collection<?>. A prima vista si sarebbe tenuti a pensare che l'esecuzione possa
stampare la seguente stringa ArrayLiSt<lnteger>, ma l'esecuzione invece mostra questo output:
Collection<?>. Un bug? No di certo. Questo "strano" comportamento è dovuto al fatto che il
compilatore durante la prima passata del codice riduce i tipi generici ai relativi tipi raw (even-
tualmente ai tipi object) e poi applica Voverloading. Quindi l'associazione tra il metodo overloaded
e il corrispondente printType è eseguita a tempo di compilazione e non a run-time.

public class MyClass<T> {

private void printType(Coilection<?> collection) I


System. ouf.println("Collection<?>");

private void printType(List<Number> list) {


System. ouf.println("List<Number>");

private void printType(ArrayList<lnteger> list) I


System, oui. pri ntl n ("Array List<l nteger>") ;

private void overloaded(List<T> t) I


printType(t);
I

public static void main(String[] args) I


MyClass<lnteger> test = new MyClass<lnteger>();
test.overloadedfnew ArrayList<lnteger>());

Chiaramente, situazioni analoghe si generano anche con 1 'overriding.

3.1.7 Porre attenzione agli attributi statici nei tipi parametrizzati


La presenza di attributi statici in tipi parametrizzati può essere oggetto di confusione. Si consi-
deri questa porzione di codice:

public class MyClass<T> I

public static int numCalls = 0;

public static void main(String[] args) I


MyClass<lnteger> myClassI = new MyClass<lnteger>();
MyClass<String> myClass2 = new MyClass<String>();
myClassI ,numCalls++\ // Non recommended
myClass2 .numCalls++\ // Non recommended
MyClass. numCalls++\ // Recommended

System. ouf.printlnfNum calls myClassI : \t"+myClass1. numCallsy,


System.ouf.pnntlnfNum calls myClass2: \t"+myClass2.numCalls);

Il fatto che il tipo generico MyClass<T> possa essere ¡stanziato con un numero infinito di
parametri concreti, potrebbe portare all'errata conclusione che ciascuna delle istanze caratte-
rizzate dal medesimo parametro concreto (per esempio tutti gli oggetti di tipo MyClass<String>)
disponga del proprio attributo statico. Anche se ciò ha una sua logica che funzionerebbe in altri
linguaggi di programmazione, questo non è il caso dei Generics Java. Per capire il perché, è
sufficiente ricordare il principio della cancellazione adottato dal compilatore e il fatto che i tipi
generici sono ridotti alle versioni base (raw type). Come controprova basti considerare l'output
generato dall'esecuzione del codice riportato nel listato visto poco sopra:

l\lum calls myClassI : 3


Num calls myClass2: 3

Anche per questo motivo, istruzioni del tipo myClass1.numCalls++ dovrebbero essere sostitui-
te con la versione classica MyClass.numCalls++.
Per gli stessi motivi, dovrebbe essere chiaro il motivo per cui non è consentito dichiarare un
attributo statico del tipo del parametro della classe, come riportato nel listato seguente.

ICONA

public class MyClass<T> I

public static T genericField; // ERRORI

public static void incorrectMethod() {


T temp = nuli; //ERRORI

// do something
I
I

Da tener presente che, sebbene la dichiarazione statica non sia consentita, quella final invece
non crea problemi.

3.1.8 Cercare sempre di utilizzare il carattere jolly (wildcard)


pur con i limiti appropriati
Il carattere jolly nel contesto dei Generics è rappresentato dal punto interrogativo: ?, ed è
utilizzato per rappresentare famiglie di tipi.
In particolare può essere utilizzato per rappresentare i seguenti tre concetti:

1. tipo illimitato (unbounded). ?. Serve per descrivere tutti i tipi.


2. tipo limitato superiormente. ? extends Type. In questo caso descrive la famiglia di tutti i
tipi sottotipo di Type incluso lo stesso Type.
3. tipo limitato inferiormente. ? super Type. Dichiara la famiglia di tutti i tipi che sono
supertipo di Type dove, questa volta, Type è escluso.

Il carattere jolly è molto utile in tutti quei contesti in cui si abbia una conoscenza limitata del tipo
di argomento. Si consideri il seguente listato: metodo fill (java.util.Collections) utilizzato per impostare
tutti gli elementi della lista specificata con l'oggetto fornito. Come si può notare, 0 tipo della colle-
zione deve essere un supertipo dell'oggetto che si desidera impostare. Una dichiarazione più gene-
rale basata sul carattere jolly non limitato (public Static <T> void fill(List<?> list, T obj)) avrebbe portato
a un codice non type-safe, carente di importanti vincoli, e quindi, in ultima analisi, fragile.

/' '

" Replaces all of the elements of the specified list w i t h the specified
' element. <p>

" This m e t h o d runs In linear time.

* @param list the list to be filled w i t h the specified element.


' @param obj The element w i t h w h i c h to fill the specified list.
* @throws U n s u p p o r t e d O p e r a t l o n E x c e p t i o n if the specified list or its
list-Iterator does not s u p p o r t the <tt>set</tt> operation.

public static <T> void fill(List<? super T> list, T obj) f


int size = list.sizef);

if (size < FILL_THRESHOLD || list instanceol RandomAccess) I


for (int i=0; ksize; i++) I
list.set(i, obj);
I
I else <
Ustlterator<? super T> itr = list.listlterator();
for (int i=0; ksize; i++) I
itr.next();
itr.set(obj);
I
(
I

Come dimostrato dal listato poco sopra, il carattere jolly "limitato" contiene un maggiore
quantitativo informativo, forzando opportuni controlli sul tipo. Pertanto, ogniqualvolta si in-
tenda utilizzare un carattere jolly senza limiti (unbounded), ci si dovrebbe interrogare se si tratti
della scelta più corretta o se sia possibile introdurre appositi limiti.
3.1.9 Evitare metodi che ritornino parametri di tipo jolly.
L'implementazione di metodi che restituiscono parametri di tipo jolly (?) non è in genere una
buona idea. Questo essenzialmente perché l'accesso al valore restituito è ristretto e le restrizio-
ni dipendono dal contesto di utilizzo del tipo jolly. Pertanto, nella maggior parte dei casi, nel
codice della classe cliente è necessario eseguire il tanto odiato down-cast.
Si consideri l'esempio del listato che segue. Come si può notare, una volta che il tipo di
ritorno è del tipo List<?>, si sono perse le informazioni sul tipo gestito dalla lista e quindi per
poter accedere ai metodi del tipo iniziale è necessario effettuare un down-cast. La versione
corretta del codice richiede di sostituire il carattere jolly con un tipo, ottenendo la seguente
firma: public static List<T> alterList(List<T> aList ).

public static List<?> alterList(List<?> aList ) I


// does something

return aList;
I

public static void main(String args[]){


Llst<String> names = new ArrayList<Strlng>();

names.addfLuca");
names. addf'Vera");
names. addf'Francesco");
names. addf'Natalya");

List<?> newNames = alterList(names);


// cannot add a string without a cast
// cannot invoke methods like newNames.add("Cinza"):

names = (List<String>) alterList(names);

Chiaramente esistono dei casi sporadici in cui quest'opzione è l'unica possibile; si consideri
per esempio il codice della classe: java.lang.Class. In questo caso diversi metodi prevedono il
punto interrogativo nel tipo di ritorno. Per esempio il metodo forName deve essere in grado di
ritornare l'oggetto Classe del tipo richiesto:

public static Class<?> forName(String name, boolean initialize, ClassLoader loader)


throws ClassNotFoundException

Bisognerebbe sempre cercare di evitare il carattere jolly nel parametro di ritorno di un metodo.

3.1.10 Controllare l'implementazione dei metodi che prevedono


un carattere jolly nei parametri
Qualora si dovesse implementare un metodo che preveda il carattere jolly come tipo di uno o
più parametri, sarà necessaria più di qualche accortezza. Questo perché la presenza di tale
carattere limita l'accesso ai metodi del tipo. In questo caso è possibile ricorrere a due strategie
come la cattura del jolly (wildcard captare), l'utilizzo di metodi di help, etc. Un'altra buona
strategia consiste nel cercare di utilizzare i metodi forniti dalla libreria Java, come quelli della
classe java.collections.
Si consideri il seguente listato. Come si può notare, per via della presenza del carattere jolly,
non è possibile eseguire una serie di operazioni, come per esempio inizializzare una copia della
lista e utilizzare il metodo set.

public static void alterList(List<?> aList ) I

List<?> tmp = new ArrayList<?>(aList); // ERRORI!!

for (int i=0; i < aList.size(); i++) (


int j = 0;
// do something
tmp.set(i, alJst.get(j)); //ERROR!!!
I

Il listato che segue illustra la tecnica della cattura del jolly. Dall'analisi del codice verrebbe
da chiedersi legittimante perché non utilizzare direttamente la firma del metodo alter. Chiara-
mente ciò è sempre consigliato, quando possibile. Tuttavia il codice mostrato è un esempio e
quindi è stato estrapolato da situazioni più complesse ove la sostituzione dei metodi non è
così immediata.

public stalic void alterList(List<?> aList ) <


a/ie/^aList);

public static <T> void alter(List<T> aList) I

List<T> tmp = new ArrayList<T>(aList);

lor (int i = 0; i < aList.sizeQ; i++) I


int j = 0;
// do something
tmp.set(i, aList.get(j));
I

3.1.11 Non confondere collezioni eterogenee con collezioni omogenee


di tipi non conosciuti
Non è infrequente il caso in cui alcuni sviluppatori debbano scrivere del codice generico che
manipoli oggetti di cui non si conosca a priori il tipo e finiscano così con l'utilizzare i Generics
in maniera errata. In particolare bisogna sempre fare attenzione a distinguere correttamente i
due casi:

1. Collection<Object> rappresenta una collezione di elementi eterogenei. Infatti può essere


popolata con tutti gli oggetti sottotipi di Object: ossia tutte le classi Java.
2. Collection<?> rappresenta una collezione di elementi omogenei di cui però non si conosce
il tipo. Una volta definito il parametro della collezione, solo elementi di quel tipo speci-
fico potranno essere manipolati dalla collezione.

Chiaramente, non esiste la definizione migliore, entrambe hanno un loro dominio di utiliz-
zo. Tuttavia è necessario capirne le differenze per poter riuscire a selezionare correttamente
l'implementazione più idonea per i propri requisiti.

3.1.12 Porre attenzione all'implementazione dei metodi "infrastnitturali"


Come lecito attendersi, la presenza dei Generics fa sì che l'implementazione dei classici metodi
infrastrutturali (equals, hash, clone, etc.) richieda qualche accortezza in più.
Nella parte seguente sono mostrati alcune implementazioni tipo di questi metod, in partico-
lare l'implementazione del metodo equals nella classe java.util.AbstractMap. Classe genitore delle
più popolari: HashMap, IdentityHashMap, TreeMap, WeakHashMap.

public boolean equals(0bject o) I


il (o == this)
return true;

if (!(o instanceof Map))


return false;

MapcK, V> t = (MapcK, V>) o;

if (t.size() != size())
return talse;

try {
lterator<Entry<K, V » i = entrySet().iterator();

while (i.hasNextQ) I
Entry<K, V> e = i.next();
K key = e.getKeyf);
V value = e.getValue();
if (value == null) {
if (!(t.get(key) == null && t.containsKey(key)))
return false;
I else I
if (lvalue.equals(t.get(key)))
return false;
1

} catch (ClassCastException unused) I


return false;
I catch (NulIPointerException unused) I
return false;
I

return true;
I

Da notare che, sebbene il codice sia stato preso direttamente dalla Java library, con poco
sforzo sarebbe stato possibile renderlo più leggibile. Inoltre, a prima vista il seguente down-
cast M a p < K , V> t = (Map<K, V>) o potrebbe sembrare un po' azzardato. Tuttavia non lo è, perché
a tempo di esecuzione non c'è differenza tra Map e Map<K, V>.

public Object clone() {


try [

ArrayList<E> v = (ArrayList<E>) super.clone();


v.elementData = (E[j) new Object[size];
System.araycopy(elementData, 0, v.elementData, 0, size);
v.modCount = 0;

I catch (CloneNotSupportedException e) I
// this shouldn't happen, since we are Cloneable
throw new lnternalError();
I

return v;

3.2 Static import


Lo static import rappresenta una delle novità introdotte con la versione Java5 di cui, questa
volta, molto probabilmente si sarebbe potuto fare tranquillamente a meno.
L'idea di base consiste nel disporre di speciali versioni del costrutto di import ( i m p o r t introdot-
to dalla parola chiave Static) che permettano di utilizzare i membri delle classi specificate, a
meno della relativa qualificazione.
Si considerino le due versione del listato riportato di seguito su due colonne. Come si può
notare, la presenza del costrutto static import permette di evitare la ripetizione di una serie di
elementi (come per esempio il nome della classe Math) e quindi rendere la produzione del
codice più veloce. Inoltre ciò dovrebbe eliminare la tentazione per alcuni programmatori di
ricorrere a trucchi poco raccomandabili per evitare la ripetizione dei nomi delle classi. A sini-
stra l'import standard, a destra l'import statico
public class Test I import static java.lang.Math. *;
import static java.lang.System.out,
public static void printValues(double val) I
it ((val >=0) && (val <=360)) I public class Test I
System.ouf.printlnfValue :"+val);
System. ouf.println("Math.sin("+val+")="+ public static void printValues(doubie val) {
Math.s/'n(val) ); if ( (vai >=0) && (val <=360)) {
System. oi/f.println("Math.cos("+val+")="+ oi/f.printlnfValue :"+val );
Math.cos(val) ); oui.println("Math.sin("+val+")= " + s/n(val));
System. ouf.println("Math.tan("+val+")="+ ouf.println("Math.cos("+val+")= " + cos(val));
Math.tìn(val) ); ouf.println("Math.cos("+val+")= " + fan(val));
I else ( I else {
System. ouf.printlnfAllowed value range: ouf.printlnfAllowed value range: 0 - 360"):
0 - 360");

3.2.1 Utilizzare con cautela le importazioni statiche


Questa regola è assolutamente atipica rispetto a quanto riportato in questo testo: dopo aver
introdotto una nuova feature, ne viene scoraggiato l'utilizzo. La ragione c'è, però. Sebbene
questo meccanismo permetta di ridurre la necessità di ripetere continuamente parti di codice,
e quindi permetta di rendere il codice più succinto, presenta anche una serie di importanti
svantaggi tali da non renderlo sempre raccomandabile. Il più importante è relativo al fatto che
il codice diviene, paradossalmente, meno chiaro. Infatti, a meno di casi evidentissimi come
quelli mostrati poco sopra, la lettura del codice richiede di dover verificare a quale elemento
(classe o interfaccia) appartengano i vari elementi utilizzati.
Pertanto, è consigliabile utilizzare questo meccanismo con parsimonia e, qualora si decida di
utilizzarlo, è opportuno limitare l'importazione statica a uno o due elementi.

3.1.2 Valutare l'utilizzo delle importazioni statiche con le costanti


La strategia di utilizzare apposite interfacce per incapsulare valori costanti è oggetto di una
lunga diatriba. Da un lato vi sono coloro che considerano questa soluzione come un vero e
proprio antipattern, dall'altra invece ci sono coloro che considerano tale strategia assolutamen-
te legittima. Come al solito, entrambe le posizioni sono fondate su alcuni validi concetti.
Nel primo caso si obietta legittimamente che le interfacce dovrebbero essere utilizzare per
dichiarare del comportamento, quindi una sorta di contratto tra le classi clienti e quelle che
implementano l'interfaccia: si tratterebbe di un utilizzo snaturato.
Dall'altra parte dello schieramento invece si argomenta che non ci sia nulla di particolarmen-
te sbagliato nell'incapsulare i valori costanti qualora questo sia l'unico modo per far sì che
diverse classi utilizzino gli stessi valori e questi non prevedano particolare e comportamento.
Chiaramente, il problema dell'abuso è sempre in agguato, e come tale si tratta effettivamente
un antipattern. In ogni modo, è consigliato l'utilizzo del meccanismo delle importazioni statiche
in presenza di costanti, come mostrato di seguito.
import static java.awt.Color.*;

public class ImportExample I


public static void main(String args[]) I
new Form(RED);
I

3.3 Auto-Boxing / Unboxing


L'auto-boxing / auto-unboxing è un altro interessante meccanismo introdotto con la versione
JDK 5, che permette di eseguire la trasformazione automatica dei tipi base nei corrispondenti
oggetti di wrapping (auto-boxing) e vice versa [auto-unboxing).
Da notare che questo automatismo non si limita al caso delle collezioni, ma funziona anche
con le normali operazioni aritmetiche, come mostrato nel codice riportato sotto, la cui esecu-
zione stampa correttamente il valore 21. Come lecito attendersi, la somma avviene tra tipi base...
Purtroppo Xoverloading degli operatori non è ancora stato introdotto in Java.

Integer numi =1: //auto-boxing


int num2 = 20;

System.ou/.println(num1 + num2); //auto-unboxing

Si tratta di un meccanismo molto apprezzato dagli sviluppatori in quanto permette di realiz-


zare codici più eleganti e puliti e, in generale, aiuta a risparmiare tempo.

3.3.1 Utilizzare il meccanismo dei boxing


Come standard, il primo suggerimento consiste nell'awalersi di questo meccanismo, ma con
intelligenza. Come visto, rende il codice più elegante, pulito, quindi più leggibile e porta a un
generale risparmio di tempo. Tuttavia, come mostrato successivamente, in alcuni contesti può
generare significative perdite di prestazioni.

3.3.2 Ricordarsi la compatibilità con il passato


In alcune situazioni, le priorità del meccanismo del boxing avvengono in un modo che non
sempre risulta chiaro in prima analisi. Si consideri il seguente il codice.

public class Test I

private void printValue(lnteger i) I


System. ouf.println("lnteger:"+i);
I

private void printValue(long i) {


System. ouf.println("long:"+i);
public static void main(String argv[|) |

Integer ¡1 = 1 ;
long 11 = 10L;
int i2 = 5;

Test myTest = new Test();

myTest.printValue(il);
myTest.printValue(M);
myTest.printValue(i2);

L'esecuzione del frammento di codice produce il seguente output:

lnteger:1
long:10
long:5

Ora, mentre per le prime due invocazioni non ci sono sorprese (il comportamento è esattamen-
te quello atteso), la stessa cosa non si può dire per la terza invocazione. In effetti, verrebbe spon-
taneo attendersi il boxing automatico della variabile i2. Ciò non avviene per questioni di compa-
tibilità con la precedente versione Java, in cui la variabile intera verrebbe "promossa" a long.

3.3.3 Fare attenzione alle performance


Il meccanismo di boxing in determinati contesti non fornisce le stesse prestazioni dei tipi
base. Questo risultato è abbastanza ovvio: il boxing si ottiene creando oggetti immutabili di
wrapping, ma è opportuno tenerlo presente soprattutto in presenza di loop estesi e in applica-
zioni real-time.

private static void printPeriod(String comment, long startTime) I

long endTime = System.na/7o7ime();


System. oo/.println( comment+" \t"+
(endTime-startTime)+"ns "+"\t"+
((endTime-startTime)/1000000)+"ms");
I

public static void main(String argv[]) I

long startTime = 0;

// ArrayList declaration
startTime = System. nanoTimeQ;
List<lnteger> listValues = new ArrayList<lnteger>();
printPeriod(" HrrayL\s\ declaration: \t", startTime);

// Array declaration
startTime = System. nanoTimeQ]
int arrValues[] = n e w i n t [ 5 0 0 0 0 0 ] ;
printPeriod(" hmy declaration: \t", startTime);

// ArrayList initialisation
startTime = System. nanoTimeQ-,
for(int i=0; ¡<500000; i++)|
listValues. add(i);
I
/7r/niPer/orf("Initialisation ArrayList:", startTime);

// ArrayList initialisation
startTime = System. nanoTimeQ:
for(int 1=0; i<500000;i++){
arrValues[l] = i;
I
printPeriod("Initialisation ArrayList:", startTime);

// boxing: values retrieval and calculations


startTime = System. nanoTimeQ,
lor(int i=0; ¡<500000; i++)(
listValues.set(i,listValues.get(i)*7+4);
I
printPeriod("Boxmg calculation: \t", startTime);

// natural type: values retrieval and calculations


startTime = System. nanoTimeQ-,
for(int i=0; ¡<500000; i++)|
arrValues[l] = arrValues[i]*7+4;
I
printPeriod("\ialura\ type calculation:", startTime);

L'esecuzione di tale listato genera risultati del tipo:

ArrayList declaration: 32127ns 0ms


Array declaration: 8885207ns 8ms
Initialisation ArrayList: 249998533ns 249ms
Initialisation ArrayList: 1501028ns 1ms
Boxing calculation: 157989633ns 157ms
Natural type calculation: 2819073ns 2ms
Sebbene si tratti di misurazioni piuttosto grossolane, che includono anche l'invocazione del
metodo di calcolo e stampa della differenza, forniscono chiare indicazioni circa i diversi ordini
di grandezza. Pertanto nella scrittura di programmi in cui le performance giocano un ruolo
centrale è opportuno fare attenzione all'utilizzo del boxing, soprattutto in loop estesi, al fine di
evitare che le continue conversioni finiscano per degradare le performance.

3.3.4 Cercare di evitare gli oggetti di wrapping nei calcoli


Dalla lettura di quanto riportato nella regola precedente, questa dovrebbe essere ridondante.
In complessi e/o lunghi calcoli matematici, il ricorso agli oggetti di wrapping e agli automati-
smi di auto-boxing e auto-unboxing dovrebbero essere, per quanto possibile, minimizzati al fine
di evitare impatti significativi sulle prestazioni.

3.3.5 Fare attenzione all'operatore di uguaglianza


Un elemento a cui bisogna assolutamente porre attenzione con i meccanismi di boxing è l'ope-
ratore di uguaglianza che potrebbe presentare qualche sorpresa. Si consideri il frammento di
codice riportato, che mostra problemi con l'operatore ==.

public static void main(String args[J) I

Integer ¡1 = new lnteger(10);


Integer ¡2 = 10;
System.oui.println("1. Equals : " + (¡1 == i2));

Integer i3 = 20;
Integer i4 = 20;
System.ouf.println("2. Equals : " + (¡3 == ¡4));

Integer ¡5 = 2000;
Integer i6 = 2000;
System.oi/f.prlntln("3. Equals : " + (¡5 == ¡6));
)

La sua esecuzione genera il seguente "magico" risultato:

1. Equals : false
2. Equals : true
3. Equals : false

Sebbene ciò possa sembrare strano e destare qualche preoccupazione, questo comporta-
mento è dovuto al fatto che la JVM quando alloca la memoria per i tipi di wrapping, e in alcuni
casi speciali, tende a riusare gli stessi oggetti.

3.4 Varargs: argomenti variabili


Varargs (variable arguments, "argomenti variabili") rappresenta un'altra feature introdotta
con Java5.
Come suggerisce il nome, si tratta di un meccanismo che permette di implementare metodi
in grado di prendere zero o un numero variabile di elementi del tipo definito.
Anche questa feature ha finito per generare una serie di polemiche, spesso legittime. Come
nel caso dei Generics, si tratta di sintassi non supportate dalla Java Virtual Machine per via dei
soliti motivi di compatibilità con le precedenti versioni Java. Queste feature sono pertanto
risolte dal compilatore attraverso appositi mapping ai costrutti esistenti, in questo caso array.
Ciò fa sì che i varargs presentino a volte dei comportamenti inaspettati, come mostrato succes-
sivamente. Prima di proseguire oltre, si consideri il codice riportato di seguito. Come si può
notare il metodo minValue può essere invocato con un numero qualsivoglia di valori, o addirittu-
ra con nessuno.

public static ini minValue(int... vallnt) I


int min = lnteger.MAX_VALUE;

if ( vallnt.length = = 0 ) 1
throw new HlegalArgumentExceptionfNo paraemter provided");
I

for (int el : vallnt) I


il (el < min) I
min = el;
I
I

return min;
)

public static void main(String args[]) I


System.out.println(min\/alue(25, 3 , 1 0 0 , 41));
System.out.println(minValue(123, 2));
System.out.println(minValue());
I

3.4.1 Utilizzare il meccanismo dei varargs con parsimonia


Anche in questo caso, si consiglia vivamente di utilizzare questa feature con parsimonia. Molto
probabilmente rientra nella categorie delle feature di cui non si sentiva grandissimo bisogno.
In effetti, tutto quello che è possibile fare con i varargs si può fare con i corrispondenti array.
Probabilmente, questo meccanismo è particolarmente utile ai programmatori meno esperti
con incertezze circa l'utilizzo degli array...
L'utilizzo dei varargs in genere non è fortemente consigliato perché può portare
all'implementazione di metodi meno robusti. Per esempio, si tende a rilassare i controlli su
parametri tipici del non utilizzo dei varargs, si possono avere più problemi nell'implementare
metodi con un numero fisso di parametri, etc.
Si consideri il codice riportato di seguito, che mostra qualche stranezza nell'utilizzo dei tipi
varargs. Qualora eseguito così com'è, genera l'output atteso (0, 0 e 3). Tuttavia il compilatore
segnala un warning nella prima esecuzione, chiedendo di eseguire il cast del valore nuli al tipo
Object. Seguendo tale suggerimento, e quindi sostituendo la prima invocazione con il seguente
codice: System.out.printlnfelements : " + elementCount( (Object)null) ); e quindi eseguendo si ottiene
il seguente inaspettato output: 1, 0 e 3. Ciò perché il compilatore sostituisce (Object)null con un
array di un solo elemento posto a nuli.

private static int elementCount(Object... elements) {


return elements == null ? 0 : elements.length;
I

public static void main(String... args) {


System.out.println("elements : " + elementCount(null));
System.out.println("elements : " + elementCountf));
System.out.printlnfelements : " + elementCountfLuca", "Antonio", "Roberto"));
)

Inoltre, l'utilizzo dei varargs può creare problemi con l'overloading. Si consideri per il esem-
pio il caso di metodi overloaded con parametri definiti attraverso varargs. In questo caso un'in-
vocazione con nessun argomento risulterebbe ambigua. Ambiguità che però è immediatamente
intercettata dal compilatore.

private static int elementCount(String... elements) I


return elements == null ? 0 : elements.length;

private static inl elementCount(lnteger... elements) I


return elements == null ? 0 : elements.length;

public static void main(String... args) I


System.oi//.println("elements : " + elementCount(\, 2, 3, 4, 5, 6, 7, 8, 9));
System.ouf.printlnfelements:" + elementCount("Luca", "Antonio", "Roberto"));
System.ouf.println("elements : " + elementCount()); // ERROR

3.5 Applicazioni multi-threaded tradizionali


La programmazione multi-threading (MT) rappresenta indubbiamente uno degli argomenti
più complessi della programmazione. Pertanto, al fine di non appesantire la trattazione e, al
tempo stesso, di fornire i lettori meno esperti dell'argomento le nozioni fondamentali, si è
deciso di dedicare al fondamentale argomento una specifica appendice (Appendice D) a cui si
rimanda per spiegazioni di maggiore dettaglio.
Cominciamo a vedere il MT tradizionale, ossia i meccanismi presenti prima dell'introduzio-
ne del nuovo package della concorrenza. Esaurite le direttive relative al multithreading tradi-
zionale, ci concentreremo sul nuovo package java.util.concurrent di Doug Lea. Il tutto viene con-
cluso con una serie di consigli indipendenti dalle particolari classi utilizzate.
3.5.1 Preferire l'implementazione dell'interfaccia Runnable
Java offre due modi per definire classi le cui istanze dovranno essere eseguite da appositi thread:

1. implementare l'interfaccia java.lang.Runnable;


2. estendere la classe java.lang.Thread.

In entrambi i casi è necessario definire l'implementazione del metodo run(). Tuttavia, qualora
si intenda definire esclusivamente il metodo run(), la prima alternativa è da preferire. Questa
tecnica è utile anche considerate le limitazioni di Java relative all'ereditarietà singola, per cui se
una classe eredita da Thread, non può ereditare da altre classi. Da tener presente che, sebbene sia
sempre possibile simulare l'ereditarietà con un'apposita composizione, alcune volte questa stra-
tegia non è utilizzabile proprio poiché è obbligatorio ereditare da un'altra classe, come nel caso
di Applet. La principale differenza tra le due strategie è che nel primo caso si ha un solo oggetto
eseguito da più thread, e nel secondo ogni thread incapsula anche l'oggetto da eseguire.

class simpleThread extends Thread I

// Questo m e t o d o è invocato q u a n d o il thread è in esecuzione


public void run() I
I

// Per creare il thread è necessario eseguire le seguenti istruzioni


Thread thread = new SimpleThread));
thread. start();

Vediamo la creazione di un thread tramite implementazione dell'interfaccia java.lang.Runanble.

class SimpleThread i m p l e m e n t s Runnable I

// Questo m e t o d o è invocato q u a n d o il thread è in esecuzione


public void run() I
I

/ / Per creare il thread è necessario eseguire le seguenti istruzioni

// 1. Creare un oggetto di tipo runnable


SimpleThread myRunnable = new SimpleThreadf);

/ / 2. Creare un thread per eseguire l'oggetto runnable


Thread myThread = n e w Thread(myRunnable);

il invocare i\ metodo start


myThread.startO;

3.5.2 Implementare classi thread in modo che terminino


programmaticamente
Analizzando programmi concorrenti scritti in Java non è infrequente imbattersi in codici che
utilizzano i metodi deprecati stop e suspend della classe thread. Si tratta di una prassi pericolosa
e quindi sconsigliata. In particolare, interrompendo bruscamente l'esecuzione di un thread
(invocandone il metodo Stop) lo si forza a rilasciare tutti i monitor precedentemente acquisiti
senza permettergli di terminare il task in esecuzione. Tale brusca terminazione avviene per
mezzo della propagazione dell'eccezione ThreadDeath. Se un oggetto precedentemente bloccato
dal thread in questione si trova in uno stato inconsistente durante la terminazione del thread,
altri thread potrebbero utilizzarlo senza aver alcuna opportunità di essere informati circa la
relativa inconsistenza (si dice che è "danneggiato", damaged). Questa situazione ha le potenzialità
di generare comportamenti randomici difficilmente diagnosticabili. A peggiorare la situazione
interviene il fatto che l'eccezione ThreadDeath termina i thread in maniera "silenziosa" senza
fornire all'utente alcun avvertimento del fatto che il programma potrebbe trovarsi in uno stato
inconsistente. Situazioni del genere, inoltre, potrebbero generare strani comportamenti solo
dopo ore o giorni, rendendone l'individuazione e la diagnosi ancora più problematica, specie in
applicazioni mission criticai quali quelle solitamente realizzate in Java (sistemi bancari, gestione
di prenotazioni etc.).
L'implementazione corretta della terminazione di un thread dovrebbe utilizzare un codice
simile a quello riportato di seguito, che non va utilizzato in caso di operazioni bloccanti.

public class EsampleThread implements Runnable {

boolean volatile endOfExecution = false;

/**

* thread run method


7
public void run() {
boolean endOfWork = false;

while ( (¡endOfExecution) && (lendOfWork) ) I

//perform the work


endOfWork = someExecutionf);

I
I

/ "

* Request the thread termination.


7
public void requestStop(){
endOfExecution = true;
I

Sebbehe il codice presentato rappresenti una buona strategia per terminare l'esecuzione di
un thread, ahimè, non sempre è applicabile. In particolare, qualora il thread sia impegnato
all'interno del metodo run nell'eseguire operazioni bloccanti, la richiesta di conclusione per
mezzo del flag non forzerebbe il thread a terminare. In questi contesti, un'altra strategia molto
efficace consiste nell'utilizzare l'eccezione di interruzione (InterruptedException). Da tener pre-
sente che questa non blocca immediatamente il thread destinatario, ma si limita a consegnare il
messaggio di richiesta di interruzione, lasciando quindi il thread il modo di terminare corretta-
mente.

public class EsampleThread extends Thread I

/**

" thread run method


7

public void run() {

try I
boolean endOfWork = false;

while ( (lislnterruptedO) && (lendOfWork) ) I

// perform blocking work


endOfWork = someExecutionQ;

I
I catch (InterruptedException ie) {
// clean-up and thread-exit
I

/**

" Request the thread termination


*/
public void requestStop()|
interruptf);
I
I

3.5.3 Utilizzare volatile


Le variabili volatile rappresentano un meccanismo Java per implementare la sincronizzazione,
tanto che spesso sono definite come la sincronizzazione leggera, soprattutto dal punto di vista
della notazione.
I meccanismi di sincronizzazione in Java hanno principalmente due importanti obiettivi:
evitare che diversi thread accedano concorrentemente ad aree di codice critiche, assicurare la
corretta visibilità degli oggetti condivisi. Per quanto riguarda quest'ultima, è una caratteristica
abbastanza complessa che interagisce con le politiche di gestione della memoria, le ottimizzazioni
eseguite dai compilatori, le cache gestite dai vari thread, etc. Comunque, in generale, la presen-
za del costrutto fa sì che gli aggiornamenti eseguiti da uno specifico thread prima di uscire
dall'area sincronizzata diventino visibili ad altri thread che si accingono ad entrare in tale por-
zione di codice.
Si consideri l'esempio di codice mostrato poco sopra. Qualora la variabile endOfExecution non
fosse dichiarata volatile, si correrebbe il rischio che ciascun thread ne gestisca una copia nella
propria memoria di cache senza aggiornarla mai con la copia master. Ciò potrebbe portare
all'ovvio risultato di avere thread che non si accorgano del relativo cambio di valore e che
quindi non terminano quando richiesto.
Da quanto detto è evidente che la parola chiave volatile fa sì che la JVM non renda possibile ai
thread di gestire copie delle variabili nella propria cache, e pertanto finisce per peggiorare le
prestazioni. Quindi, è necessario utilizzare questa parola chiave con oculatezza.
Le variabili volatile forniscono un modo elegante ed agevole per definire la sincronizzazione,
influenzando sia l'atomicità delle istruzioni, sia la relativa visibilità. Tuttavia, da sole offrono la
protezione richiesta quando la variabile è indipendente sia dal valore di altre variabili che dal
proprio valore. Come si può notare entrambe condizioni sono rispettate dal codice dell'esem-
pio. Da tener presente però che l'utilizzo delle variabili volatile può rendere il codice più fragile.

3.5.4 Non utilizzare il metodo Thread.suspend


Il metodo Thread.suspend non va utilizzato perché è rischioso e come tale è stato correttamente
deprecato. In questo caso la deprecazione è avvenuta perché il metodo è intrinsecamente sog-
getto alla generazione di situazioni di dead-lock. Il problema è relativo al fatto che, mentre un
thread è sospeso, nessun altro può accedere alle risorse che questo ha bloccato fintanto che la
relativa esecuzione viene ripresa (invocazione del metodo Thread.résumé). Ora, se il thread inca-
ricato di riawiare quello sospeso dovesse avere la necessità di accedere a un oggetto tra quelli
bloccati da quest'ultimo, ecco generata la situazione di dead-lock che si manifesta con la pre-
senza di processi congelati.
Si immagini l'impatto sul sistema di un thread bloccato che abbia acquisito una serie di lock
tra cui uno o più di importanza critica, come per esempio uno che veicola l'utilizzo di una
risorsa importante del sistema. Anche in questo caso la situazione può essere risolta in maniera
programmatica utilizzando i metodi wait e notify.

3.5.5 Evitare la manipolazione dei livelli di priorità dei thread


La manipolazione esplicita delle priorità dei thread può creare problemi legati alla dipendenza
di Java dallo OS (Operating System, sistema operativo). In teoria il linguaggio Java definisce
dieci diversi livelli di priorità: la classe java.lang.Thread definisce le seguenti costanti (attributi
statici):

public static final int MAX_PRIORITY = 10;


public static final int MIN_PRIORITY = 1;
public static final int N0RM_PRI0RITY = 5

La maggior parte di OS utilizza scheduler la cui politica di assegnazione della CPU è basata
sulla selezione del thread in stato di attesa a più alta priorità. Il problema è che diversi OS pre-
vedono differenti livelli di priorità. Qualora questi siano superiori o uguali ai dieci livelli previsti
(per esempio, Solaris assegna all'attributo di priorità 31 bit, quindi 2 " = 2 Gigabytes) si tratta di
risolvere un semplice esercizio di mapping, mentre quando il numero è inferiore (Windows NT
dispone di appena sette livelli di priorità), la situazione diviene più problematica (bisogna asso-
ciare più priorità concettuali a una stessa priorità fisica). A complicare le cose poi intervengono
alcuni servizi particolari di specifici OS (per esempio Windows NT) che operano sulla priorità
dei thread, aumentandola o riducendola, in funzione dell'esecuzione di prestabilite operazioni.
Questa tecnica è nota con il nome di priority boosting e tipicamente può essere disabilitata attra-
verso opportune chiamate native a codice, quindi non incluse nel linguaggio Java, tipicamente,
effettuate attraverso il linguaggio C. Pertanto, è sempre consigliabile evitare, per quanto possibi-
le, la gestione esplicita della priorità dei thread, e se è proprio necessaria, limitarla il più possibile.

3.5.6 Considerare i diversi modelli di esecuzione dei thread


Un importante vincolo da tener presente quando si progettano programmi concorrenti in Java
è che, in questo contesto, il linguaggio non è completamente indipendente dalla piattaforma di
esecuzione. Questa caratteristica fondamentale è ancora ottenibile a spese però di un disegno
che contempli, in maniera ridondante, i diversi modelli di funzionamento delle varie piattafor-
me (il codice deve essere quindi platform aware). La questione nodale è che la programmazione
multi-threading presenta dipendenze strutturali dal sistema operativo, le quali possono essere
minimizzate ma non eliminate. Il problema per i programmatori risiede nel fatto che il linguag-
gio Java non prevede alcun modello multi-threading di riferimento; questo è "ereditato" dalla
piattaforma di esecuzione. Ciò fa si che se si desidera scrivere programmi concorrenti in grado
di essere eseguiti su qualsiasi piattaforma e quindi per qualsiasi modello multi-threading, que-
sti devono contenere i meccanismi di funzionamento per i vari modelli.
Il modello più comune, cosiddetto preventivo o a partizione di tempo (time- slice), prevede
che la CPU sia assegnata ai thread per intervalli di tempo (slices, "fettine") ben definiti, al
termine dei quali, il thread è forzato a rilasciare la CPU. In questo caso è il sistema operativo
che si fa interamente carico di gestire la concorrenza.
Nell'altro modello, denominato cooperativo, la concorrenza è (quasi) completamente
demandata all'applicazione, quindi al programmatore il quale dichiara esplicitamente quale
siano i momenti più opportuni in cui un thread debba cedere il controllo (invocazione del-
l'istruzione yield), inoltre, potenzialmente, il passaggio del controllo tra diversi thread, può av-
venire al livello di user-mode subroutine, evitando il coinvolgimento di servizi kernel del OS, i
quali possono richiedere fino a diverse centinaia di cicli macchina.
Pertanto, i programmi multi-threading Java devono essere scritti considerando sia il fatto che
il thread possa essere forzato a lasciare la CPU per via dell'esaurimento del tempo a disposizio-
ne, sia che 0 rilascio della CPU possa avvenire solo dietro esplicita richiesta da parte dello
stesso thread (operazione di yield).

3.5.7 Evitare dead-lock


Il dead-lock ("blocco mortale") è una situazione in cui due o più thread interferiscono tra di
loro in un modo tale che nessuno possa procedere nella propria esecuzione: un thread blocca
una serie di risorse, però per proseguire e rilasciarle ha bisogno di altre risorse, bloccate da uno
o più thread che, a loro volta, per poter proseguire, hanno bisogno delle risorse bloccate dal
primo thread. Il classico cane che si morde la coda... Sebbene esista una serie di tool di suppor-
to all'individuazione di dead-lock, spesso, si tratta di situazioni difficili da individuare. Ciò
nonostante è possibile illustrare una serie di strategie che favoriscono la prevenzione di dead-
lock. In particolare:
• implementare i thread aafinché la procedura per l'acquisizione di lock segua lo stesso
ordine: pertanto, qualora un thread trovi bloccata la prima risorsa, automaticamente,
non tenta di acquisire le altre;
• cercare di inglobare i vari lock in un altro: in questo caso l'acquisizione del lock globale
causerebbe l'acquisizione degli altri lock;
• fare in modo che un thread che abbia acquisito un certo numero di risorse rilasci tutte le
risorse precedentemente acquisite qualora fallisca ad acquisire le restanti dopo un paio
di tentativi.

Il codice seguente mostra un semplice esempio di deadlock causato da due thread che tenta-
no di acquisire due risorse in senso opposto.

public class SimpleDeadLock i m p l e m e n t s Runnable I

private Object resourcel;


private Object resource2;
private String name;

public SimpleDeadLock(String aName, Object aResourcel, Object aResource2) I

name = aName;
resourcel = aResourcel;
resource2 = aResource2;

public void run() {

System.out.printlnfThread: "+name+" running...");

synchronized (resourcel ) {

System.out.println("Thread; "+name+" locked resource:"+resource1);

try I

System.out.printlnfThread: "+name+" sleeping...");

// il thread viene m e s s o a d o r m i r e per 5 0 millisecondi


Thread.sleep(50);
I catch ( I nterrupted Exception ie) I
ie.printStackTrace();

System.out.printlnfThread: "+name+" awaking...");

// al risveglio il thread tenta di effettuare il lock della seconda risorsa


synchronized (resource2) (
System.out.println("Thread: "+name+" locked resource:"+resource2);
I

public static void main(String[] args) I

String resourceA = "RisorsaJ";


String resourceB = "Risorsa_2";

SimpleDeadLock runnablel = new SimpleDeadLock("T1", resourceA, resourceB);


SimpleDeadLock runnable2 = new SimpleDeadLock("T2", resourceB, resourceA);

Thread treadl = new Thread(runnablel);


Thread tread2 = new Thread(runnable2);

treadl ,start();
tread2.start();

L'esecuzione del precedente codice genera il seguente output:

Thread: T1 running...
Thread: T1 locked resource:Risorsa_1
Thread: T1 sleeping...
Thread: T2 running...
Thread: T2 locked resource:Risorsa_2
Thread: T2 sleeping...
Thread: T1 awaking...
Thread: T2 awaking...

Da tener presente che, qualora si abbia il sospetto di una situazione di deadlock o comunque
ogni qualvolta si voglia controllare lo stato dei thread e monitor di oggetti, è possibile richiede-
re il thread ed effettuare il monitor dump premendo CTRL + BREAK in Windows e CRTL + \ in
Solaris.

3.5.8 Porre attenzione alle inizializzazioni pigre (Lazy initialization)


La lazy initialization è una tecnica di programmazione molto utilizzata la cui idea alla base
consiste nel posticipare l'esecuzione di determinate attività, spesso l'inizializzazione di oggetti
"pesanti", fino a quando non ve ne sia l'effettiva necessità, in modo da diluire nel tempo ope-
razioni costose. L'applicazione di questa tecnica impone di implementare appositi controlli per
assicurarsi che questa inizializzazione avvenga una sola volta. Ciò fa sì che spesso si ricorra
all'implementazione diSingleton. Sebbene si tratti di un problema ricorrente, è sempre possi-
bile individuare codici errati. Alcuni esempi di codice errato sono mostrati di seguito.
Qui abbiamo un esempio errato di implementazione della lazy initialization. Si assume che il
metodo costruttore, dichiarato privato, esegua operazioni pesanti.

public class HeavyObject I

private final static HeavyObject instance = null;

public HeavyObject getlnstanceQ I


il (instance == null) {
instance = new HeavyObjectQ;
I

return instance;

Dall'analisi del codice è possibile notare che questo sia errato in quanto non tiene assoluta-
mente in considerazione la possibilità che diversi thread richiedano l'oggetto per la prima volta
contemporaneamente dando luogo ad un tipico esempio di race condition.
Per risolvere questo problema certi sviluppatori tentano di rendere il codice sicuro senza
appesantire ogni singola chiamata del metodo getlnstance con il sovraccarico della sincronizza-
zione. In effetti, questa sarebbe necessaria solo per la prima invocazione. Uno dei tentativi più
frequenti è dato dal double-check come mostrato nel listato poco sotto. Come si può notare si
esegue il primo test e quindi solo se l'oggetto sia effettivamente nullo, si entra nella zona protet-
ta. Sebbene le intenzioni siano lodevoli, l'implementazione non è sicura. Per comprenderne la
ragione è sufficiente considerare la seguente sequenza di azioni:

1. T^ esegue il metodo getlnstanceQ;


2. T trova il valore nullo e quindi acquisisce il lock;
3. T b esegue getlnstance mentre l'esecuzione di T è interrotta;
4. T trova ancora il valore nullo (T è stato interrotto) però fallisce ad acquisire il lock e
quindi viene forzato a lasciare la CPU;
5. Ta acquisisce la CPU e quindi termina l'esecuzione del metodo getlnstance inizializzando
l'oggetto e rilascia 0 lock;
6. T b può finalmente acquisire il lock e quindi prosegue la sua esecuzione, inizializzando
nuovamente l'oggetto.

Ecco il pattern pericoloso del doppio check.

public class HeavyObject I

private final slatic HeavyObject instance = nuli;

public HeavyObject getlnstance() I


if (instance == null) {
synchronized(lhis) I
if (instance == null) I
instance = new HeavyObject();
I

return instance,
I

L'unica implementazione veramente sicura è sfortunatamente quella che finisce per penaliz-
zare tutte le acquisizioni dell'istanza come riportato nel codice seguente.

public class HeavyObject I

private final static HeavyObject instance = null;

public synchronized HeavyObject getlnstance() {


if (instance == null) I
instance = new HeavyObjectf);
I

return instance,

Da tener presente che in condizioni molto ben definite, quando per esempio si ha l'assoluta
certezza che un thread solo all'inizio esegua la parte critica (processo di inizializzazione), è
possibile evitare completamente la sincronizzazione. Però, si tratta di casi molto limitati che
comunque richiedono un'attentissima analisi.

3.5.9 Utilizzare ThreadLocal per implementare confinamenti


all'interno dei singoli thread
Gli oggetti ThreadLocal permettono di mantenere una copia separata della variabile gestita per
ciascun thread che ne richieda l'utilizzo, permettendo di realizzare una sorta di pool preassegnato.
Ciò fa sì che ciascun thread veda esclusivamente i valori della propria copia senza doversi
preoccupare di eventuali modifiche o letture da parte di altri thread. Così facendo si riesce a
implementare brillantemente il confinamento delle variabili all'interno dei thread. La logica
conseguenza è che la classe ThreadLocal, permette di ottenere comportamenti assolutamente
thread-safe eliminando al contempo diversi colli di bottiglia, permettendo al codice di scalare
linearmente con il numero di thread. Un primo esempio di utilizzo di ThreadLocal lo si è visto
con i tipi date. In quel caso è stato suggerito di utilizzare questa classe per risolvere il problema
legato alla caratteristica di non thread-safe della classe SimpleDateFormat.
Un altro esempio utilizzo è relativo alla condivisione delle connessioni JDBC. Poiché queste
non sono necessariamente thread-safe, la loro gestione in ambiente MT richiede qualche prov-
vedimento, come appunto l'utilizzo della classe ThreadLocal, come riportato nel listato sequente:

privale static ThreadLocal<Connection> connectionCache =


new ThreadLocal<Connection>() I
public Connection initialization() I
return DriverManager.getConnection(DBMS_URL)

public static Connection getConnectionQ I


return connectionCache.getQ-,

3.5.10 Fare attenzione all'atomicità dei tipi long e doublé


La proprietà di atomicità applicata agli attributi di un programma Java garantisce che un thread,
in un qualsiasi intervallo di tempo, acceda o al valore iniziale o quello finale di uno specifico
attributo. In altre parole, assicura che non sia possibile da parte di un thread accedere a valori
aleatori generati da diversi thread intenti a modificare contemporaneamente il valore della
stessa variabile. Questa proprietà è garantita per tutti i tipi base ad eccezione di long e doublé
(non dichiarati volatile). Questo perché la relativa implementazione utilizza una dimensione di
64bit che spesso è trattata come due word da 32bit. Pertanto ciascuna operazione di lettura e
scrittura richiede una doppia operazione che, in determinate condizioni, può aver luogo con
una distanza temporale (relativamente) significativa. Ciò, pertanto, potrebbe portare al verifi-
carsi della situazione in cui due o più thread modifichino e/o leggano la stessa variabile all'uni-
sono ottenendo un valore errato dovuto all'intrecciarsi delle operazioni da 32bit.
Pertanto, l'utilizzo di variabili o attributi di tipo long e doublé (non dichiarati volatile), in un
ambiente multi-threading va reso esplicitamente atomico. L'atomicità è condizione necessaria
ma non sufficiente per la programmazione concorrente (per esempio non garantisce l'accesso al
valore più recente della variabile).
La JVM garantisce l'atomicità di attributo o variabile di tipo long e doublé dichiarato volatile.

3.5.11 L'incremento unitario non è thread-safe


Nella scrittura di programmi MT è necessario tenere a mente il fatto che l'operatore incremento
unitario non è thread-safe. La sintassi potrebbe essere ingannevole (si tratta di un'operazione) ma
in realtà l'incremento unitario è una contrazione delle tre classiche e distinte operazioni: lettura,
aggiornamento e memorizzazione del valore. Come tali non sono atomiche. A peggiorare la situa-
zione interviene il fatto che individuare problemi di race-condition generati da incrementi unitari
è molto difficile, in quanto tendono a manifestarsi molto raramente. Ecco un semplicissimo esempio
di un programma che non fa altro che eseguire l'incremento unitario di un contatore.

public class Counter I

private static int counter.


public static void main(String[] args) I
counter++;
I
)

Si consideri il semplicissimo programma mostrato nel listato precedente. Eseguendo l'utility


javap (javap -c Counter) è possibile ottenere il seguente frammento di byte code:

public static void main(java.lang.String[]);


Code:
0: getstatlc #18; //Field counter: I
3: ¡const_1
4: iadd
5: putstatic #18;//Field counter:l
8: return

Come si può notare dai passi 3,4 e 5, l'incremento unitario non è thread-safe. A seconda dei
propri requisiti, è possibile ottenere questa caratteristica sia schermando l'operazione da appo-
siti lock oppure utilizzare i tipi atomici, decisamente più performanti. Molto importante è an-
che considerare che l'utilizzo della parola chiave volatile non migliora le cose.

3.6 Applicazioni multi-threaded Java 5


In questa sezione sono presentate una serie di linee guida più specifiche per Java 5. Poiché
l'introduzione di questa libreria è relativamente recente, non è stato possibile collezionare/
analizzare un grandissimo numero di errori tipici e trabocchetti. Tuttavia, ce ne sono già diversi
e molto importanti.

3.6.1 Non reinventare la ruota


Prima di imbarcarsi nell'implementazione di una classe/libreria è sempre opportuno verificare
se esistano delle versioni già implementate che possano risolvere il problema e soprattutto chie-
dersi se sia veramente il caso di avventurarsi in nuove implementazioni. Questo è particolarmen-
te ricorrente in librerie per ambienti MT. Il caso più emblematico sono le implementazioni di
cache. Analizzando molti sistemi si osservano spesso implementazioni ad hoc, con tutti i limiti
del caso. Pertanto, prima di lanciarsi nell'implementazione di librerie/utility etc. si consiglia di
verificare la possibilità di utilizzare qualcosa di già esistente. Inoltre, qualora sia necessario im-
plementare componenti MT è opportuno studiare attentamente il package per la gestione della
concorrenza introdotto con Java 5. Questo presenta un'enorme ricchezza di classi appositamen-
te disegnate per offrire elevate prestazioni in ambienti fortemente concorrenti. Dall'analisi di
queste classi, spesso, è possibile individuare versioni corrispondenti di proprie classi e utility.
Tuttavia, è importante assegnare la priorità alle versioni Java, ciò per una serie di motivi:

• performance. Le nuove classi sono state a lungo studiate e implementate da un team


altamente specializzato. Inoltre traggono vantaggio da una serie di nuove istruzioni al
livello di bytecode. Pertanto tendono a presentare prestazioni difficilmente superabili.
• migliore supporto. Ci sono specialisti che rivedono e migliorano le varie implementazioni.
• documentazione/curva di apprendimento. Tali classi sono parte della libreria standard
Java, sono ben documentate, supportate da articoli e libri, e sono parte del bagaglio
culturale di ogni sviluppatore Java: non dovrebbe essere necessario ulteriore training.
• elevato livello di affidabilità. Il corretto funzionamento delle classi Java è stato verificato
negli ambienti più disparati, per le più diverse applicazioni, offrendo un'affidabilità
assolutamente unica.

3.6.2 Ricordare che gii oggetti atomici non estendono automaticamente


la relativa atomicità a operazioni composte.
Con l'introduzione di Java 5, la programmazione MT è diventata indubbiamente meno com-
plessa. In particolare, la presenza dei tipi di dati atomici (java.util.concurrent.atomic) ha risolto
diversi problemi, come visto in precedenza. Tuttavia, è necessario considerare che l'atomicità è
relativa al singolo oggetto. Ecco un esempio di utilizzo non thread-safe di oggetti atomici.

public class Counter I

private AtomicLong counter = new AtomicLong(O);


private AtomicLong aggregator = new AtomicLong(O);

public long addAndResult(long element) I


long result = 0;

aggregator.addAndGet(element);
counter. incrementAndGet();

return result;

public float getAvaragef) I


float result = OF;

if (counter.get() > 0) I
result = aggregator.get() / counter.get();
I

return result;
I

L'intera implementazione è assolutamente non thread-safe. Nel metodo addAndResult, sebbe-


ne ciascuno dei due oggetti, considerato singolarmente, sia incrementato atomicamente, i due
incrementi insieme non lo sono. Quindi un thread in esecuzione potrebbe essere interrotto
dopo aver incrementato il totale (aggregator) e prima dell'incremento del contatore (counter),
mentre un altro esegue il calcolo della media, con il risultato di produrre un risultato incorretto.

3.6.3 Valutare l'utilizzo delle collezioni concorrenti


Un'altra componente molto apprezzata di Java 5 sono le collezioni concorrenti
(java.util.concurrent): C o n c u r r e n t H a s h M a p , C o p y O n W r i t e A r r a y L i s t , e C o p y O n W r i t e A r r a y S e t , alle quali si
Interfaccia/classe tradizionali Collezioni concorrenti Versione Java
HashTable ConcurrentHashMap 1.5
ArraList CopyOnWriteArrayList 1.5
Set CopyOnWrlteArraySet 1.5
SortedMap (TreeMap) ConcurrentSkipListMap 1.6
SortedSet (TreeSet) ConcurrentSkipListSet 1.6

Tabella 3.1 - Le nuove collezioni per la concorrenza.

aggiungono tutta una serie di code: ConcurrentLinkedQueue, LinkedBlockingQueue, ArrayBlockingQueue,


SynchronousQueue, PriorityBlockingQueue, e DelayQueue. A queste vanno poi aggiunte le altre colle-
zioni introdotte con Java 6, come ConcurrentSkipListMap e ConcurrentSkipListSet. Si ottiene così il
seguente quadro delle corrispondenze mostrate nella tabella 3.1.
Queste classi sono state disegnate specificatamente per ottimizzare le performance dell'uti-
lizzo delle collezioni in ambienti MT. In particolare, mentre le collezioni tradizionali permetto-
no di implementare la caratteristica thread-safe in modo assolutamente pessimistico, attraverso
lock a livello dell'intera collezione, serializzando quindi l'accesso da parte di diversi thread, e
quindi penalizzando fortemente le performance in ambienti concorrenti, le collezioni dello
specifico package per la concorrenza implementano meccanismi più granulari, spesso simili
all'optimistic-locking. Si consideri l'implementazione del metodo get della classe HashMap ripor-
tata poco sotto su due colonne. Qualora questa dovesse essere utilizza in un ambiente MT,
sarebbe necessario far sì che il thread richiedente mantenga bloccato l'intero oggetto per tutta
la durata dell'invocazione del metodo, che, in questo caso per esempio, può richiedere la
scansione di intere liste di collisioni (elementi diversi che generano lo stesso codice hash). Nella
versione concorrente, invece, si prosegue eseguendo le varie letture senza alcun lock, salvo poi
eseguire la lettura bloccata qualora accada qualcosa di imprevisto, come per esempio elemento
nullo. Questa condizione può essere generata da una riorganizzazione dell'intero oggetto Map.
Ecco il metodo della classe HashMap e della classe ConcurrentHashMap.

public V get(Object key) I public V get(0bject key) I


Object k = mas/r/Vz///(key); int hash = hash(key); // throws l\IPE if key null
int hash = hash(k); return segmentFor(hash).get(key, hash);
int i = indexFor{hash, table.length); I
Entry<K,V> e = tableji];
while (true) { (the corresponding get)
if (e == null) V get(0bject key, int hash) I
return null; if (count != 0) I // read-volatile
if (e.hash == hash && eg(k, e.key)) HashEntry<K,V> e = getFirst(hash);
return e.value; while (e != null) {
e = e.next; if (e.hash == hash && key.equals(e.key)) I
V v = e.value;
if (v != null)
return v;
return readValuellnderLock(e); // recheck
I
e = e.next;
I
I
return nuli;
I

Si noti nell'implementazione del metodo get della classe C u n c u r r e n t H a S h M a p la presenza


dell'invocazione del metodo SegmentFor. Questo ritorna un oggetto di tipo Segment, che ne è
una classe annidata. In sostanza ogni segmento gestisce una porzione degli oggetti dell'intera
collezione. Ciò è necessario per implementare una strategia di lock nota con il nome di lock
striping. Ciò fa sì che, invece di gestire un solo lock per l'intera collezione, sia possibile averne
diversi ognuno per sorvegliare un ben definito segmento.
Da tener presente che il miglioramento delle prestazioni in ambiente concorrente fornito dalle
nuove collezioni non avviene del tutto gratuitamente. In particolare, proprio per garantire un'elevata
concorrenza, è stato necessario rilassare la semantica dei metodi che operano sull'intero oggetto.
Per esempio, Size. In effetti, non essendoci lock i metodi relativi all'intero oggetto finiscono per
fornire un'indicazione, non garantita dello stato. Per esempio, mentre il metodo sta leggendo il
numero di elementi di una mappa, altri thread potrebbero essere intenti ad aggiungere nuovi elementi.

3.6.7 Utilizzare i lock al posto delle sincronizzazioni


quando le prestazioni sono importanti.
Prima dell'introduzione del package della concorrenza, l'unico modo per proteggere aree cri-
tiche del codice era ricorrere all'utilizzo della sincronizzazione (synchronized).

synchronized ( ObjectLock ) I
/ / u p d a t e the o b j e c t

public synchronized doSomething() I


/ / update the object
1

dove il primo costrutto protegge da accessi concorrenti il codice incluso nello stesso eseguendo
un lock sullo specifico oggetto, mentre nel secondo si protegge il codice definito dal metodo
bloccando l'intero oggetto a cui appartiene il metodo stesso.
In generale, il costrutto synchronized, permette di raggruppare le istruzioni incluse in un'uni-
ca esecuzione atomica. La logica conseguenza è che l'accesso da parte dei vari thread è serializzato:
solo un thread alla volta può entrare e quindi eseguire l'area protetta.
Per essere precisi, il costrutto synchronized influenza anche la visibilità. Questa è una caratteristi-
ca più complessa che interagisce con le politiche di gestione della memoria, le ottimizzazioni
eseguite dai compilatori, le cache gestite dai vari thread, etc. Ma in generale, la presenza del co-
strutto fa sì che gli aggiornamenti eseguiti da uno specifico thread prima di uscire dall'area sincro-
nizzata diventino visibili ad altri thread che si accingono ad entrare in tale porzione di codice.
Nonostante l'immediatezza, questo meccanismo soffre di una serie di limitazioni, le più im-
portanti delle quali sono l'impossibilità di interrompere un thread che si trova in stato di attesa
di acquisire un lock, impossibilità di eseguire un polling sul lock, impossibilità di tentare di
acquisire un lock senza dover attendere più di un determinato lasso temporale. Inoltre, anche dal
punto di vista delle prestazioni, la sincronizzazione non sempre risulta particolarmente efficien-
te. Per risolvere tutte queste limitazioni, Java 5 è stato dotato degli oggetti lock
(java.util.concurrent.locks.Lock), come per esempio ReentrantLock. Pertanto un blocco sincronizzato
viene trasformato come riportato di seguito in questo esempio di utilizzo di un lock rientrante.

Lock lock = new ReentrantLockf);

lock.lock();
tryl
/ / update the object
I finally {
lock.unlockQ;
I

Diversi studi hanno inoltre dimostrato che i nuovi oggetti lock, oltre ad offrire tutta una serie
di nuove funzionalità avanzate, presentano prestazioni migliori e maggiori livelli di scalabilità.
Tuttavia, i lock hanno anche qualche svantaggio da tener presente: è necessario eseguire
esplicitamente il lock ed il conseguente unlock\ quando si utilizza la sincronizzazione la JVM ne
è al corrente, e quindi le relative informazioni, utilissime per individuare dead-lock, sono mo-
strate nei vari thread dump.

3.6.9 Sostituire Thread con Executor per maggiore controllo e flessibilità


Il modo tradizionale di eseguire i thread (Thread(myRunnable).start()) presenta una serie di limita-
zioni legate ad alcune carenze, come la mancanza di disaccoppiamento tra l'invio di una richie-
sta e la sua esecuzione, la mancanza di policy di esecuzione, di cancellazione e così via. Inoltre,
vi è il problema dell'assenza del concetto di pool. Questo può creare sia problemi in termini di
performance (la creazione di un nuovo thread richiede tempo di esecuzione e quindi una con-
tinua creazione e distruzione di questi oggetti può finire per incidere sulle prestazioni) sia di
controllo del numero di thread nonché di scalabilità.
Pertanto, in tutti quei contesti in cui sia necessario disporre di un maggiore livello di flessibi-
lità e controllo, è fortemente consigliabile ricorrere alle classi che implementano Executor. Que-
sta è una semplicissima interfaccia introdotta con Java 5 che dichiara un solo metodo:

public interface Executor [


void execute(Runnable c o m m a n d ) ;
1

Executor è il mattoncino sul quale sono basate diverse classi del java.util.concurrent, che permet-
tono di risolvere le limitazioni sopraccitate.
Per esempio, l'interfaccia ExecutorService che estende Executor definisce i seguenti servizi:
public interface ExecutorService extends Executor I

void shutdown();

List<Runnable> shutdownNow();

boolean isShutdown();

boolean isTerminated();

boolean awaitTermlnation(long timeout, TimeUnit unit)


throws InterruptedException;

<T> Future<T> submit(Callable<T> task);

<T> Future<T> submit(Runnable task, T result);

Future<?> submit(Runnable task);

<T> List<Future<T» invokeAII(Collection<Callable<T» tasks)


throws InterruptedException;

<T> List<Future<T» invokeAII(Collection<Callable<T» tasks, long timeout, TimeUnit unit)


throws InterruptedException;

<T> T invokeAny(Collection<Callable<T>> tasks)


throws InterruptedException, ExecutionException;

<T> T invokeAny(Collection<Callable<T>> tasks, long timeout, TimeUnit unit)


throws InterruptedException, ExecutionException, TimeoutException;
I

Come si può notare vi è un livello di indirezione tra la proposizione di un task e la relativa


esecuzione; vi sono servizi relativi al ciclo di vita, come shutdown, isShutdown, isTerminated,
awaitTermination, e cosi via.
Le classi che implementano questa interfaccia sono ScheduledThreadPoolExecutor,
ThreadPoolExecutor a cui si aggiunge la classe astratta AbstractExecutorService.

3.7 Gestire correttamente possibili accessi concorrenti


Disegnare attentamente gli accessi concorrenti a risorse condivise è rilevante in tutti quei casi in
cui si voglia implementare un'applicazione multi-threading, ossia quando l'applicazione utente
voglia gestire diversi thread. Da notare che la JVM è multi-threading indipendentemente dalla
modalità con cui si implementano i programmi che vi dovranno funzionare. Infatti, oltre al thread
dedicato all'esecuzione del metodo main del programma applicativo, ne esistono altri (denomina-
ti daemon) demandati alla gestione di una serie di servizi di base, come il garbage collector.
La selezione del livello di lock da utilizzare, frequentemente, rappresenta un argomento de-
licato nella progettazione e implementazione di applicazioni concorrenti. Sebbene da un lato
una strategia conservatrice potrebbe evitare una serie di problemi tipici di errata sincronizza-
zione (corruzione dei dati, race conditions, comportamenti inattesi in casi non spiegabili, etc.),
dall'altro porterebbe alla generazione di un codice non particolarmente efficiente in cui il livel-
lo di concorrenza è piuttosto ridotto e, in alcuni casi addirittura alla generazione di situazioni di
dead-lock.
Ci sono tre metodi per evitare race-conditions:

1. non condividere un'area data tra più thread;


2. realizzare lo stato come un immutable;
3. sincronizzare l'accesso alle sezioni critiche.

3.7.1 Non sincronizzare metodi rientranti


I thread Java dispongono di un'area di memoria riservata per il proprio stack. Come tale, non
è accessibile da parte di altri thread. Tra gli altri dati (come per esempio l'indirizzo dell'istruzio-
ne da eseguire dopo l'esecuzione del metodo corrente), in questa area sono memorizzate le
variabili locali dei metodi, i valori dei parametri attuali dei metodi e l'eventuale valore di ritor-
no. Si ricordi che in Java i parametri sono passati by value... Chiaramente, nello stack non è
possibile memorizzare oggetti, ma solo i relativi riferimenti e pertanto, qualora sia necessario
passare un array o un altro oggetto, ciò che viene copiato nello stack è il relativo indirizzo. In
ogni modo, se all'interno di un metodo viene creato un oggetto (per esempio una stringa), il
relativo riferimento è visibile unicamente dal thread che in quel momento sta eseguendo il
metodo stesso. Metodi che utilizzano esclusivamente i parametri forniti e le variabili locali sono
definiti rientranti e come tali possono essere eseguiti, contemporaneamente, da diversi thread
senza aver bisogno di alcuna sincronizzazione.

3.7.2 Sincronizzare l'accesso a dati condivisi


Le classi Java disegnate per funzionare in modalità multi-threading devono essere progettate
per essere thread-safe\ devono funzionare correttamente qualora i relativi metodi siano eseguiti
simultaneamente da diversi thread. A tal fine è necessario identificare le aree ad accesso esclu-
sivo (aree che se accedute da più thread contemporaneamente potrebbero dar luogo a compor-
tamenti errati). Nell'accesso alle risorse di I/O queste aree sono già state analizzate e risolte
dall'implementazione delle API Java. Per esempio, la classe java.io.File prevede una serie di
metodi sincronizzati, quali:

private synchronized void writeObject(ObjectOutputStream s) throws lOException;

private synchronized void readObject(ObjectlnputStream s)


throws lOException, ClassNotFoundException

public static File createTempFile(String prefix, String suffix, File directory) throws lOException

Il metodo createTempFile possiede una sincronizzazione nell'interno della relativa


implementazione. Pertanto, nella stesura del codice è necessario identificare correttamente e
gestire conseguentemente le aree dati condivise da più thread. Questo è necessario perché la
JVM gestisce un'unica area dati (denominata heap) condivisa da tutti i thread (anche i daemon
come il Garbage Collector) destinata a memorizzare tutti gli oggetti (o meglio il valore dei
relativi attributi) gestiti dall'applicazione. Alcuni esempi di dati condivisi sono quelli scritti da
un thread che poi dovranno essere letti da altri, o viceversa dati da acquisire prodotti da altri
thread, dati di sincronizzazione, etc. Una volta identificate le aree e i dati da proteggere è
necessario stabilire il tipo di lock richiesto... Questo però è argomento di un'altra direttiva.

3.7.3 Valutare attentamente la necessità di sincronizzare i metodi


L'utilizzo della parola chiave synchronized, come lecito attendersi, ha un impatto sulle perfor-
mance, sia in termini di latenza (latency, tempo impiegato per svolgere un determinato lavoro),
sia in termini di throughput (numero di lavori eseguito nel medesimo arco temporale). Tutti gli
oggetti Java hanno, potenzialmente, un oggetto associato, denominato monitor, ed è necessario
serializzarne l'accesso da parte di diversi thread. Questo però è effettivamente utilizzato solo
quando un oggetto dichiara aree sincronizzate per mezzo della parola chiave synchronized.
Nelle versioni iniziali della, JVM, alcune statistiche evidenziavano che l'impatto di aree sin-
cronizzate poteva giungere fino ad un fattore 50. Le cose sono molto migliorare con le versioni
più recenti. Resta, tuttavia, il fatto che oltre a ridurre le performance, la sincronizzazione dei
metodi può generare colli di bottiglia e avere un effetto negativo sulla scalabilità dell'intera
applicazione. Pertanto, è necessario sincronizzare solo quando sia veramente necessario e sele-
zionare il livello opportuno di lock.

La perdita di prestazioni dovuta all'utilizzo della parola chiave Synchronized è facilmente comprensibile
considerando la politica con cui le J V M gestiscono la memoria in presenza di thread (le relative
specifiche fanno parte del J M M , Java Memory Model). In particolare, ogni thread è fornito con
un'apposita cache la cui politica di aggiornamento dei valori è fortemente influenzata dalla presenza
di aree sincronizzate. In assenza di queste, un thread è lasciato libero di evolvere accedendo alla
copia "locale" dei valori delle variabili memorizzate nella propria cache. Pertanto (sempre secondo
quanto stabilito dal J M M ) i thread sono autorizzati ad avere valori diversi relativi alla stessa variabile.
La situazione cambia notevolmente in presenza di aree sincronizzate. In questo caso le direttive
richiedono che un thread invalidi la propria cache, e quindi la aggiorni con i valori presenti nella
memoria "principale", non appena acquisito un lock (ingresso in un'area sincronizzata), e che riporti
tutte le modifiche effettuate nella memoria principale, appena prima di rilasciare il lock (uscita dall'area
sincronizzata). Pertanto è facile comprendere come ripetute richieste di sincronizzazione, da memoria
principale verso la cache del thread e viceversa, portino a una riduzione delle performance.

3.7.4 Considerare l'utilizzo di classi di sincronizzazione


Non è infrequente il caso di implementare delle classi e di non sapere a priori se dovranno o
meno funzionare in un contesto multi-threading, o meglio ancora, di implementare classi che
dovranno funzionare in entrambi gli scenari. La strategia più frequentemente utilizzata consi-
ste nell'utilizzare un approccio sicuro e quindi all'introduzione di blocchi a mutua esclusione.
Questo, se da un lato risolve il problema, dall'altro finisce per penalizzare tutti i contesti single-
threaded. Un'ottima strategia, di frequente impiego, consiste nell'implementare la classe senza
aree sincronizzate e quindi adatta per un ambiente single-threaded, e di utilizzarne un'altra che
ne incapsuli i metodi per un funzionamento multi-threading. Un po' come avviene con le classi
Collection che divengono thread-safe con l'esecuzione del comando Collections.synchronizedXXXQ.
3.7.5 Fare attenzione al tipo di lock richiesto
Per ogni blocco di codice Java è possibile selezionare una serie di protezioni per l'accesso
concorrente, quali: nessuna, lock legato alla singola istanza, e lock statico (associato alla classe).
L'acquisizione del lock istanza da parte di un thread (ingresso in un'area synchronized) blocca
eventuali altri thread che ne desiderino eseguire metodi sincronizzati. Ciò chiaramente non
blocca tali thread dall'invocazione di metodi non sincronizzati o sincronizzati a livello di classe.
Ecco il metodo isEmpty della classe java.util.Vector.

* Tests if this vector has no c o m p o n e n t s .

* © r e t u r n < c o d e > t r u e < / c o d e > if a n d only if this vector has


no c o m p o n e n t s , that is, its size is zero;
* <code>false</code> otherwise.
*/

public synchronized boolean isEmptyf) {


return elementCount == 0;
)

Come regola generale, è necessario sincronizzare, secondo la tecnica desiderata, i metodi che
modificano gli attributi delle classi le cui istanze sono condivise. Qualora, poi, sia necessario
che i thread acquisiscano sempre il valore più recente possibile, è necessario sincronizzare an-
che i metodi accessori. Si consideri una classe che contiene i dati, aggiornati in tempo reale, dei
prezzi di strumenti finanziari che, tipicamente, sono aggiornati diverse volte al secondo. In
questo caso è necessario che ogni thread acquisisca il valore aggiornato e, quindi, anche i meto-
di accessori devono essere opportunamente sincronizzati.
Non è infrequente il caso in cui lock a livello di istanza presentino un'eccessiva portata e che
quindi siano la causa di colli di bottiglia. In questi casi è necessario ricorrere a lock più granulari.
Si consideri l'esempio, di un oggetto che manipoli diverse strutture dati. In questo caso la
presenza di metodi sincronizzati potrebbe risultare non efficiente: l'acquisizione del lock da
parte di un thread per modificare una struttura dati blocca automaticamente l'accesso agli altri
thread, magari interessati a manipolare un'altra struttura dati, indipendente dalla precedente.
In questi casi, un'interessante alternativa consiste nel ricorre all'utilizzo di oggetti fittizi di cui
utilizzare il relativo monitor per l'accesso a specifiche sezioni critiche. Per esempio:

public class TestFinerLock I

// D u m m y object used f o r locking p u r p o s e s

Object lockWrite = n e w Object();

public void w r i t e ( ) (

// non criticai i m p l e m e n t a t i o n

synchronized ( lockWrite ) {
// write data in the shared objects
)

// do something else
1
)

I lock statici, infine, in maniera del tutto analoga a quanto riportato poco sopra, generano il
blocco di thread che desiderino eseguire metodi statici sincronizzati appartenenti alla stessa
classe, mentre non bloccano thread che desiderino eseguire metodi non sincronizzati o sincro-
nizzati al livello di istanza.
Come visto in precedenza, un'ottima tecnica consiste nell'utilizzare i nuovi oggetti Lock,
introdotti con Java 5.

3.7.6 Porre attenzione all'utilizzo della classe Iterator


per gli accessi concorrenti
Le collezioni Java 2 restituiscono oggetti di tipo Iterator implementati secondo la strategia fail-
fast. Ciò significa che se, dopo la creazione dell'oggetto Iterator, la corrispondente lista subisce
delle modifiche strutturali (aggiunta, rimozione di elementi e variazione delle dimensioni), non
ottenute attraverso l'esecuzione dei metodi propri dell'Iterator, questo lancia un'eccezione di
modifica concorrente (ConcurrentModificationException). Pertanto, qualora si verifichi una modifi-
ca strutturale concorrente, l'oggetto Iterator fallisce immediatamente e in maniera pulita (da cui
il nome della strategia), evitando di correre il rischio di generare problemi di comportamento
non deterministico difficili da individuare.
Riassumendo, problemi di modifica concorrente relativi agli Iterator, si verificano nel caso in
cui mentre un thread sta scorrendo un Iterator, un altro modifica la lista sottostante. Inoltre, non
è sempre possibile garantire questo comportamento soprattutto in presenza di metodi che ge-
stiscono le strutture non sincronizzati. Questa strategia è pertanto implementata secondo il
criterio best-effort. Qualora sia necessario navigare gli elementi di una lista in una situazione di
forte concorrenza, è necessario ricorrere a una delle seguenti alternative:

1. eseguire un lock della lista sottostante;


2. ottenere una copia "privata" degli elementi della lista.

Chiaramente le due alternative presentano ben definiti pregi e difetti e pertanto si configura-
no come soluzioni ideali per ben definiti scenari. La prima soluzione tende a ridurre la concor-
renza, evitando però problemi di performance dovuti alla copia di elementi e pertanto va utiliz-
zata qualora la concorrenza non sia elevata e sia relativa a strutture dati di dimensioni conside-
revoli. La seconda, ovviamente, offre una strategia opposta e pertanto è adatta qualora ci sia un
forte parallelismo su strutture dati di dimensioni limitate.
Da notare che le nuove collezioni della concorrenza non presentano questo problema. Infat-
ti, forniscono oggetti iterator che non lanciano eccezioni di modifica concorrente
(ConcurrentModificationException), questo perché questi oggetti piuttosto che implementare un
meccanismo di fallimento rapido, implementano quello di debole consistenza (weak consistency).
Ciò significa che 1' iterator accetta modifiche concorrenti mentre avviene la navigazione degli
elementi presenti quando costruito, però non garantisce (accade solo in determinate circostan-
ze) che questi siano incorporate nella propria lista iniziale e quindi presenti durante la scansione.
3.7.7 Non mischiare le politiche di lock
Ogni qualvolta si debba implementare del codice per ambienti MT è importantissimo disegna-
re attentamente la politica di locking prima di dar luogo a qualsiasi implementazione. In parti-
colare, un problema non infrequente è dovuto ad avventati mix di politiche di locking codifica-
ti in estremo per cercare di porre rimedio a soluzioni non studiate attentamente.
Un esempio classico si ha con l'utilizzo delle collezioni Java 2, come mostrato nel listato poco
sotto. Come da manuale, tali collezioni non sono nativamente thread-safe, ma lo possono diven-
tare attraverso l'utilizzo di apposite classi, come Collections. Ciò al fine di non appesantirne
l'utilizzo in contesti che non sono concorrenti. Tuttavia, ciò non risolve tutti i problemi. Nel
codice seguente, riportiamo una classe di utilità che consente di memorizzare una serie di errori
evitandone ripetizioni. Come si può notare, nonostante l'oggetto ArrayList sia reso sincronizza-
to, ciò non è sufficiente per la corretta implementazione del metodo addlfNotPresent. Un partico-
lare thread in esecuzione potrebbe essere interrotto tra il test e il conseguente inserimento,
compromettendo il requisito di non ripetizione degli errori. Una volta scoperta questa deficien-
za, alcuni programmatori potrebbero essere tentati di cercare di risolvere il problema sincro-
n i z z a n d o il m e t o d o a d d l f N o t P r e s e n t (public synchronized boolean a d d l f N o t P r e s e n t ( T n e w E r r o r ) ) . C i ò
sortirebbe ben poco effetto giacché si cercherebbe di sincronizzare l'oggetto sbagliato (si noti,
tra l'altro, la presenza del metodo di get). La soluzione al problema consiste nell'introdurre un
costrutto di sincronizzazione dell'oggetto errorList all'interno del metodo addlfNotPresent:
synchronized (errorList) ) . . . ( .

public class ErrorListManager<T> {

private List<T> errorList = Collections.synchronizedList( new ArrayList<T>() );

public boolean addlfNotPresent(T newError) {

boolean wasPresent = errorList.contains(newError);

if (IwasPresent) I
errorList.add(newError);
I

return wasPresent;
I

public List<T> getErrorList() I


return errorList;

3.7.8 Gli oggetti immutabili sono sempre thread-safe


In alcuni contesti è possibile evitare di ricorrere a dispendiosi costrutti di sincronizzazione
semplicemente realizzando oggetti immutabili, come il tipo stringa Java e gli oggetti di wrapping.
Gli oggetti immutabili sono quelli il cui stato, una volta impostato durante la costruzione, non
può più cambiare. In sostanza si tratta di classi che prevedono il passaggio dei parametri al
livello di costruttore e che non implementano metodi modificatori (setXXX).
L'implementazione di un oggetto totalmente immutabile richiede che i suoi attributi siano
dichiarati final. Ciò per evitare che questi possano essere variati attraverso l'implementazione di
una classe figlia.

3.7.9 Documentare sempre il tipo di sincronizzazione offerta


Ogniqualvolta si implementa del codice, soprattutto qualora debba essere riutilizzato in diver-
si contesti e da diverse persone, è fortemente consigliabile dichiarare esplicitamente se il codice
sia thread-safe o meno, e in caso affermativo, includere la strategia utilizzata per proteggere le
sezioni critiche del codice. Probabilmente, 0 posto migliore dove definire queste informazioni
è l'intestazione della classe.
Nel libro [JVCNPR] (probabilmente uno degli migliori sulla programmazione concorrente
in Java) viene addirittura proposta un'apposita notazione per dichiarare se la classe sia o meno
thread-safe (@ThreadSafe) ed il tipo il meccanismo utilizzato per il garantire la proprietà di thread-
safe (@GuardedBy).

3.7.10 Valutare soluzioni di tipo lock striping


La tecnica del lock striping consiste nel cercare di non bloccare l'accesso a interi oggetti ma
solo a sue parti. Questa tecnica per esempio è implementata dal CurrentHashMap dove l'intera
mappa è suddivisa in un predefinito numero di segmenti, la cui possibilità di modifica è subor-
dinata all'acquisizione di apposito lock. Ciò chiaramente risulta in un incremento di perfor-
mance e scalabilità: mentre un thread detiene il lock per un determinato segmento, gli altri
thread sono liberi di bloccare i restanti.
Capitolo
I commenti
Introduzione
Il corretto funzionamento dei sistemi implementati dovrebbere costituire un prerequisito irri-
nunciabile dell'ingegneria del software, ma pur sempre di prerequisito dovrebbe trattarsi: 0
codice dovrebbe, in teoria, funzionare comunque... Un'elevata qualità del codice, però, richie-
de il soddisfacimento di ulteriori importanti qualità come, per esempio, buona leggibilità e
manutenibilità. Pertanto, un'efficace documentazione è una caratteristica irrinunciabile di qual-
siasi software. Tutti gli sviluppatori, in qualche misura, lo sanno, ma nella pratica lavorativa non
è infrequente imbattersi in codici mal documentati, o, addirittura, non commentati.
Una delle regole fondamentali — frequentemente disattesa — per la produzione della docu-
mentazione consiste nel redigerla di pari passo con la produzione del codice. Ciò, oltre a forni-
re uno strumento di prima verifica della corretta implementazione dei vari algoritmi, facilita la
scrittura di commenti significativi ed evita l'odiosa attività, spesso e volentieri delegata al pro-
grammatore junior di turno, di scrivere frettolosamente la documentazione a posteriori,
magari nel brevissimo intervallo che precede il rilascio del codice.
Il problema sostanziale è che il tipico ciclo di vita di un qualsiasi software prevede che lo
stesso sia mantenuto, in diversi stati della sua evoluzione, da personale diverso dagli stessi
autori; e si tratta spesso di personale che non era presente nelle fasi iniziali di progettazione.
Proprio per questo motivo, il codice deve possedere la proprietà di essere compreso, in ogni
momento, sia dallo stesso autore sia da persone estranee al suo sviluppo. Purtroppo, questa
proprietà è spesso disattesa: gli autori stessi del software si trovano in difficoltà nel comprende-
re, e quindi nel modificare, parti di programma sviluppate da loro stessi, per via di una docu-
mentazione carente o per la mancata applicazione di standard di qualità.
Il monito è che software non ben documentati, e quindi poco leggibili e difficilmente
manutenibili, presentano notevoli problemi di riutilizzo e tendono a far nascere l'irrefrenabile
desiderio di riscrittura nel personale addetto alla relativa manutenzione: non vi è alcuna virtù
in un software difficilmente modificabile.
Come lecito attendersi, il linguaggio preso come riferimento è Java, per quanto, la maggior
parte delle direttive proposte mantenga la sua validità anche nell'ambito di diversi linguaggi di
programmazione. Questo è il caso anche per la documentazione JavaDoc: nessuno vieta né di
utilizzarne un'appropriata versione per linguaggi diversi da Java, né di scriverne una simile.

Obiettivi
L'obiettivo di questo capitolo è presentare una serie di tecniche, linee guida e best practice
finalizzate al miglioramento del livello di documentazione del codice; pertanto la proprietà del
software su cui si focalizza l'attenzione è, ancora una volta, la leggibilità. Come largamente
discusso nel Capitolo 1, si tratta del prerequisito irrinunciabile per la manutenibilità del codice.
Gran parte degli argomenti trattati in questo capitolo prevedono come requisito una buona
conoscenza e padronanza dell'utility Java per la produzione automatica della documentazione:
JavaDoc. Pertanto nell'Appendice A è riportata una utile e concisa presentazione dell'utility
JavaDoc inclusi i relativi tag.

Direttive
4.1 Investire nella documentazione
Sebbene questa regola sia universalmente accettata e quindi possa sembrare inutile da include-
re in questo contesto, la realtà quotidiana ci insegna come non sia affatto raro imbattersi in
codici mal documentati o non documentati affatto. Codice difficilmente leggibile e comprensi-
bile diviene complesso da mantenere e ciò prelude alla necessità di riscriverlo (anche se magari
non sarebbe necessario).

4.1.1 Produrre la documentazione contemporaneamente


all'attività di codifica
Si tratta probabilmente di una delle principali regole per l'attività di documentazione e proba-
bilmente anche di una delle più disattese. Scrivere la documentazione in linea con la program-
mazione permette di ottenere una serie di vantaggi, quali:

• fornisce un primo strumento di verifica semi-formale del codice che si sta scrivendo. La
necessità di redigere in linguaggio pseudo-naturale la spiegazione dell'algoritmo ogget-
to di implementazione stimola, implicitamente, a valutarne sia la validità sia la correttez-
za della codifica; inoltre, qualora risulti difficile commentare porzioni di codice, potreb-
be essere il segnale di allarme di un algoritmo errato, contorto o non ben scritto;
• permette di illustrare efficacemente le decisioni prese nello stesso momento in cui ven-
gono prese;
• evita stressanti e frettolose attività di documentazioni nel breve periodo che precede il
rilascio del codice.
La scrittura a posteriori dei commenti è frequentemente un'attività tediosa, e pertanto, spes-
so assegnata a programmatori junior molte volte estranei alla progettazione iniziale. Ad aggra-
vare la situazione interviene il semplice fatto che, tipicamente, documentare un codice privo di
adeguati commenti è un compito complesso. La logica conseguenza è che, per rispettare i tem-
pi di consegna, si finisce per documentare il codice troppo rapidamente e superficialmente.
Ciò, oltre a generare una riduzione di gran parti dei vantaggi derivanti da una buona documen-
tazione, e quindi a ridurre la generale qualità del codice, può portare in casi limite a situazioni
fuorviami dovute alla presenza di commenti ingannevoli.

4.1.2 Mantenere i commenti aggiornati


Gli sviluppatori esperti redigono le prime versioni del codice in modo assolutamente profes-
sionale: il codice è chiaro, leggibile, ben documentato etc. Non è infrequente però il caso in cui,
man mano che il codice subisce delle modifiche, la conseguente attività di aggiornamento dei
commenti tenda ad essere trascurata. Ciò è particolarmente ricorrente a ridosso della data di
consegna. Il risultato è la presenza di commenti non aggiornati e spesso contraddittori. Ciò è
fonte di dannosa confusione che, in alcuni casi, finisce addirittura con il fuorviare e confondere
il personale demandato alla manutenzione e quindi con il ridurre drasticamente il livello di
manutenibilità del codice.

4.1.3 Scriveve commenti chiari e concisi


L'obiettivo dei commenti è di far comprendere l'intero codice prodotto sia allo stesso autore
sia a persone estranee alla relativa implementazione. Pertanto è opportuno scrivere commenti
concisi, semplici e chiari. Commenti troppo prolissi tendono a ridurre la leggibilità del codice.
Tipicamente si preferisce scrivere i commenti utilizzando la terza persona singolare.
Si supponga di disporre di una lista ordinata di codici di determinati prodotti e di basare il
metodo di ricerca su una determinata versione di un algoritmo di ricerca dicotomica (detta
anche binaria). In questo caso nella documentazione è sufficiente riportare il nome dell'algoritmo
ed eventualmente una brevissima spiegazione, evitando però di dilungarsi nell'esporre i detta-
gli di tale algoritmo per il quale esistono specifici trattati.

4.1.4 Evitare le decorazioni


Il codice prodotto deve essere commentato e particolare attenzione va rivolta alle porzioni di
codice più complesse. I commenti, come visto in precedenza, devono essere professionali, chia-
ri e concisi evitando inutili decorazioni e inappropriati umorismi. Questi, oltre a richiedere
tempo che potrebbe essere investito in maniera migliore, tendono a diminuire la leggibilità del
codice, in quanto ne diminuiscono pulizia e chiarezza, si prestano ad essere fraintesi e spesso
possono irritare il lettore del codice impegnato a comprenderne il senso. Ecco un esempio di
metodo printStackTrace (classe Throwable) appositamente mal commentato.

* Prints this throwable and its backtrace to the specified print stream.

* @param s <code>PrintStream</code> to use for output

*/
public void printStackTracefPrintStream s) I

/ \ // * It Is necessary to synchronize the code to make it thread safe


/ / * * * * * * * * * * " * • " * • • " • " * * * * * * * * *

synchronized (s) I
s.println(this);

/'/ * Prints all trace elements present in the stack

StackTraceElement[] trace = getOurStackTrace();


for (int i=0; i < trace.length; I++)
s.println("\tat" + tracefi]);

n ..................................
// * Prints the cause, if present

Throwable ourCause = getCause();


If (ourCause != null)
ourCause.printStackTraceAsCause(s, trace);
I

4.1.5 Spiegare "che cosa" il codice esegue, "il perché" e non "il come"
L'illustrazione del "come" il codice risolva un compito, frequentemente, è una documentazio-
ne poco effettiva; infatti, le persone interessate a comprendere e a mantenere il codice sono, a
loro volta, sviluppatori. Anche qualora non conoscano specifiche istruzioni o librerie, esistono
molte fonti a cui attingere per la necessaria documentazione. Quello che invece è più difficile
da comprendere è "che cosa" il programma intenda eseguire e "il perché". Pertanto, questi
sono gli elementi sui quali è più opportuno investire il proprio tempo a disposizione.
Per esempio, si consideri il caso di un metodo la cui implementazione acceda sempre al
primo elemento di un array riportante i valori di offerta e acquisto di un determinato strumento
finanziario. Sebbene che cosa faccia un codice di questo tipo sia inequivocabile, potrebbe esse-
re meno chiaro il perché, che, sempre nel caso ipotetico, potrebbe dipendere dal fatto che la
prima posizione dell'array (indice = 0) sia riservata al prezzo più aggiornato.

4.1.6 Porre attenzione alle situazioni in cui è difficile commentare il codice


Le situazioni in cui risulti difficile commentare efficacemente e semplicemente il codice pro-
dotto dovrebbero essere oggetto di particolare attenzione. Infatti spesso situazioni del genere
sono dovute a codice non ben scritto o paradossalmente non ben compreso.

4.1.7 Una buona documentazione inizia dal codice


È appena il caso di ricordare che una buona documentazione inizia dal codice; pertanto, è
necessario investire nella leggibilità fin dalla fase di codifica.
4.2 Scrivere i commenti JavaDoc
JavaDoc è uno degli strumenti molto apprezzati inclusi nel JDK fin dalle sue prime versioni. In
particolare, permette di organizzare razionalmente e produrre (di default) un insieme di pagi-
ne HTML contenenti la documentazione del codice prodotto. JavaDoc è sicuramente uno
degli strumenti di maggiore successo del J D K tanto che anche altri linguaggi sono stati dotati di
versioni simili di questa utility.
Comunque, nel redigere la documentazione JavaDoc è necessario porre attenzione a un
insieme di regole che, se non seguite, potrebbero portare a risultati indesiderati.

4.2.1 Investire nella documentazione JavaDoc


Anche se questa regola può sembrare ormai acquisita, è sempre opportuno ricordare che una
buona documentazione JavaDoc è caratteristica fondamentale di codici Java di buona qualità.
Questa documentazione fornisce il punto di partenza che i programmatori consultano
ogniqualvolta hanno a che fare con del nuovo codice.
Inoltre, la maggior parte dei software IDE hanno servizi avanzati per assistere la produzione
di commenti Javadoc. Quindi con minimo sforzo è possibile ottenere ottimi risultati.

4.2.2 Applicare la struttura dei commenti doc


I commenti doc devono essere specificati immediatamente prima dell'elemento (classe,
interfaccia, metodo, attributo o particolare porzione di codice) di cui forniscono la documenta-
zione. La struttura prevede una concisa e completa descrizione dell'elemento, eventualmente,
seguita da un insieme di tag. La descrizione iniziale dovrebbe essere costituita da una prima
riga che descriva, quanto più sinteticamente e completamente possibile, l'elemento al fine di
fornirne un'immediata comprensione. Questa, tipicamente, è seguita da altre necessarie per
fornire ulteriori informazioni. JavaDoc considera terminata la prima riga quando incontra un
carattere di nuova riga o non appena incontra una sequenza di tipo: carattere punto e uno
spazio oppure carattere punto e un tab. Pertanto bisogna porre attenzione ad eventuali contra-
zioni, abbreviazioni etc. con punto inserite nella prima riga che potrebbero, involontariamente,
spezzare prematuramente il commento.
Qualora un commento preveda più paragrafi, questi vanno separati per mezzo di una linea
che contenga unicamente l'asterisco iniziale e il tag HTML <p> (cfr. quarta riga del commento
riportato di seguito). Da tener presente che dalla versione 1.4 di JavaDoc, non è più necessario
riportare il carattere asterisco iniziale nelle linee interne del commento. Per esempio si conside-
ri la documentazione JavaDoc fornita con la classe System.

" The < c o d e > S y s t e m < / c o d e > class c o n t a i n s several useful class fields
" and m e l h o d s . Il c a n n o l be instantlated.

"<P>
* A m o n g the tacilllies p r o v i d e d by the < c o d e > S y s t e m < / c o d e > class
' are s t a n d a r d Input, s t a n d a r d o u t p u t , a n d e r r a r o u t p u t s t r e a m s :
* access to externally d e t i n e d p r o p e r t l e s a n d e n v i r o n m e n t
" variables: a m e a n s ot l o a d i n g files and librarles: a n d a utility
" m e t h o d tor q u i c k l y c o p y l n g a p o r t l o n ot an array.

' ¡Sauthor unascribed


* @version 1.149. 06/02/04
* ©sirice JDK1.0
7
public final class System I

4.2.3 Scrivere i commenti doc utilizzando i tag HTML di formattazione


Il comportamento di default dell'utility JavaDoc, utilizzato nella stragrande maggioranza dei
casi, consiste nell'inserire i commenti prelevati dal codice in una serie di pagine HTML oppor-
tunamente organizzate. Quindi, a meno di voler ridefinire il comportamento di default di JavaDoc
(a tal proposito è necessario utilizzare la relativa API), quando si redigono commenti doc, è
necessario tenere a mente che questi verranno inseriti all'interno di una pagina HTML. Pertan-
to è opportuno porre attenzione all'utilizzo, nei commenti JavaDoc, dei caratteri considerati
speciali per l'HTML. Per esempio, le parentesi angolari < > vanno sostituite, rispettivamente,
dalle stringhe &lt e &gt. La stessa sorte spetta al carattere & che va sostituito con la stringa &amp,
e così via. Inoltre, per assicurare che la formattazione utilizzata nello scrivere i vari commenti sia
rispettata nelle pagine H T M L generate, è opportuno ricorrere all'utilizzo di tag HTML quali
<pre>, <b>, <i>, e così via. Una trattazione di tali tag è riportata nell'Appendice B.
Da tenere presente che, qualora si intenda scrivere il proprio doclet per la produzione di
documentazione JavaDoc, questo si dovrà far carico di interpretare correttamente i tag HTML
inclusi nella documentazione. Per esempio, si consideri il seguente commento attinto dalla
classe System relativo alle proprietà di sistema, riportato nel listato seguente.

/**

* System properties. The following properties are guaranteed to be defined:


* <dl>
* <dt>java.version <dd>Java version number
* <dt>java.vendor <dd>Java vendor specific string
* <dt>java.vendor.url <dd>Java vendor URL
* <dt>java.home <dd>Java installation directory
* <dt>java.class.version <dd>Java class version number
* <dt>java.class.path <dd>Java classpath
* <dt>os.name <dd>Operating System Name
" <dt>os.arch <dd>Operating System Architecture
* <dt>os.version <dd>Operating System Version
* <dt>file.separator <dd>Flle separator ("/" on Unix)
* <dt>path. separator <dd>Path separator (":" on Unix)
" <dt>line.separator <dd>Line separator ("\n" on Unix)
* <dt>user.name <dd>User account name
* <dt>user.home <dd>User home directory
* <dt>user.dir <dd>User's current working directory
* </dl>
*/

4.2.4 Utilizzare il tag <code>


Il tag <C0de> deve essere utilizzato per evidenziare elementi del codice. Pertanto va usato nei
casi in cui il commento contenga parole chiave Java, nomi di package, nomi di classi, nomi di
metodi, nomi di interfacce, attributi e argomenti e porzioni di codice. Per esempio si consideri
il JavaDoc relativo al metodo clearProperty dalla classe System.

/" *

* Removes the s y s t e m property indicated by the specified key.

' <P>
* First, if a security manager exists, its
* <code>SecurityManager.checkPermission</code> m e t h o d
* is called w i t h a <code>PropertyPermission(key. " w r i t e " ) < / c o d e >
* permission. This may result in a SecurityException being t h r o w n .
" If no exception is t h r o w n , the specified property is removed.
* <P>

* ©param key the name of the s y s t e m property to be r e m o v e d ,


* ©return the previous string value of the s y s t e m property,
or <code>null</code> if there w a s no property w i t h that key.

' © e x c e p t i o n SecurityException if a security m a n a g e r exists and its


<code>checkPropertyAccess</code> m e t h o d doesn't allow
access to the specified s y s t e m property.
* © e x c e p t i o n NullPointerException if <code>key</code> is
<code>null</code>.
* © e x c e p t i o n IHegalArgumentException if <code>key</code> is empty.
* ©see #getProperty
* ©see #setProperty
" ©see java.util.Properties
* ©see java.lang.SecurityException
* ©see java.lang.SecurityManager#checkPropertiesAccess()

" ©since 1.5

*/
public static String clearProperty(String key) I

4.2.5 Evitare l'utilizzo di tag HTML di struttura


Come visto in precedenza è possibile e consigliato ricorrere all'utilizzo di alcuni tag HTML di
formattazione per aumentare il livello di chiarezza dei commenti doc. Al contempo è fortemente
consigliato che il ricorso a questi tag non includa elementi di struttura, come <HTML>, <HEAD>,
<H1>, etc., al fine di non interferire con la preesistente struttura della pagina HTML utilizzata
dall'applicazione JavaDoc; altrimenti si corre il forte rischio di generare pagine HTML assoluta-
mente non leggibili (per esempio potrebbero comparire frame riportati nei posti più impensabili),
o addirittura non riproducibili dai vari browser. Questa regola prevede l'eccezione della docu-
mentazione doc per i package, in quanto la preparazione dell'intera pagina è demandata al pro-
grammatore e quindi è assolutamente necessario ricorrere a tag HTML di struttura.

II caso di commenti doc dei sorgenti J D K che utilizzano il tag < H 4 > è un azzardo perché potrebbe
creare problemi con evoluzioni future JavaDoc e/o con estensioni create dall'utente.
4.2.5 Fare attenzione all'inserimento di link
Nel produrre documentazione è spesso necessario inserire dei link (hyperlink) ad altri elemen-
ti. In questi casi è opportuno evitare il ricorso al tag HTML <A> e utilizzare al suo posto il tag
{©Link}.
Il beneficio prodotto dall'utilizzo del tag link consiste nell'inserire un hyperlink all'interno
della documentazione. Pertanto permette di saltare da una parte di documentazione all'altra.
Poiché il pubblico dei fruitori di documentazione doc è costituito da personale esperto, è op-
portuno utilizzare con parsimonia questi link che richiedono tempo per essere inseriti e spesso
rendono la documentazione meno chiara.
Un esempio di utilizzo del tag link è presente nel commento doc utilizzato per illustrare il
metodo setProperties presente nella classe System.

* Sets the s y s t e m properties t o the < c o d e > P r o p e r t i e s < / c o d e >


* argument.

* <P>
' First, it there is a security manager, its
* < c o d e > c h e c k P r o p e r t i e s A c c e s s < / c o d e > m e t h o d is called w i t h no
* a r g u m e n t s . This m a y result in a security exception.
' <p>

* The a r g u m e n t b e c o m e s the c u r r e n t set of s y s t e m properties f o r use


* by the |@link #getProperty(String)l m e t h o d . If the a r g u m e n t is
* <code>null</code>. then the c u r r e n t set of s y s t e m properties is
* forgotten.

* @param props the new s y s t e m properties.


* © e x c e p t i o n SecurityExceptlon If a security manager exists and Its
< c o d e > c h e c k P r o p e r t i e s A c c e s s < / c o d e > m e t h o d doesn't allow access
to the s y s t e m properties.
* @see IgetProperties
* @see java.util.Properties
* @see java.lang.SecurityExceptlon
* @see java.lang.SecurityManager#checkPropertiesAccess()
*/
public static void setProperties(Properties props) I

4.2.6 Ereditare i commenti JavaDoc


L'utility JavaDoc, in alcuni casi ben definiti, è in grado di duplicare ("ereditare") la documen-
tazione doc dei metodi evitando al programmatore un'inutile riscrittura. Anche se ciò potreb-
be essere evitato con un semplice copia e incolla, rimarrebbe il problema di dover mantenere
aggiornate le ripetizione dello stesso commento doc.
JavaDoc è in grado di "ereditare" il JavaDoc dei metodi nelle seguenti situazioni:

1. un metodo di una classe esegue l'overriding del corrispondente metodo della superclasse
(per esempio ogniqualvolta si scrive l'implementazione del metodo toStringQ di una classe);
2. un metodo di un'interfaccia esegue l'overriding del corrispondente metodo della
superinterfaccia;
3. quando un metodo di una classe implementa un metodo di un'interfaccia.

4.2.7 Scrivere i commenti JavaDoc per tutti gli elementi


Il commento doc va scritto per tutti gli elementi del proprio codice. In particolare è importante
documentare correttamente classi, interfacce, metodi e attributi. La relativa descrizione è ri-
portata nelle sezioni di pertinenza illustrate di seguito.

4.3 Valutare la possibilità di inserire


la documentazione al livello di package
Documentare i package è un'attività non strettamente obbligatoria che tuttavia fornisce impor-
tanti informazioni circa le classi e le interfacce contenute. Questa documentazione è particolar-
mente utile sia in tutti quei casi in cui il criterio di aggregazione di classi e interfacce all'interno
di un medesimo package non sia particolarmente intuitivo, e sia qualora sia necessario include-
re informazioni relative all'intero package. Pertanto, questa documentazione offre la possibili-
tà di concentrare informazioni comuni a diverse classi e interfacce che, altrimenti, dovrebbero
essere ripetute nei vari elementi creando problemi di manutenibilità.
Un altro elemento da tenere in considerazione è relativo al fatto che, alla presenza di nuove
librerie e nuovo codice da apprendere, tipicamente, gli sviluppatori avviano lo studio proprio a
partire da queste informazioni poiché permettono di acquisire un'iniziale cognizione del ruolo
e delle responsabilità dell'intero package, delle classi contenute, etc.

4.2.1 Utilizzare il formato standard di documentazione dei package


Con la versione 1.2 dell'utility JavaDoc è possibile inserire documentazione a livello di package
che poi JavaDoc stesso è in grado di includere nella documentazione del codice generata. Queste
informazioni devono essere incluse in un file H T M L di nome fisso package.html inserito
nell'apposito package insieme a classi e interfacce contenute. Il comportamento di JavaDoc consiste
nel prelevare tutte le informazioni contenute tra i tag <body> e </b0dy>. I tag JavaDoc utilizzabili
in questa documentazione sono @see, @link, @deprecated, @since.

Il template suggerito dalla Sun prevede le seguenti sezioni:

1. l'immancabile copyright dell'azienda;


2. commenti generali (tipicamente riportati subito dopo il tag HTML body), dove inserire
una breve descrizione del package e dei vari servizi offerti;
3. documentazione specifica del package, tipicamente composta da:
a. specifiche relative all'intero package; per esempio nel pacakge AWT sono riportate
specifiche relative all'interazione del framework con i vari sistemi operativi;
b. direttive per ulteriori tool di manipolazione del testo, come per esempio FrameMaker;
c. riferimenti specifici;
4. riferimenti ad altra documentazione esterna, come articoli, libri, etc.
Di seguito è riportato il listato HTML proposto dalla Sun come template per la documenta-
zione dei package Java.

<!D0CTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">


<html>
<head>
<!—

@(#)package.html 1.60 98/01/27

Copyright (c) <anno copyright <nome azienda>


All Rights Reserved

This software is the confidential and proprietary information of <nome azienda>


("Confidential Information"). You shall not disclose such Confidential Information
and shall use it only in accordance with the terms of the license agreement
you entered into with <nome azienda>.
—>

</head>
cbody bgcolor="white">

# # # # # THIS IS THE TEMPLATE FOR THE PACKAGE DOC COMMENTS. #####


# # # # # TYPE YOUR PACKAGE COMMENTS HERE. BEGIN WITH A #####
##### ONE-SENTENCE SUMMARY STARTING WITH A VERB LIKE: #####
Provides for....

<h2>Package Specification</h2>

##### FILL IN ANY SPECS NEEDED BY JAVA COMPATIBILITY KIT # # # # #


<ul>
< l i x a href="">##### REFER TO ANY FRAMEMAKER SPECIFICATION HERE #####</a>
</ul>

<h2>Related Documentation</h2>

For overviews, tutorials, examples, guides, and tool documentation, please see:
<ul>
< l i x a href="">##### REFER TO NON-SPEC DOCUMENTATION HERE #####</a>
</ul>

< ! — Put @see and @since tags down here. — >

</body>
</html>
II seguente listato riporta un esempio di utilizzo della documentazione doc al livello di package.

<!D0CTYPE H T M L PUBLIC "-//W3C//DTD H T M L 3.2 Flnal//EN">


<html>
<head>

@(#)package.html 1.7 0 4 / 0 6 / 1 7

Copyright 2004 Sun Microsystems, Inc. All rights reserved.


SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
— >

</head>
<body bgcolor="white">
<P>
Provides the classes and interfaces of
the J a v a < S U P x F O N T SIZE="-2">TM</F0NT></SUP> 2
platform's core logging facilities.
The central goal of the logging APIs is to support maintaining and servicing
software at customer sites.

<P>
There are four main target uses of the logs:
</P>

<0L>
<LI> <l>Problem diagnosis by end users and system administrators</l>.
This consists of simple logging of c o m m o n p r o b l e m s that can be fixed
or tracked locally, such as running out of resources, security failures,
and simple configuration errors.

<LI> <l>Problem diagnosis by field service engineers</l>. The logging information


used by field service engineers may be considerably more complex and
verbose than that required by system administrators. Typically such information
will require extra logging within particular subsystems.
</OL>
</P>
The key elements of this package include:
<UL>
<LI> <l>Logger</l>: The main entity on w h i c h applications make
logging calls. A Logger object is used to log messages
for a specific system or application
component.
<LI> <l>LogRecord</l>: Used to pass logging requests between the logging
framework and individual log handlers.
<LI> <l>Handler</l>: Exports LogRecord objects to a variety of destinations
including m e m o r y , output streams, consoles, files, and sockets.
A variety of Handler subclasses exist for this purpose. Additional Handlers
may be developed by third parties and delivered on top of the core platform.
<U> <l>Level</l>: Defines a set of standard logging levels that can be used
to control logging output. Programs can be configured to output logging
for some levels while ignoring output for others.
<LI> <l>Filter</l>: Provides fine-grained control over what gets logged,
beyond the control provided by log levels. The logging APIs support a general-purpose
filter mechanism that allows application code to attach arbitrary filters to
control logging output.

<U> <l>Formatter</l>: Provides support for formatting LogRecord objects. This


package includes two formatters. SimpleFormatter and
XMLFormatter, for formatting log records in plain text
or XML respectively. As with Handlers, additional Formatters
may be developed by third parties.
</UL>
<P>
The Logging APIs offer both static and dynamic configuration control.
Static control enables field service staff to set up a particular configuration and then re-launch the
application with the new logging settings. Dynamic control allows for updates to the
logging configuration within a currently running program. The APIs also allow for logging to be
enabled or disabled for different functional areas of the system. For example,
a field service engineer might be interested in tracing all AWT events, but might have no interest In
socket events or memory management.
</P>

<h2>Null Polnters</h2>
<P>
In general, unless otherwise noted in the javadoc, methods and
constructors will throw NullPointerException if passed a null argument.
The one broad exception to this rule is that the logging convenience
methods in the Logger class (the config, entering, exiting, fine, finer, finest,
log. logp. logrb, severe, throwing, and warning methods)
will accept null values
for all arguments except for the initial Level argument (if any).
<P>
<H2>Related Documentation</H2>
<P>
For an overview of control flow,
please refer to the
<a href="../../../../guide/logging/overview.html">
Java Logging Overview</a>.
</P>

< ! — Put @see and @since tags down here. — >


(«sirice 1.4

</body>
</html>

4.4 Commentare correttamente le classi


La naturale tendenza del disegno OO consiste nel generare una serie di classi opportunamente
relazionate tra loro. Commentare correttamente le classi permette di capirne il ruolo, le re-
sponsabilità e di inserirle rapidamente nel relativo contesto. Da tener presente che non sempre
lo studio di un'API richiede di analizzare tutte le classi e che, anche qualora ciò sia necessario,
la documentazione a livello di classe aiuta a concentrarsi, in ogni momento, su un numero
limitato di aspetti. Per esempio, mentre si analizza una classe, è possibile, temporaneamente,
studiare esclusivamente la descrizione generale delle altre classi relazionate, senza dover neces-
sariamente scendere, fin da subito, nei relativi dettagli.

4.4.1 Includere sempre la dichiarazione del copyright


Tutti i codici sorgenti dovrebbero essere dotati di una sezione iniziale in cui sono specificati i
termini di copyright. Questa parte deve essere riportata a partire dalla prima riga.
L'esempio classico è riportato nel listato seguente, ove le stringhe <anno copyright e <nome
azienda>, come chiaramente indicato dai relativi nomi, vanno sostituiti, rispettivamente, con
l'anno del copyright ed il nome dell'azienda. Per esempio: 2008 e MokaByte Srl.

/'

Copyright (c) <anno copyright <nome azienda>


Ali Rlghts Reserved

" This software is the confidential and proprletary Information of <nome azienda>
" ("Confidential Inlormation"). You shall not disclose such Confidential Information
" and shall use it only in accordance with the lerms of the license agreement
* you entered into with <nome azienda>.
7

4.4.2 Includere sempre un'intestazione per classi e interfacce


Ogni classe e interfaccia, secondo anche i dettami dei commenti JavaDoc [SJAVCC], deve
essere introdotta da un'opportuna intestazione (header in Inglese) che riporti una breve descri-
zione della classe, i suoi ruoli, le principali responsabilità, l'indicazione che specifichi se si tratti
o meno di una classe persistente, la versione del JDK di riferimento, la lista degli autori, la
versione e la data dell'ultimo aggiornamento ed eventuali riferimenti ad ulteriore documenta-
zione, classi ed interfacce relazionate alla presente.
Per classi 0 cui utilizzo possa non risultare immediato, è inoltre consigliato riportare alcuni
esempi di utilizzo in termini di codice. Un'altra buona norma prevede di riportare una breve
descrizione di eventuali problemi, limitazioni, etc. della classe. Infine, è da notare che dalla
versione Tiger (JDK 1.2.5), i linguaggio Java è stato (finalmente) dotato del concetto di classi
template, denominato generics. Questo fa sì che si possano avere classi parametriche. Pertanto,
in questo caso, l'header deve riportare anche la descrizione dei parametri della classe (@param).
Un esempio di tale intestazione è mostrata nel listato seguente.

* dnserire qui una breve descrizione, possibilmente di una riga >


" <inserire ulteriori informazioni >
' <P>
* <t»ResponsibiNties:</t»<ul>
* <lixinserire qui l'elenco delle principali responsabilità della classe>
* </ul>
" <p><code>
* <esempi di codice>
* </code>

* <b>Persistent:</b>Yes/No<br>

* @since JDK<*.*.x>
' @author <nome degli autori>
* ©version <x.xx.xxx> - <data dell'ultima modifica>
' @param <parametro> - <descrìzione del parametro

' @see <altre classi/interfacce relazionate>

Ed ecco un frammento dell'header della classe java.util.concurrent.Future.

/ "

* A <tt>Future</tt> represents the result of an asynchronous


* computation. Methods are provided to check if the computation is
* complete, to wait for its completion, and to retrieve the result ol
* the computation. The result can only be retrieved using method
* <tt>get</tt> when the computation has completed, blocking if
" necessary until it is ready. Cancellation is performed by the
* <tt>cancel</tt> method. Additional methods are provided to
* determine if the task completed normally or was cancelled. Once a
* computation has completed, the computation cannot be cancelled.
" If you would like to use a <tt>Future</tt> for the sake
* of cancellability but not provide a usable result, you can
" declare types of the form <tt>Future&lt;?&gt;</tt> and
* return <tt>null</tt> as a result of the underlying task.

* <P>
* <b>Sample Usage</b> (Note that the following classes are all
" made-up.) <p>
' <pre>
' interface ArchiveSearcher I String s e a r c h ( S l r i n g target); I
' class App I
" ExecutorService executor = ...
' ArchiveSearcher searcher = ...
" void s h o wSea re h ( fi n a I String target) t h r o w s InterruptedException I
" F u t u r e & l t : S l r i n g & g t : future = e x e c u t o r . s u b m i l ( n e w C a l l a b l e & l t : S t r i n g & g t : ( ) {
public String call() I return searcher.search(targel): I

I):
" displayOtherThings(): / / do other things while searching
try!
displayText(future.getO): // use future
! catch (ExecutionException ex) I cleanup(): return; 1

' </pre>
" ©see FutureTask
* ©see Executor
"©silice 15
' aauthor Doug Lea
' ©param <V> The result type returned by this Future's <tt>get</tt> m e t h o d
7
public interface Future<V> (

4.4.3 Non inserire la "history"


Alcuni testi di informatica suggeriscono di aggiungere nell'intestazione di classi e interfacce
una breve storia inerente i vari processi di modifica subiti. Questa dovrebbe prevedere una lista
di linee aventi un formato del tipo: data, autore, modifica. Per esempio:

1 0 / M a r / 2 0 0 8 - L.V.T. - Introdotto metodo per il riordino automatico degli elementi della lista.

Sebbene si tratti di informazioni indubbiamente utili e interessanti, si ritiene che il codice


non sia il luogo più opportuno per memorizzarle. In particolare, queste informazioni dovreb-
bero essere di pertinenza di sistemi di controllo dei sorgenti (source control) quali: CVS, IBM
Rational Clear Case, Ms Source Safe, etc. La presenza di queste informazioni, dopo qualche
iterazione del sistema e i conseguenti aggiornamenti del codice, tende a diventare notevole e a
rendere il codice più pesante e meno chiaro. Inoltre, superate le cinque, sei righe, queste infor-
mazioni, insieme alle altre presenti nell'intestazione delle classi ed interfacce, tendono ad essere
sistematicamente tralasciate e quindi a divenire meri dati.

4.4.4 Commentare chiaramente problemi e limitazioni del codice


Alcune volte accade che l'implementazione di una classe o interfaccia presenti problemi o
limitazioni la cui risoluzione non sia possibile o in assoluto, magari per dipendenze dall'hardware
o da software fornito da terze parti, o semplicemente in quel momento per una serie di motiva-
zioni. Alcuni classici esempi sono dati dallo scenario in cui una grande quantità di codice di-
penda da quello da modificare e pertanto, modifiche a quest'ultimo produrrebbero un effetto
domino, oppure dalla presenza di dipendenze da altro codice proprietario, oppure da situazio-
ni in cui la soluzione dei problemi e/o limitazioni non sia d'interesse per la versione attuale del
sistema, magari per mancanza di tempo e/ budget. Un ultimo esempio abbastanza ricorrente è
dato dal fatto che la classe non sia thread-safe per scelta dello sviluppatore. Comunque sia, in
questi casi è necessario documentare dettagliatamente questi problemi e limitazioni. Il punto
migliore dove includere queste informazioni è nell'intestazione della classe o interfaccia (cfr.
punto precedente). Documentare i problemi di una classe è particolarmente importante per il
riutilizzo della stessa, in quanto fornisce agli sviluppatori una serie di informazioni utili per
prendere decisioni, qualora sia possibile o meno procedere con il riutilizzo del codice. Inoltre,
è molto utile per la produzione di versioni successive del sistema e, in taluni casi, anche per la
fase di test. Ecco parte della documentazione della classe java.sql.Timestamp.

' <P><B>l\lote:</'B> This type is a c o m p o s i t e of a < c o d e > j a v a . u t i l . D a t e < / c o d e > and a


* separate n a n o s e c o n d s value. Only integral s e c o n d s are stored in the
* < c o d e > j a v a . u t i l . D a t e < / c o d e > c o m p o n e n t . The fractional s e c o n d s - the nanos - are
' separate. The < c o d e > T i m e s t a m p . e q u a l s ( O b j e c t ) < / c o d e > m e t h o d never r e t u r n s
* < c o d e > t r u e < / c o d e > w h e n passed a value of type <code>java.util Date</code>
' because the n a n o s c o m p o n e n t of a date is u n k n o w n .
' A s a result, the < c o d e > T i m e s t a m p . e q u a l s ( O b | e c t ) < / ' c o d e >
" m e t h o d is not s y m m e t r i c w i t h respect to the
* <code>java.util.Date.equals(Object)</code>
* m e t h o d . Also, the < c o d e > h a s h c o d e < / c o d e > m e t h o d uses the u n d e r l y i n g
" <code>java.util.Date</code>
" I m p l e m e n t a t i o n and therefore does not Include nanos in its c o m p u t a t i o n .
' <P>
* Due to the differences b e t w e e n the < c o d e > T i m e s t a m p < / c o d e > class
* and the < c o d e > j a v a . u t i l . D a t e < / c o d e >
* class m e n t i o n e d above, it is r e c o m m e n d e d lhat code not v i e w
* < c o d e > T i m e s t a m p < / c o d e > v a l u e s generically as an instance ol
* <code>java.util.Date</code>. The
* inheritance relationship b e t w e e n < c o d e > T i m e s t a m p < / c o d e >
* a n d < c o d e > j a v a . u t i l . D a t e < / c o d e > really
* denotes i m p l e m e n t a t i o n inheritance, and not type inheritance.

4.4.5 Commentare l'ordine di esecuzione


Non è infrequente il caso in cui l'implementazione di un metodo o i servizi offerti da una
determinata classe debbano essere eseguiti rispettando un ben definito ordine non immediata-
mente comprensibile. In questi casi è fortemente consigliato documentare attentamente tale
ordine di esecuzione. Nel caso ci si riferisca all'implementazione di un metodo, queste informa-
zioni sono necessarie solo al personale addetto alla relativa manutenzione e quindi possono
essere inserite nell'intestazione del metodo e/o all'interno del metodo stesso. Qualora invece si
voglia documentare l'ordine di esecuzione dei metodi di una classe, (consistentemente con
quando riportato al punto precedente) è necessario riportare tali informazioni nell'intestazione
della classe ed, eventualmente, inserire un rimando nell'intestazione dei vari metodi.
4.4.6 Commentare eventuali scelte/soluzioni non chiare
Nell'implementare una classe, un metodo o addirittura nella definizione della firma dei metodi
che costituiscono un'interfaccia, non è infrequente il caso in cui si ricorra a soluzioni che ini-
zialmente potrebbero sembrare poco chiare o addirittura errate ma che, invece, sono il frutto
di un attento studio e di una lunga valutazione. Tali scelte, per esempio, potrebbero essere
relative all'utilizzo di specifiche strutture dati invece di altre, alla concorrenza, alla gestione
delle eccezioni, alla visibilità di taluni metodi, e così via. In tutti questi casi è opportuno docu-
mentare chiaramente le soluzioni adottate incluse le relative motivazioni. Questa documenta-
zione è essenziale per futura memoria, per non rischiare ingiustificate riscritture del codice, per
evitarne un cattivo uso, per non ripetere dibattiti avvenuti in precedenza, e così via.

4.4.7 Commentare eventuali effetti collaterali a livello di sistema


Spesso l'esecuzione dei metodi di una classe genera effetti collaterali, non ovvi, all'interno del
sistema. In questi casi è opportuno documentare chiaramente tali effetti nell'intestazione della
classe.

4.4.8 Documentare tutti i metodi


Una buona tecnica di documentazione del codice, rinforzata dalla convenzione JavaDoc
[SJAVCC], consiste nell'introdurre tutti i metodi con una ben definita sezione di commento
che includa una breve descrizione del metodo (una riga, seguita da un commento più lungo,
come illustrato nella sezione JavaDoc), un'eventuale descrizione dei parametri formali, del va-
lore di ritorno se presente, delle eventuali eccezioni e, qualora sia il caso, l'indicazione della
deprecazione, come indicato dal listato seguente:

* <Descrizione del metodo sintetica>


' <P>
" <Ulteriori informazioni relative al metodo sintetica>

* @param <nome del parametro <descrizione del parametro


" @param <nome del parametro> descrizione del parametro>
* ©return <valore di ritorno se presente>
* @see eventuali rimandi a classi/interface contenenti informazioni utili>
* @throws <classe eccezìone> <spiegazione delle cause che la generano>
* @throws <classe eccezione> <spiegazione delle cause che la generano>
* ©deprecateci <descrizione della deprecaizone>
'I

E qui è riportato un esempio di commento doc relativo al metodo log della classe
java.util.jogging.Logger.

/**

* Log a LogRecord.
* <P>
* All the other logging methods In this class call through
' this m e t h o d to actually p e r f o r i t i any l o g g i n g Subclasses can
* override this single m e l h o d to capture ali log activity.

" « i p a r a m record the L o g R e c o r d to be published


7
public void log(LogRecord record) I

Come al solito, nella descrizione del metodo è importante descrivere cosa il metodo fa e non
come lo da. Le descrizioni dovrebbero essere indipendenti dall'implementazione. Inoltre, qua-
lora non sia immediato il perché il codice esegua determinati compiti è consigliabile aggiungere
anche una breve descrizione di ciò. Queste informazioni aiutano a inserire il metodo nel relati-
vo contesto e quindi ne semplificano la leggibilità e riutilizzabilità.

4.4.9 Documentare gli attributi


Gli attributi, specie quelli di classe tendono a sfuggire all'attenzione dei programmatori che, a
volte, ne trascurano la documentazione. Invece, anche questa assume un ruolo di primaria
importanza specie quando il relativo utilizzo non è immediato, e per la corretta comprensione
di algoritmi complessi. Come di consueto, è importante riportare una documentazione chiara e
concisa che spieghi l'utilizzo dell'attributo. In casi particolarmente complessi o poco intuitivi, è
consigliabile anche mostrare anche alcuni esempi di utilizzo.
Per gli attributi di classe (il cui campo d'azione è l'intera classe) è consigliabile ricorrere a
una documentazione in stile JavaDoc, specie per attributi la cui visibilità non sia privata. Per
gli attributi dei metodi (comunemente denominati variabili) è invece sufficiente un commen-
to in linea.
Al fine di mantenere il codice pulito e chiaro è consigliabile riportare una dichiarazione per
riga. Di seguito è mostrata la documentazione della variabile boolean della classe java.lang.Boolean
e mostrato un attributo dotato di documentazione JavaDoc.

/ "

* The value of the Boolean.


* ©serial

private final boolean value;

Il seguente è invece un commento in linea relativo all'attributo C presente nell'implementazione


del metodo put(E o) della classe java.util.concurrent.LinkedBlockingQueue.

// Note: c o n v e n t i o n in ali put/take/etc is to prese!


// locai var h o l d i n g c o u n t negative to indicate failure unless set.
int c = -1;

4.5 Commentare attentamente l'implementazione


Il linguaggio di programmazione Java prevede ben tre differenti stili di documentazione del
codice (anche questa ricchezza di stili è l'ennesima dimostrazione dell'importanza attribuita
alla documentazione del software), che sono:
* commenti JavaDoc: questi, come visto in precedenza, sono riconoscibili in quanto rac-
chiusi tra i delimitatori /** e */ (/** commento 7);
* commenti in stile linguaggio C caratterizzati dal formato /* commento */;
* c o m m e n t o di singola linea c h e consiste nel considerare c o m e c o m m e n t o tutto il testo c h e segue
due caratteri barretta obliqua (doppio slash)-. Il c o m m e n t o .

Ogni stile presenta specifiche caratteristiche che ne rendono opportuno l'utilizzo in determi-
nati ambiti a discapito di altri. Per esempio i commenti in JavaDoc sono quelli prelevati dal-
l'omonima utility e inseriti nella documentazione generata automaticamente da tale tool. Il
relativo utilizzo è pertanto consigliato come introduzione a dichiarazioni di classe, interfacce,
metodi e attributi, mentre più raro è il relativo utilizzo all'interno del codice.
I commenti stile C sono spesso utilizzati in tutti quei casi in cui il testo di commento richieda
diverse linee. Inoltre risulta particolarmente utile durante la fase di debbugging qualora si ren-
da necessario isolare porzioni di codice per individuare la parte di codice errata.
I commenti di singola linea sono tipicamente utilizzati per commentare variabili locali a
metodi e opportune porzioni di codice. Un'interessante convenzione consiste nell'allineare questi
commenti al margine destro. Tuttavia, per quanto tale convenzione sia in grado di produrre un
effetto gradevole in alcuni casi specifici, in diverse situazioni, l'ottenimento dell'effetto voluto
richiede un notevole investimento di tempo non sempre giustificato.

4.5.1 Documentare efficacemente il codice


I commenti presenti all'interno dei metodi sono molto importanti in quanto documentano
l'implementazione vera e propria. Per questo motivo è necessario porre particolare attenzione
alle consuete regole: scrivere commenti concisi e, allo stesso tempo, completi, porsi sempre nei
panni di uno sviluppatore completamente all'oscuro del codice e delle relative decisioni e ricor-
darsi di indicare le motivazioni alla base del codice (il famoso "perché").
Per esempio si consideri l'implementazione del metodo writeObject (utilizzato per serializzare
lo stato dell'oggetto) della classe java.util.ArrayList.

* Saves the state of the <tt>ArrayList</tt> instance to a stream (that


* is. serialize it).

* ©pararti s object o u t p u t stream used to serialize the object


* ©throws lOException IO p r o b l e m s o c c u r r e d d u r i n g the serialization
* © s e r i a l D a t a The length of the array backing the <tt>ArrayList</tt>
instance is emitted (int). f o l l o w e d by all of its elements
(each an <tt>Object</tt>) in the proper order.
*/

private void writeObject(java.io.ObjectOutputStream s)


throws java.io.lOException |

// Write out element count, and any hidden stuff


s.defaultWriteObject();

// Write out array length


s.writelnt(elementData.length);

Write out ali elemenls In the proper order


lor (int i=0; ksize; i++)
s.writeObject(elementData[i]);
I

4.5.2 Ricordarsi di rimuovere parti di codice non più utilizzate


Spesso, durante la messa a punto del codice prodotto o a seguito di operazioni di manutenzio-
ne dello stesso, si procede con l'isolare determinate porzioni di codice escludendone altre
racchiudendole in appositi commenti. Sebbene questa sia una prassi conveniente, può acca-
dere di lasciare commentate delle porzioni di codice perché erronee o non più necessarie. In
questi casi è opportuno rivedere il codice e rimuovere tali parti prima di consolidare il codice
(di farne il check-in). Infatti, porzioni di sorgente racchiuse in un commento (e non parte
integrante della documentazione), e quindi non più utilizzate, finiscono per confondere la
lettura del codice stesso e tendono a creare amletici interrogativi nel personale addetto alla
manutenzione.
Se proprio, per qualche importante motivo (per esempio un servizio da utilizzare in una
versione futura) non si voglia perdere tale porzione di codice, allora è importante riportare una
descrizione che spieghi sia il motivo per cui il codice è stato commentato, sia la data e l'autore
della trasformazione in commento.

4.5.3 Documentare efficacemente cicli annidati


Un codice complesso che includa diversi cicli annidati, ognuno caratterizzato da specifiche
comparazioni, raramente risulta leggibile. Pertanto, in circostanze del genere dovrebbe essere
naturale valutare la possibilità di aumentarne la leggibilità, magari delegando porzioni di codi-
ce a opportuni metodi privati. Qualora ciò non sia possibile, allora è fondamentale commenta-
re attentamente il codice, evidenziando opportunamente i vari cicli. Spesso non è sufficiente
delegare questa responsabilità alla sola indentazione del codice. In presenza di diversi cicli
annidati, quindi è consigliabile commentare attentamente la chiusura di ciascun ciclo con com-
menti del tipo 1 // end of if, 1 // end of while, e così via.
L'esempio del listato seguente è tratto dall'implementazione del metodo indexOf della classe
AbstractList.

public int index0f(0bject o) I


Listlterator<E> e = listlterator();
il (o==null) (
w h i l e (e.hasNext()) I
il (e.next() == nuli) I
return e.previouslndex();
) ;•' end of if
I '7 end of while
) else I
w h i l e (e.hasNextQ) {
it ( o.equals(e.next()) ) I
return e.previouslndex();
I // end of if
I /'/' end of while
I // end of else
return -1 ;

4.5.4 Documentare eventuali effetti collaterali


Al fine di produrre un buon livello di documentazione è necessario documentare chiaramente
eventuali effetti collaterali, non ovvi, generati dall'esecuzione dei metodi. Come di consueto
non è necessario documentare casi lampanti come, per esempio, i metodi modificatori (setXXX)
disegnati specificatamente per generare un effetto collaterale (modificare il valore di una o più
attributi).
Si consideri il metodo riportato nel listato seguente. Questo si occupa si reimpostare la posi-
zione del Buffer. Come riportato nella documentazione doc, questa operazione può avere effet-
to sull'attributo mark. Si tratta appunto di un necessario effetto collaterale.

! "

' Sets this buffer's position. If the mark is defined and larger than the
* new position then it is discarded. </p>

* @paramnewPosition The new position value: m u s t be non-negative


and no larger than the current limit

* © r e t u r n This buffer
* ©throws IHegalArgumentExceptlon If the p r e c o n d i t i o n s on
< t t > n e w P o s i t l o n < / t t > do not hold
•/

public final Buffer p o s i t i o n a l newPosition) {


if ((newPosition > limit) || (newPosition < 0))
throw new lllegalArgumentExceptionQ;

position = newPosition;

il (mark > position)


mark = -1 ;

return this;

4.5.5 Documentare eventuali limitazioni dell'implementazione di un metodo


Analogamente a quanto riportato a livello di classe (cfr. 4.4.4), non è infrequente il caso in cui
l'implementazione di un metodo presenti qualche problema e/o limitazione che, magari per
questione di tempo o per la presenza di dipendenze da altre parti di codice, non possano
essere immediatamente sistemate. In questi casi è opportuno documentare chiaramente tali
problemi e/o limitazioni. Ciò è fondamentale per semplificare la manutenzione e la riusabilità
di tale codice.

/ "

' Returns the current value of the most precise available system
* timer, in nanoseconds.

* <p>This method can only be used to measure elapsed time and is


* not related to any other notion of system or wall-clock time.
* The value returned represents nanoseconds since some fixed but
* arbitrary time (perhaps in the future, so values may be
" negative). This method provides nanosecond precision, but not
* necessarily nanosecond accuracy. No guarantees are made about
* how frequently values change. Differences in successive calls
* that span greater than approximately 292 years (2<sup>63</sup>
* nanoseconds) will not accurately compute elapsed time due to
* numerical overflow.

' <p> For example, to measure how long some code takes to execute:
" <pre>
* long startTime = System.nanoTime():
* / / . . . the code being measured ...
'long estimatedTime = System.nanoTimeQ - startTime;
* </pre>

* ©return The current value of the system timer, in nanoseconds.


* @since 1.5
7
public static native long nanoTimeQ;
Strategia di gestione
delle eccezioni
Introduzione
In questo capitolo l'attenzione è focalizzata su un altro aspetto fondamentale della program-
mazione che ha un ruolo centrale nella realizzazione di applicazioni affidabili: la gestione delle
eccezioni. Indipendentemente dall'impegno profuso nel disegnare e implementare sistemi
affidabili, e da tutti gli sforzi compiuti per raggiungere un elevato livello qualitativo, l'insorgen-
za di un ben definito insieme di problemi è inevitabile: per esempio l'impossibilità di instaurare
connessioni con il sistema di gestione del database, un network particolarmente lento, servizi
non disponibili, e così via. Credeteci: tali problemi si verificano molto più spesso di quanto
sarebbe auspicabile. Pertanto, la strategia adottata per gestire queste anomalie fa la differenza
tra sistemi affidabili e non affidabili. Chiaramente, la sola strategia di gestione delle eccezioni
non è in grado di sopperire a eventuali carenze qualitative: si tratta del necessario completamento
per conseguire un elevato livello di qualità.
L'assenza di una ben definita strategia di gestione delle eccezioni, d'altro canto, può generare
una serie di problemi. In particolare, ogni sviluppatore si trova costretto, nella migliore delle
ipotesi, a utilizzarne una propria, oppure, nei casi peggiori, a decidere, volta per volta, quale
metodologia impiegare. Considerando che i progetti tipici necessitano di decine di sviluppatori, si
genera una situazione decisamente caotica. Il risultato finale, pertanto, comprende tutta una serie
di scenari, che vanno dalla generazione di non poca confusione per il personale addetto al suppor-
to del sistema, dovuta, per esempio, a eccezioni notificate in modo incoerente o addirittura rese
note nelle parti meno appropriate del sistema, alla generazione di sistemi instabili e/o difficili da
monitorare, alla carenza e alla incoerenza nella generazione di log, con conseguente limitata effi-
cacia, o addirittura impossibilità, di utilizzare software forniti da terze parti atti a monitorare il
sistema in produzione, e così via. Una valida strategia di gestione delle eccezioni applicata all'in-
tero sistema è quindi un prerequisito irrinunciabile di qualità per una serie di ragioni:

• realizzare sistemi in grado di esibire un comportamento efficace e coerente al verificarsi


di errori e anomalie;
• agevolare il riutilizzo del codice;
• semplificare le attività manuali di investigazione necessarie dopo errori critici;
• agevolare l'uso di applicazioni esterne per monitorare il corretto funzionamento.

Recenti studi hanno dimostrato come lo sviluppo di sistemi robusti sia un compito comples-
so. A seconda degli studi considerati, la percentuale dei progetti software falliti varia in inter-
vallo compreso tra il 5 0 % e il 70%. A complicare la situazione interviene la tendenza moderna
di realizzare sistemi sempre più grandi, più complessi, costituiti da un insieme di sottosistemi
comunicanti dispiegati in diverse aree geografiche che forniscono servizi real-time. Logica con-
seguenza è che il requisito di affidabilità assume un ruolo di primaria importanza.
I moderni linguaggi di programmazione offrono sofisticati meccanismi di supporto per le
eccezioni (rilevazione e comunicazione), ma spesso il codice scritto male non è in grado di
gestire efficacemente, sistematicamente e consistentemente eventuali condizioni anomale.

Obiettivi
L'obiettivo di questo capitolo è fornire una serie di direttive operative, best practice e quant'al-
tro, necessarie per la realizzazione di sistemi robusti e affidabili. La trattazione è focalizzata sul
linguaggio di programmazione Java, sebbene molti concetti presentino una validità che pre-
scinde dallo specifico linguaggio di programmazione, e che include concetti architetturali di
più ampia portata. La lettura di questo capitolo dovrebbe fornire ai lettori materiale necessario
per la definizione di efficaci politiche di gestione delle eccezioni in grado sia di semplificare il
lavoro degli sviluppatori, sia di produrre sistemi robusti e quindi di maggiore qualità. Dalla
lettura di questo capitolo è possibile evidenziare come una corretta gestione delle eccezioni
richieda una serie di accorgimenti, che non sempre i programmatori considerano, come ad
esempio una consistente struttura dei record di log argomento del prossimo Capitolo 6.

Un po' di teoria
I concetti di "eccezione" e relativa gestione non sono certo nuovi nella comunità informatica. La
formulazione iniziale, infatti, è attribuibile a J.B. Goodenough il quale, nel "lontanissimo" 1975
[EFFCJA], propose formalmente di inserire un simile costrutto nei linguaggi di programmazione.
Ciò nonostante, fu necessario attendere ben cinque anni per vederne una concreta manifestazione:
nel 1980 la Microsoft, infatti, incluse il costrutto 0 N E R R O R G O T O nell'allora popolare GWBasic
(celebre dialetto del linguaggio BASIC inizialmente studiato per conto della Compaq, il cui nome
è un omaggio alle iniziali dal suo ideatore: Greg Whitten). L'exception è Xcxceptional event\ un
evento che si verifica durante l'esecuzione del programma e che scompagina il normale flusso di
esecuzione delle istruzioni. All'iniziale formulazione del concetto di eccezione seguirono accesi
dibattiti inerenti il suo utilizzo. In particolare, la comunità informatica si divise in due grandi gruppi:
da una parte si schierarono le persone che consideravano le eccezioni come uno strumento demandato
esclusivamente a notificare l'insorgere di condizioni di errore, e dall'altra coloro che invece ne
proponevano un utilizzo più ampio paragonabile a quello di qualsiasi altro costrutto come i cicli for,
while e do [EXCPRL], Lo stesso J.B. Goodenough nella formulazione iniziale ne propose un utilizzo
alquanto esteso, destinato ad includere le seguenti principali situazioni: gestione delle condizioni di
errore; elaborazione di un costrutto supplementare atto a comunicare, in modo artificioso,
informazioni circa il corretto completamento di un'operazione; controllo delle operazioni in corso
di esecuzione. La visione attualmente accettata e quindi considerata in questo libro, è chiaramente
la prima, che come espresso nel 1987 da Melliar-Smith e Randall asserisce che le "eccezioni sono
una proprietà dei linguaggi di programmazione atta a migliorarne la caratteristica di affidabilità
alla presenza di condizioni di errori o di eventi inaspettati. Le eccezioni non sono destinate a
fornire costrutti di controllo generale. Un utilizzo liberale non dovrebbe essere considerato
sufficiente per fornire ai programmi una completa tolleranza degli errori".

Pertanto, come illustrato di seguito, il meccanismo di gestione delle eccezioni permette di


risolvere elegantemente il classico problema della gestione degli errori, evitando il continuo
passaggio, tra diversi moduli, di codici di errore per via dello stack.

La gerarchia delle eccezioni in Java


Gerarchia
Il linguaggio di programmazione Java prevede una ben definita gerarchia delle eccezioni come
mostrato nel diagramma delle classi di figura 5.1.
La gerarchia delle eccezioni ha inizio con la classe java.lang.Throwable (introdotta fin dalla
versione JDK 1.0) che quindi rappresenta la classe antenata (superclasse) di tutti gli errori e le
eccezioni del linguaggio Java. Pertanto tutti gli oggetti istanze di classi discendenti da questa
sono trattati come eccezioni e come tali possono essere lanciati dalla JVM e/o dalle applicazioni
Java per mezzo della parola chiave throw. Allo stesso modo, solo le classi che derivano da Throwable
possono essere utilizzate come argomenti dei costrutti catch.
Tuttavia, le applicazioni non dovrebbero mai definire delle classi che ereditano direttamente
da Throwable né, tantomeno, dovrebbero intercettare (catch) o lanciare (throw) eccezioni di que-
sto tipo. La classe java.lang.Error (introdotta anch'essa fin dalla versione JDK 1.0) è utilizzata per
comunicare gravi condizioni di errore e pertanto le applicazioni non dovrebbero cercare di
intercettarla. Questo è il motivo per il quale Error deriva direttamente da Throwable e non dalla
classe Exception. La classe java.lang.Exception (introdotta fin dalla versione J D K 1.0) è usata per
comunicare problemi che necessitano di essere gestiti ma che non sono così gravi come quelli
riportati da oggetti di tipo Error. La classe Exception a sua volta, prevede due specializzazioni:

1. eccezioni "a tempo di esecuzione" (runtime exception), implementate ereditando da


RuntimeException;
2. eccezioni controllate (cbecked) dette anche "non a tempo di esecuzione" (non-runtime
exception): tutte quelle che non hanno RuntimeException nella linea di ereditarietà.
java.lang.Object

Z\
i
java .lang.Throwable

java.lang. Error j j a v a . lang. E x c e p t i o n

" 1 j a v a . lang. R u n t l m e E x c e p t l o n

Figura 5.1 - Gerarchia delle eccezioni ]ava.

Eccezioni runtime e checked


Il primo tipo di eccezioni (runtime) è stato ideato per incapsulare errori che si verificano all'in-
terno dell'ambiente di esecuzione Java spesso causati da errori di programmazione. Alcuni esempi
famosi sono: NulIPointerException, ArithmeticException, IndexOutOfBoundException, etc. Una delle ca-
ratteristiche principali di questo tipo consiste nel non richiedere obbligatoriamente la gestione
esplicita, e per questo sono dette anche non controllate (unchecked). Pertanto i metodi che pos-
sono lanciare eccezioni runtime possono ometterne la dichiarazione (clausola throws nella firma
del metodo). Allo stesso modo, metodi che ne invocano altri che possono lanciare eccezioni
runtime non sono obbligati né a gestirle esplicitamente (catch) né a dichiararle sulla linea di
comando. Si consideri, per esempio, il metodo statico di conversione di stringhe in numeri:
lnteger.parselnt(<string>). Per quanto questo possa lanciare un'eccezione NumberFormatException
qualora il parametro fornito non rappresenti un valido valore numerico, non è necessario rac-
chiuderla nel ciclo try ... catch (NumberFormatException nfe) giacché questa è di tipo runtime (eredi-
t a d a RunTimeException).
Le eccezioni controllate, invece, rappresentano errori che si verificano in parti di codice "al
di fuori" dell'ambiente di esecuzione Java, quali errori di I/O. In questo caso, il compilatore
Java ne forza una gestione esplicita: è necessario o effettuarne il catch esplicito oppure dichia-
rarle nella firma del metodo, rinviandone la gestione al metodo chiamante. Ecco l'esempio di
un semplice metodo per la lettura di un file testo.

public String readFile(String filePath)


throws FileNotFoundException, lOException I

File file = new File(filePath);


BufferedReader fileReader = null;
StringBuffer strBuffer = null;

try I
fileReader = new BufferedReader(new FileReader(file));

boolean eof = false;


String nextLine = null;
strBuffer = new StringBuffer();

while (leof) I

nextLine = fileReader.readLine();

eof = (nextLine == null);

if (leof) (
strBuffer.append(nextLine);

I catch (FileNotFoundException fnfe) I


throw fnfe;

I catch (lOException ioe) I


throw ioe;

I finally I
tryl
if (fileReader != nuli) I
fileReader.close();
I
I catch (lOException ioe) I
logException(ioe); // this is an internai method
I
I
return strBuffer.toString();
I

Si consideri, per esempio, di dover leggere delle informazioni presenti in un file di testo. A
questo punto, la prima cosa da farsi consiste nel crearne una rappresentazione logica del file (File
file = new File(filePath); ) e quindi passarne 0 riferimento a uno stream di lettura (BufferedReader
fileReader = new BufferedReader(new FileReader(file)) ). Giacché il file potrebbe non esistere, il
costruttore della classe java.io.FileReader può lanciare un'eccezione java.io.FileNotFoundException che
deriva da java.io.lOException che a sua volta eredita java.lang.Exception. Trattandosi di un'eccezione
checkcdh obbligatorio o gestirla attraverso apposito costrutto catch oppure rilanciarla al metodo
chiamante (dichiarazione nella definizione del metodo). Quest'ultima soluzione è adottata nel
listato visto poco sopra. Qualora ciò non avvenga, il compilatore genera un apposito messaggio
di errore. La tabella 5.1 è una comoda sintesi delle proprietà dei due diversi tipi di eccezione.

La controversia relativa all'uso


Per quanto i principi base che hanno portato all'ideazione delle due eccezioni siano piuttosto
chiari, altrettanto chiaro non sembrerebbe esser il loro utilizzo. Non a caso esiste una dichiarata
controversia relativa all'utilizzo (http://java.sun.com/docs/books/tutorial/essential/exceptions/runtime.html).
Pertanto si è ritenuto opportuno riportare le direttive promosse da Sun Microsystem.
Il fatto che il linguaggio di programmazione Java non richieda ai metodi di specificare ecce-
zioni runtime o errori, potrebbe tentare i programmatori a scrivere codice che lanci esclusiva-
mente eccezioni runtime o a definire proprie eccezioni derivanti da java.lang.RuntimeException.
Queste "scorciatoie" permettono agli sviluppatori di scrivere del codice senza preoccuparsi di
gestire le eccezioni, bypassando i controlli del compilatore.
Sebbene, a prima vista, possa sembrare una tecnica molto comoda, questa elude gli intenti
dei requisiti "cattura o specifica" (catch or specify) alla base del meccanismo Java delle eccezioni
e può generare una serie di problemi ai programmatori che dovranno utilizzare classi imple-
mentate con questa tecnica.
Perché i progettisti hanno deciso di forzare i metodi a specificare tutte le eccezioni checked
non gestite che possono essere lanciate nel loro ambito? Ogni eccezione che può essere lanciata
da un metodo fa parte dell'interfaccia pubblica dello stesso. I metodi chiamanti devono essere
messi a conoscenza delle eccezioni che un metodo può lanciare affinché siano in grado di deci-
dere come trattarle. Queste eccezioni sono parte dell'interfaccia programmativa del metodo
così come lo sono i parametri e il valore di ritorno.
La prossima domanda potrebbe essere: se è così valido documentare l'API di un metodo,
incluse le eccezioni che questo può lanciare, perché non forzare la specifica esplicita anche
delle eccezioni runtime? Le eccezioni runtime rappresentano anomalie che sono il risultato di
un problema di programmazione e, come tale, non si può ragionevolmente richiedere alle API
del client di eseguire qualche azione per risolvere il problema e/o di effettuarne una qualsiasi
gestione. Questi problemi includono eccezioni aritmetiche (come la divisione per zero), ecce-
zioni di riferimento (come per esempio il tentativo di accedere alle proprietà di un oggetto
attraverso un puntatore nullo) ed eccezioni di indicizzazione (come per esempio un tentativo di
accedere ad un elemento dell'array attraverso un indice il cui valore sia superiore o inferiore

Eccezioni r u n t i m e Eccezioni checked

D i c h i a r a z i o n e d e l l e eccezioni Non necessario Obbligatorio


lanciate nella f i r m a del m e t o d o
Il m e t o d o c h i a m a n t e deve

Gestione delle eccezioni n e l Non necessario


gestire esplicitamente

metodo c h i a m a n t e l'eccezione (catch) oppure

r i l a n c i a r l a (IhrOWS)

Exception
Classe antenata RunlimeExceplion o Error

Tabella 5.1 - Proprietà delle eccezioni runtime e checked.


alle posizioni valide dell'array). Le eccezioni runtime possono avvenire in ogni parte del pro-
gramma e tipicamente possono essere numerose. Pertanto l'obbligo di dover aggiungere la
dichiarazione di eccezioni runtime in ogni metodo ridurrebbe drasticamente la leggibilità del
programma. Per questo motivo, il compilatore non richiede obbligatoriamente di gestire o
dichiarare eccezioni runtime, (sebbene nessuno vieti di farlo).
Un caso in cui è pratica comune lanciare eccezioni a tempo di esecuzione è relativo a
invocazioni scorrette di metodi. Per esempio, un metodo può verificare se gli argomenti speci-
ficati siano o meno nulli, ed in caso siano nulli, lanciare un'eccezione NulIPointerException che
appunto è di tipo runtime. In generale, non si dovrebbe lanciare una RuntimeException or creare
una sotto classe per il semplice motivo che si voglia evitare di specificare esplicitamente le
eccezioni lanciate dai propri metodi.

Direttive
5.1 Utilizzare le eccezioni
Il meccanismo delle eccezioni permette di risolvere una serie di anomalie causate dalle prece-
denti strategie di gestione degli errori, e in particolare garantisce

• Migliore organizzazione del codice. Ciò grazie alla separazione elegante e pulita tra il codi-
ce necessario per implementare il particolare servizio e quello richiesto per gestire even-
tuali errori. Inoltre, non è necessario ritornare esplicitamente codici di errore che riduco-
no la chiarezza delle API con continui passaggi di codici di controllo e forzano la ripetizio-
ne di blocchi di istruzioni necessari per controllare tali valori in tutti i metodi chiamanti.
• Maggiore livello di robustezza. Codici di errori sono difficili da mantenere, specialmente
a seguito a refactoring del codice e possono facilmente generare situazioni inconsistenti
come quelle dovute a stessi codici utilizzati per rappresentare anomalie diverse, o a codici
non più generati da una classe che comunque persistono nel sistema. Pertanto, a differen-
za delle eccezioni, i codici di errore possono facilmente sfuggire al controllo.
• Scrivere codici più leggibili. In particolare, poiché i parametri restituiti dai metodi non
devono essere compromessi per via della necessità di restituire codici di errore, è possibile
dar luogo a firme dei metodi non artificiosi. Inoltre, il meccanismo delle eccezioni rappre-
senta la pratica quotidiana che consiste nello specificare una serie di richieste e quindi
fornire ulteriori informazioni relative alla situazione in cui non sia possibile soddisfare
l'elenco principale. Un esempio classico è quello della spesa "acquista 1 kg di riso e una
bottiglia di Barolo; se non trovi il Barolo, prendi pure una bottiglia di Nebbiolo" e così via.
• Dare luogo a un migliore disegno. Questo grazie al fatto che, spesso, il punto in cui un
errore si verifica non è il posto migliore dove gestirlo e, utilizzando i codici di errore, è
molto frequente che accada che il percorso necessario per la propagazione a ritroso
dell'errore generi la perdita di informazioni relative al contesto dell'errore verificatosi.
Le eccezioni, invece, includono tutte le informazioni necessarie dal punto in cui si veri-
ficano sino al punto in cui vengono gestite.
• Migliorare le prestazioni. Ciò essenzialmente per via del fatto che non è necessario veri-
ficare continuamente i valori restituiti dai metodi.
5.1.1 Le eccezioni posso verificarsi: è un fatto inevitabile
Nel mondo ideale si sarebbe tentati di pensare che, in presenza di un codice ben scritto, le
eccezioni non dovrebbero mai verificarsi, quindi l'infrastruttura dovrebbe sempre funzionare
correttamente, nessun processo business dovrebbe mai interrompersi, tutti i messaggi dovrebbe-
ro essere corretti e consegnati nell'ordine previsto, il Database Management System dovrebbe
essere sempre perfettamente funzionante e così via. Purtroppo, nella realtà la situazione è decisa-
mente diversa e, per quanto accuratamente si tenti di scrivere il codice, le eccezioni comunque si
verificano. Chiaramente, se poi il codice è scritto in modo trasandato, allora le eccezioni tendono
a presentarsi frequentemente e l'applicazione tende a presentare un elevato livello di instabilità.
Pertanto, sebbene sia necessario produrre il massimo sforzo per aumentare la qualità del
codice e per realizzare opportuni meccanismi atti a diminuire la probabilità del verificarsi delle
eccezioni, queste comunque si verificano e quindi solo un attento e ben progettato sistema di
gestione è grado di fare la differenza tra sistemi affidabili e di qualità e sistemi problematici.

5.1.2 Evitare di comunicare situazioni di errore


attraverso i parametri di ritorno
Il meccanismo di gestione delle eccezioni permette di risolvere elegantemente il classico pro-
blema della gestione degli errori, evitando il continuo passaggio di codici di errore, tra diversi
moduli, attraverso lo Stack. Quest'ultima strategia genera una serie di problemi.

• Aumento della complessità. Tutti i moduli chiamanti coinvolti in una sequenza di


invocazioni sono costretti ad occuparsi in maniera esplicita degli errori. Questo implica
la ripetizione della stessa logica necessaria per verificare il valore del parametro di ritor-
no e quindi intraprendere le necessarie operazioni a seconda di tale valore.
• Problemi di firma dei metodi. Questo problema deriva dal fatto che il valore di ritorno
di diversi metodi è utilizzato per fornire possibili codici di errore. Pertanto sono neces-
sarie strategie alternative per gestire il normale passaggio dei valori di ritorno.
• Ridotta capacità informativa. Poiché gli errori sono comunicati esclusivamente attraverso
un singolo codice, ne segue che l'elenco dei codici utilizzati deve essere conosciuto da
tutti i moduli presenti nella catena delle invocazioni con possibili problemi di incongruenza.

Si consideri il codice assolutamente sbagliato riportato nel listato poco sotto. Si tratta del
metodo già riportato nel listato visto in precedenza, ove le eccezioni sono state sostituite da
codici di errore. Come si può notare, la firma del metodo risulta decisamente meno intuitiva e
non c'è un modo formale per inserire la lista dei possibili errori che possono verificarsi. Per
quanto riguarda il codice restituito è stata utilizzata la seguente convenzione:

• ritorno = 0, non si sono verificati errori; e quindi il parametro di input/output, result, è


valido e contiene il contenuto del file;
• ritorno > 0; è analogo al caso precedente con la variazione che il valore numerico indica
il verificarsi di problemi marginali (warning); nel caso proposto non ci sono warning;
• ritorno < 1 rappresenta l'insorgere di problemi, per esempio -1 rappresenta un errore di
"file not found" e -2 problemi di IO; il problema principale di questo codice è relativo
alla classe invocante (e di diverse tra quelle presenti nella sequenza di invocazione).

Ecco il codice precedente, modificato affinché il metodo ritorni codici di errore e non eccezioni.
public String readFilefString tilePath, String result) I

int errorCode= 0;
result = null;
File file = new File(filePath);

BufferedReader fileReader = null;


StringBuffer strBuffer = null;

try I
fileReader = new BufferedReaderfnew FileReader(file));

boolean eof = false;


String nextLine = null;
strBuffer = new StringBuffer();

while (leof) I

nextLine = fileReader.readLine();

eof = (nextLine == null);

if (leof) I
strBuffer.append(nextLine);

result = strBuffer.toStringf);

I catch (FileNotFoundException fnfe) I


errorCode = -1;

I catch (lOException ioe) I


errorCode = -2;

I finally {
try I
if (fileReader != null) I
fileReader.close();
)
) catch (lOException ioe) I
logException(ioe); // this is an internal method
I
I
return errorCode;

Ed ecco i controlli del codice di ritorno da ripetersi nella lista di metodi inclusi nella sequen-
za di invocazione.
Sting fileContent = "";

int errorCode = readFile(filePath, result);

if (errorCode == 0) I
/ / s e q u e n z a di istruzioni necessarie per gestire
// il caso di successo
I else if (errorCode == -1) I
// sequenza di istruzioni atte a gestire
// la situazione di file not f o u n d
I else if (errorCode == -2) I
// sequenza di Istruzioni atte a gestire
// la situazione di p r o b l e m i di IO

5.1.3 Utilizzare le eccezioni esclusivamente per comunicare


condizioni di errore
Come illustrato nei paragrafi precedenti, il meccanismo delle eccezioni così come lo si intende
ora è il risultato di una evoluzione relativamente lunga. In particolare, sebbene agli albori fosse
stato proposto un utilizzo più ampio non necessariamente relegato alla gestione delle anomalie,
questo si è dimostrato causa di diversi problemi e confusione nel codice. Pertanto la versione
moderna ne prevede un utilizzo confinato alla gestione degli errori. Quest'ultima strategia è quel-
la divenuta standard e pertanto, onde evitare la generazione di codici confusi e poco leggibili, è
necessario utilizzare le eccezioni solo ed esclusivamente per la gestione delle condizioni di errore.

5.2 Utilizzare correttamente il blocco finally


Un tipico modello di gestione delle eccezioni prevede di chiudere accuratamente (tecnicamen-
te clean-up, "pulire") le varie risorse prima di passare il controllo a ritroso. Il linguaggio di
programmazione Java prevede un apposito costrutto per gestire queste operazioni: il finally. Si
tratta di una parte opzionale del costrutto try (così come lo è la parte catch) e fornisce un mecca-
nismo elegante e opportuno per porre il sistema in uno stato adatto indipendentemente da
quanto succede nel corpo del try ... catch.
Come regola generale, si tenga presente che la clausola finally viene sempre eseguita anche
quando potrebbe sembrare il contrario, come per esempio, in presenza di istruzioni di return
all'interno del try. Una delle pochissime eccezioni a questa regola è data dalla presenza del-
l'istruzione System.exit. Si consideri il seguente frammento di codice che è stato modificato per
includere l'istruzione di return all'interno del costrutto try.

try I
fileReader = new BufferedReader(new FileReader(file));

boolean eof = false;


String nextLine = nuli;
strBuffer = new StringBuffer();
while (!eof) I

nextLine = fileReader.readLine();

eof = (nextLine == nuli);

il (!eof) I
slrBuffer.append(nexlLine);

return strBufter.toString();

I catch (FileNotFoundException fnfe) I


throw fnfe;

I catch (lOException ioe) I


throw ioe;

) finally I
try I
if (fileReader != null) I
fileReader.close();
I
} catch (lOException ioe) I
logException(ioe); // this is an internal method

Si noti che la clausola finally è eseguita sia quando non intervengono problemi (ossia la J V M
esegue il return presente nel costrutto try), sia quando ci sono dei problemi, ossia quando ven-
gono eseguite le clausole catch.

5.2.1 Utilizzare il blocco finally per il clean-up


Utilizzare il costrutto finally ogni qualvolta nel corso del costrutto try ... catch si utilizzano degli
oggetti da porre in un determinato stato prima di passare il controllo a ritroso. Ecco l'ennesima
variazione del listato principale con la chiusura di uno stream senza il blocco finally.

public String readFile(String filePath)


throws FileNotFoundException, lOException I

File file = new File(filePath);

BufferedReader fileReader = nuli;


StringBuffer strBuffer = nuli;
try!
fileReader = new ButferedReader(new FileReader(file));

boolean eof = talse;


String nextLine= null;
strBuffer = new StringButfer();

while (ieof) I

nextLine = tileReader.readLine();

eof = (nextLine == null);

if (!eof) I
strBuffer.append(nextLine);
I
)

try!
if (fileReader != null) I
fileReader.close();
)
I catch (lOException ioe) I
logException(ioe); // this is an internal method
I

I catch (FileNotFoundExceptlon fnfe) I

try I
if (fileReader != null) I
fileReader.close();
I
I catch (lOException ioe) I
logException(ioe); / / t h i s is an internal method

throw fnfe;

I catch (lOException ioe) {

try)
it (fileReader 1= null) {
fileReader.close();
I
) catch (lOException ioe) (
logException(ioe); // this is an Internal method
I
throw ioe:
return strBuffer.toString();
I

Il mancato utilizzo del blocco finally ha richiesto di ripetere il blocco delle istruzioni di chiusu-
ra (fileReader.close()) in diverse parti del codice (all'interno del blocco try e in tutti i vari catch).
Ciò, oltre a causare un'inutile ripetizione di codice (in parte risolvibile con l'implementazione di
un apposito metodo), potrebbe generare problemi qualora un aggiornamento del codice richie-
da di gestire nuove eccezioni e ci si dimentichi di ripetere il blocco di chiusura dello stream.

5.2.2 Valutare la necessità di inserire un try... catch


all'interno del blocco finally
Spesso le operazioni di pulizia presenti all'interno del blocco finally possono generare delle
eccezioni come nei casi presentati in precedenza. In questi casi è opportuno inserire un oppor-
tuno costrutto try ... catch all'interno del costrutto finally. In questo caso però quasi mai è oppor-
tuno riportare, qualora insorgesse, la nuova eccezione: quella che ha generato il problema ini-
ziale è tipicamente più importante. Comunque è buona pratica riportare eventuali ulteriori
eccezioni nel file di log.

5.2.3 Utilizzare finally al posto di finalize


Come visto nei capitoli precedenti, non è opportuno ricorrere al metodo finalize per effettuare
il clean-up degli oggetti. Questo perché non vi è garanzia del momento esatto in cui il GC
(Garbage Collector) reclamerà lo spazio di memoria occupato da un oggetto e quindi invocherà
il metodo finalize. Nel codice Java funzionante all'interno di un application server, il metodo
finalize potrebbe anche venir invocato dopo diverse ore dal momento in cui l'oggetto stesso è
stato referenziato. Pertanto, per eseguire la pulizia degli oggetti, è buona pratica includere
opportuni blocchi finally all'interno dei metodi come visto in precedenza. Chiaramente il co-
strutto finally, ha un funzionamento assolutamente diverso dal metodo finalize. Tuttavia, è conve-
niente utilizzarlo per realizzare un sistema più accurato e affidabile di pulizia delle risorse.

5.3 Non utilizzare i tipi base delle eccezioni


Come visto in precedenza, il linguaggio di programmazione Java dispone di una serie di classi
fondamentali utilizzate per notificare anomalie (figura 5.1). Queste sono: Throwable, Errar, Exception
e RuntimeException. L'idea alla base del meccanismo delle eccezioni è che queste classi (ad ecce-
zione della classe Error che dovrebbe essere utilizzata solo in contesti particolarissimi) rappresen-
tano comodi punti di estensione per permettere la definizione di opportune gerarchie, come
quelle dei package Java. Pertanto, bisognerebbe evitare un riferimento diretto a questi tipi.

5.3.1 Non utilizzare direttamente Throwable, Error,


Exception, RuntimeException
Le applicazioni non dovrebbero mai utilizzare direttamente le seguenti classi: Throwable, Error,
Exception e RuntimeException. In particolare è opportuno che framework e package usino eccezio-
ni derivate da Exception o RuntimeException standard e/o appartenenti a un'opportuna gerarchia.
La comunicazione di eccezioni attraverso le classi Throwable ed Error può severamente com-
promettere la possibilità di eseguire, all'interno della stessa JVM, librerie, framework e package
scritti da terze parti e di riutilizzare il codice.
5.3.2 Non ereditare direttamente dalle classi Throwable o Error
Le applicazioni non dovrebbero mai definire eccezioni derivanti direttamente dalla classe
Throwable né cercare di intercettare (catch) o lanciare (throw) eccezioni di questo tipo. Allo stesso
modo, le applicazioni non dovrebbero definire nuove classi di errore derivanti direttamente da
java.lang.Errar né tantomeno tentare di intercettarle. Si tratta di classi con un significato ben
definito lanciate dalla JVM per segnalare gravi anomalie che non possono essere gestite.

5.3.3 Non utilizzare i tipi base per intercettare le eccezioni


Le eccezioni non dovrebbero mai essere intercettate utilizzando i tipi base come Throwable,
Error, Exception e RuntimeException.
Come regola generale è opportuno cercare di intercettare le eccezioni specifiche evitando di
definire dei "filtri" troppo ampi che possono facilmente causare situazioni di errore. Per esem-
pio, una rifattorizzazione del codice potrebbe portare all'inserimento di istruzioni aggiuntive in
grado di generare nuovi tipi di eccezione; vista la mancanza di selettività della clausola catch, tali
eccezioni finirebbero comunque per essere accomunate alla gestione prevista dal caso generale,
senza fornire al programmatore alcuna indicazione: nella maggior parte dei casi sarebbe fonte
di problemi non facilmente individuabili. Si consideri per esempio il seguente codice:

try I

String content = fileManager.readFile(xmlFile);

I catch (Exception e) I
// qualche operazione di gestione
I

Come si può notare il costrutto catch intercetta, in maniera impropria, istanze della classe
java.lang.Exception. Ora, si supponga di estendere il precedente codice invocando un metodo che
effettui il parsing del contenuto del file XML, trasformando la stringa in un apposito grafo di
oggetti (ConfigVO), come riportato nel seguente frammento:

try {

String content = fileManager.readFile(xmlFile);


configVO = (ConfigVO) genericStringUnmarshaller(content);

I catch (Exception e) {
// qualche operazione di gestione
I

Si supponga, come è lecito fare, che il metodo sia in grado di generare un'eccezione. Come si
può notare, la presenza del costrutto catch (Exception e) finisce per intercettare la nuova eccezio-
ne senza dare comunicazione al programmatore. Il caso in questione potrebbe sembrare co-
munque senza troppe conseguenze: in fondo, sono coinvolte solo due istruzioni... Si immagini
però il caso tipico di una serie abbastanza lunga di invocazioni, in cui diversi metodi siano
composti da circa 10-15 istruzioni, oppure la situazione abbastanza frequente in cui sia neces-
sario cambiare la firma di un metodo, aggiungendo una nuova eccezione, e che questo sia
utilizzato in molte parti del sistema.
In questi casi si capisce come è facile generare lo scenario in cui la nuova eccezione venga
intercettata in un posto dove non dovrebbe esserlo e quindi viene gestita in modo errato. A
complicare le cose interviene il fatto che problemi di questo tipo, normalmente, sono difficil-
mente individuabili.

5.3.4 Valutare l'opportunità di implementare


una propria gerarchia di eccezioni
Nel disegno e nella codifica di framework, package e strati logici è spesso opportuno prevedere
l'implementazione di una specifica gerarchia di eccezioni. Ciò fa sì che le classi client siano
esposte a un insieme di interfacce consistente e minimale. In tali circostanze, tuttavia, è neces-
sario che le nuove eccezioni siano codificate in modo tale da incapsulare quelle originali. Per-
tanto, sebbene implementare proprie gerarchie di eccezioni per librerie, framework, etc. sia
un'ottima pratica per agevolarne la gestione da parte delle classi fruitrici, è comunque
consigliabile mantenere le originali nel codice interno del package, framework, etc.
In questi casi, prima di creare una nuova eccezione, è consigliabile porsi queste domande:

• esiste una eccezione base Java in grado di descrivere il problema che si intende comunicare?
• l'implementazione di nuove eccezioni migliora il codice? In particolare, le classi client
ricevono un chiaro beneficio dalla presenza della nuova eccezione?
• la stessa porzione di codice lancia altre eccezioni in qualche modo legate alla nuova?
• la nuova eccezione o quelle fornite da una terza parte, sono accessibili alle classi client?

Qualora una o più delle precedenti domande presenti una risposta negativa, è il caso di
verificare opportunamente la necessità di ricorrere all'implementazione di una nuova eccezione.

5.3.5 Non perdere le informazioni relative all'eccezione iniziale


Un'importante miglioria introdotta con la versione JDK 1.4 è la possibilità di annidare eccezio-
ni. In particolare ciò è stato possibile grazie a un'opportuna revisione della classe
java.lang.Throwabie. Questo perfezionamento permette di lanciare nuove eccezioni senza perde-
re informazioni relative a quella originale. Si tratta, pertanto, di una caratteristica molto utile in
quando rende possibile sviluppare framework che, per comodità delle classi fruitrici, possono
lanciare proprie eccezioni con le eccezioni originali incapsulate all'interno.
Quindi, in tutti i casi in cui si decida di procedere con l'implementazione di opportune classi
eccezione, è importante includere i costruttori riportati nel listato seguente.

/ "

" Constructor melhod


• @ p a r a m errar encapsulated error
*/
public MyExceptionfThrowable error) I
super(error);
| | «v& lant Ei< non [

J )iv&lanu.A untimi E «tfpi Ion | r )a*M«Cu'ily 1


! Goncral$p<urityE»Ceptlon

I |»v»lan». I invillanì. javiutur ty. javuecurlty. )av«wcurity |«v&se<umy. |


IlleialPafameterfiteplion I $ecurilyC»ceplinn I I P'Ovider£«cc ption NòSuchAlqorilhmExCPpl'On Unrecoverable EnlryEnceptionl NoSuchPioviderEiceplion 1 Jnr CQvprablclleyCicppiion J | Keytiception J

javuMurlty. Java.ieci/rity. )ava.*<umy. javikw curily. laviwtunty.


KeyStoreCnception 1 KeyManagcTOntCìcepUon 1 hwilidAlgorillunParameicrEicpplionl Signature Eicvptlor I I DiqeMticcptiOn 1

|a»a.*curny II Java»«urUy. I (" |ava.iecufity.


Invai idPararrwIcrCjicffpl lori 1 1 AcccnComrolE« W o n 1 invalidKcyE «ception

Figura 5.2 - Esempio di gerarchia delle eccezioni definito nel package java.security.

* Constructor method
* ® p a r a m excMessage exception message
" @ p a r a m error encapsulated o c c u r r e d
'/
public MyException(String excMessage, Throwable error) (
super(excMessage, error);

5.4 Fare attenzione alla modalità di notifica delle eccezioni


Non è infrequente il caso in cui le eccezioni che si presentano in un sistema sono relative a
problemi non gestibili. In questo caso è necessario fare in modo che le cause che hanno genera-
to il problema siano opportunamente riportate e illustrate. Qualora, l'eccezione si verifichi
durante l'espletamento di un servizio richiesto da un utente, magari collegato con un browser
remoto, è necessario renderlo consapevole del problema che si è presentato. In ogni caso, le
varie anomalie vanno opportunamente segnalate sia al fine di semplificare eventuali investiga-
zioni manuali, sia per fornire sufficienti informazioni a eventuali meccanismi automatici predi-
sposti per la risoluzione di problemi.

5.4.1 Non assorbire mai le eccezioni silenziosamente


Si dice che un'eccezione è assorbita "silenziosamente" quando il codice incluso nella relativa clau-
sola catch non risolve il problema e non esegue alcuna operazione per notificare il verificarsi del-
l'eccezione stessa. Il listato seguente mostra un esempio di eccezione assorbita silenziosamente:

A
tryi

cistruzione che può causare l'eccezione>


<instruzione>
<instruzione>
) catch (<eccezione> e) (

l // eccezione assorbita sllezlosamente


Anche se si pensa che l'eccezione non si verificherà mai, è sempre il caso di aggiungere una
segnalazione, magari un semplice record di log. In effetti, può sempre succedere che scenari di
eccezione apparentemente innocui diventino problematici a seguito di processi di manutenzione
del codice. Individuare eccezioni assorbite silenziosamente può richiedere un impegno elevatissimo.

5.4.2 Mantenere l'utente informato


Qualora un'eccezione si verifichi durante la gestione di uno stimolo generato da un utente, è
necessario far in modo che opportune informazioni circa l'anomalia siano notificate all'utente.
Chiaramente questo vale per tutte le eccezioni non temporanee. E pertanto necessario:

1. presentare un messaggio che sia comprensibile all'utente;


2. fornire all'utente diverse opportunità sul da farsi e, in casi specifici, fornire la possibilità
di tentare nuovamente la stessa operazione.

Un sistema di qualità dovrebbe sempre fornire all'utente informazioni su quanto accade


dietro le quinte, non solo in presenza di problemi, ma anche qualora l'operazione sia rallentata.

La direttiva di mantenere l'utente sempre informato su quanto accade nel sistema si applica a tutti
gli scenari e non solo a quelli di errore. Per esempio, quando si devono far eseguire al sistema
lunghi processi (come le tipiche procedure di revisione che si eseguono in banca a chiusura della
giornata, i famosi E O D , End Of Day), è sempre opportuno cercare di suddividere il processo
sull'intero insieme di dati in un numero di iterazioni dello stesso su opportune ripartizioni del
dominio. Ciò non solo per avere l'opportunità di fornire informazioni all'utente circa lo stato di
avanzamento del processo stesso, tra un'iterazione e quella successiva, ma anche per gestire
transazioni di minori dimensioni, per salvare risultati intermedi molto utili se il processo fallisce: in
tal caso non bisognerà ricominciare da capo ma dall'elemento successivo all'ultimo processato.

5.4.3 Pianificare e applicare in maniera uniforme e coerente


il formato dei log riferiti alle eccezioni
La realizzazione di sistemi ad alta disponibilità/affidabilità, richiede di includere diversi accor-
gimenti non necessari in altre situazioni. Per esempio, è necessario progettare meccanismi in
grado di rilevare tempestivamente eventuali componenti e sistemi malfunzionanti o gravati da
stress enorme. Una delle tecniche più largamente impiegate consiste nel ricorrere a sistemi
dedicati a monitoraggio e analisi dei file log prodotti dalle varie applicazioni. Ciò al fine di
individuare dei pattern predefiniti che identifichino situazioni di malfunzionamento (per esempio
una linea di testo iniziante con la stringa "ERROR").
Al fine di rendere possibile l'impiego di questi sistemi di controllo è opportuno assegnare ai
log opportuni formati, specialmente a quelli relativi all'insorgere di un'eccezione. Una struttura
abbastanza ricorrente è la lista di attributi separati da caratteri bianchi, in cui le diverse infor-
mazioni prevedono i dati come: data e ora dell'eccezione, livello di log, identificatore univoco
del problema ed informazioni di dettaglio. Per esempio:

2 0 0 6 - 1 2 - 0 3 12:51:07.103 W R N SDS-CN01 com.mokabyte.


tool.umlclassgenerator.service.DiagramParser - ' d i a g r a m not found' diagram=class34

Questo argomento è presentato in dettaglio nel Capitolo 6.


5.5 Valutare attentamente il ciclo di vita delle eccezioni
Il meccanismo delle eccezioni fornisce un'ottima soluzione per notificare eventuali anomalie.
Queste però, oltre a essere opportunamente segnalate devono, ovviamente, essere anche pro-
priamente gestite. Un elemento di particolare importanza nell'implementazione delle proce-
dure di gestione consiste nel selezionare il luogo migliore in cui gestirle. In effetti, si deve
evitare una gestione in posti in cui non è chiaro il da farsi e, al tempo stesso, non è opportuno
notificare all'infinito un'eccezione qualora sia possibile gestirla.

5.5.1 Non provare a gestire un'eccezione in un posto


in cui non è possibile farlo
Un ragionevole principio di programmazione prescrive di non tentare la gestione di un'ecce-
zione in una parte di codice in cui non siano disponibili sufficienti informazioni per farlo. In
caso contrario, nella migliore delle ipotesi, si finisce con il rendere il codice molto complesso e
difficilmente riutilizzabile, mentre nella peggiore si finisce per assumere alcune informazioni,
non sempre valide, generando un insieme di problemi rilevabili solo all'insorgere di casi di
eccezione, spesso non facilmente riproducibili e testabili.
Si consideri per esempio il codice riportato nel listato presentato per primo in questo capito-
lo. Nel caso in cui si verifichi un'eccezione di tipo FileNotFoundException non si hanno sufficienti
informazioni per selezionare l'opportuna gestione, pertanto la cosa migliore da farsi è rimanda-
re la gestione alla classe chiamante. Le eccezioni, quindi, vanno intercettate (catch) quando si
hanno sufficienti informazioni per poterle gestirle correttamente. Questa regola può essere
scavalcata nel caso in cui l'eccezione debba essere incapsulata in un'altra più generale. In que-
sto caso si ha un catch simbolico, come nel caso del listato principale.

5.5.2 Gestire le eccezioni non appena possibile


Questa regola a prima vista potrebbe sembrare in contraddizione con quella precedente. In
realtà, le eccezioni dovrebbero essere gestite non appena si giunge, nel percorso a ritroso della
successione delle invocazioni, a un metodo che abbia sufficienti informazioni per gestirla. Ciò,
in alcuni scenari può coincidere con il comunicare indietro l'eccezione fino al metodo che ha
eseguito la chiamata iniziale della catena di invocazioni.

5.6 Considerare la natura delle eccezioni


Un criterio di raggruppamento delle eccezioni consiste nel suddividerle in base alla natura del
problema che le ha originate. Ciò porta a individuare due gruppi di eccezioni: business e di
sistema. Le prime si riferiscono a eventi che violano una o più business rules (le regole alla base
della logica applicativa), la cui conseguenza sta nell'impossibilità da parte del sistema di fornire
il servizio dove l'eccezione si è verificata. Le eccezioni di sistema, invece, si riferiscono a eventi
che si verificano nel sistema (per esempio, il database diventa indisponibile) e che compromet-
tono la fornitura di un insieme di servizi per un intervallo più o meno lungo. Pertanto problemi
di sistema sono potenzialmente più gravi di quelli di business.

5.6.1 Gestire correttamente le eccezioni business


Le eccezioni di tipo business si riferiscono ad eventi che violano una o più regole della logica
applicativa (business rules) e pertanto non permettono al sistema di portare a termine corretta-
mente il relativo servizio. Per esempio, un sistema di back office bancario potrebbe ricevere un
messaggio contenente un trade relativo a un cliente (counter-party) delle cui necessarie infor-
mazioni il sistema stesso non dispone. Il sistema, pertanto, non può che lanciare un'eccezione
di business relativa all'impossibilità di processare automaticamente tale trade. Situazioni come
questa, come la quasi totalità delle eccezioni di tipo business, richiedono l'intervento umano.
Nel caso in questione, un operatore dovrebbe occuparsi di inserire i dati mancanti nel sistema
oppure di correggere l'identificativo errato del cliente.
Sistemi medio/grandi atti a gestire importanti business (come nell'esempio precedente) ri-
chiedono di progettare meccanismi atti a risolvere prontamente questo tipo di problemi. Nel
caso precedente, per esempio, sebbene il sistema non fosse in grado di processare il trade, non
è pensabile che questo possa tranquillamente rifiutare o scartare il trade: nei sistemi bancari
viaggiano spesso trade relativi a contratti da centinaia di milioni di dollari.
Pertanto, se da un lato è necessario progettare sistemi informatici in grado di processare gran
parte degli eventi automaticamente (i famosi sistemi STP, Straight-Through Processing), dall'al-
tro è necessario prevedere meccanismi da attuare qualora si verifichino eccezioni durante l'ela-
borazione automatica. Questi meccanismi, tipicamente, richiedono di realizzare un apposito
sottosistema, denominato sistema di gestione delle eccezioni business (BEM, Business Exception
Management) incaricato appunto di gestire eccezioni di tipo business. In particolare, questo
sistema si dovrebbe occupare di:

1. predisporsi a ricevere notifiche relative a eccezioni di tipo business (per esempio sotto-
scrivendo il canale delle eccezioni);
2. ricevere e analizzare le varie segnalazioni generate dai sottosistemi assistiti;
3. per ogni messaggio ricevuto, valutare, in base a un opportuno sistema di regole, la prio-
rità da assegnare alla corrispondente gestione;
4. creare un record relativo ad ogni eccezione ricevuta, inserirlo in un'apposita coda interna
e, contestualmente, inviare una segnalazione agli opportuni operatori sulla sua presenza;
5. verificare continuamente i record presenti nelle varie code al fine di aumentare la priori-
tà di quelli che sono presenti nella coda da un eccessivo intervallo temporale.

Da tener presente che per quanto l'intervento umano sia una soluzione molto flessibile,
tipicamente è anche molto costosa, e pertanto è buona pratica minimizzarne l'utilizzo.

5.6.2 Valutare l'estensione temporale delle eccezioni


Le eccezioni di business tendono per loro natura a essere di tipo permanente. Se per esempio
l'identificativo di un cliente presente in un messaggio è errato, questo rimarrà tale fin quando
non viene intrapresa un'opportuna azione correttiva. Quindi all'interno del servizio non c'è
nulla da fare: una volta generata l'eccezione questa non ha alcuna possibilità di risolversi auto-
maticamente.
Un discorso diverso vale per le eccezioni di sistema. In questo caso, le cause che hanno
portato alla generazione dell'eccezione potrebbero automaticamente risolversi all'interno del-
lo stesso servizio in cui si è manifestata. Si consideri, per esempio, un servizio che tenti di
connettersi al Database Management System e che fallisca per un eccesso temporaneo di traf-
fico presente nella rete. Un altro esempio è relativo a un sistema che tenti di connettersi a un
altro e che, per un eccessivo e temporaneo stress di quest'ultimo, non riceva una risposta entro
l'intervallo di tempo previsto (timeout). Eccezioni di sistema presentano spesso una persistenza
temporanea e quindi una corretta gestione prevede di ritentare l'istruzione che può generarla,
per un numero di predefinito di volte, prima di notificarla a ritroso.
Un esempio di possibile gestione delle eccezioni di sistema è presentata nel listato seguente.

int attempts = 0; // counís the number of failed atlempts


boolean success = false; // success flag
tryl
while ( (attempts < MAX_FAILED_ATTEMPTS) && (¡success) ) I
tryl
< istruzione che può generare un'eccezione>
<istruzione>
•¡istruzione che può generare un'eccezione>
<instruction>
success = true;
I catch (<eccezione specifica> es) I
attempts++;
il (attempts < MAX_FAILED_ATTEMPTS) I
Iryl
Thread.sleep(BASIC_TIME_WAIT*attempts);
I catch (InterruptedException ie) (
effettuare il log dell'eccezione e quindi proseguire>
I
I
I
I
I finally I
<parte del codice richiesto dal finally>
I

5.6.3 Gestire correttamente il perdurare delle eccezioni di sistema


In tutti quei in cui un'eccezione di sistema non si risolva in un arco temporale abbastanza breve
(per esempio il codice del listato visto poco sopra fallisca le varie iterazioni) nasce il problema
intricato di come gestire la situazione. Il sistema, indubbiamente, si trova in una situazione
critica in cui, difficilmente, potrà soddisfare altre richieste. Situazioni del genere, tipicamente,
richiedono l'intervento umano, magari semplicemente per riawiare uno dei sistemi entrato in
un stato di malfunzionamento.
Le alternative disponibili includono: l'avvio della procedura controllata di shut-down, far
transitare il sistema in un particolare stato di inattività (¿die). Tale stato dovrebbe essere carat-
terizzato dal fatto che il sistema inibisca i canali di comunicazione di input (non accetti più
stimoli), eccetto quelli provenienti da un'opportuna console di amministrazione e, a intervalli
di tempo via via crescenti, tenti di verificare l'effettivo perdurare del problema. Questa secon-
da alternativa, sebbene più complessa, permette al sistema di riprendere automaticamente il
corretto funzionamento non appena il problema di sistema sia risolto.
Quale sia la soluzione da implementare, come al solito, dipende da un insieme di fattori,
quali: i requisiti utente, il particolare tipo di eccezione e i meccanismi presenti atti a segnalare
tempestivamente, a un operatore umano, l'insorgere del problema.
Indipendentemente dalla soluzione scelta, è necessario far in modo che i processi business
colpiti dal problema siano gestiti correttamente, come visto in precedenza. Ciò può limitarsi a
inviare una comunicazione all'utente, a cercare di salvare opportune informazioni, e cosi via.

5.7 Considerare i classici problemi relativi all'impiego


di sistemi di messaggistica
La tendenza degli ultimi anni nei sistemi enterprise è stata caratterizzata dalle tematiche di
integrazione. Il recente passato ha visto l'affermarsi di moderni sistemi di integrazione software
come gli Enterprise Service Bus (ESB, Bus di Servizio Aziendale); ciò nonostante i sistemi di
messaggistica sono ancora molto utilizzati. Questi permettono di disegnare e implementare
grandi sistemi in termini di un insieme di sottosistemi interconnessi con elevato grado di
disaccoppiamento. La loro presenza, tuttavia, nella quasi totalità dei casi, richiede di valutare
attentamente e di gestire le seguenti due problematiche: messaggi "avvelenati" (poisoned
messages) e ricezione di messaggi fuori sequenza.

5.7.1 Gestire correttamente i messaggi "avvelenati" (poisoned)


Con il termine di messaggi "avvelenati", ci si riferisci a messaggi viziati da qualche problema che
tendono a restare perennemente nel sistema se non opportunamente rimossi. Questo scenario si
può verificare in situazioni in cui, per un particolare canale, il Message Oriented Middleware
(MOM, infrastruttura orientata al messaggio) è impostato per un funzionamento a consegna
garantita (guaranteed delivery, il messaggio viene consegnato una e una sola volta). In particola-
re, questa situazione si ha quando un sistema riceve un messaggio che viola una o più pre- e/o
post-condizioni del sistema ricevente; il ricevente, quindi, non può far altro che scartarlo gene-
rando un'opportuna eccezione. Il sistema di messaggistica, non ricevendo la conferma dell'avve-
nuta ricezione del messaggio (ossia il commit), dopo un leggero intervallo di tempo, presenta
nuovamente il medesimo messaggio al sistema. Ciò perché si è impostata la modalità di consegna
garantita. Questa successione potrebbe continuare all'infinito qualora nessuna azione sia intra-
presa. Alla presenza di diversi messaggi avvelenati, si potrebbe generare una serie di conseguen-
ze indesiderate come una notevole perdita di performance del sistema, perdita delle informazio-
ni contenute nel messaggio stesso giacché questo non viene gestito, e così via.
Una buona tecnica per gestire problemi di questo tipo consiste nel richiedere ai sistemi
destinatari di gestire apposite tabelle in cui memorizzare tre campi: l'identificatore univoco dei
messaggi, il timestamp dell'ultima ricezione e un contatore di ricezioni. In questo modo, quan-
do questo contatore raggiunge un valore prefissato (per esempio 3), il sistema ricevente è in
grado di accorgersi delle presenza di un messaggio avvelenato e quindi procedere alla sua gestio-
ne. Questa, invece di generare un'eccezione (che spingerebbe nuovamente il messaggio nel
MOM), si deve occupare di spostare il messaggio in un'apposita coda, denominata normalmen-
te "ospedale dei messaggi" e di comunicare, al sistema di messaggistica l'avvenuta ricezione del
messaggio, in modo analogo a quando tutto funziona correttamente. La coda dei messaggi avve-
lenati dovrebbe poi essere monitorata da un opportuno meccanismo in grado di comunicare ad
appositi operatori umani la presenza di messaggi avvelenati che devono essere gestiti.

5.7.2 Messaggi fuori sequenza


Questo scenario, come suggerisce il nome, si riferisce a situazioni in cui un sistema destinatario
riceva dei messaggi in ordine diverso da quello di trasmissione. Sebbene non sempre questo
scenario costituisca un problema insormontabile, in alcune situazioni potrebbe però generare
scenari molto dannosi. Si consideri per esempio il caso di un sistema bancario in cui i messaggi
relativi al prezzo di uno stesso prodotto finanziario siano ricevuti in ordine errato.
Sebbene molti sistemi di messaggistica recenti dispongano di meccanismi interni atti a gesti-
re questo problema, la loro efficacia, in particolari scenari è decisamente ridotta. Si consideri,
per esempio, il caso di un sistema a elevato livello di parallelismo; per un qualsiasi problema,
uno dei sottosistemi diventa improvvisamente instabile ("va in crash") e necessita di essere
riawiato. Si supponga, ulteriormente, che questo sottosistema abbia sottoscritto alcuni canali
con consegna garantita dei messaggi. In questo scenario, i messaggi presenti nei relativi buffer
passano automaticamente in uno stato di bloccaggio finché i corrispondenti socket non vengo-
no chiusi. Ciò può richiedere un intervallo di tempo non trascurabile. Se nel frattempo, un
meccanismo disegnato per l'elevata disponibilità riawia il sottosistema "crashato", magari su
un server diverso da quello originario, ecco che si verifica un'elevata probabilità di avere mes-
saggi recapitati con un ordine diverso da quello in cui sono stati immessi nella coda.
Anche se lo scenario presentato può sembrare un caso limite, in sistemi complessi non lo è
affatto! Il dato di fatto è che il problema dei messaggi fuori sequenza non solo può verificarsi
ma si verifica. Pertanto, qualora ciò possa arrecare danni, è necessario disegnare ed implemen-
tare meccanismi per la gestione di queste anomalie.
Un sistema di intercettazione di questo problema richiede l'utilizzo di un meccanismo simile
a quanto visto per i messaggi avvelenati. In particolare è necessario che i sistemi destinatari
gestiscano una tabella in cui memorizzare l'identificativo del messaggio ricevuto e il momento
in cui è stato ricevuto. Questo identificativo deve essere ottenuto dall'entità incapsulata nel
messaggio stesso, e quindi deve avere una valenza per così dire business.
A questo punto, quando il sistema destinatario riceve un messaggio deve verificare se abbia o
meno già ricevuto un messaggio con il medesimo identificativo del messaggio. In caso negativo,
ovviamente, non ci sono problemi, ed è sufficiente creare un record relativo al nuovo messaggio.
In caso positivo, invece è necessario controllare il timestamp dell'ultima occorrenza ricevuta con
quella del messaggio. Se quest'ultimo si dimostra essere più recente, allora non ci sono problemi
ed è sufficiente aggiornare il relativo record nella tabella. Mentre, in caso contrario, il nuovo
messaggio è stato ricevuto fuori sequenza e quindi è necessario avviare la relativa gestione. Que-
sta in funzione della situazione in cui si presenta, può richiedere diverse soluzioni.
Per esempio, nel caso dei prezzi, qualora si riceva un messaggio fuori sequenza, basta scartar-
lo: il prezzo attuale è sicuramente più recente di quello ricevuto in ritardo; ma in altri contesti
si può giungere fino alla situazione di dover richiedere l'intervento dell'operatore umano.
Capitolo
Il logging
Introduzione
In questo capitolo presentiamo un argomento d'importanza fondamentale per la produzione
di software di qualità: la strategia di logging. Con questo termine ci si riferisce alla politica
utilizzata da un'applicazione per generare informazioni (log record, sorta di "diari di bordo")
relative alle attività eseguite. I log possono essere utilizzati per diversi scopi; per esempio:

• analisi statistiche;
• attività di controllo del programma, come riproduzione di scenari di errore, analisi di
specifiche transazioni, etc.;
• implementazione di meccanismi di backup e recovery;
• analisi dello stato dell'applicazione da parte di altri sistemi di amministrazione.

Più in generale, il logging è un modo sistematico e controllato di rappresentare lo stato di


un'applicazione in una forma comprensibile alle persone. ([APCL4J], Samandra Gupta).
I record di log sono tipicamente scritti su un apposito file, detto appunto file di log, e, meno
frequentemente, in un'apposita base di dati. Queste informazioni rappresentano una preziosa
risorsa di consultazione per l'analisi di eventuali situazioni anomale. Un'applicazione FTP, per
esempio, potrebbe gestire appositi file di log in cui memorizzare i dati relativi ad ogni azione
eseguita: ricezione e trasmissione di file, cambiamento del nome di un file, etc. Questi log
potrebbero includere: il percorso completo del file, la specifica attività eseguita (invio, ricezio-
ne, etc.), il codice dell'utente connesso, data e tempo di inizio e di conclusione del processo,
server di destinazione, ecc. Pertanto in caso di errore, sarebbe sufficiente consultare il file di log
per capire rapidamente cosa sia andato storto. Inoltre, lo stesso file potrebbe essere consultato
per individuare eventuali tentativi di eseguire operazioni illecite.
Il logging, rappresenta la più tradizionale e basilare strategia di debugging del codice (per
questo definita a basso livello) basata sull'analisi di opportuni messaggi emessi dal sistema. Il
logging, infine, costituisce una forma di auditing ("revisione") del sistema. Per esempio, sareb-
be sufficiente analizzare periodicamente i file di log per individuare tentativi di eseguire azioni
non autorizzate. Un buon logging è fondamentale per una serie di attività, come:

1. diagnosi dei problemi in sistemi in produzione: non sempre è possibile usufruire


dell'ausilio di sofisticati debugger per diagnosticare eventuali situazioni anomale. Que-
sto è per esempio il caso di sistemi in produzione. Si consideri la necessità di dover
ricostruire un'anomalia generatasi in uno sportello bancomat (ATM). In questo caso i
file di log, non solo quelli gestiti dallo sportello, rappresentano una risorsa preziosissima
per poter ricreare lo scenario di malfunzionamento.
2. diagnosi dei problemi in sistemi ad elevato grado di concorrenza: in queste situazioni,
spesso i debugger, anche i più sofisticati non sono il migliore ausilio per l'analisi di
malfunzionamenti. Questo sia perché l'introduzione del debugger finisce per influenza-
re il grado di concorrenza dell'applicazione, sia perché rende proibitivo l'intercettazio-
ne di una situazione di errore di un sistema in un contesto di stress test.
3. mantenimento della storia dell'applicazione: il debugging è un'attività transiente, men-
tre il logging è permanente.

Sebbene tutti gli sviluppatori esperti condividano l'importanza di un'appropriata, consisten-


te e controllata strategia di logging, si tratta dell'ennesima attività fin troppo spesso trascurata,
realizzata di fretta nei giorni precedenti al rilascio del sistema e/o eseguita in maniera casuale
senza seguire specifiche linee guide. Altro scenario molto frequente è il caso del logging affida-
to all'esperienza e alla buona volontà dei singoli sviluppatori del team di sviluppo al posto di
seguire una precisa strategia. Gli inconvenienti generati da questi approcci emergono in tutta la
loro drammaticità quando, una volta installato (deployed) il sistema in ambiente UAT (User
Acceptance Test, test di accettazione utente o di staging) o, peggio ancora, in produzione, si
abbia la necessità di correggere i primi malfunzionamenti... L'esperienza insegna che gli errori
possono accadere anche in sistemi ad elevata qualità; anzi, a essere precisi, accadono! Solo a
questo punto, venendo meno la possibilità di poter utilizzare sofisticati strumenti di debug, ci si
rende conto di quanto sia difficile analizzare anomalie senza un logging chiaro e consistente...
La riproduzione di un'anomalia è ovviamente propedeutica alla sua correzione.
Una politica di logging non ben pianificata è in grado di creare tutta una serie inconvenienti.
Per esempio, un logging eccessivo tende a incidere significativamente sulle performance del
sistema tanto da divenire un serio problema in sistemi gravati da stringenti requisiti non funzionali
(bassissima latenza ed elevato throughput). Inoltre, logging eccessivamente verbosi, finiscono
per generare dati inutili, "rumore", piuttosto che preziose informazioni. Pertanto, il logging
delle applicazioni software deve seguire una ben precisa strategia pianificata a priori e basata su
opportune best practice, che sono presentate nei paragrafi delle direttive.

Obiettivi
L'obiettivo di questo capitolo è fornire una serie di direttive e best practice per l'implementazione
di efficaci strategie di log.
Sebbene molto frequentemente il concetto di logging sia inevitabilmente associato con la
libreria Log4J (l'esempio che spiega il concetto, l'istanza che illustra la classe) e per quanto
anche questo libro ne faccia largo utilizzo, il presente capitolo non è e non può essere una guida
di riferimento di Log4J (a tal fine esistono appositi libri, cfr. l'Appendice E). Tuttavia parlare di
strategia di logging senza presentare minimamente i principali tool di logging equivarrebbe a
parlare di programmazione senza citare alcun linguaggio di programmazione.
Ciò premesso, le varie regole presentate nel corso di questo capitolo hanno una loro validità
che esula dall'uso di un particolare software.

Un po' di storia
Ogni volta che si parla di logging, si pensa a Log4j (http://logging.apache.org/log4j/). Ciò è abba-
stanza normale considerando sia che si tratta di un software che ha contribuito notevolmente a
trasformare questa attività in una vera e propria pratica ingegneristica, sia il grandissimo suc-
cesso riscosso. Successo ulteriormente confermato da vari porting del codice, come Log4net,
Log4xx, etc.
L'importanza delle strategie di logging, tuttavia, è stata compresa dagli uomini e applicata
per secoli negli ambienti più disparati... Basti pensare ai diari di bordo dei grandi navigatori
del XVII e XVIII secolo, tuttora utilizzati per i fini più diversi, come studi storici generali o
relativi alle tecniche di costruzione navale e di navigazione, alle conoscenze di cartografia, o alle
variazioni climatiche del pianeta Terra e così via.
Il metodo più semplice, e sicuramente meno efficace, spesso utilizzato dagli sviluppatori alle
prime armi per eseguire il logging dell'applicazione consiste nell'eseguire stampe a video:

System.out.printlnf activity message received. (message=" + newMessage.toString() + ")" );

Per quanto questo metodo offra il vantaggio della semplicità, e possa risultare molto comodo
per attività a brevissimo termine, presenta un insieme di seri problemi tali da sconsigliarne
l'utilizzo. Alcuni dei più importanti sono:

1. non esiste un meccanismo automatico che consente di inibire i messaggi. Ciò risulta
assolutamente necessario al momento di mettere il sistema in produzione.
2. non è immediato implementare un sistema atto ad organizzare i log per categorie, come
trace, info, debug, etc. e quindi non è sempre possibile eseguire il tuning dei log, specie in
presenza di sistemi complessi. Si consideri, per esempio, la necessità di voler escludere i
log della libreria di integrazione con il database e, allo stesso tempo, mantenere i log
della restante parte del sistema.
3. i messaggi di log sono inviati nel dispositivo standard di output, tipicamente il video, e
pertanto risultano poco utili sia per la fase di sviluppo, in cui è possibile utilizzare sofisti-
cati strumenti di debug, sia per la diagnosi del sistema in produzione.
4. spesso il deployment di sistemi richiede che diverse applicazioni condividano uno stesso
server, per esempio non è assolutamente infrequente lo scenario di un server dotato di
diverse CPU ospiti diversi server, in questa configurazione (a seconda delle impostazioni)
i vari log potrebbero finire o per mescolarsi o per creare una moltitudine di finestre
difficilmente consultabili.
Dati gli evidenti enormi svantaggi generati dal logging eseguito attraverso all'output sulla
console, è evidente come una corretta strategia di logging sia fondamentale per la produzione
di un sistema di qualità.
L'esperienza insegna che sistemi di bassa qualità tendono a presentare tantissimi problemi
una volta messi in produzione, ma anche, che gli errori possono accadere (e lo fanno! parola di
Murphy) anche in sistemi accuratamente progettati ed implementati. Quindi un'opportuna
strategia di logging è in grado di far risparmiare tempo e denaro nell'individuazione di errori e
a mantenere elevata la confidenza da parte degli utenti del sistema.

Log4J
Log4J è probabilmente il più famoso software di logging. Le sue caratteristiche principali sono
la semplicità di utilizzo, l'elevata affidabilità, le buone prestazioni, la ricchezza di feature, e
l'estensibilità. Si tratta di uno dei software di maggior successo tra quelli appartenenti a quella
fucina di idee open source denominata Apache. L'ottima riuscita di Log4J è testimoniata da
diversi fattori, come il larghissimo utilizzo in applicazioni professionali e non solo, la presenza
di versioni realizzate per altri linguaggi (porting Log4Net, Log4xx, etc.) e dalla conquista dello
status di standard de facto, status che neanche l'introduzione dell'API "standard" java.util.logging
è riuscita a sminuire.
L'introduzione di Log4J ha portato molti benefici, tra i quali uno dei più importanti consiste
nell'aver enormemente semplificato e standardizzato il meccanismo del logging. Prima della
produzione di Log4J, il logging delle applicazioni era un problema serio. Tanto che molte ap-
plicazioni venivano rilasciate senza appropriate funzionalità di logging, oppure con soluzioni
basilari implementate in qualche modo dai singoli team di sviluppo, i quali spesso si affidavano
alla visualizzazione di messaggi sulla console (con tutti le problematiche riportate nei paragrafi
precedenti). Meno frequenti invece erano le situazioni in cui i team di sviluppo investivano
tempo e denaro per produrre vere e proprie librerie. Anche in queste situazioni, tali librerie
molto spesso erano realizzate rapidamente (bisognava investire il tempo sull'automazione del
business) finendo per implementare funzionalità piuttosto basilari con tutti i limiti del caso:
impossibilità di eseguire il tuning dei log, significativo impatto sulle performance, etc.
Tuttavia, uno dei tentativi degni di nota in cui si tentò di implementare una vera e propria
libreria di logging fu quello eseguito all'interno del progetto della comunità europea SEMPER
(,Secure Electronic Marketplace for Europe, mercato elettronico sicuro per l'Europa, 1996), di
cui Joe-Luis Abad-Peiro fu l'autore iniziale. Questa libreria costituì la base di partenza utilizza-
ta da Ceki Gùlcu, che dopo diversi ripensamenti, revisioni e cambiamenti portò alla realizza-
zione di jZRLog. Questa prima versione fu poi ulteriormente rielaborata grazie anche alla par-
tecipazione di persone come: Andreas Fleuti, Micheal Steiner e N. Asokan fino al consegui-
mento della pubblicazione ad ottobre del 1999 di Log4J su alpha Works.

Struttura e funzionamento
Gli elementi fondamentali di Log4j sono tre: Logger, Appender e Layout (figura 6.1). Le loro
istanze cooperano per ottenere la produzione di opportuni messaggi in formati prestabiliti
nelle destinazioni specificate. I Logger (dalla versione Log4J 1.2 hanno rimpiazzato gli iniziali
elementi Category) hanno come responsibilità principale la cattura dei messaggi. Sono organiz-
zati secondo una ben definita gerarchia che permette di filtrare i vari messaggi. Si tratta di
elementi dotati di un nome univoco (necessario per il relativo reperimento) e sono organizzati
secondo una gerarchia in grado di rispecchiare i package Java. Pertanto, il logger com.mokabyte
risulta genitore del logger com.mokabyte.financing. Gli Appender hanno la responsibilità di pubbli-
care le informazioni di log su opportuni target. Per esempio, l'appender che invia messaggi a
video (ConsoleAppender) ha la console come target. Questi possono utilizzare una serie di filtri: in
questo caso tutti i filtri devono abilitare il log affinché questo venga incluso in nel target dell'ap-
pender. Infine i Layout sono responsabili per la formattazione dei vari messaggi.
Nella figura 6.2 è mostrato il diagramma delle classi relativo alla struttura interna di Log4j.
Come si nota dal diagramma delle classi (figura 6.2), Log4J offre un elevato grado di flessibi-
lità ed estensibilità: gli elementi fondamentali, come per esempio Appender, sono rappresentati da
un'interfaccia (Appender), spesso corredata da una classe base astratta (AppenderSkeleton) che in-
clude il comportamento base condiviso da tutte le specializzazioni. Ciò permette di implementa-
re più agevolmente le diverse versioni del concetto in questione (ConsoleAppender, FileAppender).
Tali elementi rappresentano vari e propri punti di estensione, socket in cui inserire le varie
customizzazioni. La presenza di ben ponderati punti di estensione e l'elevato numero di plug-in
disponibili hanno contribuito notevolmente al grande successo di Log4J. Per poter utilizzare
Log4J all'interno di una classe è necessario:

• importare la libreria log4J;


• dichiarare e reperire lo specifico logger (questo è possibile poiché le Category sono orga-
nizzate in un namespace gerarchico: la classe Category ha un attributo nome e una auto-
relazione denominata parent (fig. 6.2).

I principali metodi della classe org.apache.log4j.Logger sono riportati di seguito.

public class Logger {

// Creation & retrieval methods;

Ha la resposabilità di Ha la responsabilità di
Ha la resposibilitàdi catturare
pubblicare'nessaggi di log su formattare i messaggi nello
messaggi di log
uno o più target stile desiderato

ii ^ C r
Logger Appender Layout
p V J

Applicazione

-vC
Universo e s t e m o

Filler Target
Tutti i fi Iter
devono
abilitare il log

Figura 6.1 - Schema a blocchi di Log4j.


. ::k>g5j.spi::
LoggerRepository

...::Log4h:spi::
Appenderwttachable

::log4j::Logger

...::log4j;:hetpers
AppenderAttachablelmpl

...::log4j:: ...:iog4j::
AppenderSkeleton BasIcConf Ig urator
• closed boolean
#name Slnng

:log4J::
PropertyConfigurator

.. ::log4j::spi:: log4j::spi:
Configurator LoggerFactory

::log4j::
Layout ...::k>g4J::spi
* closed boolean Filter
0 name String
D E N Y ml
...::k>g4j::spi:: N E U T R A L inl
Option Handler A C C E P T int
• ^f fy I 7TT
«headl ;

Figura 6.2 - Parte della struttura interna di Log4j.

public static Logger getRootLogger();


public slatic Logger getLogger(String name);

// printing methods:
public void trace(0bject message);
public void debug(0bjecl message);
public void info(0bject message);
public void warn(0bject message);
public void error(Object message);
public void fatal(0bject message);

// generic printing method:


public void log(Level I, Object message);
)

La classe Logger (grazie all'eredità da Category) dispone di una composizione con un'istanza
Level (anche se sarebbe stato più corretto associarvi la classe Priority, questa è stata introdotta in
un secondo momento), che, come lecito attendersi, permette di definire il livello di severità
della specifica istanza del Logger. I diversi livelli di severità o di log sono rappresentati da istanze
statiche della classe Level, la cui semantica è descritta nella tabella 6.1. Oltre ai livelli di logging
illustrati nella tabella 6.1, Log4J mette a disposizione altri due livelli particolari:

• ALL: trattandosi del livello di logging più basso in assoluto, è utilizzato per abilitare tutti
i restanti livelli di log;
• OFF: si tratta del livello di log più alto e quindi utilizzato per inibire completamente il
logging.

La seguente relazione mostra la relazione di maggioranza che lega i vari livelli di severità:

ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF

I livelli di log predefiniti dovrebbero essere più che sufficienti per gestire le tipiche necessità
di logging di un'applicazione. Tuttavia Log4J permette di definire ulteriori livelli di logging
personalizzati. Questa pratica è sconsigliata a meno che non si abbiano delle esigenze di inte-
grazione particolari non prettamente legate al meccanismo di log.
In Log4j, tutti le istanze Logger hanno un livello di log. Questo può essere assegnato esplici-
tamente oppure ereditato da un logger antenato. La regola afferma che un log a cui non sia
stato assegnato esplicitamente un livello di log, eredita quello assegnato al più prossimo antena-

Categoria Descrizione
Si tratta del livello di logging più severo ed è utilizzalo per segnalare messaggi

FATAL estremamente importanti, errori fatali appunto, che impediscono all'applicazione di

funzionare.

Si tratta di un livello molto alto, utilizzato per segnalare condizioni di errore. La

differenza rispetto a FATAL è che problemi appartenenti a questa categoria tendono ad


ERROR
influenzare un numero limitato di transazioni e pertanto non precludono

a l l ' a p p l i c a z i o n e di p o t e r c o n t i n u a r e a f u n z i o n a r e .

Questo livello è riservato p e r m e s s a g g i di a v v e r t i m e n t o circa condizioni che pur non


WARN
c r e a n d o problemi immediati risultano potenzialmente dannose.

M e s s a g g i di i n f o r m a z i o n e utilizzati p e r r i p o r t a r e i n f o r m a z i o n i relativi alla progressione


INFO
d e l l ' a p p l i c a z i o n e a d u n l i v e l l o di a s t r a z i o n e a b b a s t a n z a elevalo.

DEBUG M e s s a g g i relativi alla c o n f i g u r a z i o n e e s e t u p dell'applicazione.

S i t r a t t a d e l l i v e l l o d i l o g a s e v e r i t à p i ù b a s s a e d è u t i l i z z a t o p e r il d e b u g d i e v e n t i molto
TRACE
d e t t a g l i a t i . T i p i c a m e n t e l o si u t i l i z z a p e r e s e g u i r e il l o g d i f l u s s i d i e s e c u z i o n e .

Tabella 6.1 - Livelli di logging di Log4j definiti nella classe org.apache.log4j. Level.
to. Questo, nella maggioranza dei casi è la root, l'antenato di tutti i logger, che, secondo le
regole, deve necessariamente avere un livello assegnato. L'assegnamento del livello di log può
avvenire o programmaticamente (LOGGER.setLevel(Level.INFO), strategia sconsigliata), oppure,
molto più frequentemente, attraverso opportuni file di configurazione.
Pertanto, qualora si esegua una richiesta di log, (invocazione di un metodo di log), questa è
abilitata, e quindi produce un record (a meno di filtri), se e solo se, il relativo livello è superiore
o uguale a quello del logger di appartenenza. Per esempio, l'invocazione LOGGER.debugftrade
new value ="+trade.getValueAmount()) viene abilitata se, e solo se, il livello del logger è impostato a
DEBUG o a uno dei livelli inferiori. Più formalmente: una richiesta di log di livello n eseguita in
un logger di livello m è abilitata se è solo se n >- m
La figura 6.3 mostra un esempio di configurazione di Log4J, in cui due classi utilizzano il
relativo logger ereditando le impostazioni del nodo di root. Poiché i nodi a livelli intermedi (come
com), non sono presenti o non utilizzano il relativo logger, questi non vengono inseriti nella
generarchi di log4j. Così si incrementano le performance e si riduce l'occupazione di memoria.
La tabella 6.2 mostra la mappa della verità del livello di log delle invocazioni e quello dei
logger. Alcuni appender disponibili in Log4j sono:

• ConsoleAppender: invia i messaggi di log alle console di output standard: System.out e


System.err, dove la prima è quella di default. Questo appender dovrebbe essere utilizzato
esclusivamente per fini di semplici test.
• FileAppender: questo appender scrive i record di log in un apposito file.
• DailyRollingFileAppender: si tratta di un'estensione all'appender precedente, in cui il com-
portamento aggiuntivo consiste nel gestire una serie di file di log al fine di limitare la
dimensione di ciascun file. La condizione per dar vita ad un nuovo file è basata su op-
portune impostazioni temporali definite dall'utente.
• RollingFileAppender: questo appender è molto simile al precedente con l'unica variante che
il nuovo file di log è generato qualora quello esistente raggiunga una data dimensione.
• WriteAppender: si occupa di aggiungere i record di log a un dato oggetto Writer or
OutputStream a seconda delle impostazioni utente.
• SMTPAppender: questo appender si occupa di comunicare record di log attraverso l'invio di
e-mail. Si tratta di un buon espediente per comunicare errori molto seri come quelli fatali.

root : C a t e g o r y D E B U G G E R : Priority

FIlaAppandar Appender
ApponderAttacheablelmpI '

cpm.mphabytaHnanc«Teat : cpmmokabvtaflnancaMaln
Category : Category

Figura 6.3 - Diagramma a oggetti di una possibile configurazione di Log4],


DEBUG INFO WARN ERROR FATAL

DEBUG Y Y Y Y Y

INFO N Y Y Y Y

WARN N N Y Y Y

ERROR N N N Y Y

FATAL N N N N Y

ALL Y Y Y Y Y

OFF N N N N N

Tabella 6.2 - Tabella della verità tra livello di log delle richieste e quello del Logger.

• SocketAppender: con questo appender i messaggi di log sono inviati ad un log server re-
moto, tipicamente un SocketNode.
• SocketHubAppender: con questo appender i messaggi sono inviati in forma di oggetti
LoggingEvent ad un insieme di log sever remoti, tipicamente SocketNodes.
• SysIogAppenders: questo appender invia i log ad un servizio di syslog remoto.
• TelnetAppender: in questo caso i messaggi vengono inviati a un socket di sola lettura.
• JMSAppender: come lecito attendersi, in questo caso il record di log viene inviato su un
topic di un sistema di messaggistica JMS.
• JDBCAppender: si tratta di una semplice implementazione di un appender atto a registrare
i record di log in un database.

Un discorso a parte merita l'appender asincrono (AsyncAppender). Questo è stato disegnato


per superare il tipico accoppiamento temporale presentato dagli appender tradizionali. Questo
prevede che l'istruzione di log presente nel codice debba essere eseguita completamente prima
di permettere al codice di passare all'istruzione successiva. Questo comportamento bloccante
può divenire problematico in tutte quelle situazioni in cui sia necessario fornire determinati servi-
zi nell'ordine del secondo. Requisiti simili sono molti frequenti negli ambienti di front office
bancari, in cui, per via dell'agguerrita concorrenza, è necessario fornire determinati servizi con
latenze bassissime onde non perdere la possibilità di aggiudicarsi importanti transazioni finanzia-
rie. Si consideri per esempio il caso delle richieste di quotazione (Request For Quote): le statisti-
che mostrano come nel 9 0 % dei casi i clienti selezionino una delle prime tre quotazioni ricevute.
In questi casi, invece di rinunciare completamente al meccanismo di log, è consigliabile ri-
correre a questo appender in grado di creare uno strato di indirezione, grazie ad apposite code
interne, tra il codice chiamante, e gli altri appender che si occupano di eseguire l'effettiva tra-
scrizione del log. Da quanto riportato risulta evidente che questi software di logging sono stru-
menti molto potenti e, volendo, si prestano ad essere utilizzati anche per scopi diversi fino a
diventare veri tool di integrazione, ma questo esula dai fini di questo capitolo.
Ogni Appender ha una serie di importanti attributi e relazioni (diagramma delle classi di
figura 6.2), tra cui i più importanti sono:

• layout: si tratta di una relazione con la classe Layout, che è necessaria per formattare,
tipicamente in modo comprensibile a persone, le informazioni fornite.
• target: ogni appender ha un target associato: può essere la console, un file, un socket, e così
via. Il target è intrinseco nel log: è dato dall'implementazione dell'intero appender.
• level: il livello è rappresentato dall'associazione con la classe Priority (da non utilizzarsi
direttamente, di cui Level è l'estensione accessibile), denominata threshold. Questo svolge
la stessa funzionalità del livello del logger, ma ne è indipendente e quindi rappresenta un
ulteriore livello di abilitazione/inibizione.
• fi Iter: ogni appender può essere dotato di una lista concatenata di filtri. L'appender con-
serva il riferimento alla testa della lista (associazione headFilter) e alla coda (associazione
tailFiiter). Ogni elemento filtro (implementazione della classe astratta ...log4j.spi.Filter)
mantiene il riferimento al successivo (auto-relazione next) e include il metodo astratto
decide(LoggingEvent event) c h e r e s t i t u i s c e u n a d e l l e c o s t a n t i : ACCEPT, D E N Y , N E U T R A L .

Come nel caso degli appender, in Log4j anche la classe astratta Layout prevede una serie di
specializzazioni, in particolare:

• SimpleLayout: si tratta di un layout molto semplice che si limita ad includere il livello di


severità del log ed il relativo messaggio.
• PatternLayout: questo layout si occupa di produrre in output un messaggio di log formattato
secondo il pattern fornito nel costruttore (qualora questo non venga definito, utilizza il
pattern di default). I pattern possono essere definiti secondo una ben definita gramma-
tica. Per esempio: %r [%t] %-5p %c %x - %m%n.
• DateLayout: si tratta di un layout astratto utilizzato per gestire le date.
• TTCCLayout: questo layout rappresenta una specializzazaione del precedente, e deve al
nome alla specifica tipologia di formattazione che include il tempo, il thead, la categoria,
e il contesto di informazione di diagnosi annidato (Time, Thread, Category and nested
diagnostic Context information).
• HTMLLayout: questo layout si occupa di formattare i messaggi di log in opportune tabelle
definite attraverso i tag HTML.
• XMLLayout: in questo caso la formattazione segue le regole della grammatica XML, o
quasi: l'XML prodotto non è ben formattato. L'idea è quella di affidare ad un altro
processore di testo il compito di sistemare l'XML prodotto. L'output è costituito da una
serie di log4j:event elements così come definito nel log4j.dtd.

Il listato seguente mostra una configurazione di Log4J utilizzato per un'applicazione Tomcat.
Questo file deve essere inserito nella cartella /webapps/ping-server/WEB-INF/<d/recfory applicazione>.

log4j.debug=TRUE
log4j.rootLogger=INFO, R
log4j.appender.R=org.apache.log4j.RollingFileAppender
log4j.appender.R.File=/home/l/h/ihrname.ch/tomcatlogs/tomcat.log
log4j.appender.R.MaxFileSlze=100KB
log4j.appender.R.MaxBackuplndex=5
log4j.appender.R.layout=org.apache.log4j. PatternLayout
log4j.appender.R.layout.ConversionPattern=%d|yyyy-MM-dd HH:mm:ss.SSSS) %p %t %c - % m % n

Come si può notare la configurazione imposta:


1. l'abilitazione del debug;
2. il livello di log dell'elemento root a INFO;
3. l'appender della root (e quindi di tutti gli altri) a RollingFileAppender che si occupa di
scrivere i messaggi su un insieme di file di log. In particolare, si occupa di crearne un
altro non appena quello corrente abbia raggiunto una dimensione prefissata (in questo
caso 100KB). Inoltre il numero di file di log utilizzati sono 5: quindi l'appender parte dal
primo, poi dopo aver raggiunto una dimensione di circa 100 kilobyte si muove sul se-
condo, e così via, fino al completamento del quinto, per poi ricominciare dal primo;
4. la formattazione avviene attraverso il PatternLayout ed il pattern selezionato è: %d(yyyy-
M M - d d HH:mm:ss.SSSS) % p %t %c - % m % n .

La configurazione seguente imposta Log4J a inviare e-mail in caso di messaggi di errore:

log4j.appender.mail=org.apache.log4j.net.SMTPAppender
log4j.appender.mail.To=@ERROR-MAILTO@
log4j.appender.mail.From=@ERROR-MAILFROM@
log4j.appender.mail.SMTPHost=@ERROR-MAILHOST@
log4j.appender.mail.Threshold=ERROR
log4j.appender.mail.ButferSize=1
log4j.appender.mail.Subject=CCT Application Error

log4j.appender.Mail.layout
=org.apache.log4j.PatternLayout log4j.appender.Mail.layout.ConversionPattern=%d % - 5 p % c % x - % m % n

E possibile specificare la configurazione per Log4J anche attraverso il formato XML. Proba-
bilmente si tratta dell'alternativa più elegante, sebbene, probabilmente per motivi storici, il
formato classico (property file) tenda ancora a essere molto utilizzato. Tuttavia, la tendenza è
sempre più quella di utilizzare il formato XML tanto che gli appender più recenti, come per
esempio AsyncAppender, prevedono solo questa modalità di configurazione.

< ? x m l version="1.0" encoding= UTF-8" ?>


< ! D 0 C T Y P E log4j:configuration S Y S T E M log4j.dtd ">
<log4j:contiguration>
< ! — — >

<!— SMTPAppender —>


<!— —>
«appender name= " E M A I L APP' class="org.apache.log4j.net.SMTPAppender">
<param name="BufferSize" value= "512 / >
<param name="SMTPHost" value="smtp.foobar.com" />
<param n a m e = " F r o m " v a l u e = " a p p l J n z @ m b s e r v e r 0 1 . i t " / >
<param name="To" v a l u e = " a p p l _ n n z _ s u p p @ m b . i t " / >
<param n a m e = " S u b j e c t " v a l u e = " [ S M T P A p p e n d e r ] S u p p o r t m e s s a g e " />
<layout class="org.apache.log4j.PatternLayout">
<param name= " M y C o n v e r s i o n P a t t e r n "
value="[%dllS08601IJ%n%n%-5p%n%n%c%n%n%m%n%n" />
</layout>
<filter class="org.apache.log4j.varia.LevelRangeFilter">
< p a r a m n a m e = ' LevelMin ' v a l u e = " E R R O R " / >
< p a r a m n a m e = " L e v e l M a x " value="FATAL"/>
</filter>
</appender>

<!— —>
< ! — setup l o g o ' s root logger — >
<!— —>
<root>
clevel value="all" / >
<appender-ref ref= "EMAIL A P P " / >
</root>
</log4j:configuration>

java.util Jogging
java.util.logging è una API introdotta in Java relativamente tardi con la versione 1.4 (JSR 47, http://
jcp.org/en/jsr/detail?id=47) rilasciata nel maggio del 2002. Si tratta di una API chepresenta molte
similitudini con Log4J. L'architettura concettuale di Java Logging, come mostrato in figura 6.4, è
del tutto equivalente a quella di Log4j. Considerate le similitudini con Log4J, non è necessario
illustrare in dettaglio questa libreria: sono presenti gli stessi concetti con nome diverso.
Anche in questo caso, ogni classe che intenda registrare appositi log deve:

1. importare la libreria:
i m p o r t java.util.logging.Logger

2. d i c h i a r a r e e inizializzare la " c o s t a n t e " d i logging:


private static Logger LOGGER
= L o g g e r . g e t L o g g e r ( < c l a s s N a m e > . class. g e t N a m e Q ) ;

Figura 6.4 - Architettura concettuale di Java Logging.


Categoria Descrizione

Si tratta del livello più alto, utilizzato per segnalare messaggi estremamente importanti,
SEVERE
c o m e e r r o r i f a t a l i c h e i m p e d i s c o n o al s i s t e m a d i f u n z i o n a r e .

WARMING C l a s s i c i m e s s a g g i di a v v e r t i m e n t o .

INFO U t i l i z z a t o p e r m e s s a g g i di informazione

CONFIG M e s s a g g i relativi alla c o n f i g u r a z i o n e e s e t u p dell'applicazione

FINE Utilizzata p e r s e g n a l a r e m e s s a g g i di d e t t a g l i o , c o m e debugging.

FINEST Si t r a t t a d e l l i v e l l o p i ù b a s s o , u t i l i z z a l o p e r s e g n a l a z i o n i di d e t t a g l i o e l e v a t o

Tabella 6.3 - Categorie di log utilizzate dalla libreria java.util.logging.

3. utilizzare il logging:
LOGGER.finestfHello logging!");

Dall'analisi del frammento di codice riportato al punto 3 è possibile notare immediatamente


che le categorie (i livelli) utilizzate dalle due librerie sono diverse. In particolare, il logging
nativo di Java utilizza le categorie riportate nella tabella 6.3.
Analogamente a Log4j, anche java logging dispone dei due ulteriori livelli di severità Level.ALL
e Level.OFF utilizzati, rispettivamente, per abilitare e disabilitare tutti i log.
Come nel caso di Log4J, il Logger è una sorta di proxy che delega la scrittura del log ad una
particolare specializzazione di una classe di Handler. Le istanze di questa classe stabiliscono se
riprodurre o meno il log sul dispositivo di output in base al livello di log impostato. Per essere
precisi, anche i Logger hanno un livello di severità impostato. Questo tipicamente è posto al
livello di INFO. Pertanto tutti i messaggi di quel livello, o di livello superiore, sono notificati
all'handler. Da tener presente che sia i logger, sia gli handler, possono essere dotati di opportuni
filtri (classi che implementano l'interfaccia java.util.logging.f¡Iter dotata dell'unico metodo
isLoggable(LogRecord record) ). Questi forniscono l'opportunità di eseguire controlli a basso livel-
lo al di là delle restrizioni sul livello del log.
Nella java logging gli Appender sono stati sostituiti dagli Hander. In questo caso le opzioni
disponibili (come riportato in figura 6.5) sono:

• MemoryHandler: si tratta di un buffer circolare e pertanto memorizza i messaggi di log


(istanze della classe LogRecords) in un buffer di memoria di dimensione finita. Pertanto,
qualora questo risulti saturo, i log più vecchi sono rimossi per far posto ai nuovi. In
funzione a specifici eventi (livello del log, chiamata esplicita etc.), i vari log vengono
forniti all'handler di destinazione.
• ConsoleHandler: si occupa di pubblicare i messaggi di log nel System.err, formattati, per
default, in base alle direttive del SimpleFormatter.
• FileHandler: scrive i messaggi di log o su un file specifico o, a rotazione, su un insieme di
file. In quest'ultimo caso, non appena un file raggiunge una determinata dimensione, il
file viene chiuso, rinominato per causare la rotazione. La strategia per assegnare il nome
dei file di log usati è definita da un pattern fornito dall'utente. Per default, al nome è
aggiunto un numero progressivo. Il formatter di default di questo handler è XMLFormatter.
• SocketHandler: in questo caso i messaggi di log sono inviati ad un network stream remoto.
Anche in questo caso il formatter di default è XMLFormatter.

Apache Java Logging (JCL)


JCL (http://commons.apache.org/logging/guide.html) rappresenta una valida alternativa ai precedenti
due tool di logging. Per essere precisi non si tratta di una vera e propria alternativa giacché JCL
non è uno strumento di logging, bensì un sottile livello di astrazione dai tool di logging, un
bridge (o proxy) che consegna i messaggi ai tool specifici (figura 6.6). JCL è dotato di un
semplicissimo meccanismo di logging, il cui utilizzo è assolutamente sconsigliabile.
Grazie a JCL gli sviluppatori possono implementare codice utilizzando le API di questo
sottile strato di indirezione, demandando poi al tempo di configurazione la selezione dello
specifico strumento di logging desiderato.
JCL è in grado di funzionare con Log4J, il logging standard Java e Avalon LogKit (http://
avaion.apache.org/closed.html); tuttavia la relativa API somiglia molto a quella di Log4J. In effetti,
JCL è basato essenzialmente su due concetti: Log (il classico logger) e LogFactory (il meccanismo
che permette di generare/acquisire istanze di della classe di Log). Il meccanismo utilizzato per
individuare (discovery) quale log utilizzare è basato sui seguenti passi:

1. Cerca di individuare il valore della seguente proprietà: org.apache.commons.logging.Log. Nomi


minuscoli (log) sono accettati per garantire la retrocompatibilità (convenzione delle ver-
sioni precedenti alla 1.0). Gli attributi di configurazione possono essere specificati o diret-
tamente nel codice (pratica decisamente sconsigliabile) o, come avviene più comunemen-
te, in un apposito file: commons-logging.properties. Questo deve essere presente nel classpath.
Qualora questo file sia presente, ogni sua entry è acquisita come un parametro del log
factory. Qualora siano presenti diversi di questi file di configurazione il comportamento

java::utili::k>gging
Handler

java::ulìli::logging
SocketHandler

Figura 6.5 - Handler della java logging.


Apache JCL j

Log4J Avalon LogKit Java Logging

k J ^ > /

Figura 6.6 - Vista concettuale di Apache JCL.

varia a seconda della versione. Versioni precedenti alla 1.1 utilizzano il primo ritrovato,
mentre le versioni successive selezionano il file a più alta priorità. Questa è definita da un
apposita parola chiave (priority). Qualora poi diversi file dovessero presentare la stessa
priorità, allora nuovamente verrebbe selezionato 0 primo file incontrato a più alta priorità.
2. Qualora il passo precedente dovesse fallire, tenta di reperire la proprietà di sistema
denominata: org.apache.commons.logging.Log. Come nel caso precedente, la proprietà scritta
in lettere minuscole viene accettata ugualmente.
3. Ulteriore tentativo consiste nel cercare di reperire Log4J dal classpath; in tal caso J C L si
limita a utilizzare la corrispondente classe wrapper: Log4JLogger.
4. Ultimo tentativo: si prova a utilizzare il logging standard Java. Questo è ovviamente
possibile solo per J D K 1.4 e superiori. La classe di wrap utilizzata è Jdk14Logger.
5. Fallito anche il precedente tentativo, J C L utilizza un semplice logging wrapper chiama-
to appunto SimpleLog.

Come si può notare Log4j rappresenta lo strumento di log di default per JCL, questo sia per
motivi diciamo di appartenenza (la "squadra Apache" è la stessa), sia perché prediligere TAPI
Java, di fatto, non avrebbe dato alcuna possibilità a Log4J di essere selezionato: è sempre più
raro trovare applicazioni precedenti alJDK1.4. Una volta configurato JCL, (il che equivale a
configurare uno dei tool da utilizzare) è necessario:

1. importare le librerie JCL:


import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

2. ottenere il logger per la specifica classe:


public class MyClass I
private Log log = LogFactory.getLog(MyClass.class);

3. utilizzare i m e t o d i p e r il logging in b a s e alle specifiche esigenze:


log.fatal(Object message);
log.fatal(Object message, Throwable t);
log.error(0bject message);
log.error(Object message, Throwable t);
log.warn(Object message);
log.warn(Object message, Throwable t);
Iog.in1o(0bject message);
log.info(Object message, Throwable t);
log.debug(Object message);
log.debug(Object message, Throwable t);
log.trace(Object message);
log.trace(Object message, Throwable t);

4. q u a l o r a necessario, utilizzare le seguenti c o n d i z i o n i di g u a r d i a :


log.isFatalEnabledQ;
log.isErrorEnabled();
log.isWarnEnabledO;
log.islntoEnabled();
log.isDebugEnabled();
log.isTraceEnabled();

Come si può notare il tutto è molto simile a Log4J e inoltre la presenza del meccanismo di
discovery rende l'utilizzo di JCL assolutamente non intrusivo.

Quale tool di logging utilizzare?


La selezione del tool di logging da utilizzare è una delle decisioni ricorrenti in ogni progetto. La
notizia buona è che, caso raro, non rientra nell'insieme delle decisioni in grado di minare il
successo del progetto: tutte e tre le opzioni risultano decisamente valide. La notizia negativa
invece è che una volta stabilita una strategia, considerato l'elevato grado di intrusività intrinse-
co del logging, risulta abbastanza complicato cambiarla.
La scelta personale dell'autore tende a ricadere sull'Apache Java Logging. Questo perché si
tratta di un leggerissimo grado di indirezione verso i vari tool, quindi la selezione del tool
specifico è rinviata a tempo di configurazione e può essere variata più facilmente.
La 6.4 tabella mostra alcuni tra i più importanti vantaggi e svantaggi dei vari tool.

Direttive
6.1 Investire nel logging
Questa regola potrebbe sembrare inutile vista la sua universale accettazione... almeno dal pun-
to di vista teorico. Tuttavia non è infrequente imbattersi in sistemi completamente privi di
meccanismi di log, con log effettuati attraverso semplice stampa a video, o dotati di log incon-
sistenti e caotici, tanto da generare il mal di testa del personale addetto a supporto e manuten-
zione del sistema. Sistemi di questo tipo rendono problematico e lento il processo di analisi di
eventuali problemi che possono verificarsi e tipicamente occorrono a sistemi in produzione.
Lunghi tempi di analisi e soluzione di problemi finiscono inevitabilmente per ingenerare negli
utenti l'idea che il sistema non sia sufficientemente robusto e di buona qualità.

6.1.1 Produrre la documentazione contemporaneamente


all'attività di codifica
Come spesso accade in altri contesti, la strategia migliore per implementare efficaci meccani-
smi di logging consiste nello stabilire una politica di logging standard anche prima di dar luogo
all'implementazione del codice e quindi nell'applicarla rigorosamente durante l'attività codifi-
ca. Questa strategia permette di ottenere ottimi risultati evitando sprechi di tempo.
Troppo spesso accade di dover rendere consistente il logging o addirittura di doverlo inserire
a ridosso della messa in esercizio del sistema o a posteriori. In questi casi, per una serie di
fattori, non ultimo lo "stress da rilascio", si tende a produrre modesti risultati con un grande
investimento di tempo.

6.1.2 Non eseguire il logging attraverso il metodo System.out.println


Come illustrato nel corso del capitolo, sebbene una strategia di logging basata sull'utilizzo del
metodo System.out.println possa sembrare semplice e rapida da implementare, in realtà genera
tutta una serie di gravi problemi, tra i quali i più severi sono:

• non esiste un meccanismo automatico per abilitare/inibire i messaggi di log;


• non è immediato implementare un sistema atto ad organizzare i log per categorie;
• non è possibile eseguire un fine-tuning dei log;

Log4J J a v a logging

Maturità E s t a r o il p r i m o t o o l r e s o d i s p o n i b i l e e È s t a t o r i l a s c i a t o c o n la v e r s i o n e J D K 1 . 4 ,
quindi è q u e l l o c h e p r e s e n t e da più e q u i n d i p r e s e n t a un m i n o r e g r a d o di
tempo e quindi maggiormente m a t u r i t à s e b b e n e la relativa a r c h i t e t t u r a
consolidato. è stata c o m p l e t a m e n t e b a s a t a su q u e l l a di
Log4j.

Grado di D a t a la l u n g a p r e s e n z a sul m e r c a t o si S e b b e n e sia il l o g g i n g i n c l u s o n e l J D K ,


utilizzo tratta del t o o l m a g g i o r m e n t e u t i l i z z a t o n o n è a n c o r a r i u s c i t o a c o n t r a s t a r e il
dai s o f t - w a r e realizzati d a t e r z e p a r t i . g r a n d e s u c c e s s o di L o g 4 j . C o m u n q u e si
t r a t t a della s c e l t a o b b l i g a t o r i a di tutti i
prodotti Sun.

Stabilità M o l t o elevata. E l e v a t a , m a i n f e r i o r e a q u e l l a di L o g 4 j .

Ricchezza di Molto ricco R i c c o . H a t u t t e le f u n z i o n i n e c e s s a r i e , m a


feature n o n p r e s e n t a un i n s i e m e c o s ì r i c c o c o m e
q u e l l o di L o g 4 j .

Tempo medio P o i c h é si t r a t t a di u n t o o l o p e n s o u r c e L a c o r r e z i o n e d e i b u g e il r i l a s c i o d e v e
di correzione e m e n o f o r m a l e , i b u g t e n d o n o ad s e g u i r e i t e m p i e il p r o c e s s o f o r m a l e
dei bug e s s e r e risolti a b b a s t a n z a v e l o c e m e n t e . stabilito per le correzioni del J D K . U n
fix di q u e s t o p a c k a g e n o n p u ò e s s e r e
rilasciato da solo.

Dipendenza E n e c e s s a r i o i n c l u d e r e le l i b r e r i e di Nessuna
da J A R esterni Log4j.

Tabella 6.4- Pro e contro dei due principali tool di logging.


• inefficienza implementativa che incide pesantemente sulle performance del sistema;
• difficoltà di consultazione dei record di log. Messaggi inviati al dispositivo standard di
output sono poco utili sia per la fase di sviluppo, sia per la diagnosi in produzione;
• ridotta fattibilità,: per esempio gli application server non mostrano questi messaggi.

6.1.3 Utilizzare un logger per classe


Come visto nei paragrafi precedenti, i logger sono degli oggetti dotati di un nome univoco che
ne consente il reperimento. Pertanto, sebbene sia possibile utilizzare diverse convenzioni circa
il nome da assegnare, quella migliore resta utilizzare il percorso completo della classe:

private static Logger LOGGER = Logger.getLogger(<className>.class.getName());

Nella letteratura informatica è possibile incontrare altri approcci, come quello relativo alla
definizione di log tematici: performace (Logger LOG_PERFORMANCE = Logger.getLoggerfperformance")),
sicurezza (Logger LOG_SECURITY = Logger.getLogger("security")), etc. Sebbene questa strategia sia
basata su una valida intuizione, si consiglia di mantenere la strategia standard, demandando ad
altri tool l'analisi di aspetti specifici come le performance, la sicurezza, e così via.

6.2 Porre attenzione al contenuto del log


La misura dell'efficacia del meccanismo di log è direttamente proporzionale alla qualità del
contenuto dei messaggi. Pertanto, anche il più sistematico degli approcci potrebbe non genera-
re alcun vantaggio qualora i vari record producano dati, rumore e non informazione. Quindi, è
necessario porre attenzione al contenuto dei record dei log. In particolare è consigliabile ripor-
tare frasi ben strutturate, concise ma complete.

6.2.1 Ponderare l'utilizzo del metodo toString()


Uno dei metodi più immediati di eseguire il logging consiste nell'invocare il metodo toString()
sull'oggetto di cui si vuole mostrare il contenuto. Per esempio

LOGGER.infofReceived a new (rade message ("+newMessage.toString()+") ");

Sebbene, in linea di principio si tratti di una valida prassi, questa non è esente da effetti
collaterali. In particolare, bisogna porre attenzione ad alcuni elementi, quali:

• non sempre il metodo toStringQ è implementato correttamente e produce output leggibile.


• spesso eseguire il log di un grafo di oggetti finisce per incidere sulle performance. Per
esempio, nel caso predente, il messaggio ricevuto potrebbe essere la radice di un grafo
di oggetti molto complesso la cui riproduzione sul target del log potrebbe risultare one-
roso in un contesto di elevato throughtput. In questi casi, spesso è probabilmente più
conveniente limitare il contenuto del log ad alcune informazioni autoesplicative, come il
codice univoco del trade, la counterparty, la propria unità organizzativa, etc.

6.2.2 Riportare solo le informazioni necessarie, ma tutte


I file di log di applicazioni medio/complesse tendono rapidamente a diventare estremamente
corposi. Non a caso i tool di logging, come per esempio Log4J, prevedono opportuni meccani-
smi (appender) in grado di utilizzare una serie di file di log invece di uno solo. Ciò nonostante
è opportuno curare le informazioni riportate nel log, per una serie di motivi, come aumentare
l'efficienza del codice e agevolare le attività di reperimento delle informazioni desiderate. Per-
tanto, una buona strategia consiste nel riportare solo le informazioni strettamente necessarie,
ma tutte, evitando, quanto possibile inutili decorazioni.

6.2.3 Valutare l'eventualità di implementare utility da utilizzare


per la "formattazione" dei messaggi di log
Un'importante caratteristica che i log dovrebbero sempre implementare è la consistenza. Que-
sta proprietà offre una serie di importanti vantaggi, come per esempio:

• semplifica il lavoro del personale addetto alla manutenzione del sistema: per esempio è
possibile effettuare delle ricerche mirate nel file di log;
• agevola l'addestramento del personale addetto alla manutenzione del sistema;
• permette di realizzare sistemi esterni atti a monitorare i file di log al fine di identificare
specifiche condizioni, come per esempio tentativi di eseguire delle operazioni sensibili
per la sicurezza del sistema.

La consistenza dei record di log è particolarmente utile per tutti i livelli utilizzati dal sistema
in produzione, pertanto per tutti quelli a partire da info. Tuttavia i vari tool di log non offrono
alcun supporto nell'implementazione di log consistenti. Pertanto, una buona idea consiste
nell'implementare una propria classe helper per la formattazione dei messaggi.

6.2.4 Evitare facili ironie nel file di log


Sebbene questa regola possa sembrare fin troppo scontata, non è infrequente imbattersi in log
colmi di commenti ironici o presunti tali. Sebbene l'ironia sia, in generale, una caratteristica
molto positiva, utilizzarla per i log di un'applicazione rappresenta una scelta sbagliata: di solito
il personale destato nel cuore della notte per risolvere un improvviso problema nel sistema non
trova poi così utile o simpatico leggere messaggi del genere. Molto più apprezzati sono messag-
gi chiari, concisi, in grado di agevolare e semplificare il lavoro di individuazione dei problemi.
Lasciamo la sacrosanta ironia per i libri e gli articoli.

6.3 Utilizzare correttamente i livelli di log


Quasi tutti i tool per il logging offrono la possibilità di organizzare i propri log in funzione di
un insieme ben definito di log. Log4J, per esempio, rappresenta queste categorie attraverso la
classe Priority implementata dalla classe Level. I livelli di log offrono tutta una serie di vantaggi
che però vengono meno qualora non siano utilizzati correttamente.

6.3.1 Utilizzare correttamente il livello di severità


I messaggi di log devono essere comunicati utilizzando il corretto livello di severità. Ciò è
necessario per una serie di importanti motivi come:

• permettere un corretto fine-tuning del logging delle applicazioni. Logging molto detta-
gliati sono necessari durante la fase di sviluppo del software ma non sono auspicabili per
sistemi in produzione, per via dell'impatto sulle performance, della difficoltà nel reperire
le informazioni desiderate in file di larghe dimensioni, etc. Pertanto, è pratica comune
quella di limitare il log delle applicazione in produzione al livello info.
• consentire a sistemi esterni di monitorare il file di log al fine di scoprire rapidamente
possibili condizioni di errore

Le linee guida circa l'utilizzo dei livelli di severità sono mostrate nelle tabelle 6.1 e 6.3.

6.3.2 Porre attenzione all'utilizzo dei livelli di debug e info


Questa regola è una sorta di corollario alla precedente, che tuttavia è il caso di riportare conside-
rando sia le discussioni, sia gli errori commessi nell'uso di questi livelli. Sebbene a prima vista i due
livelli possano sembrare molto simili, ci sono importanti differenze che bisogna tenere a mente. In
particolare, debug deve essere utilizzato per informazioni relative alla configurazione dell'applica-
zione e per informazione di dettaglio, mentre info deve essere utilizzato per riportare informazioni
relative all'applicazione, per segnalare eventi significativi, come quelli relativi al ciclo di vita di
importanti entità manipolate dall'applicazione, etc. In particolare, i messaggi di info relativi a uno
specifico servizio dovrebbero essere in grado di specificare in maniera chiara lo stato del servizio
stesso. Per esempio, la ricezione di un messaggio dovrebbe essere un logging info, mentre la comu-
nicazione di ogni dettaglio del messaggio dovrebbe essere riportato con il livello debug.
In caso di incertezza, una semplice regola pratica consiste nel porsi il quesito se sia o meno
necessario che il messaggio in questione sia riportato dal sistema in produzione. In caso di
risposta affermativa, allora il livello del log deve essere info, altrimenti si tratta di un debug.
Tutte le informazioni non desiderabili per sistemi in esercizio vanno riportate come debug.
Da tener presente che un uso scorretto dei due livelli potrebbe portare ai seguenti due scenari:

1. importanti informazioni non riportate (caso in cui messaggi di info siano stati comuni-
cati con il livello debug);
2. log eccessivi (caso contrario in cui log di livello debug siano riportati come info).

6.3.3 Valutare bene la necessità di definirsi dei propri livelli di log


Quasi tutti i tool per il log forniscono l'opportunità di definirsi un proprio insieme di livelli di
log o di estendere quello esistente. In Log4j, per esempio, ciò è possibile estendendo la classe
Level. Sebbene ciò sia possibile, si tratta di una pratica sconsigliata: l'insieme dei livelli disponi-
bili dovrebbe essere più che sufficiente per la stragrande maggioranza dei sistemi.

6.4 Valutare l'impatto sulle prestazioni


Come spesso accade durante la produzione del software, è necessario prendere delle decisioni
oculate tra esigenze contrastanti. Ciò si applica anche al caso dei logging. Infatti, se da un lato
potrebbe essere utile eseguire un logging dettagliato dell'applicazione, dall'altro ciò risultereb-
be eccessivamente penalizzante dal punto di vista delle performance. Pertanto, durante
l'implementazione del meccanismo di logging è necessario tenere sempre a mente l'impatto
sulle performance.

6.4.1 Evitare un log eccessivo


Un prima condizione utile alle performance del sistema consiste nello scegliere il corretto livel-
lo di dettaglio del logging. Si tratta di una decisione difficile giacché non esiste una regola
semplice e spesso è necessario prendere delle decisioni basate sul contesto. A volte è possibile
incontrare dei veri "adepti del logging": per evitare ogni problema alla radice, si esegue il log di
ogni passo o addirittura ad ogni ingresso e uscita da ciascun metodo. Questa strategia, sebbene
molto semplice, non è una tecnica vincente. Ciò finisce inevitabilmente per creare una serie di
problemi: il codice diviene meno chiaro per via delle troppe istruzioni dedicate al logging, i
troppi dati prodotti finiscono per generare rumore e non informazione e, fatto importante, si
penalizzano fortemente le performance.

6.4.2 Ricordarsi di inserire opportune guardie del codice


Con il termine di "guardie del codice" (code guards in inglese) si fa riferimento a opportune
porzioni del codice utilizzate per abilitare/inibire l'esecuzione di specifiche istruzioni di codi-
ce. In questo contesto si tratta di meri costrutti if utilizzati per abilitare/inibire le invocazioni
del logger. Per esempio:

i( ( LOGGER.isDebugEnabledf) ) {
logger.debug( "New dictionary: " + dictionary );
I

Dall'analisi del codice ci si potrebbe interrogare circa la necessità di includere questi blocchi
di if... In fondo, qualora un logger sia impostato ad un livello superiore di quello dell'invocazio-
ne (per esempio info), quest'ultima comunque non verrebbe riportata nel target del logger.
Sebbene ciò sia vero, c'è da notare che comunque, prima di eseguire l'invocazione, i parametri
devono essere risolti. Pertanto, qualora tra i parametri sia presente un'istruzione dispendiosa in
termini di cicli macchina, questa comunque verrebbe eseguita per poi accorgersi all'interno
della librerira che non era necessario eseguirla. Inoltre, elemento ancora più importante, ogni
logger per comprendere se una data invocazione debba essere abilitata o meno, deve navigare
nella propria gerarchia di log. Questo scorrimento di oggetti, tipicamente, richiede di risalire
fino al nodo di root. Quindi, anche qualora la gerarchia sia ridotta al minimo, comunque può
avere un impatto sulle performance. Da quanto riportato, dovrebbe risultare chiaro che ha
senso inserire apposite guardie solo per i livelli più bassi: debug e trace.

6.4.3 Evitare servizi non richiesti e dispendiosi


Dovendo poter essere utilizzati nelle circostanze più diverse, i vari tool di log finiscono per
offrire tutta una serie di servizi: molti di questi non sono necessari nella maggior parte dei casi,
e comunque gravano sulle performance. Un esempio tipico consiste nel meccanismo utilizzato
da Log4J di comunicare i record di log allo specifico handler e a tutti i suoi antenati. Questa
funzionalità, nella maggior parte dei casi, non è necessaria, ma comunque utilizza utili risorse.
Questo meccanismo può essere eliminato attraverso il seguente setting:

LOGGER.setUseParentHandlers(false);

6.4.4 Utilizzare appender asincroni


Molto spesso di fronte ad applicazioni vincolate da requisiti prestazionali molto stringenti in
cui anche frazioni di secondo siano in grado di fare la differenza, molti tecnici decidono di
rinunciare al logging. Prima di optare per una soluzione così drastica è consigliabile valutare la
possibilità di ricorrere a appender asincroni.
Quasi tutti i tool forniscono meccanismi di questo tipo in grado di disaccoppiare
temporalmente la richiesta del log (che quindi non risulta più bloccante) dall'effettivo
espletamento. Per esempio, in Log4j AsynchAppender.

6.5 Implementare un corretto logging delle eccezioni


Una corretta gestione delle eccezioni, come visto nel Capitolo 5, rappresenta un meccanismo
fondamentale per ciascun sistema. Un elemento importante di questo processo è costituito dal
logging delle eccezioni. Spesso 0 sistema non è in grado di gestire l'eccezione e quindi non resta
altro da fare che assicurarsi di riportarla correttamente. Si tratta di un'attività importantissima
sia perché i log spesso forniscono al personale addetto alla manutenzione le uniche informazio-
ni disponibili su eventuali problemi e sia perché i file di log sono tipicamente monitorati da
sistemi esterni atti a individuare condizioni di errore che richiedono un intervento immediato.

6.5.1 Evitare di invocare lo stacktrace


Ogniqualvolta sia necessario eseguire il log delle eccezioni, è necessario ricordare che i tool per
il logging sono dotati di opportuni metodi. Pertanto, è sufficiente utilizzare tali metodi (per
esempio LOGGER.error(" exception", ioException) ) invece di cercare di implementare comporta-
menti già definiti, come per esempio riportare lo stacktrace in una stringa (myException
.printStackTrace(myPrintStram)).

6.5.2 Riportare correttamente le eccezioni


Il logging delle eccezioni è una parte molto importante del meccanismo di gestione delle ecce-
zioni e pertanto è opportuno assicurarsi di riportare tutte le informazioni necessarie. Quindi, a
meno di casi specifichi in cui le eccezioni siano state attentamente codificate o il contesto sia
chiarissimo, bisogna eseguire il log dell'intera eccezione, evitando scorciatoie come quella di
riportare il solo messaggio (LOGGER.errorfEccezione :"+ioExc.getMessage()).

6.5.3 Non eseguire il log ad alta severità delle eccezioni gestite


Le eccezioni sono utilizzate per segnalare condizioni anomale verificatesi in parti del codice in
cui non si abbiano sufficienti informazioni per attuare un comportamento specifico atto a risol-
verle. Pertanto attraverso il meccanismo delle eccezioni si rimanda indietro, a porzioni del
codice chiamante, la segnalazione dell'anomalia nella speranza che si giunga a un livello dotato
di maggiori informazioni per intraprendere opportuni passi di correzione. Spesso, ahimè, non
resta altro da fare che segnalare l'eccezione, mentre, in altri casi, è possibile tentare di corregge-
re l'eccezione. Per esempio, in presenza di determinate eccezioni di sistema, è possibile ritentare
lo scenario sperando che l'anomalia abbia carattere temporaneo. Qualora un'eccezione sia
gestibile, è opportuno non eseguirne il log oppure riportare l'eccezione a livello di debug.
Segnalare come errore un'eccezione gestita genera l'unico effetto di procurare il mal di testa al
personale addetto alla manutenzione del sistema.

6.5.4 Riportare le eccezioni con il corretto livello di severità


Eccezioni non gestibili devono essere segnalate o come errore (LOGGER.error("Exception ", ioExcp))
nel caso in cui l'anomalia crei problemi unicamente alla transazione corrente, oppure come
eccezione fatale nel caso in cui il sistema sia impossibilitato a procedere.
6.5.5 Eseguire il log delle eccezioni una sola volta
L'implementazione di un qualsiasi servizio non banale richiede la collaborazione di un certo
numeri di metodi, spesso forniti da classi poste a diversi livelli dell'architettura. Poiché poi le cose
possono andare male (e lo fanno!), è necessario implementare una corretta gestione delle eccezio-
ni per rendere il sistema più robusto. Questo fa sì che un'eccezione generata a un livello basso
dell'architettura, frequentemente, venga rinviata indietro via via fino ai livelli più alti. A questo
punto bisogna porre attenzione a evitare che una stessa eccezione sia riportata più volte nel log.
Eccezioni riportate diverse volte rallentano le prestazioni del sistema, riducono l'efficacia
del personale coinvolto nell'analisi di possibili problemi, e rendono più difficile il compito di
eventuali tool atti a monitorare i file di log.

6.5.6 Cercare di utilizzare una strategia di log al più alto livello possibile
Un elemento importante della strategia di log consiste nel decidere a quale livello effettuare il
log delle eccezioni. In particolare, sono disponibili le seguenti tre alternative:

• dal basso: ciò consiste nell'eseguire il log delle eccezioni nel punto in cui si manifestano;
• nel mezzo: ciò equivale a riportare le eccezioni in un livello intermedio, possibilmente
non appena maggiori informazioni siano disponibili;
• in alto: l'eccezione viene riportata nel livello più alto della catena di invocazioni.

Sebbene la prima opzione possa sembrare la migliore (strategia semplice ed efficace per cui
tutti i problemi vengono riportati nel log), essa crea una serie di importanti effetti collaterali:

• richiede di scrivere molto codice di log;


• fa sì che tutte le eccezioni siano riportate nel log, anche quelle gestibili;
• tende a creare qualche problema di incoerenza qualora il codice sia riutilizzato in diversi
sistemi;
• può ridurre l'efficacia del logging giacché raramente il componente alla fine dello stack
ha sufficienti informazioni per riportare un log coerente e completo;
• tende a generare l'odioso problema di log multipli per lo stesso problema.

Un primo tentativo di mitigare alcuni degli effetti collaterali della precedente strategia consi-
ste nell'eseguire il log in un livello intermedio. Questa strategia tuttavia continua a presentare
molti dei precedenti problemi, per quanto spesso in forma mitigata. Inoltre ne pone di nuovi.
Per esempio, tende a creare incertezze ed ambiguità relativi al giusto punto in cui effettuare il
log, richiede di comunicare informazioni aggiuntive, etc. Ovviamente, le informazioni contenu-
te nelle eccezioni possono sempre essere arricchite, alleviando parzialmente questo problema.
La strategia di riportare le eccezioni al livello più alto, da un punto di vista concettuale, è
sicuramente la migliore:

• vi è un solo punto logico di controllo;


• è più semplice da implementare;
• si evita agevolmente il problema dei record multipli;
• non si creano problemi nel riutilizzo dei componenti per via dei una diversa strategia di
logging, e così via.
A questi vantaggi, tuttavia vanno controbilanciati alcuni svantaggi: per esempio, non sempre
è possibile evitare che alcuni problemi siano riportarsi dal basso (questo è il caso in cui si
utilizzino librerie offerte da terze parti).

6.5.7 Valutare la possibilità di eseguire un log fortemente strutturato


La realizzazione di sistemi ad alta disponibilità/affidabilità richiede di includere diversi accor-
gimenti che altrimenti non sarebbero necessari in altre situazioni. Per esempio, è necessario
progettare meccanismi in grado di rilevare tempestivamente eventuali componenti e sistemi
malfunzionanti o gravati da un eccessivo stress. A tal fine, una delle tecniche largamente utiliz-
zate consiste nel ricorrere a sistemi dedicati al monitoraggio e alla analisi dei file log prodotti
dalle varie applicazioni. Ciò al fine di individuare dei pattern predefiniti che identifichino situa-
zioni di malfunzionamento (per esempio una linea di testo iniziarne con la stringa "ERROR") o di
warning.
Al fine di rendere possibile l'impiego di questi sistemi di controllo è opportuno assegnare ai
log, specialmente a quelli relativi dall'insorgere di un'eccezione, opportuni formati.
Una struttura abbastanza ricorrente consiste in una lista di attributi separati da caratteri
bianchi, in cui le diverse informazioni prevedono:

• data e ora dell'eccezione secondo il classico formato: yyyy-mm-dd hh:mm:ss mmm; onde
evitare problemi di cambiamento di ora e di fusi orari si consiglia di utilizzare sempre lo
Universal Time (per esempio: 2006-07-07 15:13:10.99);
• il livello di log;
• identificatore univoco: si tratta di un'informazione utile per semplificare il lavoro di
possibili agenti automatici demandati all'esecuzione di opportune procedure di gestio-
ne degli errori; pertanto l'assegnazione di un identificatore è necessaria esclusivamente
per gli ultimi tre livelli di log: warning, error e fatai.
• percorso completo o semi-completo della classe in cui si è verificato il problema;
• il messaggio relativo al problema verificato;
• eventuale lista con gli argomenti del problema.

Ricapitolando:

<exception_date_time> <log_level> <unique_id> <class_path> <message> ([arguments]]

Per esempio:

2 0 0 6 - 1 2 - 0 3 12:51:07.103 W R N SDS-CN01 com.mokabyte


.tool.umlclassgenerator.service.DiagramParser -'diagram not found' diagram=class34

Da quanto riportato risulta evidente che i primi tre attributi sono necessari per una gestione
automatizzata delle procedure di gestione degli errori, mentre i restanti sono utili per un'ispe-
zione manuale.
Qualora si decidesse di ricorrere a questa alternativa, è fortemente consigliato utilizzare clas-
si di supporto, sia per rinforzare la struttura, sia per disporre di un "registro" in cui memoriz-
zare i vari codici.
itolo 7
Test di unità
Introduzione
Il presente capitolo è dedicato all'illustrazione dei test di unità (unti test); in particolare, analo-
gamente ai capitoli precedenti, sono presentate una serie di tecniche, framework di supporto,
linee guida e best practice utili per la produzione efficace di validi test di unità
Al momento in cui viene scritto questo libro, l'ingegneria del software annovera una serie di
processi di sviluppo del software che spaziano da quelli più centrati sul codice, come XP (eXtreme
Programming) a quelli via via più formali, per esempio RUP (Rational Unified Process) e MDA
(Model Driven Architecture). Nonostante le fondamentali differenze filosofiche e pratiche alla
base dei vari processi, è comunque possibile individuare alcune aree in cui vi è unanime con-
senso: una di queste è relativa all'importanza dei test. Verifiche formali, chiaramente, devono
essere eseguite durante tutte le fasi del processo di sviluppo del software, non appena nuovi
manufatti (per esempio il modello dei requisiti utente, il disegno del sistema, etc.) diventano
disponibili. Tuttavia, il fine ultimo dei processi di sviluppo del software è realizzare sistemi
software, pertanto è fondamentale che il codice prodotto sia verificato accuratamente. Ciò è
possibile corredando il sistema con approfonditi test automatizzati.
Come si vedrà meglio nel prossimo capitolo, esistono diversi livelli di test. In questo capitolo
l'attenzione è centrata su un primo stadio di verifica fornito dai test di unità. Con questo termi-
ne ci si riferisce alle procedure utilizzate per verificare che una particolare porzione di codice
(tipicamente una classe, un componente) presentino il comportamento aspettato: in una paro-
la, si verifica che l'unità funzioni correttamente.
I test di unità, e più in generale l'intero insieme di test (unità, integrazione, sistema, etc.), sono
fondamentali non solo per esaminare il funzionamento delle varie parti del sistema al momento
della loro scrittura, ma anche per disporre di uno strumento formale che eviti un problema
fondamentale: i processi di aggiornamento del codice non devono produrre malfunzionamenti
in parti del sistema correttamente funzionanti prima dell'incorporazione degli aggiornamenti.
Disponendo di una batteria di test automatizzati, è quindi possibile eseguirla dopo ogni insieme
di modifiche (questo processo è tipicamente denominato test di regressione, regression test) al
fine di individuare, istantaneamente, eventuali errori introdotti con la nuova versione. I test di
regressione tendono a essere un formidabile supporto per contrastare la paura dei cambiamenti
che spesso si annida nella mente degli sviluppatori. Pertanto, come logico attendersi, la presenza
di questi test riduce i fattori di rischio intrinseci a ogni processo di modifica e aumenta il livello
di sicurezza del team di sviluppo. Da tenere presente che la mancanza di approfonditi test auto-
matici fa si che il codice prodotto diventi legacy già prima del suo rilascio.

Obiettivi
L'obiettivo di questo capitolo è fornire una serie di direttive relative alla produzione di efficaci
test di unità. In particolare, la maggior parte delle direttive presentate fanno riferimento al
framework JUnit che, al momento in cui viene redatto questo libro, è lo standard de facto per la
produzione di test di unità in Java.
Produrre i test di unità è un'attività indispensabile del ciclo di vita del software. La loro
esistenza offre molti vantaggi, come l'immediata individuazione di eventuali errori, l'aumento
della qualità del codice prodotto, la semplificazione del processo di refactoring, la semplifica-
zione del processo di integrazione e la produzione di documentazione supplementare. Tutta-
via, test di bassa qualità o il cui livello di copertura sia eccessivamente limitato possono produr-
re disastrosi risultati — per esempio un falso livello di sicurezza circa la qualità del sistema — e
portare alla consegna di un sistema dalla qualità piuttosto limitata, etc.
Quindi, per evitare questi effetti paradossali, è necessario porre attenzione a una serie di
fattori, come il dominio di applicazione dei test di unità, la copertura dei test, l'impatto dei
processi di sviluppo iterativi e incrementali sul processo di manutenzione dei test, etc. Questi
argomenti, chiaramente, sono oggetto di studio del presente capitolo. Mentre concetti come
per esempio test di integrazione, processo di build e relativi tool (quali Ant e Maven), integra-
zione continua, per quanto strettamente interconnessi con i test di unità, sono trattati nei ri-
spettivi capitoli.

Un po' di teoria
Con il termine test di unità ci si riferisce alle procedure utilizzate per verificare che un partico-
lare frammento di codice presenti il comportamento aspettato.
Al fine di comprendere chiaramente l'idea alla base di questo tipo di test, si consideri la pro-
duzione di schede elettroniche. Ciascuna scheda contiene una grande quantità di componenti:
resistenze, condensatori, chip, etc. Ogni singolo componente, prima di essere assemblato in
circuiti complessi, viene sottoposto a un processo di verifica. Per esempio, si verifica che i resistori
presentino la resistenza dichiarata con un'approssimazione del 3 % o 5 % (a seconda della qua-
lità della resistenza), che i condensatori presentino la capacità attesa, sempre entro certi limiti, e
così via. Pertanto, ciascun componente è verificato singolarmente, indipendentemente dagli altri
e dalla scheda in cui verrà inserito. Queste verifiche, eseguite nell'ambito dell'elettronica, sono
l'equivalente dei test di unità eseguiti nel dominio della produzione del software. In questo
dominio, l'obiettivo dei test di unità è analogo: verificare che ogni singola classe/componente
presentino il comportamento atteso, indipendentemente dal contesto di utilizzo. Chiaramente,
come si vedrà di seguito, si tratta di test necessari ma non sufficienti: il fatto che un componente
elettronico funzioni come previsto non significa assolutamente che automaticamente la scheda
montata funzioni altrettanto correttamente. La completa esecuzione senza errori dei test di unità
rappresenta l'evento di avvio dei test di integrazione che, come si vedrà nel capitolo successivo,
servono a verificare che componenti funzionanti singolarmente, una volta correttamente assemblati
tra loro diano luogo a elementi più grandi ancora funzionanti correttamente.
L'oggetto di analisi dei test di unità sono le singole classe ed i singoli componenti. Ora qualo-
ra si utilizzino dei container, come gli application server o Spring, la definizione di component
è abbastanza immediata. Quando invece si fa riferimento a pure applicazioni Java stand-alone,
l'individuazione di un componente è meno immediata. In questo caso con le dovute precauzio-
ni ed i dovuti distinguo in linea di massima è possibile considerare i package come componenti.

I vantaggi dei test di unità


Produrre i test di unità è indubbiamente un lavoro dispendioso; tuttavia, si tratta di un'attività
fondamentale e come tale, va considerata come un vero e proprio investimento e non come una
dissipazione di risorse. Un'opportuna implementazione dei test di unità offre una indubbia
serie di vantaggi.

• Individuare rapidamente gli errori. In particolare, questi test permettono di individuare


eventuali malfunzionamenti, non appena redatto il codice (secondo alcuni estremisti
anche prima!). Inoltre, l'indagine è spinta a un livello molto approfondito, tipicamente
superiore a quello raggiunto con il classico test randomico. La logica conseguenza è un
significativo risparmio di tempo e quindi di denaro. Frequentemente, errori individuati
in fasi successive del processo di sviluppo del software, durante i test di accettazione o
addirittura a sistema in produzione, risultano difficili da investigare, da risolvere e, in
ultima analisi, decisamente più onerosi.
• Aumentare la qualità del codice. La verifica estensiva del codice permette di individuare
e risolvere un'elevata quantità di errori, spesso anche quelli più nascosti, e quindi di
garantire una maggiore qualità del sistema nel suo complesso. Ciò, inoltre, semplifica la
produzione del codice basato su quanto già prodotto, aumentando, contestualmente, il
grado di confidenza degli stessi programmatori.
• Semplificare il processo di refactoring. La presenza di estesi test di unità e di integrazio-
ne permettono di minimizzare gli inevitabili fattori di rischio connessi a estesi processi
di refactoring e, allo stesso tempo, aumenta il livello di sicurezza del team di sviluppo. In
effetti, eventuali problemi introdotti dai processi di aggiornamento (chiamanti regression
bug, errori regressivi) sono immediatamente individuati dalla batteria di test. La presen-
za di questi test, quindi, minimizza il rischio di degenerare la qualità del sistema a segui-
to di processi di modifica del codice e fornisce ai programmatori la tranquillità e la
fiducia necessaria per affrontare importanti aggiornamenti del codice. Non è infrequen-
te il caso in cui l'assenza di estesi testi di unità, finisca per far desistere o comunque per
scoraggiare i programmatori dall'eseguire importanti variazioni del codice.
• Semplificare il processo di integrazione . Sebbene la pertinenza dei test di unità, per
loro stesso principio, non riguardi l'integrazione di parti del sistema, il fatto che ogni
elemento sia stato verificato in dettaglio, prima di essere assemblato in elementi più
grandi, riduce il livello di incertezza e quindi semplifica e velocizza il processo stesso di
integrazione.
• Migliorare la documentazione . In alcuni casi, lo studio di determinate API o di parti di
codice è notevolmente semplificato dall'analisi di esempi concreti come quelli imple-
mentati nei test di unità. Pertanto, tali test possono essere considerati a tutti gli effetti
come parte integrante della documentazione del codice, oltretutto assolutamente in li-
nea con l'ultima versione del codice, cosa che spesso non succede con la tradizionale
documentazione: non è infrequente il caso in cui i programmatori tendano a dimentica-
re di mantenere la documentazione sincronizzata con il codice.

Processi iterativi e incrementali


Oltre all'importanza dei test, un'altra pratica che trova consenso tra la quasi totalità dei moder-
ni processi di sviluppo del software, indipendentemente dalla loro diversa natura, è la necessità
di integrare approcci iterativi e incrementali nel processo di sviluppo del software prescelto.
L'idea alla base di questi consiste nel produrre il sistema finale attraverso una serie opportuna-
mente pianificata di incrementi. L'intero processo di sviluppo del software si risolve in un
insieme di mini progetti, ognuno dei quali produce una nuova versione (incremento) del siste-
ma finale. Gli incrementi possono essere relativi sia allo sviluppo di nuovi servizi, sia alla varia-
zione tecnologica di alcune parti del sistema (processi di refactoring).
Questo approccio è molto conveniente per una serie di motivi: permette di controllare più
accuratamente l'evoluzione del sistema, di gestire attivamente i fattori di rischio concentrando
rischi maggiori nelle prime versioni del sistema, fa sì che, ad ogni iterazione, il personale possa
concentrarsi su un ben definito insieme di elementi, permette di ottenere il feedback continuo
ed anticipato degli utenti, e così via.
Tuttavia, a dispetto di questi importantissimi vantaggi esistono degli inevitabili effetti
collaterali. In particolare, è tipico degli approcci iterativi e incrementali che stesse porzioni di
codice siano variate con una certa frequenza durante l'evoluzione del processo. Logica conse-
guenza di ciò è che anche i relativi test di unità debbano essere soggetti a continui processi di
aggiornamento/riscrittura. Ciò comporta una serie di importanti conseguenze.

• I test di unità devono presentare un adeguato livello di qualità. Pertanto la tradizionale


equazione codice test = bassa qualità perde di validità. In primo luogo, test mal scritti ten-
dono a generare ripercussioni negative e quindi a ridurre i vantaggi propri relativi alla loro
presenza; poi, il processo di manutenzione risulta complicato e quindi, in ultima analisi,
può addirittura causare il rallentamento dell'intero processo di sviluppo del sistema.
• I test devono essere disegnati ed implementati pensando all'estensibilità, ovviamente
senza cadere nella facile tentazione àéRover-engineering. Ciò perché è tipico dei proces-
si iterativi ed incrementali generare la variazione di determinate parti di codice. Pertan-
to, gli stessi codici di test devono essere scritti accuratamente soprattutto per quanto
riguarda le parti che presentano un'elevata possibilità di dover essere variati.
• i test diventano parte del sistema e come tale sono soggetti a modifiche richieste dalle
varie iterazioni. Pertanto, sebbene sia necessario verificare la maggior parte possibile del
sistema, è altresì necessario evitare di scrivere test ridondanti che, come tali non aggiun-
gono valore, ma comunque devono essere mantenuti.
La copertura dei test di unità
Uno degli argomenti ricorrenti nella produzione dei test di unità, oggetto soprattutto in passa-
to di accesi dibattiti, riguarda il livello di copertura di tali test (rapporto tra le linee di codice
eseguite dai test e quelle totali). In teoria, questi test dovrebbero interessare tutte le classi
incluse nel sistema che si sta sviluppando (1 a 1), escludendo, ovviamente, le classi appartenen-
ti al JDK e le librerie esterne, il cui rilascio è soggetto al superamento di uno specifico e appro-
fondito processo di test e di Quality Assurance. Anche se questo processo non sempre è infal-
libile, interviene un problema di ordine pratico: tentare di verificare tutte le classi del J D K o
delle librerie fornite da terze parti causerebbe un dispendio di energie assolutamente impropo-
nibile. Pertanto, giocoforza, è necessario far affidamento sui test eseguiti dai fornitori delle
librerie utilizzate, ed eventualmente scrivere specifici test solo qualora ci siano fondati sospetti
che determinate classi/metodi non presentino il funzionamento atteso. Infine, in ultima analisi,
è necessario considerare che le classi appartenenti a librerie esterne sono comunque verificate,
sebbene indirettamente, attraverso i test relativi alle classi sviluppate che ne incapsulano il
comportamento. I test di unità, quindi, dovrebbero riguardare tutte le classi prodotte. Il quesi-
to però che resta da sciogliere è relativo a quali metodi verificare e come.
Nella pratica, ci si può imbattere in professionisti un po' estremi che suggeriscono test in grado
di verificare ogni possibile dettaglio, ogni possibile linea di codice del sistema. Questa strategia,
contrariamente a quanto si sarebbe portati a pensare e a dispetto del relativo enorme consumo di
tempo e budget, raramente risulta efficace, ed è inoltre percorribile solo in un limitato insieme di
casi. La copertura esaustiva (100%) del sistema è inutile e spesso addirittura deleteria.
In primo luogo, i test di unità non verificano assolutamente i requisiti del sistema ma si
occupano di verificare il funzionamento dei singoli componenti; qualora lo facessero poi, non è
sempre possibile anticipare tutte le possibili combinazioni degli stimoli di input di un sistema,
tanto meno è possibile ricreare tutti gli scenari di evoluzione dei thread presenti in un sistema
multi-threaded da verificare, e così via. Pertanto anche nel caso in cui si riuscisse, attraverso i
test di unità, a esercitare tutte le linee di codice del sistema (copertura = 100%), ciò non assicu-
rerebbe assolutamente il corretto funzionamento dello stesso. I test di unità, sono solo un pri-
mo livello delle procedure di test del sistema, le quali devono essere pianificate attentamente
anche prima dell'inizio dell'implementazione del sistema. Il matematico Kurt Godei [GDLPRF]
ha ulteriormente dimostrato come non sia possibile provare tutti gli aspetti di un sistema dal-
l'interno del sistema stesso. In particolare, ha dimostrato che tutti i sistemi logici contengono
necessariamente una serie di assiomi, ossia verità che devono essere accettate per buone e la cui
verifica può essere effettuata solo al di fuori del sistema stesso.
Coperture esaustive sono difficilmente raggiungibili anche per motivi pratici; per esempio,
tutti i sistemi includono delle parti di codice semplicemente non raggiungibili, come catch che
possono anche non verificarsi mai. Alcuni tecnici, in tali condizioni, commettono l'errore di
modificare il codice per "ottenere" una copertura completa. Quindi, è importante capire che
realizzare test esaustivi del sistema è una strategia non sempre efficace e realizzabile, e pure
dispendiosa. Si consideri il frammento di codice riportato di seguito. Si tratta dell'implementazione
del metodo equals di un Data Transfer Object/Value Object (DTO/VO, ossia semplici oggetti
Java, POJO, utilizzati per trasportare dati). Questo oggetto possiede i seguenti attributi: id, login,
name, middlename, stimarne, dob (date of birth), email (principale), addresses (oggetto composto che
contiene ogni tipo di recapito aggiuntivo: e-mail, telefono, cellulare, pager, indirizzo, etc.),
userStatus (stato dell'utente) e orgllnit (organhational unti, dipartimento di appartenenza).
public boolean equals(Object obj) I

if (this == obj) {
return true;

it (Isuper.equals(obj)) I
return false;
I

if (getClass() != obj.getClass()) I
return false;
I

final UserDTO other = (UserDTO) obj;

if (id == null) I
if (other.id != null) I
return false;
I
I else if (!id.equals(other.id)) I
return false;
I

if (login == null) I
if (other.login != null) I
return false;
I
I else if (Mogin.equals(other.login))
return false;
I

if (name == null) I
if (other.name != null) {
return lalse;
I
I else if (!name.equals(other.name)) I
return false;

if (middlename == null) I
if (other.mlddlename 1= null) I
return false;
I
I else if (!middlename.equals(other.middlename)) I
return false;
it (surname == null) I
il (other.surname != null) I
return false;
I
I else it (¡surname.equals(other.surname)) I
return false;

if (dob == null) I
if (other.dob != null) I
return false;
I
I else if (!tomaiDafe(dob).equals(tormafDate(other.dob)))
return false;

il (email == nuli) {
il (other.email 1= nuli) I
return false;
I
I else if (lemail.equals(other.email)) {
return lalse;

if (userStatus == null) I
if (other.userStatus != null) I
return lalse;
I
I else if (!userStatus.equals(other.userStatus)) I
return false;
I

if (addresses == null) I
if (other.addresses != null) I
return false;
I
1 else if (!addresses.equals(other.addresses)) I
return false;

if (orgUnit == null) I
if (other.orgUnit != null) (
return false;
this - - obi..
lalu:
!super.equals(obj)
^jlse
ir
getClass() '= obi gelClass()

id == null

(her id !=null 'kj equals(other id|

login ==null

!login.equals{other.login)
olher.login != nuli

orgUnit — nuli

lorgllnit.equals(other orgUnit)
other.orgUnrt nuli

return true return false return false return false return false return false return false return false return false return true

Figura 7.1 - Grafo dei diversi percorsi presenti nel metodo equals.

} else il (!orgUnit.equals(other.orgUnit)) I
return false;
I

return true;
I

La verifica completa di questo semplice metodo richiederebbe di scrivere circa 25 test! La


figura 7.1 fornisce una rappresentazione del grafo dei diversi percorsi che un test completo
dovrebbe coprire. I test da produrre per verificare i diversi percorsi del metodo equals sono
illustrati nella tabella 7.1.
E veramente necessario produrre test capaci di esercitare tutti i possibili percorsi del metodo
equals? In definitiva, no... anche se ogni percorso non analizzato finisce con il ridurre la percen-
tuale di copertura. Il costo di produzione iniziale e di manutenzione di test esaustivi è, nella
quasi totalità dei casi, insostenibile. Nel caso del metodo equals, si consideri, per esempio, l'im-
patto generato sul codice di test relativo all'introduzione di un nuovo attributo.
Quindi è importante discernere tra i metodi che non è necessario verificare approfondita-
mente e quelli di cui invece è necessario verificare ogni singolo dettaglio.
Si consideri come ulteriore esempio il caso di semplici metodi getXXX e setXXX; a meno che
questi non eseguano ulteriori operazioni, non è estremamente utile verificarli. Inoltre, poiché i
metodi accessori (getXXX) dovrebbero essere l'unico modo per accedere ai dati di un oggetto,
tali metodi sono comunque oggetto di investigazione da parte di altri test.
Qualora il metodo da verificare fosse l'implementazione di un algoritmo di un servizio business
di rilevanza fondamentale, allora probabilmente la situazione sarebbe diversa. In tal caso infatti,
a differenza del metodo di uguaglianza, sarebbe opportuno verificarne tutti i possibili percorsi.
Sebbene non sia possibile dar luogo a una copertura esaustiva, questa deve essere sicuramen-
te estensiva. Coperture insufficienti possono generare una serie di dannosissimi effetti collaterali,
quali ad esempio una bassa qualità del codice, una falsa sensazione di robustezza, continui
malfunzionamenti del sistema in produzione a seguito di processi di aggiornamento dovuti a
test di regressione incapaci di eseguire approfondite analisi del sistema, etc.
Purtroppo, nella pratica lavorativa, accade anche spesso di imbattersi in progetti in cui i test
di unità coprono una minima porzione del codice... Coperture inferiori al 50-60% del codice
dovrebbero generare qualche preoccupazione al capo progetto di turno.
Anche se la produzione di questi test può sembrare un'attività eccessivamente dispendiosa e
poco sostenibile, soprattutto in situazioni in cui il progetto risulti costretto da severi vincoli in
termini di tempo e denaro, si tratta di una strategia che permette di realizzare più rapidamente
sistemi di maggiore qualità; e la loro validità è evidenziata ad ogni iterazione/processo di
refactoring. Pertanto, l'impegno profuso nella scrittura dei test di unità è ripagato, nel medio/

Num Descrizione Esito


i T u t t i gli a t t r i b u t i d e i d u e o g g e t t i i m p o s t a t i a l l o s t e s s o v a l o r e n o n n u l l o true

2 I n v o c a z i o n e del m e t o d o s u l l o s t e s s o o g g e t t o true

3 I n v o c a z i o n e del m e t o d o s u d u e o g g e t t i c o n un e l e m e n t o d e l l a c l a s s e a n t e n a t a d i v e r s o false

I n v o c a z i o n e d e l m e t o d o su d u e o g g e t t i di t i p o d i v e r s o e r e d i t a n t i d a l l o s t e s s o a n t e n a t o
-1 false
c o n gli e l e m e n t i in c o m u n e u g u a l i

I n v o c a z i o n e del m e t o d o s u d u e o g g e t t i u g u a l i c o n tutti gli e l e m e n t i i m p o s t a t i a n u l i true

I n v o c a z i o n e d e l m e t o d o su d u e o g g e t t i c o n tutti gli e l e m e n t i u g u a l i , e c c e t t o l ' i d del


6 false
p r i m o i m p o s t a t o a nuli

I n v o c a z i o n e d e l m e t o d o su d u e o g g e t t i c o n tutti gli e l e m e n t i u g u a l i e c c e t t o gli id


7 false
(diversi m a n o n nulli)

I n v o c a z i o n e d e l m e t o d o su d u e o g g e t t i c o n t u t t i gli e l e m e n t i u g u a l i , e c c e t t o l ' a t t r i b u t o
8 false
l o g i n del p r i m o i m p o s t a t o a nuli

I n v o c a z i o n e del m e t o d o su d u e o g g e t t i c o n t u t t i gli e l e m e n t i u g u a l i , e c c e t t o l ' a t t r i b u t o


9 false
login (diversi m a n o n nulli).

false

I n v o c a z i o n e d e l m e t o d o su d u e o g g e t t i c o n tutti gli e l e m e n t i u g u a l i , e c c e t t o l ' a t t r i b u t o


24 false
orgllnit del p r i m o i m p o s t a t o a nuli

I n v o c a z i o n e d e l m e t o d o su d u e o g g e t t i c o n tutti gli e l e m e n t i u g u a l i , e c c e t t o l ' a t t r i b u t o


25 false
orgllnit (diversi m a n o n nulli)

Tabella 7.1 - Elenco dei test dei percorsi del metodo di equals.
lungo termine, in termini produzione di sistemi di qualità superiore, che, in ultima analisi,
producono economie di tempo e budget.
Tutto il codice rilasciato, anche se non utilizzato, deve essere sottoposto a opportuni proces-
si di test. Paradossalmente, anche le funzioni meno utilizzate devono essere verificate appro-
fonditamente, proprio perché la normale pratica difficilmente potrebbe portare a individuarne
i problemi. Indipendentemente dalla strategia utilizzata per la scrittura dei test di unità, è
importante misurarne il livello di copertura. A tal fine sono disponibili una serie di tool, molti
dei quali sono illustrati all'indirizzo

http://java-source.net/open-source/code-coverage

La maggior parte di questi tool non si limita a indicare la percentuale di codice esercitata dai
test, ma fornisce una serie utilissima di dati. Tra questi ci sono anche le classi/metodi non verifi-
cati, la traccia dei percorsi analizzati, etc. Pertanto, nella produzione dei test, invece di aggiun-
gerne altri in modo randomico, è più conveniente introdurre test mirati che vadano a esercitare
proprio le parti di codice non controllate da appositi test di unità. Ciò permette di evitare un'inutile
ridondanza nella produzione di test, da evitare al fine di minimizzare il processo di manutenzio-
ne degli stessi test generato dalla variazione del corrispondente codice del sistema.

JUnit
Al momento in cui viene scritto questo libro non è possibile parlare di test di unità in Java senza
menzionare JUnit (http://www.junit.org/index.htm): nella comunità dei programmatori Java i due
concetti sono spesso utilizzati come sinonimi. JUnit, indubbiamente, appartiene di diritto alla
cerchia dei progetti open source più popolari nella comunità dei programmatori Java e non
solo. Tanta è la popolarità di questo strumento da avergli fruttato il titolo di strumento standard
de facto per la produzione di test di unità in Java. Sviluppato inizialmente da Kent Beck e Eric
Gamma nel contesto del movimento XP, Martin Fowler ha commentato JUnit in mondo
trionfalistico scrivendo che "mai, nella disciplina dell'ingegneria del software, così tanto è stato
dovuto a così poche linee di codice". Per quanto l'obiettivo di questo capitolo non sia presen-
tare in modo diffuso JUnit (esistono interi libri dedicati all'argomento, cfr. [JUNREC],
[JUNACT], e [JUNPCK]), si è ritenuto che una minima conoscenza di questo framework sia il
prerequisito irrinunciabile per la comprensione di quanto riportato di seguito.
L'introduzione del JDK5, inoltre, ha avuto un grosso impatto non solo sui package interni di
Java ma anche sulla maggior parte delle librerie fornite da terze parti. A questa regola non fa di
certo eccezione JUnit che, come si vedrà a breve, ha subito significative variazioni.
Tuttavia, poiché la versione JUnit 4, al momento in cui viene scritto questo libro, non è
ancora largamente utilizzata e poiché non sempre è possibile abbandonare il JDK4, si è deciso
di mostrare, brevemente, entrrambe le versioni.

JUnit prima della versione 4


Tradizionalmente una classe di test presentava una struttura simile a quella riportata di seguito.

0 package com.mb.mypackage;
1
2 thire! party imporls
3 import junit.tramework.TestCase;
4
5 public class MyTest extends TestCase I
6
7 public void testBasicTest() I
8
9 UserVO newUser = UserFactory.getUser(O);
10 assertlMotlMull(newUser);
11
12 I

Dall'analisi di tale listato è possibile evidenziare gli elementi fondamentali delle classi di test:

• la classe di test doveva necessariamente ereditare dalla classe TestCase (linea 5);
• il nome dei metodi di test doveva necessariamente iniziare con il suffisso test (linea 7), al
fine di poter essere riconosciuti come tali tramite i meccanismi della riflessione (package
java.lang.reflect);
• all'interno dei vari metodi di test era necessario utilizzare le varie versioni dei metodi
assert, come per esempio assertNotNull (linea 10).

Oltre questi elementi base era possibile definire del comportamento da invocare, rispettiva-
mente, all'atto dell'avvio del test (setllpQ) e alla conclusione (tearDownQ). Metodi utili sia per
allocare e rilasciare risorse (come per esempio connessioni al database), sia per inizializzare og-
getti successivamente verificati. Pertanto, questi metodi permettono di evitare notevoli
duplicazioni di codice. In effetti, se non ci fossero sarebbe necessario, per ogni test, riportare il
codice necessario per impostare le condizioni necessarie per l'avvio dei test stessi e per la chiusu-
ra pulita, soprattutto in caso di errore. Il listato riportato di seguito mostra un esempio di utilizzo
dei metodi di setUp and tearDown. In particolare, la procedura di setUp si occupa di inizializzare il
"componente" del business Service layer da verificare (UserBS). Questo, a sua volta, utilizza tre
"componenti" del livello inferiore: business object. Poiché l'obiettivo del test è verificare il com-
ponente e non l'integrazione di diversi layer, le classi del livello BO sono sostituite da opportuni
mock up (oggetti che implementano la stessa interfaccia del componente vero, ma che presenta-
no implementazioni semplici). Il metodo di chiusura si occupa di riportare metodi non invocati
: se si definisce il mock up di un metodo è legittimo attendersi che questo debba essere invocato.

protecled void setUp() I

orgllnitBO = EasyMock.createMock(IOrgUnitBO.class):
profileBO = EasyMock.createMock(IProfileBO.class);
userBO = EasyMock.createMock(IUserBO. class);

UserBS userBS = new UserBS();

userBS.setUserBO(userBO);
userBS.setProfileBO(profileBO);
userBS.setOrgl)nitBO(orgUnitBO);

this.userBS = userBS;
I

protected void tearDown() throws Exception {

EasyMock.ve/-/fy(new Object[] ( userBO, profileBO, orgUnitBO I);

Una buona pratica di programmazione consiste anche nello scrivere una classe (TestSuite) atta
a invocare le varie classi di test. Lo scheletro di tale classe assume una forma del genere:

public class MyUnitSuite I

// CONSTANTS SECTION

// list ot test classes to run


private static Test[] testToRun = (
XYZTest.suite(),
MyTest.suite()

// ATTRIBUTES SECTION

// METHODS SECTION

/ "

' Return the test suite defined in this class


' Other test classes has to be added in the testToRun attribute
" © r e t u r n TestSuite - test suite related to the VO classes
7

public static TestSuite suite() {

TestSuite suite = new TestSuite("My Test Suite");

for (int ind = 0; ind < testToRun.length; lnd++) I

suite.addTest(testToRun[ind]);
I

return suite;
JUnit versione 4
La versione 4 del framework presenta alcune significative differenze dovute principalmente
all'inclusione delle annotazioni introdotte con la versione Java 5.0.

Brevemente, il meccanismo delle annotazioni permette di introdurre, direttamente nel codice


s o r g e n t e , delle m e t a - i n f o r m a z i o n i . Q u e s t e r a p p r e s e n t a n o i n f o r m a z i o n i , più p r e c i s a m e n t e
"dichiarazioni", circa il codice stesso. Il meccanismo è del tutto equivalente a quello utilizzato
dal parser J a v a D o c per generare la documentazione: vi è una parola chiave, preceduta dal carattere
Tuttavia, in questo caso, l'utilizzo è decisamente più circoscritto per evitare ambiguità. Per
esempio è possibile dichiarare che un m e t o d o sia deprecato p r e m e t t e n d o alla relativa dichiarazione
la parola chiave @deprecated:

@deprecated public void destroy()

Si tratta del m e t o d o destroy della classe Thread, izialmente ideato per s o p p r i m e r e i thread in
maniera brusca non consentendogli neanche di rilasciare eventuali monitor. Per questa ragione
l'implementazione di questo m e t o d o rappresenta un ottimo sistema per generare dead-lock.

Il listato seguente mostra l'adattamento dell'esempio riportato nel primo listato, modificato
in base alle direttive della versione JUnit 4.

0 package com.mb.mypackage;
1
2 //
. third party imports
3 import static org.junit.assert.assertNotNull;
4 import org.junit.Test;
5 public class MyTest I
6
7 ©Test public void BasicTest() I
8
9 UserVO newllser = UserFactory.getUser(O);
10 assertNotNull(newllser);
11
12 I

In particolare:

• non occorre importare il package: junit.framework.TestCase;


• bisogna importare le annotazioni di test (org.junit.Test);
• non è più necessario che le classi di test estendano dalla classe TestCase;
• non è obbligatorio aggiungere il suffisso test ai metodi di test: questi sono evidenziati
attraverso l'utilizzo dell'annotazione @Test.

Qualora poi, si voglia far sì che la versione 4 del framework JUnit esegua correttamente alcune
classi di test scritte per una versione precedente, è necessario includere il seguente metodo:
public static junit.framework.Test suite() {
return new JUnit4TestAdapter(<nome della classe di testxclass);

L'uso delle annotazioni, come lecito attendersi, non è limitato ai soli metodi di test, ma è
utilizzato in altri contesti. Per esempio, è possibile utilizzare le annotazioni @Before e ©After,
contenute nei package org.junit.Test.Before e org.junit.Test.After, per definire comportamento da
eseguire, rispettivamente, prima e dopo l'esecuzione di ogni metodo di test. Queste annotazio-
ni vanno premesse a opportuni metodi di inizializzazione e rilascio dei test, ed è possibile defi-
nirne quanti se ne vuole. Da notare che qualora si decida di implementare una classe antenata
di test, tutte le classi figlie erediteranno i vari metodi @Before e ©After.
Nella versione JUnit 4 è possibile dichiarare delle annotazioni, @BeforeClass e @AfterClass, che
sono invocate una sola volta per tutti i test. Coerentemente con la relativa definizione è possibi-
le specificarne una sola coppia per classe.
Per terminare questa sezione, è importante considerare che l'annotazione @Test permette di
definire diversi parametri, come per esempio:

• eventuali eccezioni attese: @Test (expected=<nome eccezione;-.class); test definiti in questo


modo hanno successo solamente se l'eccezione attesa è effettivamente eseguita;
• un eventuale timeout: @Test (timeout=6000); questo parametro permette di specificare il
tempo massimo, espresso in millisecondi, a disposizione del test per essere eseguito:
un'eventuale durata eccedente il timeout genera l'automatico fallimento del test.

Un'altra annotazione degna di nota è @lgnore. Questa, come lecito attendersi, permette di
ignorare un test e di includere una stringa che riporti la motivazione dell'esclusione.

Direttive
7.1 Investire nei test di unità
Sebbene questa regola possa sembrare scontata, si continuano a trovare sistemi privi di test di
unità. La mancanza di tali test, inevitabilmente, conferisce minore robustezza al codice, rende
più rischiosi i processi di aggiornamento e quindi riduce la voglia/possibilità di eseguire pro-
cessi di refactoring, aumenta il grado di difficoltà dell'attività di integrazione, e spesso ne ridu-
ce la comprensibilità. Per alcuni tecnici, un sistema non dotato di test automatici è un sistema
non funzionante per definizione.
Indipendentemente dall'impegno profuso nell'attività di disegno del sistema e dalla metico-
losità utilizzata nello scrivere il codice, è sempre possibile/frequente commettere errori. Per-
tanto la presenza di test automatizzati e quindi ripetibili fa spesso la differenza tra codice robu-
sto e non.

7.1.1 Investire nella qualità dei test di unità


Dalla definizione dei test di unità consegue che il rapporto esistente tra questi test e il codice
testato dovrebbe essere un'approssimazione della relazione l a i . Giocoforza, gli stessi test
finiscono per far parte del sistema stesso. Pertanto non dovrebbe sorprendere il fatto che prò-
cessi di refactoring del codice generino importami ripercussioni sui test di unità. Logica conse-
guenza quindi è che i processi di refactoring non sono limitati al codice sorgente ma si estendo-
no, necessariamente, ai test. Al fine di semplificare il relativo processo di aggiornamento è
quindi necessario che questi presentino un adeguato livello di qualità, altrimenti si potrebbe
giungere alla fastidiosa situazione caratterizzata dal processo di sviluppo rallentato dalle attivi-
tà di manutenzione dei test.

7.1.2 Scrivere i test di unità non è un'attività banale


Come si vedrà nel corso di questo capitolo, scrivere test di unità non è assolutamente un'attivi-
tà banale. I test di unità sono parte integrante del sistema e come tali necessitano di essere
rivisitati e aggiornati con l'evoluzione del sistema; la loro scrittura richiede la conoscenza di una
serie di strumenti e tool quali i sistemi di controllo di copertura, di implementazione di oggetti
mock up etc. La scrittura dei test di unità, inoltre, fornisce una prima valutazione sulla qualità
del disegno delle parti oggetto di verifica, e così via. Pertanto, la scrittura dei test di unità è
un'attività che va pianificata accuratamente, assegnata a personale qualificato e soprattutto
eseguita contemporaneamente alla scrittura del relativo codice.

7.1.3 Evitare l'over-engineering


Implementare i test ponendo attenzione alla loro qualità non equivale assolutamente a dissipa-
re tempo nella scrittura di test ultra flessibili, super efficienti, etc. I test sono un'importante
parte dello sviluppo del software, sono oggetto di continui refactoring, ma comunque non è
opportuno renderli più flessibili e sofisticati di quello che dovrebbero effettivamente essere. Il
tempo necessario per implementare test eccessivamente raffinati, over-engineered, per metterli
a punto, per spiegarli ad altre persone, etc., potrebbe essere investito in maniera decisamente
migliore.

7.1.4 Evitare di scrivere metodi di test eccessivamente lunghi


Ogni classe di test, come visto in precedenza, dovrebbe far riferimento a un ben definito compo-
nente, tipicamente a una classe. Inoltre è opportuno che queste classi contengano una serie di
semplici metodi di test, ciascuno demandato alla verifica di uno specifico e limitato aspetto.
Ciò sia per far sì che i test siano più facilmente comprensibili e mantenibili, sia per facilitare
l'analisi dei report prodotti dall'esecuzione dei test: tool comeJUnit, infatti, si limitano a riferi-
re solo il primo test fallito di una batteria di test.
L'inconveniente relativo alla scrittura di molti semplici metodi di test potrebbe risiedere
nella necessità di dove ripetere diverse volte una stessa porzione di codice. Questa eventualità
in passato veniva risolta scrivendo opportuni metodi privati alla classe di test da invocare nel
codice di ogni metodo di test. Ciò non è più necessario con la versione 4 di JUnit e in partico-
lare grazie all'introduzione dell'annotazione @Before.
Nel codice seguente, viene mostrato come decomporre un unico test complesso suddividen-
dolo in alcuni test semplici.

Test complesso
@Test
public void testABCQ I
// inizializzazione dei singoli
// test. Diverso da setUP!
assertTrue(conditionl);
assert l\lotl\lull(o);
assertEquals(o1 ,o2);
I

Test sempltci
@Before
public void testlnit() {
I

@Test
public void testCondition! () I
assertTrue(conditionl);
I

@Test
public void testCondition2() I
assertNotNull(o);

@Test
public void testCondition3() I
assertEquals(o1,o2);
]

7.1.5 Scrivere i test di unità insieme alle classi cui si riferiscono


Questa regola potrebbe risultare abbastanza controversa. In effetti, vi è tutto il movimento
basato sullo "sviluppo guidato dai test" (TDD, Test Driven Development), di cui XP è una delle
istanze più popolari, che evoca la scrittura dei test addirittura prima della scrittura delle stesse
classi da implementare. In sostanza si tratterebbe di un vero e proprio strumento per disegnare
il sistema. Questa regola non intende assolutamente fornire direttive circa il processo di svilup-
po da adottare, tuttavia è importante che i test di unità siano prodotti con il codice stesso. Que-
sto sia per evitare processi di scrittura dei test frettolosi e tipicamente superficiali, sia per far sì
che questi assolvano al loro compito: permettere il rilascio di codice di qualità e quindi sempli-
ficarne il processo di integrazione, fornire informazioni circa l'utilizzo delle varie classi, etc.

7.1.6 Non assegnare la scrittura dei test al personale junior


Un altro problema che spesso interviene nella redazione dei test di unità è che la loro scrittura
sia demandata, immediatamente prima della consegna, a programmatori junior. Questa prati-
ca, spesso, si traduce nella scrittura veloce e superficiale dei vari test e quindi genera una ridu-
zione o addirittura l'annullamento dei vantaggi propri della scrittura dei test di unità. I test,
come visto in precedenza, vanno scritti non appena una classe si rende disponibile o, se si
desidera abbracciare tecniche estreme, addirittura prima di redigere il codice stesso.
Una pratica spesso utilizzata consiste nel far scrivere le classi e i relativi test a diverse perso-
ne. Ciò però è ben diverso dallo scrivere i test di corsa poco prima della consegna del sistema.
7.2 Utilizzare JUnit
JUnit è uno dei framework open source di maggiore successo, tanto che ormai è diventato lo
standard de facto per la produzione di test automatizzati in Java. Si tratta di un framework ben
progettato, efficace, collaudato, divenuto utilizzato come normale strumento di lavoro per tutti
i programmatori Java, etc. Pertanto il suo utilizzo non dovrebbe essere oggetto di discussione.

7.2.1 Utilizzare gli Assert


G l i assertXXX (assertTrue, assertNotNull, AssertEquals, Fail, etc.) rappresentano u n o dei meccanismi
fondamentali del framework JUnit. In particolare questi metodi permettono a JUnit di verifica-
re l'esito dei vari test. Pertanto, ogni metodo di test deve includere almeno uno statement di
assert. Sebbene questa regola possa sembrare piuttosto ovvia, capita di imbattersi in codici di
test in cui la verifica sia affidata a meccanismi diversi, tra cui eccezioni, stampe a video, etc. Un
altro valido accorgimento sta nell'utilizzare il campo etichetta degli assert, specialmente nei casi
in cui uno stesso metodo di test ne contenga diversi. Sebbene l'etichetta sia un campo opzionale,
in caso di fallimento può semplificarel'individuazione del problema, anche se i tool moderni
permettono, attraverso un click del mouse, di posizionarsi direttamente sull'assert non riuscito.

7.2.2 Non utilizzare il System.out.println a fini di test


Può capitare di visionare metodi di test in cui la verifica del corretto funzionamento del sistema
sia affidato a stringhe inviate a video (System.out.println). Si tratta chiaramente di una pratica
fortemente sconsigliata per una serie di motivi: il più importante è che la verifica del corretto
funzionamento del sistema è così demandata alla presenza di persone. Pertanto, ciò rende
impossibile automatizzare le verifiche, eseguire continui controlli anche durante orari nottur-
ni, etc. Inoltre, rende gli stessi controlli poco efficaci e decisamente complessi. La verifica di
algoritmi non banali, infatti, spesso richiede di controllare molteplici informazioni, difficil-
mente verificabili attraverso semplice ispezione di schermate video. Si considerino per esem-
pio le tipiche schermate che riportano i prezzi di prodotti finanziari.

7.2.3 Implementare le classi TestSuite


Nel disegno e nell'implementazione di sistemi software è possibile individuare una serie di
"macro-componenti" i cui elementi sono accomunati da importanti caratteristiche. Per esem-
pio, una tipica architettura multistrato di un sistema prevede:

• strato di presentazione (presentation layer);


• strato di business service (business service layer);
• srato di business object (business object layer)\
• strato di integrazione (integration layer).

Alcuni di questi strati, poi, sono ulteriormente composti da sottostrati; lo strato di integrazio-
ne tipicamente è suddiviso per le varie sorgenti integrate: database, sistema di messaggistica, etc.
Inoltre, sono presenti ulteriori componenti atti a ospitare elementi base, come eccezioni comuni,
value object utilizzati per scambiare informazioni tra i vari strati dell'architettura etc. Pertanto è
opportuno far sì che tutti i test degli elementi di macro-componenti (spesso anche di importanti
package) siano invocabili da un'opportuna classe "suite di test". Questa, a sua volta dovrebbe
essere inclusa in una suite a livello più generale fino a giungere a quella di livello globale.
Da notare che le singole classi di test (elementi foglia) e le varie suite (elementi composti)
sono state implementate secondo le direttiva del Composite Pattern e quindi è possibile avere
un numero illimitato di classi "suite di test".

7.2.4 Inserire le classi di test in una struttura equivalente


a quella delle corrispondenti classi verificate
Nell'implementazione delle classi di test è molto utile far sì che il package (e non il path) delle
classi di codice e di test coincidano, (figura 7.2). Ciò è molto utile per fare in modo che le classi di
test possano accedere agevolmente alle risorse friendly (elementi con visibilità di default) delle
classi da verificare senza interferire nella struttura. Non è infrequente che alcuni sviluppatori
decidano di sistemare le classi di test insieme alle corrispondenti classi implementate. Chiaramen-
te ciò non è assolutamente una buona pratica, in quanto rende meno chiari i package di codice,
rende meno agevole il processo di implementazione (bisogna sempre porre molta attenzione alle
classi da aprire), rende più difficile l'impacchettamento in JAR dei sorgenti, e così via.

7.2.5 Assegnare alle classi di test lo stesso nome delle classi testate
Come visto in precedenza, nel contesto dei test di unità, ogni classe di test dovrebbe verificare
il comportamento di una classe del sistema. Pertanto, la convezione utilizzata per i nomi delle

E] ^ ^ main

0 _ j java

• 2 ) com

B mokabyte

B ^dataioader

B ^ base

B 3 V 0

Q DataTableVO.java

Q DataRowVO.java

Q ColumDescrVO.java

B test

02 i ava
• com

B mokabyte

B ' ^jdataloader

B base

B 3 V 0

Q DataTableVOTest.java

Q DataRowVOTest.java

Q ColumDescrVOTest.java

Figura 7.2 - Package delle classi di test e di quelle di codice.


classi di test consiste nel riportare il nome della classe verificata con l'aggiunta del suffisso
"test", come mostrato in figura 7.2.

7.2.6 Utilizzare la parola "test" per enfatizzare metodi di test


Qualora si utilizzi una versione JUnit precedente alla 4.1, è obbligatorio premettere al nome
dei metodi la parola "test" per distinguere i metodi di test (che quindi devono essere invocati
dal framework) dagli altri di utilità. Qualora invece si utilizzi una versione JUnit 4.1 (o superio-
re), i metodi di test sono individuati per mezzo dell'apposita annotazione: @Test. Tuttavia, rap-
presenta una buona strategia identificare i metodi di test riportando questo nome direttamente
nel nome del metodo.

7.3 Test di unità e mock objects


I test di unità, per definizione, sono utilizzati per verificare che un particolare frammento di
codice presenti il comportamento atteso. Pertanto, il campo d'azione dei test di unità è costitu-
ito da singole classi e componenti. Domini di maggiore estensione sono oggetto dei test di
integrazione e di sistema. Il problema di mantenere i test di unità nel proprio ambito di appli-
cazione deriva dal fatto che i sistemi Object Oriented tendono naturalmente a essere costituiti
da una miriade di classi interrconnesse. Quindi il corretto funzionamento di ciascuna di queste,
quasi sempre, dipende dal corretto funzionamento di altre classi, e di componenti, package,
sistemi esterni, e chi più ne ha più ne metta. Per esempio, spesso l'erogazione di determinati
servizi è subordinata all'autenticazione e autorizzazione dell'utente che sono generalmente re-
sponsabilità di un sistema esterno. Ancora, in architetture multi-tier, componenti appartenenti
allo strato di presentazione dipendono da quelli del business service layer; questi, a loro volta,
dipendono da quelle appartenenti al business object layer, e così via. Come fare quindi ad
implementare test di unità che non eccedano il proprio campo d'azione?
La risposta viene dai mock objects. Si tratta di oggetti che fingono il comportamento di deter-
minati oggetti reali di cui si ha bisogno. In particolare, questi oggetti, sviluppati rapidamente
grazie ai relativi framework, implementano la medesima interfaccia degli oggetti reali simulati;
però, invece di implementarne il comportamento reale, forniscono risposte pre-definite e/o
comportamenti molto semplici. Si consideri per esempio un oggetto che si occupi di effettuare
l'autenticazione degli utenti di un sistema, magari eseguendo un'invocazione remota. Il relativo
oggetto mock up potrebbe limitarsi a fornire sempre una risposta affermativa oppure autenti-
care solo gli utenti il cui id è memorizzato in un'apposita lista eventualmente hard-coded, e così
sia... Quindi i mock objects servono per sostituire, con semplici simulazioni, le entità che col-
laborano con quella oggetto di test.
Al termine di questa spiegazione viene riportato un listato. L'oggetto del test è una classe che
fornisce il servizio di autenticazione (AuthenticationService). A tal fine, utilizza i servizi esposti
dalle classi ProfileBO e UserBO, entrambe localizzate nel Business Object layer (da cui il suffisso
BO). Poiché si tratta di un test di unità (e non di integrazione) non è necessario coinvolgere
anche queste classi nel test in questione. Quindi, in questo contesto, al loro posto è possibile
utilizzare appositi mock objects. La verifica del corretto funzionamento delle classi in questio-
ne (ProfileBO e UserBO), come lecito attendersi, è affidato alle relative classi di test.
Per la realizzazione delle classi di mock up, si è deciso di utilizzare il tool EasyMock (http://
www.easymock.org/) la cui guida all'uso non è oggetto di questo libro. Il funzionamento base,
però, è abbastanza semplice:
1. si genera l'implementazione di mock up relativa alle classi di cui è necessario simulare il
c o m p o r t a m e n t o ( p e r e s e m p i o : IProfileBO profileBO = EasyMock.createMock(IProfileBO.class));
2. per ciascun metodo degli oggetti simulati coinvolti nel test:
a. si dichiara la relativa invocazione corredata dai parametri attesi (per esempio:
profileBO.findActiveProfileByLoginld("invalid"));
b. si specifica la risposta attesa (per esempio EasyMock.expectLastCall().andReturn(null););
3. quando tutti i metodi richiesti dal test sono stati specificati, è necessario far sì che il
framework ne prenda atto; ciò fa sì che l'oggetto simulante, dopo aver memorizzato
invocazioni e risposte, si predisponga a ripetere le risposte (per esempio EasyMock.replay(
new Object[] I userBO, profileBO})).

Il listato di seguito riportato presenta due semplici test, il primo negativo e il secondo positi-
vo. In particolare, il primo serve a verificare che a fronte di una richiesta relativa a un utente non
presente nel sistema (il login specificato non appartiene ad alcun utente), il sistema sia in grado
di rilevare l'anomalia e quindi lanci un'opportuna eccezione. Il secondo test invece verifica che
qualora tutti i dati siano corretti, il sistema sia in grado di autenticare l'utente specificato.

public class AuthenticationServiceTest extends TestCase I


/ * * The profile BO. */'
private IProfileBO profileBO = EasyMock.creafe/Woc/r(IProfileBO.class);
/ " * The profile BO. */
private lUserBO userBO = EasyMock.creafe/Woc/((IUserBO.class);
/ " The authentication service. */
private AuthenticationService authenticationService = null;

/ * * Default Constructor */
public AuthenticationServiceTest() I
authenticationService = new AuthenticationServiceQ;
authenticationService.setProfileBO(profileBO);
authenticationService.setUserBO(userBO);
I

/**

* This method verifies that the service is able to detect


* requests related to not existent user.
* This is a negative test
* @throws Exception a problem occurred during the process
*/
public void testlnvalidLoginld() throws Exception I

profileBO.findActiveProfileByLoginldfinvalid");
EasyMock.expecf/.asfCa//().andReturn(null);

userBO.findRefl)serByLogin("invalid");
EasyMock.expecf/.as/Ca//().andReturn(null);
replayf);
try {
authenticationService.authenticate(createSubject("invalid"));
fail(" MySystemSecurityException should be thrown");
I catch ( MySystemSecurityException e) I
// ignore this exception
assert 7rt/e(true);
I

" This method verifies that the service, in case everything is correct
" is able to correctly deliver the authentication service
' @throws Exception a problem occurred during the process

public void testCorrectUserAuthentication() throws Exception I

RefUserDTO correctUser = new RefUserDTOQ;


correctUser.setLoglnf'correctUser");
correctUser.setPassword(generateTmpPassword());
comclUser.se\Smus(UserStatusDTO.USER_ACTIVE),
correctUser.setValidFromfnew Date(new Date().getTime() - 1000000));
correctUser.setValidTo(new Date(new Date().getTime() + 1000000));

RefProfileDTO correctProfile = new RefProtileDTO();


correc\Proti\e.se\S\a\us{ProfileStatusDTO.PROFILE_ACTIVE);
correctProfile.setValidFrom(new Date(new Date().getTime() - 1000000));
correctProfile.setValidTo(new Date(new Date().getTime() + 1000000));
correctProfile.setUser(correctUser);

profileBO.findActiveProfileByLoginld(correctUser.getLogin());
EasyMock.expectLastCall().andReturn(correctProfile);

replay();

try I
authenlicationService.authenticale(
createSubject( correctUser.getLogin()));
) catch I (MySystemSecurityException e) I
fail("MySystemSecurityException should not be thrown");
I

7.3.1 Utilizzare i test di unità per verificare singoli componenti


La prima regola di questa sezione non poteva che prescrivere di utilizzare i test di unità compa-
tibilmente al relativo principio base: verificare che particolari frammenti di codice presentino il
comportamento atteso. Ciascun test, quindi, dovrebbe verificare una determinata caratteristica
considerata isolatamente. Ora, in sistemi non banali, la verifica di singole parti considerate iso-
latamente è un'attività spesso molto complessa. Pertanto è consigliabile utilizzare appositi
framework progettati per consentire ai programmatori di generare rapidamente oggetti di mock
up: semplici oggetti in grado di simulare specifici comportamenti dei corrispondenti oggetti del
sistema reale. Il ricorso a oggetti di mock up permette di implementare test di unità più appro-
fonditi e più dettagliati e quindi, in ultima analisi, sistemi di maggiore qualità.

7.3.2 Implementare un sistema facilmente verificabile


Questa regola, probabilmente, potrebbe appartenere ai capitoli relativi l'implementazione del
sistema e non dei relativi test. Tuttavia, alcuni problemi di disegno si scoprono proprio durante la
scrittura dei test. Inoltre, questa regola è di facile comprensione nel contesto dei mock objects.
Tutti i sistemi dovrebbero essere realizzati considerando, tra gli altri fattori, l'attività di test.
Ciò non significa che si debba cambiare il disegno del sistema per semplificarne la verifica,
come per esempio implementare tutti metodi pubblici in modo da semplificare l'implementazione
dei relativi test; si tratta però di considerare, nella scelta delle diverse alternative implementative
di uno stesso disegno, anche la facilità di verifica.
Per esempio, qualora una classe utilizzi i servizi esposti da altre classi (come nel listato prece-
dente AuthenticationService utilizza i servizi esposti dalle classi ProfileBO e UserBO), una buona
implementazione consiste nel fornire i riferimenti a tali istanze attraverso il costruttore di
AuthenticationService o per mezzo di opportuni metodi di set (come nel caso in questione). In
altre parole, si tratta di utilizzare il pattern Inversion Of Control (inversione del controllo). Ciò
non solo risolve correttamente il disegno: al tempo stesso, ne semplifica il test.
Un'altra alternativa valida consiste nell'utilizzare il pattern di Factory. In questo caso però,
nella scrittura dei test è necessario estendere la classe Factory con una necessaria ai fini di test.
Questa classe estesa, in particolare, si deve occupare della generazione degli oggetti mock up
utilizzati durante il test. Uno scenario che invece rende complessa l'implementazione dei test si ha
quando la generazione delle istanze delle classi utilizzate avviene all'interno del codice. In questo
caso la sostituzione degli oggetti reali con appositi mock up è un'attività piuttosto complessa.

7.3.3 Utilizzare mock objects per simulare condizioni


difficilmente riproducibili
La realizzazione di alcuni test di unità può risultare estremamente complessa, per esempio quan-
do è necessario verificare la risposta del sistema a situazioni anomale, per esempio connessioni
perse durante la sessione, sistema remoto che transita in uno stato di errore, e così via. Tali
situazioni si prestano a essere simulate efficacemente attraverso i mock objects. In particolare,
questi framework possono essere utilizzati o per implementare un proxy del server oppure un'op-
portuna simulazione del server stesso. E quindi è possibile decidere, programmaticamente, quan-
do far simulare comportamenti corretti e quando far generare degli errori.

7.4 Utilizzare tool di analisi della copertura


Utilizzare un test di copertura è assolutamente importante per una serie di motivi.

• Disporre di una valutazione quantitativa del livello di copertura dei test. Per alcuni tec-
nici (estremisti) questi indici, indirettamente, costituiscono una misura del livello di qualità
del sistema. Di sicuro c'è la necessità di aver ben chiaro il livello di affidabilità dei test di
regressione e, in ultima analisi, il valore del superamento dei test di unità. Non è infre-
quente infatti che i team di sviluppo abbiamo la falsa sensazione di aver prodotto un
codice di alta qualità giacché il sistema passa indenne i vari test anche quando questi
coprono esclusivamente una piccola percentuale del codice.
• Individuare aree di codice non testate sufficientemente e che quindi beneficerebbero
dall'aggiunta di nuove classi di test. Questo evita inutile dispendio di tempo e denaro
derivante dall'aggiunta di test in modo randomico quando invece occorre aggiungere
test solo per affrontare le aree effettivamente non coperte opportunamente.
• Individuare test ridondanti che risultano deleteri nel momento in cui sia necessario ese-
guire delle operazioni di refactoring che necessariamente si riflettono anche sulle classi
di test. Quindi, maggiori sono le classi di test, maggiore è l'impegno richiesto dai proces-
si di refactoring.

7.4.1 Eseguire periodicamente il test di copertura


Come appena visto, è molto importante utilizzare appositi tool di analisi della copertura dei
test. Chiaramente si tratta di una condizione necessaria ma non sufficiente. E necessario esegui-
re frequentemente l'analisi della copertura per esaminarne i risultati prodotti. In particolare,
bisognerebbe eseguire l'analisi ogni qualvolta si includano nuove classi, siano queste di codice
o di test. In genere non è necessario eseguire una nuova analisi per ogni singola classe aggiunta
(a meno che questa non sia una classe che include un numero significativo di test); è sufficiente
eseguire una nuova scansione all'aggiunta di nuovi package, dopo sostanziali processi di aggior-
namento/refactoring. Un discorso diverso ovviamente vale qualora si stia cercando di
razionalizzare i test: aggiunta di test atti ad aumentare la copertura, rimozione di test ridondan-
ti, e così via. In questo caso può aver senso eseguire i test di copertura più frequentemente.

7.4.2 Aggiungere nuovi test in maniera oculata


Il livello di copertura del codice da raggiungere è un valore che dipende da diversi fattori, per
esempio il dominio dell'applicazione. Per esempio, è plausibile che dovendo implementare il
sistema di controllo di un reattore nucleare si voglia raggiungere una copertura molto prossima
al 100% (sebbene questo non ne garantisca automaticamente il corretto funzionamento...),
mentre è altrettanto probabile che si accetti un fattore di copertura decisamente più rilassato
quando si implementa un sistema di gestione degli ordini.
In ogni modo, qualora il livello di copertura ottenuto non risulti conforme a quello prestabilito,
è opportuno implementare ulteriori classi di test. Tuttavia, invece di procedere come spesso av-
viene (implementare nuovi metodi in modo randomico, il che aumenta ben poco il livello di
copertura), è consigliabile studiare i risultati dell'analisi di copertura al fine di identificare aree
poco esercitate dai test e che quindi beneficierebbero maggiormente di ulteriori verifiche. Chiara-
mente è opportuno anche fare in modo che aree di particolare interesse/sensibilità presentino un
livello molto elevato di copertura. Nella produzione di test è facile notare come sia abbastanza
semplice e veloce raggiungere coperture dell'ordine del 60-70% e come poi piccoli incrementi
richiedano un elevato impegno. Pertanto è opportuno mirare attentamente le aree oggetto di test.

7.4.3 Valutare la possibilità di rimuovere test ridondanti


La presenza di test ridondanti non è un grosso problema, tuttavia finisce per accrescere l'im-
patto dei processi di refactoring. Questi, durante l'ultimo decennio, sono diventati una prassi
nella pratica lavorativa, principalmente come conseguenza dell'adozione sempre più frequente
di processi di sviluppo del software che incorporano approcci iterativi e incrementali.

7.5 Scrivere test chiari ed efficaci


Alcuni programmatori proclamano di considerare non funzionante, parzialmente o interamen-
te, qualsiasi codice privo di una copertura estesa di test automatici. Sebbene tale affermazione
sia abbastanza estrema, è necessario considerare che la presenza di test scritti rapidamente, in
modo superficiale e senza un'attenta analisi può generare una serie di conseguenze negative,
per esempio la falsa sensazione di aver prodotto un codice perfettamente funzionante. Sebbe-
ne questo problema possa essere minimizzato grazie software atti a misurare la copertura dei
test di unità, non sempre è possibile valutare l'efficacia dei vari test prodotti. In particolare,
non è immediato verificare se i test coprano correttamente scenari di errore, verifichino le
dimensioni delle strutture dati e così via. Pertanto è importante produrre i test in modo che
questi siano veramente utili ed efficaci.

7.5.1 Limitare l'utilizzo dei costruttori nelle classi di test


Nell'implementazione dei test di unità, è necessario limitare l'utilizzo dei metodi costruttori. In
particolare, è opportuno non implementare costruttori:

• di test, per i quali esistono gli appositi metodi; il problema principale è che qualora si
verifichi un problema nel metodo costruttore, questo verrebbe riportato in termini di
un'eccezione invece che del fallimento di una assertion;
• di procedure di inizializziazione dei test; a tal fine sono stati predisposti opportuni me-
todi di setup.

7.5.2 Implementare classi di help


Non è infrequente il caso in cui l'esecuzione di diversi test JUnit richieda di disporre di una
stessa serie di oggetti prepopolati che costituiscono appunto la base del test. In questi casi, al
fine di evitare lavoro tedioso e ridondanze del codice, è opportuno creare delle classi helper o
Object Builder atte proprio a fornire oggetti prepopolati necessari per l'esecuzione dei vari
test. Queste classi, dovrebbero essere implementate a livello di package.
Per esempio, potrebbe risultare necessario disporre di un generatore di "oggetti" di prova di
tipo utente, sia per verificare i metodi della classe stessa, sia per verificare il funzionamento di
altri servizi come quelli di autenticazione, di rilascio di un nuovo profilo, di memorizzazione di
informazioni di auditing, e così via. In effetti, è abbastanza normale che, all'interno di un siste-
ma, una stessa classe dati (DTO/VO) sia utilizzata in diversi contesti. Riportiamo di seguito il
frammento di una classe di help utilizzata per produrre istanze della classe UserDTO:

public class UserTestMelper (

/ * * users data */
public static final Object USERS_DATA[] [] = I
[ "1", UserSlatusDTO.USER_ACTIVE, "123435467", "VeraD" ,
"Vera", "Grigorievna","Dianova", "22-02-1982",
"VG@Dianova.com" ."VeraGD","2342342", 10-01-2006', "10-01-2008" I,
I "2", UserStatusDTO.US£ÌMC77l/£, "454564564", "MarcoM",
"Marco", nuli ."Materazzi", "19-08-1973",
"MarcoMaterazzi@WorldCupChampion2006.com", "MarcoB", "02033423",
"12-02-2006", "15-05-2008" I,

I "3", UserStatusDTO.USEfì_DEACTIVED, "093445943", "AlbertE",


"Albert", nuli,"Einstein", "14-03-1879",
"Albert@Einstein.com","AlbertE", "34567", "06-10-1886", "10-10-1890")

I;

i"
' Return a pre-set user
* @param index index ot the requested user
' ©return the requested user

public static UserDTO getRequestedUser(int index) I

UserDTO userDTO = nuli;

if userDTO
( (index >= -1) && (index < USERS,DATA.Iength) ) {

new UserDT0(
new Long((String)USERS_DATA[index] [0]), // id
(UserStatusDTO) USERS_DATA[index] [1], // status
(String)USERS_DATA[index] [2], // gpn
(String)USERS_DATA[index] [3], // login
(String)USERS_DATA[index] [4], // name
(Strlng)USERS_DATA[index] [5], // middlename
(String)USERS_DATA[index] [6], // surname
getReqDate((String)USERS_DATA[index] [7]), // dob
(String)USERS_DATA[index] [8], // e-mail
(String)USERS_DATA[index] [9], // nick name
(String)USERS_DATA[index] [10], // off tel
getReqDate((String)USERS_DATA[index] [11]), // start
getReqDate((String)USERS_DATA[index] [12]) //end
);

userDTO.addAddress(
new UserAltAddressDTO(
null, UserAltAddTypeDTO.ADDRESS_HOME, "Home Address"));
userDTO.addAddress(
new UserAltAddressDTO(
n u l l , UserAltAddTypeDTO.ADDRESS_MOBILE, "Mobile phone"));
userDTO.addAddress(
new UserAltAddressDTO(
null, UserAltAddTypeDTO.ADDRESS_PAGE, "Pager"));
I
return userDTO;
)

7.5.3 Non variare il disegno del sistema per facilitarne il test


Durante il processo di revisione dei sistemi, può capitare di imbattersi in classi il cui codice
presenti delle stranezze; per esempio, possono esserci metodi che, sebbene siano assolutamente
da dichiarare privati, sono invece pubblici. Spesso si tratta di variazioni di implementazione
effettuate durante la stesura del codice di test per agevolare oltremisura la scrittura dei test di
unità. Si tratta ovviamente di una pratica pessima.
Sebbene il codice debba essere scritto tenendo in mente anche la facilità di verifica, ciò non
significa assolutamente che il disegno sia alla mercé dei test. L'obiettivo principale della scrittu-
ra dei test è aumentare la qualità del sistema prodotto e non di ridurla.

7.5.4 Inizializzare eventuali risorse esterne


Molto spesso il corretto funzionamento di determinati test dipende dalla corretta disponibilità
di risorse esterne quali database, directory, file, etc. In questi casi è opportuno che il codice di
test si occupi sia di inizializzare correttamente tali risorse prima dell'avvio dei test, sia di rila-
sciarle a test avvenuti o in caso di errore.

7.5.5 Verificare tutti gli scenari


Nella scrittura delle classi di test è necessario implementare sia i test positivi (positive test), sia test
negativi (negative test). I primi servono a verificare che, in condizioni corrette (dati in ingresso
validi, sistema funzionante, e così via), l'applicazione sia in grado di produrre i risultati previsti.
Mentre i test negativi servono per verificare che 0 sistema sia robusto, ossia sia in grado di indivi-
duare condizioni anomale, come dati in ingresso non validi, e quindi di eseguire le corrispondenti
procedure di gestione, per esempio il lancio di un'opportuna eccezione, l'esecuzione di un per-
corso alternativo, la ripetizione controllata dell'istruzione che ha generato il problema, etc.
Nella scrittura delle classi di test è quindi necessario verificare anche che, in caso di errore, il
sistema comunichi le eventuali eccezioni previste come mostrato nei seguenti listati. In partico-
lare, tra quelli seguenti, il primo listato mostra mostra la versione funzionante con JUnit 4,
mentre quello successivo mostra l'implementazione necessaria con le versioni precedenti.
Il seguente test ha successo solo se l'eccezione attesa è effettivamente lanciata.

@Test(expected=lndexOutOfBoundsException.class)
public void testlndexOutOfBoundsException() I
ArrayList emptyList = new ArrayList();
Object o = emptyList.get(O);
I

Il seguente metodo di test ha successo solo in caso in cui il sistema lanci l'eccezione attesa (lo
statement catch contiene un assertTrue(true)) mentre fallisce se il servizio non la lancia. Infatti,
dopo l'istruzione successiva all'invio dell'eccezione c'è una fail.
' This tesi veiifies that it an invalid login
1 is specified, then the s y s t e m t h r o w s a proper
" exception

' © t h r o w s Exception - a p r o b l e m o c c u r r e d d u r i n g the test

public void testlnvalidLoginld() throws Exception I

protileBO.tindActiveProfileByLoginld("invalid");
EasyMock. expectLastCallQ. andReturn(null);

userBO.findUserByLoginfinvalid");
EasyMock.expectLastCall().andReturn(null);

replay();

try I

authenticationService.authenticate(createSubject("invalid"));
fail("SecurityException should be thrown");

I catch (MySystemSecurityException secExp) I

assertTrue(lrue);
I
I

7.5.6 Implementare test in grado di verificare i costrutti decisionali


Un aspetto molto importante da verificare sono i costrutti decisionali. In particolare, è oppor-
tuno scrivere diversi test in grado di verificare le combinazioni dei vari percorsi. Questa attività
può risultare decisamente tediosa, in quanto, frequentemente, il numero delle combinazioni
dei percorsi presenti all'interno di ciascun metodo sono elevati. Il lato positivo è che i tool di
copertura sono in grado di rilevare eventuali percorsi intentati e che non è necessario verificare
ogni dettaglio di tutte le classi implementate.

7.5.7 Verificare i cicli


Questa regola è molto simile alla precedente. Tuttavia è importante sottolineare il rilievo di
verificare attentamente i cicli. Verificare che questi non diano luoghi a cicli infiniti, ma che
terminino quando sia previsto.

7.5.8 Verificare i limiti delle strutture dati


Qualora l'oggetto dei test da implementare sia una classe che incapsula strutture dati (tipica-
mente si tratta di Value Object o di Data Transfer Object) è importante verificare la capacità
della stessa classe di gestire correttamente tentativi di violare i limiti delle strutture dati. Le
tipologie delle violazioni sono diverse e quelle applicabili dipendono dall'oggetto utilizzato per
memorizzare la struttura: array, liste, tabelle hash, etc. Per esempio, è possibile eseguire tenta-
tivi di inserire più dati di quelli gestibili, di leggere oltre l'ultimo elemento e così via.
In questo caso la mancanza di opportuni test non è rivelata da tool come quelli di copertura.

7.5.9 Non implementare test con dipendenze temporali


Nella scrittura dei test di unità è importante evitare che il corretto funzionamento di alcuni test
dipenda dall'awenuta esecuzione senza errore di altri. Pertanto i test devono essere scritti
senza effettuare alcuna supposizione circa l'esecuzione degli altri. Ciò è necessario qualora si
utilizzi JUnit, poiché questo non fornisce alcuna direttiva circa l'ordine di esecuzione dei test
inseriti in una classe. Ciò è una diretta conseguenza del fatto che JUnit utilizza i meccanismi
della riflessione Java e che il JDK non fornisce alcuna garanzia circa l'ordine con cui i metodi di
una classe sono restituiti qualora siano riferiti attraverso i meccanismi della reflection.
Ecco di seguito un esempio errato di test ad accoppiamento temporale:

A
public slatic Test suite() {

suite.addTest(new FirstTestCase("TestToPer1ormFirst"));
suite.addTest(new
return suite; SecondTestCasefTestToPerformSecond"));

7.5.10 Valutare attentamente eventuali test con effetti collaterali


È importante valutare attentamente l'implementazione di test in grado di generare effetti
collaterali, che possano per esempio cambiare lo stato del sistema, lasciare dati nel database e
così via, poiché:

• altri test potrebbero fallire in quanto lo stato del sistema è diverso da quello atteso;
• tipicamente è necessario intervenire manualmente per porre nuovamente il sistema in
uno stato neutro.

7.5.11 Implementare test efficienti


Per quanto l'implementazione di test caratterizzati da buone performance non sia di certo un
requisito prioritario nello scrivere i test, è comunque bene puntare a soluzioni con le migliori
prestazioni, quando ciò sià possibile. Sistemi di dimensioni medie e grandi sono generalmente
costituiti da diverse migliaia di classi; a questo numero corrisponde una quantità confrontabile
di test, che solitamente vanno eseguiti molto di frequente durante il processo di sviluppo del
software. Pertanto, ricorrere a soluzioni più efficienti evita che l'esecuzione delle procedure di
test impieghi tempi eccessivi. Chiaramente, scegliere soluzioni efficienti non significa eseguirne
l'over-engineering.
Capitolo 8
Test di integrazione
Introduzione
I test di integrazione rappresentano un'importante attività del processo di sviluppo del software
a carico (almeno parzialmente) del team di sviluppo. Da un punto di vista logico, seguono i test
di unità e precedono quelli di accettazione da parte utente (UAT, User Acceptance Test): dopo
aver verificato il corretto funzionamento dei singoli elementi, è necessario assicurarsi che gli
stessi, una volta assemblati in elementi più complessi, continuino a funzionare correttamente e
implementino correttamente i requisiti.
Il termine di test di integrazione è utilizzato per identificare una vasta gamma di test (figura
8.1): si va dal test di più componenti integrati, fino a giungere all'intero sistema (tanto è vero
che lo stesso UAT è un test di integrazione), includendo i test delle performance e gli stress test.
I test di integrazione rappresentano una parte fondamentale del workflow di test e una buo-
na strategia permette di ottenere importanti vantaggi, come:

• significativo risparmio di tempo e quindi di budget (per esempio è più facile e veloce
individuare, e quindi correggere, i malfunzionamenti del sistema);
• drastica riduzione dei costi dovuti ai difetti;
• miglioramento del livello qualitativo del sistema.;
• aumento del livello di soddisfazione degli utenti

Tuttavia, l'implementazione di un'efficace strategia di test non è assolutamente un'attività


immediata, soprattuto nel contesto di processi moderni di sviluppo del software iterativi e
incrementali (iterative and incrementai) dove anche questa attività deve ubidire ai principi tipici
di tale approccio. Nei tradizionali processi di sviluppo del software basati su una strategia a
cascata iwaterfall), specialmente nelle loro versioni iniziali, tutto era (concettualmente) più
facile per via del fatto che questi prevedevano un sola fase di test, rigidamente pianificata nel
periodo precedente la consegna. Sebbene questa fosse una strategia sconsigliabile per tutta una
serie di importanti motivi (feedback degli utenti fortemente posticipato, ridotta capacità di
gestione dei rischi, enorme costo di correzione dei difetti, etc.), offriva il magro vantaggio reltivo
la semplificazione dell'iniziale pianificazione di tali test. I moderni processi di sviluppo del
software, invece, sono basati su strategie di tipo iterativo e incrementale e prevedono che l'inte-
ro progetto sia organizzato in una serie di sottopogetti ognuno dei quali genera una nuova
versione del sistema sottoposta alla valutazione degli utenti. Pertanto, mentre una parte del
team procede all'implementazione dei requisiti della nuova fase, un altro gruppo di sviluppatori
segue i test della fase precedente. Questa strategia, sebbene risolva tanti dei problemi tipici
degli approcci tradizionali, crea una serie di nuove problematiche, come per esempio la mag-
giore difficoltà di pianificazione/gestione dei test di integrazione, di coordinamento tra le varie
fasi e tra team. In particolare i test devono essere rivisti ad ogni iterazione generando la neces-
sità di scriverne di nuovi, di aggiornare preesistenti e spesso anche di rimuovere alcuni resi
inutili/ridondandi dalla nuova versione del sistema. Pertanto, l'adozione di approcci iterativi e
incrementali richiede di individuare un'adeguata strategia capace di bilanciare la necessità di
verificare completamente il sistema e la riduzione del tempo utilizzato per l'implementazione
di test transitori.
A questo va poi aggiunto un altro classico problema spesso presente in molti progetti: il
tempo dedicato ai test di sistema... Spesso non viene allocato un sufficiente lasso di tempo,
oppure accade che il tempo inizialmente previsto sia consumato da eventuali ritardi di
implementazione. La logica consequenza è che può succedere di di mettere in esercizio un
sistema non sufficientemente testato con tutte le conseguenze del caso.

Obiettivi
L'obiettivo di questo capitolo è fornire una serie di suggerimenti utili e linee guida relative alla
delicata fase dei test di integrazione. Come visto in precedenza, si tratta di un'attività che,
almeno logicamente, si colloca tra i test di unità e quelli di accettazione da parte degli utenti.
Pertanto, questo capitolo rappresenta la logica continuazione di quello precedente sia perché
almeno una buona parte dei test di integrazione sono a carico del team di sviluppo, sia perché
molti dei suggerimenti presentati nel capitolo percedente mantengono la loro validità anche in
questo contesto.
I test di integrazione, presentano inoltre molte analogie con gli UAT (che in effetti rappre-
sentano il test di integrazione finale, in un ambiente molto simile a quello di produzione con
firma finale di accettazione da parte degli utenti). Questo perchè si inizia con il verificare che
un certo numero di classi/componenti, una volta assemblate insieme, funzionino correttamente
e si procede ad assemblare fino a giungere alla verifica dell'intero sistema, verifica che è oggetto
fondamentale degli UAT. Pertanto, anche se i test di integrazione, almeno nelle sue versioni
finali, somigliano molto agli UAT, esistono importanti differenze, quali:

• principali utenti: nel primo caso questi sono responsabilità del team di sviluppo (anche
se questa responsabilità può essere delegata) mentre nel secondo si tratta di una rappre-
sentanza degli utenti finali;
• strategia: i test di integrazione devono copiare da vicino l'andamento del progetto;
• finalità: promozione all'ambiente di staging nel primo caso, accettazione da parte degli
utenti nel secondo.

L'argomento dei test di integrazione è molto vasto, ma qui viene trattato solo limitatamente
alle esigenze minime del team di sviluppo. In particolare, l'attenzione è focalizzata sui test di
integrazione interni al team di sviluppo (IST, Internai, figura 8.1). Alcuni suggerimenti presenti
in questo capitolo finiscono per essere più di carattere manageriale che implementativo. Tutta-
via si è deciso di inserirli lo stesso, sia per favorire una migliore comprensione dell'argomento,
sia per fornire ai lettori una serie di linee guida utili per i propri progetti.

Catalogazione dei test di integrazione


Dallo studio della letteratura specifica si nota subito un certo disorientamento circa la
nomenclatura e struttura delle diverse tipologie di test di integrazione. Questa confusione è poi
spesso accentuata nei vari ambienti professionali. Pertanto, prima di procedere oltre, si è deciso
di presentare una catalogazione razionale di questo insieme di test (figura 8.1). In primo luogo,
i vari test di integrazione possono essere suddivisi in due grandi categorie: funzionali e non-
funzionali. I primi si occupano di verificare che i servizi offerti dal sistema siano corretti, ossia
implementino effettivamente i relativi requisiti, mentre i secondi focalizzano l'attenzione su aspetti
di carattere non funzionale, come performance, sicurezza, fault-tolerance, etc. I test funzionali
poi si suddividono in test a carico del team di sviluppo (IST, Development) e i test UAT. Come
visto in precedenza, questi sono dei particolarissimi test di integrazione il cui superamento rap-
presenta la condizione necessaria ma non sufficiente per mandare il sistema in produzione.
I test di sviluppo, a loro volta, prevedono quattro specializzazioni: interni, esterni, sanity check e
smoke test. Questi ultimi due sono argomento del paragrafo successivo. I test di integrazione inter-
ni verificano più componenti integrati insieme, i singoli servizi, fino a giungere all'intero sistema (o
sottosistema), ma senza coinvolgere strutture esterne, quali database, sistemi di messaggistica e
altri componenti del sistema. La loro presenza, invece, caratterizza degli IST test veri e propri.
Entrambe le tipologie sono a carico dei team di sviluppo utilizzati a fini implementativi.

1ST
Integration System Test i

F u n c t i o n a l 1ST N o n f u n c t i o n a l IST

UAT
j I 1ST-Development j 1ST - P e r f o r m a n c e 1ST-Stress Test
User A c c e p t a n c e e s t

Sanity Check

Figura 8.1 - Una classificazione dei test di sistema.


Sanity Check e Smoke Test
Smoke Test (test di fumo) e Sanity Check (test di sanità) sono due particolari esempi di test di
integrazione spesso confusi tra loro. Ciò è dovuto al fatto che queste due tipologie di test,
sebbene differenti dal punto di vista semantico, presentano forti analogie.
Smoke test rappresenta un test non esaustivo (e pertanto "superficiale") atto a verificare che
le funzionalità principali del sistema non vengano compromesse da recenti variazioni del codi-
ce. Come tale è tipicamente utilizzato non appena nuove porzioni di codice sono integrate
(check-in) nel sistema di gestione dei sorgenti (CVS, SVN, ClearCase, etc.). Chiaramente si
tratta di un meccanismo molto utile in presenza di sistemi di dimensioni medio-grandi.
Il nome smoke test deriva da una pratica spesso usata in passato nell'ingegneria elettronica.
In particolare, dopo aver cambiato o riparato alcuni componenti, questi venivano riassemblati
nella scheda di provenienza e quindi alimentati. Il test passava, qualora il componente non si
bruciava e quindi non produceva fumo; falliva, negli altri casi.
Il sanity check, sebbene presenti alcune similitudini con lo smoke test (la più importante è
che il suo obiettivo è verificare il corretto funzionamento delle sole principali funzionalità), ha
anche caratteristiche fondamentali che lo contraddistinguono. In particolare questo è tipica-
mente utilizzato per verificare che la nuova versione del sistema sia pronta per ulteriori test
(qualora sia eseguito su un sistema installato in un ambiente di test) o che sia stata correttamen-
te installata e configurata nell'ambiente di produzione. Quindi smoke test si presta ad essere
utilizzato come controllo dei sorgenti mentre il sanity check per la verifica di nuovi deployment.
I dettagli relativi a questi test sono molto interessanti, ma esulano dagli obiettivi del capitolo.

Alcuni tool
Presentiamo brevemente alcuni tool, come punto di partenza, demandando ai lettori ulteriori
approfondimenti. In primo luogo JUnit mantiene completamente la propria validità. Tuttavia,
in questo contesto la semplicità che lo contraddistingue finisce per essere un problema: si tratta
comunque di un tool nato per implementare i test di unità. Per sopperire a questa carenza di
feature, esiste tutto un fiorire di framework/estensioni il cui scopo è risolvere specifici proble-
mi. I tool più interessanti sono:

• TestNG (http://testng.org/doc/) disegnato per unire il meglio di JUnit e NUnit introducen-


do al tempo stesso importanti features che lo rendono capace di risolvere elegantemente
le esigenze di diversi tipi di test (unit, funzionali,integrazione, di sistema, e cosi via).
Alcune delle nuove features sono: esteso utilizzo delle annotazioni, elevato grado di
flessibilità di configurazione, avanzato modello di esecuzione non più basato sui TestSuite,
supporto dei test guidati dai dati, e così via.
• DBunit (http://dbunit.sourceforge.net/) che, come suggerisce il nome, è un'estensione a JUnit
atta a semplificare l'implementazioni di test che prevedono l'accesso al database come
passo fondamentale del processo di verifica. In particolare, DBUnit fornisce una serie di
interessanti feature come: importazione/esportazione automatica dati nel e dal database
attraverso opportuni file XML. Ciò permette di implementare rapidamente i classici
passi che ogni sviluppatore eseguirebbe manualmente: set-up del database, esecuzione
di una serie di servizi, verifica visuale basata su un predefinito numero di SELECT.
• JunitEE (http://www.junitee.org/) progettato per semplificare il test di applicazioni Java
EE. In particolare, questo implementa una versione di TestRunner in grado di emettere
l'output in formato HTML-XML e una servlet da utilizzare per agganciare i propri test.
• HttpUnit (http://httpunit.sourceforge.net/). Questa estensione è stata disegnata per imple-
mentare una serie di test di siti web focalizzando l'attenzione sulle comunicazioni HTTP.
• htmlUnit (http://htmlunit.sourceforge.net/). Anche questa estensione è stata disegnata per
verificare il funzionamenti di siti web, in questo caso però l'attenzione è focalizzata sulla
gestione di pagine HTML. In particolare, vi sono feature atte a "complilare" eventuali
form, simulare "click" del mouse, etc.
• xmlUnit (http://xmlunit.sourceforge.net/). Questa estensione fornisce una serie di feature
assolutamente utile per il test e la manipolazione di formati XML.

Un altro tool concepito come estensione di JUnit è FIT (http://fit.c2.com/). Tuttavia in questo
caso si tratta di un vero e proprio tool la cui peculiarità risiede sulla logica di funzionamento
basata su tabelle HTML. Ciò lo rende un valido strumento anche per l'implementazione di
UAT. In particolare, il tool richiede che ad ogni tabella corrisponda un'implementazione nota
come "correzione" (fixturé) scritta dagli sviluppatori. La tabella è organizzata nel seguente modo:

1. la prima riga è utilizzata per dichiarare la particolare fixture in grado di interpretare la


tabella;
2. la seconda riga riporta i nomi dei parametri e il metodo da invocare per produrre il
risultato da verificare;
3. le restanti forniscono le «-pie (inputl, input 2, ..., input k, output).

Un altro tool degno di nota è Cactus. Si tratta di un framework di test disegnato apposita-
mente per verificare componenti web come Filters, pagine J S P e Servlet supportando il test
all'interno dei container. Come tale si presta a verificare — indirettamente (qualcosa dovrà pur
generare i dati da mostrare in pagine HTML!) — sia componenti EJB, sia tag libraries.
Un tool particolarmente efficace nella realizzazione di test automatici per thin UI è Selenium
(http://selenium.openqa.org/). I test implementati con Selenium girano direttamente nei browser
simulando fedelmente un utente reale. Il principio di funzionamento si basa su una serie di
JavaScript e ¡Frames al fine di includere il motore di automatizzazione dei test nel browser.
Infine, come lecito attendersi, questa è la fase in cui la necessità di implementare classi mock
raggiunge i massimi livelli. Pertanto, tool quali EasyMock, MockEJB, Mock Objects, Mock
Creator, jMock, Mock Runner, e chi più ne ha più ne metta, trovano largo utilizzo.

Direttive
8.1 Investire negli use case e test case
Sebbene dovrebbe essere ormai chiaro il motivo per cui sia importante investire nei test di
integrazione, alcuni lettori potrebbero trovare l'associazione con i casi d'uso non immediata;
tuttavia, come riportato di seguito, i due manufatti sono strettamente correlati. In effetti
[TSTDSG] mentre i casi d'uso descrivono esattamente cosa dovrebbe fare il sistema, soprat-
tutto dal punto di vista dei servizi da fornire, i test case definiscono le procedure necessarie per
verificare che il sistema implementi correttamente quanto dichiarato nei casi d'uso. Inoltre,
qualora gli use case siano chiari, completi e accurati, il processo di scrittura dei test case risulta
assolutamente meccanico [UMLING], mentre, negli altri casi, la loro generazione permette di
individuare lacune, imprecisioni e aree ambigue. Quindi la redazione dei test case rappresenta
un'ottima occasione per effettuare una revisione formale dei casi d'uso. Infine, i casi d'uso sono
indubbiamente una delle migliori notazioni per analizzare e documentare i requisiti funzionali
dei sistemi, tanto da essersi agiudicato lo status di standard de facto. Una caratteristica che li
rende veramente unici consiste nel favorire l'analisi e la documentazione sistematica dei requi-
siti di correttezza e robustezza del sistema. I primi sono assicurati dalla presenza dello scenario
principale e di quelli alternativi, ossia quelli in cui tutto funziona correttamente e pertanto il
sistema è in grado di erogare correttamente il servizio. Il requisito di robustezza del sistema (in
termini di servizi business) è ottenuto per mezzo degli scenari di eccezione. Questi descrivono
sia le condizioni dei casi anomali, sia le misure che il sistema deve attuare per gestirli.
La logica consequenza è che, dato un insieme di casi d'uso, è possibile derivare direttamente
e meccanicamente i relativi test case. Questi, a loro volta, rappresentano ottimi requisiti per
implementare sistemi automatici per testare il sistema. Tali sistemi rappresentano ottimi ausili
per ogni nuova release del sistema che può essere sottomessa ad un test esaustivo in grado di
neutralizzare immediatamente un gran numero di eventuali malfunzionamenti. Da notare che i
requisiti funzionali restano la principale fonte di informazione per il test del sistema.

8.1.1 Valutare la possibilità di implementare un sistema di test


Uno dei problemi classici di ciascun sistema sta nel verificare che il sistema implementi corretta-
mente i propri requisiti. La più semplice, e decisamente meno efficace, delle strategie consiste
nell'eseguire i test manualmente: si invocano dei metodi e quindi si analizza il risultato, magari
accedendo al database... Tale approccio, oltre a soffrire di una serie di evidenti problemi (alto
costo, difficile riproducibilità, curva di apprendimento del personale, e così via), presenta anche
il grande svantaggio legato a processi di refactoring/rilascio del sistema. In effetti, ogni nuova
versione che non sia limitata a piccoli bug fixing, tende ad azzerrare tutti i test eseguiti fino a quel
momento sul sistema. Pertanto, in presenza di test manuali, si causa uno spreco totale di risorse.
Una strategia decisamente migliore, in grado di risolvere molti degli svantaggi presentati dal
precedente metodo, consiste nell'implementare un sistema automatico capace di verificare il
corretto funzionamento del sistema alla presenza di ogni nuova versione. La presenza di questi
test fa sì che il sistema generato non sia già legacy nel momento in cui è prodotto ma fornisce ai
team di sviluppo strumenti fondamentali per far crescere il sistema.
Poiché questo sistema è a sua volta un progetto software a tutti gli effetti, presenta dei propri
requisiti funzionali: l'insieme dei test case del sistema da verificare deve inoltre obbedire a un
piano che, ad ogni release, tenga conto dei casi d'uso implementati dal sistema da verificare.
Sistemi del genere sono di immediata implementazione eccezion fatta per le GUI che risultano
ostiche da verificare automaticamente. Infine, l'implementazione di questi sistemi rappresenta
un tipico task che si presta, senza grandissimi rischi, ad essere assegnata a team off-shore.

8.2 Gestire correttamente i vari progetti di test


Ogni progetto di dimensioni medio-grandi richiede l'implementazione di un insieme di pro-
getti di test (come illustrato in figura 8.1), veri e propri sistemi eseguibili di grandissimo valore
per il successo del programma. Il problema è che alcune di queste tipologie di test presentano
delle differenze, spesso così sottili che difficilmente vengono comprese dai vari sviluppatori.
Tuttavia è importante che i vari membri del team di sviluppo siano sincronizzati sulle varie
differenze tra questi progetti, sappiano quali servizi di test implementare, in quale progetto
inserirli, e così via.
Le batterie di test sono veri e propri progetti a sé di importanza fondamentale. Questi sono
indubbiamente importanti durante i primi rilasci del sistema, e, contrariamente alle tipiche
credenze, diventano ancora più indispensabili durante le fasi successive. Ciò perché è di fonda-
mentale importanza assicurarsi che le nuove versioni non compromettano il corretto funziona-
mento dei servizi già consegnati, sia in termini di funzionalità sia in termini di caratteristiche
non funzionali.
Presentiamo di seguito una serie di direttive molto pratiche per aiutare a comprendere le
demarcazioni fra i vari progetti di test.

8.2.1 Non sovraccaricare i test di unità


Molto spesso diverse tipologie di test vengono implementate utilizzando direttamente JUnit.
Anche se ciò apparentemente non sembrerebbe essere un problema, in realtà questa soluzione
presenta alcuni effetti collaterali. In primo luogo, JUnit da solo non è adatto per ogni tipologia
di test. Pertanto il suo utilizzo al di fuori dei test di unità richiede l'implementazione di una
serie di servizi che altri framework forniscono di base. Un altro grosso problema, inoltre, deriva
dall'errata associazione mentale che si genera nella mente degli sviluppatori: "si utilizza JUnit,
quindi si tratta di test di unità". Questo spesso porta ad alcune controindicazioni. Per esempio
si finisce con il sovraccaricare i test di unità a discapito degli altri, generando tutta una serie di
problemi, come test di unità troppo pesanti e lunghi da eseguire, test di integrazione troppo
leggeri, e così via. I test di unità dovrebbero limitarsi a singole classi fino a giungere a singoli
componenti/package.
Ora, un'altra importante parte dei test ohe il team di sviluppo dovrebbe implementare è
costituito dai test IST interni ove i componenti vengono via via assemblati tra loro e sottoposti
a test. Anche se questi, per essere precisi, appartengono ai test di integrazione, possono essere
tranquillamente inseriti tra i test di unità. Importantissimo però è assolutamente non includere
sistemi esterni come il database, il sistema di messaggistica, altri sistemi, etc. che vanno simulati
con appositi oggetti/componenti di mock-up.
La presenza di elementi esterni, come il database e il sistema di messaggistica, sono parte dei
test IST esterni e come tali vanno allocati in un altro progetto.

8.2.2 Implementare i test di integrazione esterni


La presenza dei test precedenti è la classica condizione necessaria ma non sufficiente: i test di
unità e di integrazione interni si occupano di verificare che i singoli elementi funzionino corret-
tamente e che, una volta assemblati in entità più grandi, implementino correttamente i requisi-
ti. Tuttavia non rappresentano ancora un banco di test dove il sistema è installato e quindi
sottoposto a veri e propri stimoli esterni attraverso i canali previsti. Questo è l'obiettivo dei test
di sistema esterni: il sistema (o sotto-sistema) viene installato nel relativo ambiente di test e
quindi viene sottoposto a un processo automatico di verifica utilizzando tutti gli elementi ester-
ni richiesti: sistema di messaggistica, database, etc.
La differenze tra questi test e i precedenti, non è solo relativa ai confini del sistema, ma anche
a quando eseguirli e al tempo a disposizione per la relativa esecuzione. In particolare, i test
precedenti (unità e IST interni) devono essere eseguiti durante lo sviluppo e assolutamente
prima di integrare il proprio test nel sistema di versionamento. Ciò fa sì che siano eseguiti
molto frequentemente, e ne consegue l'esigenza di poter essere eseguiti velocemente. I test di
integrazione esterni, invece, devono verificare accuratamente il corretto funzionamento del
sistema. Pertanto, possono richiedere molto più tempo fino alla classica esecuzione notturna.
Chiaramente, questi devono essere eseguiti dopo aver integrato del codice nel sistema di
versionamento.

8.2.3 Valutare il ricorso all'utilizzo dei sanity check


Molto spesso, soprattutto in progetti di medio-grandi dimensioni, accade di avere dei test di
unità e di integrazione interni molto veloci da seguire ma in grado di verificare solo il corretto
funzionamento dei componenti interni, e poi di disporre di test di integrazione che, per poter
verificare il corretto funzionamento dell'intero sistema, finiscano per richiedere molto tempo.
Per questo motivo, molto frequentemente finiscono per essere eseguiti di notte.
In queste situazioni, per evitare di attendere la notte per verificare il corretto funzionamento
del sistema e per non appesantire troppo i test di unità, spesso si decide di ricorrere ai sanity
check (test di integrazione esterni atti a verificare il corretto funzionamento di un numero molto
limitato di servizi). I sanity check pertanto rappresentano un buon compromesso tra l'esigenza
di mantenere i test di unità leggeri e quella di iniziare a verificare più in dettaglio i vari servizi.

8.3 Gestire correttamente i vari ambienti


Come organizzare i vari ambienti? Per quanto insolito possa sembrare, non è infrequente il
caso in cui i vari ambienti (sviluppo, IST e UAT) possano condividere importanti risorse (come
per esempio database) creando non pochi problemi. Uno dei più classici consiste nell'assistere
al fallimento di importanti e prolungati test di integrazione per via del fatto che, avendo predi-
sposto uno schema del database condiviso tra gli ambienti di integrazione e di UAT, il test di
integrazione in corso viene alterato da un altro eseguito da un altro utente. Oppure, che impor-
tanti informazioni presenti nel database necessarie per eseguire ulteriori test o per investigare
cause di fallimento, siano eliminate/alternate da test di altra natura.

8.3.1 Assegnare ad ogni sviluppatore il proprio ambiente


Non è una novità: ogni sviluppatore deve disporre di un proprio ambiente, incluso database,
per poter eseguire il propri test in isolamento senza essere disturbato dai test eseguiti da altri
sviluppatori o da processi automatici, e senza intralciare, a sua volta, gli altri.

8.3.2 Mantenere gli ambienti di IST e UAT completamente divisi


Analogamente a quanto prescritto nel caso precedente, è di fondamentale importanza far si
che gli ambienti IST e UAT siano per quanto possibile separati. Mentre, per ovvi motivi econo-
mici, non è sempre possibile disporre di tutti i server necessari e di diversi network per mante-
nere i vari ambienti completamente separati anche dal punto di vista fisico, è assolutamente
necessario salvaguardare almeno la separazione di alcune risorse fondamentali come il database.

8.4 Pianificare correttamente i test di integrazione


Il disegno della stategia di integrazione è un'altra attività estremamente improtante per i pro-
getti software. Mentre nei processi tradizionali era sufficiente allocare il doppio del tempo
ritenuto necessario per tali test, nei processi moderni di tipo iterativo e incrementale la situa-
zione è notevolmente più complessa, poiché questi vanno ripetuti, rivisti, manutenuti, etc.
Chiaramente, la strategia di integrazione è largamente subordinata al processo di sviluppo
del software utilizzato e al progetto elaborato dal Project Manager. Per esempio, qualora il
progetto preveda di dedicare le prime milestone al rilascio del'archtitettura, è chiaro che la
strategia di integrazione, volenti o nolenti, debba riflettere tale approccio. Ugualmente, qualo-
ra il piano devesse prevedere di rilasciare interamente il servizio X, prima di procedere con altri
servizi, la strategia di integrazione dovrebbe procedere nella stessa direzione. I test di integra-
zione devono "copiare" il progetto studiato dal Project Manager. Detto ciò, è altresì importan-
te notare che, tipicamente, all'interno degli orizzonti posti dal progetto, è sempre possibile
trovare degli spazi per la strategia di integrazione.

8.4.1 Selezionare una corretta strategia per i test di integrazione


Le strategie per eseguire i test di integrazione sono essenzialmente due:

1. Top-down (dall'alto verso il basso). Questo approaccio consiste nell'inziare a verificare


il funzionamento dei moduli a più alto livello per poi scendere. Il principio alla base di
questo approccio sta nell'eseguire, il prima possibile, i controlli dei principali flussi e
servizi gestiti dal sistema. Sebbene questo approccio offra evidenti vantaggi, presenta
anche alcuni inevitabili svantaggi. Per esempio non è molto compatibile con la filosofia
di cominciare, appena possibile, a rilasciare progressive versioni del sistema. Un altro
piccolo svantaggio consiste nel dover creare un numero spesso significativo di oggetti
di mock-up per simulare gli strati sottostanti. Questo problema, tuttavia, può essere
ridotto utilizzando opportuni tool atti a generare rapidamente oggetti di mock-up (come
visto nel capitolo precedente). Inoltre è sempre molto utile disporre di test focalizzati
sui singoli livelli del sistema considerati isolatamente.
2. Bottom-up (dal basso verso l'alto). Come è lecito attendersi, si tratta dell'approccio op-
posto al precedente e quindi è basato sul procedere dall'integrazione dei livelli sottostanti
via via verso l'alto. I vantaggi di questo approccio pertanto sono relativi al fatto che si
minimizzi la necessità di creare oggetti di mock-up e che, entro certi limiti, sia possibile
eseguire iniziali rilasci dell'applicazione. In effetti, una volta che i vari livelli sottostanti
siano disponibili, è possibile iniziare a rilasciare servizi completi e non semplicemente
simulati. Gli svantaggi invece sono relativi al fatto che spesso l'implementazione di livelli
di test intermedi richiede di implementare logiche complesse e quindi si finisce con il
rimandare sempre l'avvio dei test relativi ai servizi/flussi principali.

Come spesso accade in presenza di approcci diametralmente opposti, come i due testé espo-
sti, è possibile utilizzare approcci ibridi. In questo caso tale approccio è spesso denominato a
ombrello per via del fatto che ci si focalizza su specifiche aree (quelle "coperte dall'ombrello").
Si tenta di beneficiare dei vantaggi forniti da entrambi gli approcci, mitigandone gli svantaggi.
In particolare, si cerca di sviluppare appena possibile determinati servizi/flussi di dati partendo
dal basso. I vantaggi sono che si conferisce enfasi agli scenari più importanti/critici, la possibi-
lità di dar luogo a iniziali consegne, il minimizzare la necessità di implementare oggetti di mock
up, etc. D'altro canto però, c'è sempre un certo ritardo nell'iniziare a verificare i principali
servizi, si ha una ridotta enfasi nella realizzazione dell'architettura, etc.
8.4.2 Allocare e mantenere un sufficiente lasso temporale
Questa regola è una sorta di riassunto di quanto espresso in precedenza. I test di integrazione
rappresentano una fase importantissima del processo di sviluppo del software. Pertanto è di
fondamentale importanza assicurarsi di disporre di un tempo sufficiente per eseguire questi
test. Spesso accade che il tempo allocato sia decisamente sottostimato o che questo venga
depauperato dai ritardi di sviluppo finendo con l'annullare tutti i vantaggi della strategia di test.
Inoltre, mentre i processi tradizionali prevedevano che questa fase fosse eseguita solo alla fine
dell'implementazione, quelli più moderni, iterativi e incrementali, prevedono che i test siano
eseguiti svariate volte, almeno alla fine di ogni iterazione. L'implementazione di test di integra-
zione automatici ovviamente fornisce un validissimo supporto.

8.4.3 Assicurarsi che i test di integrazioni evolvano pari passo con il progetto
Questa regola è una logica conseguenza delle strutture sempre più iterative e incrementali dei
processi moderni. Pertanto è molto importante che la strategia di test (ed eventualmente il
relativo sistema) sia assolutamente affine a quella del progetto. Inoltre, la sua pianificazione
deve seguire passo passo quella del progetto al fine di eseguire tutti i necessari aggiustamenti al
piano per salvaguardare il tempo necessario.
Infine, è necessario che la strategia di test tenga conto delle diverse versioni del sistema e
delle relative pianificazioni. Per esempio, se la versione O.x del sistema è orientata al rilascio
parziale dei servizi x e y, allora il relativo test di integrazione, per la stessa data, deve prevedere
che tali versioni di servizi siano verificabili. Inoltre, è importante assicurarsi di rivedere i test in
funzione del rilascio: questo perchè in un rilascio successivo potrebbe rendersi necessario cor-
reggere il sistema, per esempio i servizi x e y vengono rilasciati nella loro interezza e quindi i
test devono prevedere le necessarie correzioni.

8.5 Verificare gii elementi principali


Mentre nel caso dei test di unità è chiarissimo cosa verificare (o almeno dovrebbe esserlo),
spesso la situazione non è altrettanto chiara nel caso dei test di integrazione. In questa fase è
necesario focalizzare l'attenzione su servizi esposti, ciclo di vita delle principali entità business
e meccanismi di delegazione e cascading.

8.5.1 Verificare tutti i servizi esposti dal sistema


Questa regola dovrebbe essere fin troppo ovvia, tuttavia non è infrequente il caso in cui i test di
integrazioni siano interamente focalizzati sui principali servizi trascurandone altri secondari.
Sebbene la logica alla base di tale strategia sia assolutamente condivisibile, è altrettanto impor-
tante ricordare una regola d'oro: tutto il codice ditribuito (di cui si effettua il deployment), e
quindi a maggior ragiore tutti i servizi esposti, devono essere sottoposti a test intensivi. Qualora
ciò non sia possibile per questione di budget/tempo, è cosnigliabile valutare la possibilità di
non abilitare i servizi non verificati accuratamente. Probabilmente è meglio ritardare la conse-
gna di qualche servizio secondario invece che correre il rischio di figure imbarazzanti.

8.5.2 Verificare il ciclo di vita delle principali entità business


Nella stragrande maggioranza dei sistemi software è possibile identificare una serie di entità
business il cui ciclo di vita è sancito da un ben definito insieme di stati e di stimoli. Una buona
pratica consiste nel rappresentare queste macchine a stati finiti attraverso opportuni diagram-
mi di stato. Alcuni esempi sono un biglietto areo, un trade, una richesta di quotazione, un
thread in esecuzione etc. La rappresentazione di queste macchine a stati finiti è molto impor-
tante non solo per l'implementazione, ma anche per la codifica dei test di integrazione. Infatti,
è importante implementare insiemi di test in grado di verificare:

• il corretto funzionamento con input corretti (stimoli ammissibili per il determinato stato)
• la capacità del sistema sia di riconoscere input non corretti e sia di intraprendere le
procedure di gestione previste.

Per quanto attiene al primo punto, c'è da notare che un test esaustivo è molto oneroso e
spesso impossibile. Questo è il caso in cui la macchina a stati preveda dei cicli.
Tuttavia, è consigliabile rappresentare l'evoluzione dell'entità attraverso una matrice in grado
di rappresentare il prodotto cartesiano (insieme stati x insieme eventi) —> stato. Ciò va operato
tenendo conto sia del fatto che nell'insieme degli stati vanno considerati anche uno o più stati di
errore, sia che è assolutamente necessario definire l'array degli stati iniziali (eventualmente ridot-
to a un elemento). Ciò rende possibile implementare un'applicazione di test in grado di proces-
sare la matrice al fine di generare i necessari test di integrazione, eliminando cicli infiniti.

8.5.3 Verificare i principali meccanismi di delegazione e cascading


Ogni qualvolta si implementa un sistema software, è necessario affidarsi a diversi meccanismi di
delegazione e cascading. Il corretto funzionamento di questi meccanismi è fondamentale per il
corretto funzionamento dei servizi esposti. Pertanto, è assolutamente necessario implementare
appositi test che verifichino l'effettivo funzionamento di questi neccanismi. Per esempio, si con-
sideri uno scenario abbastanza comune di un sistema che riceva dei dati via un sistema di
messaggistica. In questo caso, è necessario assicurarsi le capacità del sistema di individuare possi-
bili scenari di optimistic locking e di gestirli correttamente. Una possibilità, per esempio, consiste
nel non fornire l'acknowledge del messaggio e quindi di richiederne la riconsegna posticipata.

8.6 Test di applicazioni Java EE


Tutti coloro che hanno implementato applicazioni Java E E sanno bene quanto difficile sia
implementare i relativi test di itegrazione.
Con l'introduzione della versione EJB 3 la situazione è drastricamente migliorata, i compo-
nenti somigliano sempre di più ai vecchi oggetti Java (POJO), l'Inversion Of Control facilita
l'impostazione dell'ambiente, etc. Tuttavia, verificare componenti dispiegati (deployed) negli
application server è ancora problematico.

8.6.1 Verificare i singoli strati (layei) isolandoli dagli altri


Ogni applicazione Java E E che si rispetti è organizzata in strati, i più comuni sono:

• Presentazione. Questo livello è tipicamente scomposto in funzione delle diversi tipi di


protocolli e di UI gestiti. Alcuni esempi sono: thin UI che include Servlet, J S P e handler
per interfacce HTML, MDB utilizzati per gestire messaggi JMS, etc.
• Logica Business tipicamente suddivisa in servizi business, oggetti business. Il primo
contiene servizi business, mentre il secondo rappresenta complessi oggetti business come
trade, ordini, etc.
• Integrazione. Anche questo strato è tipicamente suddiviso in tanti sotto-strati quante
sono le tecnologie da integrare (database, messaggistica, risorsa LDAP, etc.).

Caratteristica peculiare di queste architetture è la stretta dipendenza di uno strato da quello


immediatamente sottostante: il livello di presentazione dipende da quello dei servizi business,
quest'ultimo dipende dallo strato degli oggetti business, e così via.
Una parte importante dei test di integrazione consiste nel verificare che i singoli strati funzio-
nino correttamente in isolamento. Per ottenere ciò, è necessario focalizzare l'attenzione su un
determinato strato e quindi implementare opportuni oggetti atti a simulare lo strato sottostante.
Come al solito, questo non è l'unico test di integrazione, ma ne rappresenta una parte importante.

8.6.2 Implementare dei test al di fuori del container (out-of-containei)


Implementare dei test fuori dal container è un'attività molto utile per sistemi Java EE. Questo
per una serie di importanti motivi. Per esempio: implementare test all'interno del container è
un'attività complessa richiede script e deployment particolari, l'esecuzione dei test è molto labo-
riosa e richiede molto tempo, implementare molti test ha un'incidenza significativa sul budget.
Con l'introduzione di EJB 3, gli enterprise JavaBeans sono divenuti POJO. Come tali si pre-
stano ad essere testati al di fuori del container. Al fine di rendere ciò possibile è necessario trarre
0 massimo vantaggio da parte dell'introduzione dell'inversione di controllo. Quindi non deve
essere il componente che si "cerca" le necessarie risorse, ma un'entità "esterna" (l'application
server o il framework di test) deve impostarle. Tutto ciò però è la classica condizione necessaria
ma non sufficiente. Infatti, gli application server sono utilizzati proprio perché forniscono tutta
una serie di servizi, come la gestione del multithreading, del ciclo di vita, della transazionalità e
così via. Tuttavia, per i fini dei test funzionali, è spesso sufficiente utilizzare delle implementazioni
leggere fornite dai software dedicati, come per esempio MockEJB (http://www.mockejb.org/).

8.7 Investire sui test della GUI


Implementare o meno i test della GUI (Graphic User Interface, interfaccia utente grafica) è un
dilemma che a lungo tempo ha tenuto banco nelle discussioni di molti addetti ai lavori. Da un
punto di vista teorico l'intera comunità concorda con l'importanza di implementare tali test. In
alcuni ambienti estremi, come per esempio i front-office delle banche di investimento, le GUI
devono essere in grado di supportare migliaia di messaggi per frazioni di secondo, e contempora-
neamente gestire gli stimoli dell'utente che magari ha la necessità di prenotare rapidissimamente
un trade da decine di milioni di dollari. In questi contesti, abbastanza estremi, si immagini quali
ripercussioni possano creare alcuni malfunzionamenti, come per esempio frapposizioni di prezzi
e race condition. Nonostante questo consenso almeno in linea teorica, all'atto pratico la situazio-
ne si complica, complici anche i primi tentativi di costruire dei tool di supporto all'implementazione
automatica di tali test. Tutti coloro che si sono trovati a implementare delle GUI dei sistemi di
front-office testé menzionati cominciando più di dieci anni fa, sanno benissimo a quali pene si
andava in contro nel costruire sofisticati sistemi di test. In effetti i primi esempi di tool di suppor-
to ai test delle UI tentavano di pilotare le interfacce utenti creando delle mappe grafiche dei vari
oggetti sfruttandone le coordinate cartesiane. Sebbene, dopo diverse iterazioni, questi tool riu-
scissero ad alleviare l'onere della configurazione iniziale, grazie a meccanismi atti a memorizzare
1 movimenti del mouse, restava il problema della fragilità rispetto ai cambiamenti: ogni variazione
della UI generava un grosso lavoro di manutenzione dei test tanto da scoraggiarne l'utilizzo. I
tool attuali, basati su una visione a oggetti (anche per questo motivo la versione precedente è
spesso erroneamente denominata "analogica") e quindi in grado di riconoscere più agevolmente
i vari elementi, hanno risolto molti di questi problemi: ma comunque il danno è fatto.
La GUI svolge un ruolo importantissimo: è "l'involucro" con cui l'utente interagisce e che è
in grado di giudicare. Ben poca soddisfazione avrebbe l'utente qualora si realizzasse un sofisti-
catissimo sistema, se poi la relativa interfaccia utente risultasse difettosa e/o difficile da utilizza-
re. Paradossalmente, gli utenti tendono ad accettare più volentieri la situazione opposta.
Volenti o nolenti le UI devono essere verificate approfonditamente: pertanto si può scegliere
di assumere delle persone per eseguire manualmente e ripetutamente gli stessi test, oppure
implementare una volta per tutte un insieme di test da eseguire ad ogni cambiamento.

8.7.1 Implementare i test per le GUI "business"


Nell'ambito di uno stesso progetto, soprattutto in contesti di progetti di medie-grandi dimensio-
ni, è abbastanza frequente implementare tutta una serie di sistemi UI, di cui alcuni strettamente
a carattere business e altri necessari per eseguire delle routine di servizio, amministrare il sistema,
e cosi via. Come lecito attendersi, mentre per quanto concerne le seconde è possibile rilassare la
quantità e qualità dei test, lo stesso approccio non può assolutamente essere adottato per le
interfacce business. Queste sono l'ambiente di lavoro degli utenti che vi interagiranno per la gran
parte della loro giornata lavorativa. Inoltre, la GUI è la parte del sistema che gli utenti sanno ben
valutare (fin troppo bene), grazie alla loro esperienza, anche qualora non dispongano di sofistica-
te nozioni informatiche. Malfunzionamenti a queste GUI possono nel migliore dei casi infastidire
il pubblico degli utenti, mentre nel peggiore possono addirittura generare grossi danni.
Si incontra personale "esperto" che "risolvono" il problema alla radice implementando GUI
semplicissime e mantenendole più semplici possibile. Sebbene difficilmente si potrebbe obiet-
tare sull'importanza di disegnare UI lineari, pulite, consistenti, etc., l'esperienza insegna che gli
utenti desiderano ambienti sempre più evoluti, interattivi, ergonomici, la cui apparente sempli-
cità cela complessi meccanismi. Pertanto, GUI così semplici da non richiedere opportuni siste-
mi di test tendono ad essere assolutamente poco interessanti, soprattutto per gli utenti.

8.7.2 Verificare il funzionamento dell'API


Tutti i sistemi, inclusi quelli demandati a gestire l'interfacciamento con l'utente, dovrebbero
essere disegnati e implementati secondo i dettami delle architetture a strati. In questo conte-
sto l'interesse è centrato sulla necessità di separare chiaramente, attraverso un insieme di
interfacce ben definite, lo strato di presentazione da quello dei servizi. Questa separazione
offre tutta una serie di importanti vantaggi, come per esempio, il poter implementare diverse
interfacce utente anche basate su tecnologie diverse, il poter disporre isolare i vari strati
schermandoli dai cambiamenti degli altri e il poter verificare il corretto funzionamento dei
vari strati. Quindi è importante disegnare architetture a strati, basare l'interfaccia utente su
API ben architettate e, soprattutto, implementare opportuni sistemi di test sia per la UI sia
per le API su cui si poggia.

8.7.3 Scegliere accuratamente il tool di supporto al test


La scelta del tool di supporto più idoneo per le proprie esigenze è importante in quasi ogni
lavoro che si intraprenda, tuttavia in alcuni casi il tool più adeguato può veramente fare la
differenza: è il caso dei tool per il supporto dei test dell'interfaccia utente. In particolare, è
importante selezionare framework in grado di:
1. raggiungere un elevato livello di astrazione dagli aspetti grafici dell'interfaccia; per esem-
pio, è fortemente consigliato evitare tool che tentino di costruire la mappa dell'interfaccia
utente attraverso coordinate cartesiane;
2. semplificare enormemente non solo la configurazione iniziale dell'interfaccia utente del
sistema, ma anche le inevitabili variazioni;
3. simulare il più fedelmente possibile le tipiche interazioni dell'utente.

8.7.4 Non reinventare la ruota


Questo suggerimento ha indubbiamente una valenza universale... Soprattutto in tutti quei casi
in cui ciò che si vuole reinventare non è strettamente collegato al business degli utenti, bensì
l'oggetto è uno strumento di supporto, come in questo caso. Non è infrequente infatti imbat-
tersi in team di sviluppo impegnati a implementare propri framework di test che, inevitabil-
mente, finiscono per evidenziare tutte le tipiche limitazioni de sistemi ideati/disegnati male,
costruiti in breve tempo senza una larga base conoscitiva etc. In questi casi, una strategia deci-
samente migliore consiste nel ricorrere a framework open source: se proprio non si resiste
all'irrefrenabile desiderio di costruire qualcosa di proprio, è possibile partecipar allo sviluppo
comune di un progetto.
Un esempio non infrequente di contravvenzione a questa regola basilare è data da team che,
dopo aver implementato interfacce utente utilizzando le API Java Swing, decidano di implemen-
tarne il sistema di test ad hoc ricorrendo alla classe Java Robot (java.awt.Robot). Questa classe è
stata introdotta con la versione Java DSK 1.3 al fine di facilitare l'implementazione di test auto-
matici della UI (ciò però non significa che i team di sviluppo debbano utilizzare un tool a così
basso livello quando sono disponibili una miriade di framework a più alto livello). Robot è utiliz-
zata per generare nativi eventi di input, demo autogestite e altre applicazioni che richiedano il
controllo del mouse e/o tastiera. Questo approccio, nella stragrande maggioranza dei casi si
rivela uno grosso spreco di risorse; i limiti emergono in tutta la loro drammaticità con le inevita-
bili variazioni dell'interfaccia utente per via del grandi ripercussioni generate sul sistema di test.

8.8 Test di integrazione delle performance


I precedenti paragrafi sono stati dedicati ai test di carattere prettamente funzionale: il loro obiet-
tivo consiste nel certificare che il sistema implementi correttamente i vari servizi o, adottando
un'ottica meno ottimista, nell'individuare il maggior numero possibile di errori. Sebbene queste
verifiche siano molto importanti, da sole non sono assolutamente sufficienti. Infatti, un altro
importante insieme è dato dai requisiti non funzionali e in particolare dalle performance. La
verifica del raggiungimento di questi requisiti richiede la realizzazione di tutta una serie di test.
Da tener presente che la definizione dei vari servizi dovrebbe essere accompagnata dal loro
livello di qualità (SLA, Service Level Agreement). Questo, tipicamente, stabilisce una serie di
accordi come quelli relativi a latenza e throughput del servizio sia quando il o i server sono a
riposo, sia quando questi sono sottoposti a un grande stress. Queste informazioni sono di vitale
importanza in tutti quegli ambienti in cui il fattore tempo giochi un ruolo fondamentale. Tanto
che in questi è molto sentita la necessità di implementare una serie di test atti a verificare il
soddisfacimento dei SLA con gli utenti e/o altri sistemi client.
Per avere un'idea della complessità di questo argomento è sufficiente considerare i principa-
li tool open source disponibili per implementare tali test (Apache JMeter, Benerator, CLIF,
Curl-loader, DOTS Database Open-source test suite, DBMonster, Deluge, Dieseltest, Faban,
FunkLoad, Grinder, Hammerhead, Hammerora, httperf, http load, JCrawler, Lobo, NTime,
OpenSta, OpenWebLoad, p-unit, Seagull, Siege, Sipp, SLAMD, Soap-stone, Stress_driver,
TestMaker, TPTest, Tsung, Valgrind, Web Application Load Simulator...). Pertanto, compati-
bilmente con gli obiettivi del presente testo, in questo paragrafo vengono fornite un'insieme di
regole di sopravvivenza, demandando ai lettori più volenterosi i necessari approfondimenti.

8.8.1 Implementare i test delle performance


Ogni sistema che si rispetti dovrebbe essere corredato da un'insieme di informazioni relative
agli aspetti non funzionali salienti, come per esempio la latenza tipica di ogni servizio, quella a
sistema sotto-stress, la degradazione del servizio in funzione del numero di utenti contempora-
neamente collegati, etc. Queste informazioni costituiscono uno dei criteri principali per deci-
dere se il sistema sia pronto o meno per essere messo in esercizio.
La logica conseguenza è che, volenti e dolenti, questi test devono essere eseguiti almeno
prima del rilascio di ogni nuova release del sistema. Inoltre, i risultati di questi test rappresen-
tano informazioni di fondamentale importanza per altre attività come il tuning dell'applicazio-
ne. Pertanto, le opzioni disponibili sono due: eseguire ogni volta questi test manualmente con
tutti i relativi svantaggi, oppure implementare, una volta per tutte, una batteria automatica di
test atti a valutare le performance del sistema. L'esperienza insegna che la seconda strategia
tende a evidenziare tutti i suoi vantaggi nel medio-lungo termine.

8.8.2 Valutare l'implementazione del test continuo delle performance


Come visto in precedenza, i dati relativi alle performance del sistema costituiscono una sorta di
scheda caratteristica ("personale") del sistema e come tali dovrebbero essere verificati ad ogni
major release dello stesso. In alcuni contesti, come per esempio i sistemi di front-office bancari,
in cui la performance dei vari servizi assume un'elevata criticità, è consigliabile implementare
soluzioni in grado di monitorare continuamente le performance dei vari servizi, o perlomeno di
quelli particolarmente critici, al fine di intercettare immediatamente eventuali deviazioni dai
requisiti non funzionali. Chiaramente la condizione necessaria ma non sufficiente per imple-
mentare tali test è l'aver definito chiaramente il SLA dei servizi oggetto di test.

8.8.3 Utilizzare un ambiente appropriato


Sebbene l'ambiente di sviluppo rappresenti l'habitat naturale per mettere a punto i vari test
delle performance, questo rarissimamente rappresenta l'ambiente ideale per eseguirli. Questo
per una serie di ben ovvi motivi, tra cui i principali sono relativi al fatto che l'ambiente è tipica-
mente molto differente da quello di produzione, e che è decisamente più difficile controllare gli
elementi ambientali. Eseguire i test nell'ambiente di sviluppo per ottimizzare il sistema può
essere addirittura dannoso: i dati forniti potrebbero essere drammaticamente diversi da quelli
che si otterrebbero nell'ambiente di produzione. Ciò potrebbe addirittura portare a realizzare
delle presunte ottimizzazioni che in realtà risultano pessimizzazioni.

8.8.4 Minimizzare le influenze ambientali


Realizzare accurate misurazioni dei sistemi software è spesso un'attività tutt'altro che banale.
Ciò per via di una serie di motivi, incluse le condizioni ambientali: efficienza del network, stato
dei vari server, servizi in funzione, etc. Per comprendere quanto queste possano incidere sui
propri test, è sufficiente considerare che, anche nell'infrequente situazione in cui un server ese-
gua esclusivamente l'applicazione da monitorare, il sistema operativo è comunque impegnato
da una serie di task che generano un significativo impatto sulle misurazioni. Le attività del siste-
ma operativo e di altre applicazioni tendono ad aumentare il livello di competizione nell'aggiu-
dicarsi le risorse del sistema come il microprocessore, la memoria, gli accessi sul disco e l'utilizzo
del network. Tale aumento è in grado di interferire significativamente con le varie misurazioni.
Pertanto, in tutti quei casi in cui sia necessario ottenere misurazioni accurate e/o dell'ordine
delle frazioni di secondo, è necessario minimizzare le variabili ambientali. Ciò si ottiene, per
esempio, riducendo il numero di programmi in esecuzione (possibilmente solo il sistema da
monitorare), minimizzando i servizi del sistema operatvo in esecuzione, cercando di utilizzare
un sub-network dedicato o almeno in orari in cui vi siano condizioni ripetibili di carico, etc.

8.8.5 Valutare il sistema a regime


Non è infrequente individuare test automatici (e a volte anche operati dal personale) che si
limitino a eseguire un determinato servizio un paio di volte al fine di ottenere le misurazioni
desiderate. Chiaramente non si tratta di una strategia ideale, e anzi è il modo migliore per ottene-
re dati altamente suscettibili alle condizioni ambientali. Un approccio decisamente migliore con-
siste nell'ottenere questi parametri come risultato della media dei valori ottenuti eseguendo lo
stesso servizio migliaia di volte, con diversi dati in input e soprattutto scartando le prime cinque-
cento/mille invocazioni al fine di permettere al sistema di stabilizzarsi, di simulare più fedelmen-
te l'ambiente reale e di scartare eventuali iniziali fattori aleatori (per esempio la JVM tende a
eseguire ottimizzazioni a runtime). Questo stesso approccio dovrebbe essere utilizzato per il
tuning del sistema. In particolare, l'efficacia di ogni modifica va verificata con la stessa strategia.

8.8.6 Cercare di simulare situazioni reali


La qualità della valutazione del comportamento del sistema, come lecito attendersi, è diretta-
mente proporzionale alla qualità della simulazione dell'ambiente di produzione. Pertanto è
necessario investire tempo nel cercare di ricreare il più fedelmente possibile tali condizioni. Per
esempio, sebbene sia corretto utilizzare tool di test delle performance del sistema in grado di
creare un certo numero di utenti virtuali contemporaneamente interagenti con il sistema, è
altresì importante tenere nella giusta considerazione il modus operandi di un utente vero. In
particolare, un utente medio tende a valutare la risposta ottenuta dal computer, prima di sotto-
porre un'altra; pertanto una buona simulazione dovrebbe tener conto di fattori come questo e
quindi aggiungere dei ritardi tra l'invio di due richieste successive.

8.8.7 Pianificare attentamente i dati di partenza


La corretta esecuzione di tutti i test di integrazione richiede che il sistema disponga di un certo
quantitativo di informazioni iniziali tipicamente memorizzate nel database. Scegliere tale insie-
me di dati è attività molto importante. In particolare si vuole che questi rappresentino l'insieme
che fa funzionare sia i test positivi sia quelli negativi, e che l'insieme sia il minimo possibile: è
molto importante evitare dati ridondanti. Ciò soprattutto perché questi devono essere mantenu-
ti e quindi è opportuno minimizzare l'impatto dei cambiamenti. Inoltre, questi dati devono
essere memorizzati su formati (come per esempio fogli elettronici, file di testo, etc.) facili da
gestire e da inserire nel sistema prima dell'esecuzione di ogni test. Ciò spesso comporta la neces-
sità di implementare tool atti a inserire automaticamente tali dati nel database del sistema.
Capitolo
Organizzazione
di un progetto, build
e deploy
Introduzione
Questo capitolo è dedicato ad una serie di argomenti organizzativi di importanza fondamenta-
le per progetti di medie e grandi dimensioni, come la struttura da utilizzare per organizzare i
progetti software, e i processi di build e deploy. Sebbene si tratti di argomenti di estrema impor-
tanza, non è infrequente il caso in cui questi siano trascurati fino al momento in cui il livello di
entropia del progetto abbia raggiunto livelli tali per cui non sia più possibile procedere oltre
senza prima aver dato luogo a una totale riorganizzazione dei vari moduli del progetto, ad una
razionalizzazione del processo di build e deploy, e così via.
Il problema di fondo è che ogni progetto dovrebbe disporre di almeno una risorsa dedicata
a tale scopo. Ciò, spesso anche per false economie, non è possibile e quindi ci si ritrova nella
spiacevole situazione di dover assegnare questo compito ad un membro del team dopo aver già
prodotto una serie di iterazioni.Nonostante ciò sia stato studiato appositamente per applicazio-
ni software sviluppate in Java, i concetti introdotti presentano una validità generale che esula
dallo specifico linguaggio di programmazione. Inoltre, sebbene la struttura presentata sia ab-
bastanza indipendente dai vari tool, come Ant e Maven, questi ultimi presentano vincoli e
offrono importanti vantaggi che, probabilmente, sarebbe poco intelligente non considerare.
Le linee guida proposte, coerentemente con l'intera impostazione del libro, sono frutto di
un approfondito studio atto ad incorporare diversi importanti elementi, come una vasta espe-
rienza nello sviluppo di sistemi software di grandi dimensioni con team dislocati in diverse aree
geografiche, utilizzo di eterogenee tecnologie, impiego di tool moderni per la produzione del
software, e così via.
Disporre di una razionale organizzazione del progetto è particolarmente utile per una serie
di motivi, tra i quali i principali sono:

• permette di avere un build sempre corretto e quindi ad avere un sistema sempre pronto
ad essere installato;
• evita che il processo di build diventi un evento completamente aleatorio in cui l'unica
certezza sia il momento di avvio del processo stesso, mentre la corretta esecuzione ed il
tempo necessario risultino completamente imprevedibili;
• facilita l'evoluzione del progetto: l'aggiunta di nuovi componenti, la modifica o rimozione
di altri diviene un'attività di normale amministrazione;
• semplifica la comprensione dell'organizzazione dei progetti, soprattutto di quelli più
complessi. Ciò, ovviamente, riduce la curva di apprendimento del progetto e quindi
facilita l'allocazione dinamica di risorse umane/sotto progetti;
• migliora e semplifica la comunicazione tra i diversi team di sviluppo e tra le diverse
figure professionali (project manager, business analyst, architetti, sviluppatori, etc.)
coinvolte nello sviluppo di un tipico sistema software. Inoltre rimuove alla basa la necessità
di tutto un insieme di comunicazioni, guerre religiose, relative al modo migliore di
organizzare i progetti;
• permette di minimizzare il tempo speso nella gestione dei vari ambienti e nella
manutenzione di applicazioni complesse: ciò si ottiene riutilizzando script di compilazione,
build, test, file di configurazione, etc.
• supporta la produzione di applicazioni di migliore qualità ed in particolare permette di
avere un sistema sempre complete mante verificato, evitare l'errata allocazione di file o
la mancanza di produzione di importanti file, e così via;
• semplifica e supporta la standardizzazzione delle strategie di CCM (Change Control
Management, gestione del controllo dei cambiamenti)

Obiettivi
Obiettivo di questo capitolo è fornire una serie di linee guida, suggerimenti e orientamenti
molto pratici su come organizzazione razionalmente i progetti software. In particolare, l'atten-
zione è stata focalizzata sui seguenti tre elementi fondamentali:

1. struttura del progetto dal punto di vista del file system;


2. processi di build e deploy

Le tematiche presentate sono stata elaborate considerando i seguenti principi base:

• compatibilità con i principali processi di sviluppo del software, a partire da quelli più
leggeri, centrati sul codice, fino ad arrivare a quelli più formali;
• scalabilità, intesa come possibilità di utilizzare quanto esposto sia per progetti di modeste
dimensione, sia per progetti molto grandi, che prevedano team di produzione dislocata
in diverse aree geografiche;
• razionale organizzazione, basata su poche linee guide, semplifica il relativo utilizzo senza
aver bisogno di dover sempre leggere la relativa documentazione,
• vantaggi e vincoli offerti dai principali tool utilizzabili in questo spazio.

Sebbene coerentemente con l'impostazione di questo libro, la presentazione dei principali tool
utilizzabili per l'automazione del processo di build non sia argomento di questo capitolo, tale è
l'importanza ed il successo di alcuni di loro, come Ant e Maven, che una loro trattazione, sebbene
minima, sia inderogabile. Questi, in effetti, offrono tutta una serie di vantaggi e vincoli da tener
presente per poter organizzare razionalmente i propri progetti. Per esempio, sarebbe probabil-
mente poco intelligente, definire una struttura del file system dei progetti senza tener conto della
struttura "standard" proposta da Maven, così come sarebbe poco furbo organizzare un progetto
senza considerare la necessità di dover pubblicare i manufatti generati. Quindi, gioco forza, que-
sti tool esistono e giocano un ruolo di primaria importanza da imporne la relativa considerazione.
Prima di proseguire con la presentazione dei due tool, tuttavia, è necessario definire cosa si
intenda con il termine di processo di build. Nella sua eccezione più essenziale rappresenta
l'insieme di attività che permettono di convertire un insieme di codici sorgente nei corrispon-
denti codici eseguibili. Questa definizione, abbastanza semplice (utile proprio per questo mo-
tivo), non tiene però conto del fatto che linguaggi moderni come Java, combinano i file "com-
pilati" in insiemi più complessi (.jar, .war, .ear) che non necessariamente possano essere eseguiti
direttamente (come per esempio librerie), e che comunque la relativa esecuzione richieda, ri-
spettivamente, l'esecuzione della macchina virtuale, il deployment all'interno di container come
web server e application server...
Inoltre, le visioni di diverse figure professionali non sempre convergono circa le responsabi-
lità del processo di build. Infatti, dal punto di vista dei molti sviluppatori, un processo di build
dovrebbe limitarsi a:

1. generare su richiesta i file di progetto per l'IDE richiesto;


2. supportare lo sviluppo del codice e l'esecuzione dei test;
3. generare nuovi manufatti da "pubblicare" in luogo condiviso;
4. eseguire i test di integrazione e i regression test (possibilmente sempre all'interno dell'IDE)

Altre figure professionali, come per esempio il personale addetto al deployment, tendono ad
avere maggiori esigenze. In particolare i relativi requisiti possono prevedono la necessità di:

1. integrare il processo di build con altri tool, come per esempio:


a. quello per la gestione dei sorgenti (SCM, Software Configuration Management quali
CVS, SVN, ClearCase, etc.)
b. quello dedicato alla continua integrazione del sistema, necessario per verificare con-
tinuamente che lo stato del build del sistema, identificare tempestivamente possibili
errori notificandoli all'apposito personale;
2. disporre un sito in cui pubblicare le varie informazioni relative al build;
3. produrre rapidamente i vari manufatti richiesti dal sistema, pubblicandoli in un apposi-
to spazio condiviso;
4. personalizzare i vari manufatti necessarie per l'installazione nei vari ambienti: integration
test, UAT e produzione, previa corretta esecuzione dei vari test previsti;
5. generare nuove versioni, opportunamente etichettate, in maniera consistente;
6. fornire la possibilità di eseguire il roll back ad una precedente versione funzionante
qualora quella corrente presenti dei malfunzionamenti.

I tool: filosofia e uso di Ant e Maven


Ant
Ant è uno strumento atto ad automatizzare il processo build delle applicazioni, ideato e svilup-
pato in Java, presso quella fucina di idee e software open source di Apache (http://ant.apache.org/).
Inizialmente disegnato per supportare il build delle applicazioni Java, Ant ha riscosso un succes-
so così clamoroso che ne è stato effettuato il porting del codice per altri ambienti come per
esempio .Net. La versione disponibile al momento in cui viene scritto questo capitolo è la 1.7.0
(rilasciata nel dicembre 2006).
L'autore iniziale di Ant, James Duncan Davidson, scelse il nome come acronimo di "Another
Neat Tool" (un altro strumento lineare, "pulito" se traduciamo letteralmente). Tuttavia
l'omonimia con il nome "formica" fu ovviamente voluta... In effetti, si volevano enfatizzare
concetti come la capacità delle formiche di eseguire un ottimo lavoro nel costruire ("ants do an
extremely good job in building things"), e il fatto che, per quanto molto piccole, le formiche
possono caricarsi di pesi dozzine di volte superiori al proprio ("ants are very small and can
carry a weight dozens of times their own").
L'ideazione di Ant avvenne all'interno del progetto TomCat per risolvere, come spesso acca-
de, un problema pratico: semplificarne il processo di build evitando i problemi esistenti dei
build implementati ricorrendo agli esistenti make script. Da tener presente che ogni nuova ver-
sione di TomCat doveva essere preparata (effettuarne il build) e verificata sulle principali piatta-
forme di produzione, e pertanto era spesso necessario aggiornare tutta una serie di build scripts.
La prima versione "utilizzabile" di Ant fu la 1.1. rilasciata nel Luglio del 2000 insieme alla
versione TomCat 3.3. Da allora tale è stato il successo che attualmente è presente in moltissimi
progetti Java.
Ant è un parente (remoto) della famiglia delle applicazioni in grado di eseguire script di
build. Alcuni antenati famosi sono make, gnumake, namake e jam. Sebbene si trattasse di tool
molto utili, questi presentavano serie limitazioni di cui la più importante, probabilmente, era
dovuta alla loro dipendenza dalla piattaforma (lava ancora non era stato implementato!). So-
prattutto le versioni per ambienti Unix erano a tutti gli effetti dei script Shell. Ciò, se da un lato
era conveniente perché ne aumentava la flessibilità ed estendibilità, dall'altro ne limitava il
livello di standardizzazione (apprendere il funzionamento di un uno di questi file era pressoché
equivalente ad apprendere un modulo di codice!), difficili da implementare e controllare, etc.
Inoltre, erano strumenti datati non più compatibili con le esigenze dei moderni linguaggi di
programmazione. Per esempio, il build delle applicazioni Java richiede di compilare tutte le
singole classi. L'esecuzione di uno script Make in tali circostanze richiederebbe di lanciare, per
ogni singola classe, il compilatore Java (JavaC). Moltiplicando ciò per mille, diecimila classi,
l'impatto era decisamente notevole, specialmente considerando il numero di volte che, durante
10 sviluppo di un'applicazione, questa viene compilata per essere verificata. Questi script, inol-
tre, erano veri e propri mini programmi difficili da scrivere, manutenere e verificare. Inoltre,
non presentando alcuna interazione con la macchina virtuale Java, non erano in grado di
interagirvi per realizzare servizi avanzati, etc.

Struttura
Poiché l'illustrazione dello strumento Ant esula dagli obiettivi di questo capitolo, si è deciso di
limitare l'esposizione alla presentazione di un semplice esempio di file di build (denominato
progetto, project), corredato dalle necessarie spiegazioni, demandando al lettore l'approfondi-
mento dell'argomento e dei comandi (chiamati task) messi a disposizione dei programmatori
per redigere versioni più complesse.
Gli script Ant sono scritti in XML. Sebbene inizialmente tale scelta sembrò essere la miglio-
re, con il passare del tempo sono emersi diversi problemi. Lo stesso autore, James Duncan
Davidson, ha scritto "In retrospettiva, e molti anni dopo, XML, probabilmente, non fu una
scelta poi così buona come sembrò allora. Attualmente, ho visto file build Ant lunghi centinaia
o, addirittura migliaia di linee e, con queste dimensione, si evidenzia come XML non sia poi un
formato così "friendly" da maneggiare, come sperai. Inoltre, quando si mischiano XML e gli
interessanti elementi basati sulla riflessione interni di Ant che ne permettono una facile
estensibilità definendo propri task, si ottiene un ambiente che ha la potenza e la flessibilità dei
linguaggi di script, ma che anche dà nel suo complesso un certo del mal di testa nel tentativo di
esprimere questa flessibilità attraverso delle parentesi angolari".
Questi file XML, tipicamente ma non necessariamente, realizzati per automatizzare i processi di
build, vengono eseguiti dal tool Ant, invocabile da linea di comando (c:\prj\myproject\build> ant).
Qualora non si specifichi alcun parametro sulla linea di comando, Ant, per default, cerca il file
build.xml e se lo trova esegue il target (questo elemento è spiegato in dettaglio di seguito) di default.
11 tool Ant prevede tutta una serie di parametri illustrati in dettaglio nella relativa documentazione.
In Ant un concetto fondamentale è quello di task, che, come ne suggerisce il nome, rappre-
senta un comando o "codice" che può essere eseguito. La relativa dichiarazione richiede, ovvia-
mente, opportuni elementi xml. In particolare, la struttura generale è:

<nome-task attributo1="valore1" attributo2="valore2"... />

La riga successiva mostra un esempio di utilizzo del comando per la cancellazione dei file:

<delete file="xyz.txt" failonerror="false" />

dove delete è il nome del task, file è un attributo che dichiara il file da eliminare, e failonerror è
l'attributo che specifica se interrompere o meno l'esecuzione dello script in caso di errore (in
questo caso no).
Ant, come lecito attendersi, dispone di una miriade di task predefiniti, vi sono per esempio
task per compilare sorgenti, per creare file .jar, .war, .ear, per interagire con il file system, per
manipolare file zip/tar, per cambiare i diritti di accesso di file e directory, per interagire con
sistemi come CVS, ClearCase, e così via. Altri task Ant sono continuamente sviluppati da terze
parti e donati ad Apache. Inoltre, è possibile definire propri task. Il sito Ant fornisce tutte le
informazioni necessarie per generare ulteriori task, nonché i codici sorgenti di molti task
predefiniti. Di seguito sarà riportato il listato con un esempio di progetto:
<?xml version="1.0" encoding="UTF-8"?>
<project name="myProject" basedir="..V." default="all">

<description>
This buildfile is used an example for myProject within the XYZProject
</description>

< ! — set global properties for this build — >


<property name="src" location="src/main/java" />
<property name="lib" location="/lib" />
<property name="build" location="target/classes" />
<property name="dist" location="target" />
<property name="webxml" location="src/main/resources/META-INF" />
«property name="prjdepjar" location^'../.." />
«property name="distr.filename" value="myProject.war" />

< ! — ant all — >


ctarget name="all" depends="-init, -compile, -war, -term" />

< ! — ant tnit — >


«target name="-init">
< ! — Create the time stamp — >
<tstamp/>
< ! — cleanup the existing jar file — >
«delete file="$ldist|/$(distr.filenamel" failonerror="false" />
< ! — cleanup the .class files directory — >
«delete dir="$|buildl" includeEmptyDirs="true" failonerror="false" />
«mkdir dir="$|buildl" />
</target>

< ! — compilation — >


«target name="-compile">
«javac destdir="|build)">
<classpath>
«pathelement location=
"${lib|/commons-cli/commons-cli/1.0/commons-cli-1,0.jar"/>
«pathelement location="${lib}/log4j/log4j/1.2.14/log4j-1.2.14.jar"/>
«pathelement location=
"$|libl/javax/servlet/servlet-api/2.2/servlet-api-2.2.jar"/>
«pathelement location =
"$lprjdepjar)/xmlservices/implementation/dist/ xmlservice.jar"/>
«pathelement location=
"$|prjdepjar)/common/implementation/dist/filepoller.jar"/>
</classpath>
<src path="$|src|" />
«/javac>
</target>

< ! — war — >


<target name="-war">
<war destfile="$ldlst|/$|distr.filename|" webxml="$(webxml]/web.xml">
<classes dir="$|build|" />
<lib file="$|libl/log4j/log4j/1.2.14/log4j-1.2.14.jar" />
<lib file="$(lib|/commons-cli/commons-cli/1.0/commons-cll-1.0.jar"/>" />
<lib file="$(lib]/javax/servlet/servlet-api/2.2/servlet-api-2.2.jar " />
<lib file="$|libl/com/sun/xml/bind/jaxb-impl/2.1,3/jaxb-impl-2.1,3.jar "/>
elib file="$|lib]/javax/xml/jaxb-api/2.0/jaxb-api-2.0.jar"/>
elib flle="$|lib|/com/sun/xml/bind/jaxb-impl/2.1.3/jaxb-impl-2.1.3.jar"/>
<lib file="${lib]/com/sun/xml/bind/jaxb-xjc/2.1,3/jaxb-xjc-2.1,3.jar"/>
<lib file="$|lib|/javax/xml/bind/jsr173_apl/1.0/jsr173_api-1.0.jar
<lib file="$lprjdepjar|/xmlservices/lmplementation/dist/xmlservice.jar" />
<lib file="$lprjdepjar|/common/implementation/dist/filepoller.jar" />
</war>
</target>

< ! — final clean-up — >


<target name="-term" >
< ! — cleanup the .class files directory — >
<delete dir="$|build|" includeEmptyDirs="true" failonerror="false" />
cmkdir dir="$|buildl" />
</target>

</project>

Il primo elemento che si incontra, dopo la definizione del file XML, è l'elemento progetto
(project) dotato di tre attributi:

1. name: attributo non obbligatorio, che permette di dichiarare il nome del progetto;
2. default: rappresenta la destinazione (target) di default, ossia il target da utilizzare quando
nell'invocazione da linea di comando non sia presente alcun target;
3. basedir: directory di base dalla quale sono derivate tutte le altre definite successivamente.
Tale definizione può impostata anche attraverso la proprietà (property) "basedir". In tal
caso, questa impostazione dovrebbe essere rimossa dalla sezione project. Qualora lo script
non preveda alcuna definizione basedir, la directory "genitore" viene assunta come
directory base.

Nel listato visto poco sopra, il nome del progetto è myProject, la directory di default è A. e
ciò accade perché si suppone che lo script Ant sia memorizzato nella directory myproject\build\ant
e quindi la directory di default coincide con quella del progetto, mentre il target di default è
ali. Sempre opzionalmente è possibile specificare un elemento (description) con la descrizione
del progetto.
La sezione successiva è relativa alla proprietà. Ogni progetto può dichiarare un insieme
qualsiasi di proprietà attraverso il task property. Volendo è possibile impostarle al di fuori di
Ant. Una proprietà è data dalla classica coppia (nome, valore) dove, secondo tradizione Java, il
nome è case-sensitive (pertanto libdir è diverso da libDir). Una volta definite le varie proprietà, è
possibile utilizzarle riportandone il nome tra i terminatori: $| e ). Alcuni esempi di proprietà
utilizzate nel listato sono:

<property name="src" location="src/main/java" />


<property name="distr.filename" value="myProject.war" />

La prima definisce il path dei sorgenti Java, mentre la seconda definisce il nome del file di
distribuzione. Si tratta di .war molto utilizzati nei deployment di applicazioni all'interno di web
server. La proprietà src è usata nel seguente attributo del task di compilazione illustrato di seguito.

<src path="$(src)" />

Ant, come è lecito attendersi, dispone della seguente lista di proprietà predefinite (built-in):

• basedir: percorso assoluto della directory di base del progetto


• ant.file: percorso assoluto del file di build
• ant.version: versione di Ant che sta eseguendo il build
• ant.project.name: nome del progetto in corso di esecuzione
• ant.java.version: versione della JVM individuata da Ant

Il passo successivo consiste nel definire l'elemento target. In particolare, ogni progetto deve
averne almeno uno, anche se è prassi definirne diversi, come per esempio uno per
l'inizializzazione, uno per la compilazione, uno per la produzione del file jar, .war, .ear, uno per
la rimozione dei file intermedi generati, uno che includa tutti, etc.
Un target può includere diversi task atti ad eseguire un ben definito compito. La forma più
semplice prevede di dichiararne semplicemente il nome. Questo è l'unico attributo obbligato-
rio ed è molto importante perché è quello invocabile dalla linea di comando. Tuttavia è possibi-
le definirne eventuali dipendenze. Nel listato visto poco sopra, è definito il seguente target ali
che rappresenta una sorta di standard:

<target name="all" depends="-init, -compile, -war, -term" />

Come si può notare questo target attraverso l'attributo depends dichiara le seguenti dipenden-
ze: init, compile, war e term, nonché l'ordine di esecuzione (da sinistra verso destra). Questo,
tuttavia, potrebbe essere alternato dalle dipendenze presenti nei singoli target. Per esempio, se
per qualche motivo imprecisato, init avesse al suo intero una dipendenza dal target term, allora
ecco che quest'ultimo sarebbe il primo task ad essere eseguito. Un target può includere nella
propria dichiarazione condizioni che ne vincolano l'esecuzione (solo del target in cui sono defi-
nite e non di eventuali che ne hanno dichiarato una dipendenza). In particolare, è possibile
specificare le seguenti clausole:

if = "proprietà-presente"
unless = "proprietà-presente".

La prima specifica che il target deve essere eseguito solo se la relativa proprietà è stata impo-
stata nella linea di comando (clausola -D), mentre la seconda specifica l'opposto.
Per finire, target prevede la possibilità di specificare l'attributo description che, come lecito,
attendersi, permette di associare una breve descrizione al target stesso. Il progetto presentato
nel listato visto sopra prevede i seguenti target:

• ali: rappresenta una sorta di main, il cui compito è di far eseguire, nel corretto ordine,
tutti i target che permettono, a partire dai file sorgenti, di produrre il file di distribuzio-
ne (0 war). In questo progetto non è stata inclusa l'attività di produzione della documen-
tazione JavaDoc.
• init: permette di eseguire una serie di attività propedeutiche allo svolgimento di altri
target. In questo caso vi è il task tstamp (si tratta di una pratica fortemente consigliata) che
si occupa di impostare le proprietà DSTAMP, TSTAMP e TODAY, contenti informazioni di
carattere temporale. Inoltre, vi sono i comandi necessari per eliminare il precedente file
di distribuzione (in questo caso myProject.war), per eliminare e quindi ricreare la directory
con i file .class.
• compile: questo target si fa carico della compilazione dei file sorgente. A tal fine utilizza il
task javac. La configurazione vista nel listato include sia delle librerie esterne, sia librerie
(.jar) parte dello stesso progetto, ossia file di distribuzione generati da altri file Ant.
Questa soluzione non è esente da diversi problemi e anche per risolvere simili problemi
è stato creato il tool Maven.
• war: questo target permette di generare il file di distribuzione. La peculiarità dei file war è
di includere le librerie (.jar) referenziate dal progetto in questione. A tal fine è necessario
utilizzare l'attributo lib. Come si può notare, non disponendo di repositories, la gestione
delle librerie deve avvenire manualmente con tutti i problemi che ne seguono a partire
dalla definizione del corrispondente file system.
• term: si occupa delle pulizie finali, ossia della rimozione dei file temporanei.

A questo punto si dovrebbe disporre del minimo insieme di informazioni per comprendere
sia come funzioni Ant, sia come leggere e/o creare file Ant.

Maven
Dopo aver letto la breve presentazione di Ant riportata nei paragrafi precedenti, la lettura di
questa presentazione di Maven potrebbe portare all'errata conclusione che si tratti "solo" di un
altro strumento di build e/o che si tratti di una mera sostituzione di Ant. Anche se in ciò c'è un
fondo di verità, Maven è molto di più... Maven, principalmente, è uno strumento completo per
la gestione di progetti software Java, in termini di compilazione del codice, distribuzione, docu-
mentazione e collaborazione del team di sviluppo. Secondo la definizione ufficiale (http://
maven.apache.org/), si tratta di un tentativo di applicare pattern ben collaudati all'infrastruttura
del build dei progetti al fine di promuoverne la comprensione e la produttività del team coin-
volto allo sviluppo, fornendo un percorso chiaro all'utilizzo di best practice. Per questo motivo
Maven è definito, sinteticamente, tool per la gestione e comprensione dei progetti. Maven è
quindi un insieme di standard, una struttura di repository e un'applicazione demandati alla
gestione e la descrizione di progetti software. Maven, inoltre, definisce un ciclo di vita standard
per il build, il test ed il deployment di file di distribuzione Java.
Nel libro [BTRMVN] è presente un'interessante definizione, destinata, secondo una nota
degli stessi autori, principalmente al pubblico dei manager: "Maven è uno strumento dichiarativo
per la gestione dei progetti Java che permette di ridurre il tempo totale di sviluppo dei progetti
(time-to-market) attraverso un efficace utilizzo delle sinergie disponibili. Maven permette di
ridurre il numero delle risorse umane e contemporaneamente permette di ottenere elevate
efficienze operazionali".
Le caratteristiche di Maven fanno sì che diverse persone, anche inizialmente estranee al pro-
getto, possano lavorare insieme produttivamente senza dover spendere molto tempo nel cerca-
re di comprendere la struttura del progetto, il funzionamento, il processo di build, etc. Tutti
coloro che si sono trovati, almeno una volta nella loro vita, a dover intervenire in progetti di
grandi dimensioni in corso d'opera, sicuramente sanno quale frustrazione possa derivare dal
dover capire, rapidamente, come le varie parti del progetto interagiscano tra loro, l'ordine e
dipendenze del processo di build, etc.
Le aree prese in considerazione da Maven sono: build (sicuramente), documentazione,
reportistica, gestione delle dipendenze, SCM (Software Configuration Management), rilascio e
distribuzioni di nuove versioni. Tutti i progetti, indipendentemente dal relativo dominio, esten-
sione e tecnologia, presentano una serie di necessità standard, quali: la conversione dei sorgenti
in codici "eseguibili" (build), la verifica (test), l'assemblaggio, documentazione ed eventual-
mente il "dispiegamento" (deployment).

Breve storia di Maven


L'idea iniziale, come spesso accade nel mondo dell'Open Source, e in maniera completamente
analoga a quella che ha portato alla realizzazione di Ant, è nata dall'esigenza di risolvere un
problema pratico, inizialmente scaturito, nell'ambito del progetto Jakarta Alexandria (attual-
mente abbandonato) e quindi migrata al progetto Turbine (framework Servlet disegnato per la
rapida produzione di applicazioni web). L'obiettivo iniziale era di implementare un tool atto a
semplificare, uniformare ed automatizzare il processo di build di sistemi complessi. A tal fine
era necessario sia creare un modello di progetto, sia una struttura di file system standard. Ciò
anche per risolvere un'altra problematica, ossia quella di far in modo che i diversi progetti
Apache funzionassero in maniera analoga. In effetti, prima del rilascio di Maven, ciascun pro-
getto Apache, presentava differenti approcci alla compilazione, distribuzione e alla generazio-
ne del sito web relativo al progetto, creando evidenti problemi relativi all'allocazione delle
risorse umane, al riutilizzo di script, all'applicazione di best practice, e così via. Questi proble-
mi non erano da poco, visto che non era infrequente il caso in cui sviluppatori dovevano spen-
dere una notevole porzione del loro tempo a configurare i vari ambienti, a comprenderne il
funzionamento e a mantenere i vari script di compilazione. Ciò, oltre a distrarli da quello che
invece doveva essere il loro obiettivo principale: realizzare sistemi software di elevata qualità,
spesso finiva anche per far desistere nuovi potenziali collaboratori.
Dato l'elevato livello di interdipendenza dei progetti open-source, la capacità di Maven di
standardizzare l'ubicazione dei vari file (sorgenti, documentazione, distribuzione ,etc\), di for-
nire una struttura comune per i vari progetti, di permettere il reperimento dei file di distribu-
zione attraverso un meccanismo di repository condiviso e di far sì che il processo di build
diventasse più agevole, trasparente e meno complesso, furono elementi chiave che ne assicura-
rono il successo fin dalle prime versioni.
Già Ant aveva risolto molti problemi del processo di build; Maven, al netto di malfunzionamenti,
ha fatto un passo in avanti, risolvendo diversi problemi presenti nello sviluppo di sistemi com-
plessi, basati su sotto-progetti interdipendenti. In particolare, Maven ha permesso di:

• ridurre considerevolmente la necessità di disporre di diversi script di build (questi sono


sostituiti da file POM) da eseguire secondo un rigido ordine, di copiare porzioni di file
di script da un progetto ad un altro con la conseguente necessità di doverne gestire una
moltitudine;
• rimuovere l'esigenza di copiare i vari file JAR, soprattutto di quelli prodotti dai vari
sotto-progetti. Copiare i file .jar è una pessima soluzione che può essere eliminata anche
con Ant, ma solo attraverso l'implementazione di complessi plugin;
• risolvere i problemi relativi alla condivisione delle librerie (diverse versioni, mancanza
di allineamento, memorizzate più volte e quindi l'aggiornamento di tutte era spesso
problematico, e così via).

Alcuni di questi vantaggi sono derivati dall'esistenza (non presente in Ant) di un progetto
"genitore" dal quale far ereditare sotto-progetti, etc.
Dalla versione iniziale Maven ha compiuto molti progressi contribuendo enormemente a
semplificare le quotidiane attività del team di sviluppo. L'ultima versione disponibile al mo-
mento della scrittura di questo capitolo (aprile 2008) è la 2.0.8.

Obiettivi di Maven
Gli obiettivi che l'adozione di Maven dovrebbe permettere di ottenere sono:

• semplificazione del processo di build. In particolare, Maven si fa carico di risolvere tutta


una serie di dettagli senza dover ricorrere all'utilizzo di file di script;
• sviluppo di un ambiente uniforme di build. Maven gestisce progetti basati sul proprio
Modello Oggetto del Progetto (POM, Project Object Model). Per quanto molti aspetti
possano essere personalizzati, Maven dispone di una serie di "standard", studiati
appositamente per essere utilizzati efficacemente nei vari progetti. Pertanto, una volta
compreso il funzionamento di un progetto gestito da Maven è automatico comprendere
qualsiasi altro (a meno di forti personalizzazioni). Ciò evidentemente si traduce in un
notevole risparmio di tempo, energie e frustrazione.
• produzione di informazioni qualitative circa il progetto. Per quanto Maven non sia né uno
strumento di documentazione né un generatore di siti web, offre un'ottima infrastruttura
che permette di utilizzare tutta una serie di plug-in per la generazione di informazioni utili
relative ai progetti. Per esempio, permette di generare documenti relativi alle variazioni
effettuate (interagendo con i sistemi di source control), ai riferimenti incrociati dei sorgenti,
alle mailing list, alle dipendenze, ai rapporti relativi alla copertura del codice da parte dei
test, etc. Il tutto in maniera assolutamente trasparente ed automatica.
• erogazione di linee guida corredate da un opportuno supporto per l'applicazione di
best practice per lo sviluppo di sistemi. L'esempio più evidente è relativo alla presenza
esplicita, nel processo di build standard, della definizione, specifica ed esecuzione dei
test di unità. Inoltre vi sono workflow necessari per il rilascio e la distribuzione di nuove
release del sistema.
• supporto alla migrazione, quanto mai trasparente, verso nuove feature.
Caratteristiche principali di Maven
Le principa