
di Claudio De Sio Cesari
-Introduzione alle Asserzioni
Dalla versione 1.4 di java, è stata introdotta una nuova e clamorosa caratteristica al linguaggio. Clamorosa perché si è dovuto addirittura modificare la lista delle parole chiave con una nuova: assert. Un’asserzione è un’istruzione che permette di testare eventuali comportamenti che un’applicazione deve avere. Ogni asserzione richiede che sia verificata un’espressione booleana che lo sviluppatore ritiene debba essere verificata, nel punto in cui è dichiarata. Se questa non è verificata, allora si deve parlare di bug. Le asserzioni possono quindi rappresentare un’utile strumento per accertarsi che il codice scritto si comporti così come ci si aspetta. Lo sviluppatore può disseminare il codice di asserzioni, in modo tale da testare la robustezza del codice in maniera semplice ed efficace. Lo sviluppatore può infine disabilitare la lettura delle asserzioni da parte della JVM, in fase di rilascio del software, in modo tale che l’esecuzione non venga in nessun modo rallentata. Moltissimi sviluppatori pensano che l’utilizzo delle asserzioni sia una delle tecniche di maggior successo per scovare bug. Inoltre, le asserzioni rappresentano anche un ottimo strumento per documentare il comportamento interno di un programma, favorendo la manutenibilità dello stesso.
-Sintassi
Esistono due tipi di
sintassi per poter utilizzare le asserzioni:
1) assert espressione_booleana;
2) assert espressione_booleana: espressione_stampabile;
Con la sintassi 1) quando l’applicazione esegue l’asserzione valuta
il valore dell’espressione_booleana. Se questo è true, il programma
prosegue normalmente, ma se il valore è false viene lanciato l’errore
AssertionError. Per esempio l’istruzione:
assert b > 0;
è semanticamente equivalente a:
if (!(b>0)) {
throw new AssertionError();
}
A parte l’eleganza e la compattezza del costrutto assert,
la differenza tra le precedenti due espressioni è notevole. Le asserzioni
rappresentano più che un’istruzione applicativa classica, uno strumento
per testare la veridicità delle assunzioni che lo sviluppatore fa della
propria applicazione. Se la condizione che viene asserita dal programmatore
è falsa, l’applicazione terminerà immediatamente mostrando
le ragioni tramite uno stack-trace (metodo printStackTrace()
della classe Throwable). Infatti
si è verificato qualcosa che non era previsto dallo sviluppatore stesso.
È possibile disabilitarne la lettura delle asserzioni da parte della
JVM, una volta rilasciato il proprio prodotto, al fine di non rallentarne l’esecuzione.
Ciò evidenzia la differenza tra le asserzioni e tutte le altre istruzioni
applicative.
La sintassi 2) permette di specificare anche un messaggio esplicativo tramite
l’espressione_stampabile.
Per esempio
assert b > 0: b;
oppure
assert b > 0: “il
valore di b è ” + b;
oppure
assert b > 0: getMessage();
o anche
assert b > 0: “assert
b > 0 = ” + (b > 0);
l’espressione_stampabile
può essere una qualsiasi espressione che ritorni un qualche valore (quindi
non è possibile invocare un metodo con tipo di ritorno void).
La sintassi 2) permette quindi di migliorare lo stack-trace delle asserzioni.
-Progettazione per contratto
Il meccanismo delle asserzioni,
deve il suo successo ad una tecnica di progettazione nota con il nome di “Progettazione
per contratto” (“Design by contract”), sviluppata da Bertrand
Meyer. Tale tecnica è una caratteristica fondamentale del linguaggio
di programmazione sviluppato da Meyer stesso: l’Eiffel (per informazioni
http://www.eiffel.com). Ma è possibile
progettare per contratto, più o meno agevolmente, con qualsiasi linguaggio
di programmazione. La tecnica si basa in particolare su tre tipologie di asserzioni:
pre-condizioni, post-condizioni ed invarianti (le invarianti a loro volta si
dividono in interne, di classe, sul flusso di controllo, etc…).
Con una pre-condizione
lo sviluppatore può specificare quale deve essere lo stato dell’applicazione
nel momento in cui viene invocata un'operazione. In questo modo si rende esplicito
chi ha la responsabilità di testare la correttezza dei dati. L’utilizzo
dell’asserzione riduce sia il pericolo di dimenticare completamente il
controllo, sia quello di fare troppi controlli (perché si possono abilitare
e disabilitare). Dal momento che si tende ad utilizzare le asserzioni in fase
di test e debugging, non bisogna mai confondere l’utilizzo delle asserzioni
con quello della gestione delle eccezioni. Nel prossimo paragrafo verranno esplicitate
delle regole da seguire per l’utilizzo delle asserzioni.
Con una post-condizione lo sviluppatore può specificare quale
deve essere lo stato dell’applicazione nel momento in cui un'operazione
viene completata. Le post-condizioni rappresentano un modo utile per dire cosa
fare senza dire come. In altre parole è un altro metodo per separare
interfaccia ed implementazione interna.
È infine possibile utilizzare il concetto di invariante, che se
applicato ad una classe, permette di specificare vincoli per tutti gli oggetti
istanziati. Questi possono trovarsi in un stato che non rispetta il vincolo
specificato (detto “stato inconsistente”), solo temporaneamente
durante l’esecuzione di qualche metodo, al termine del quale lo stato
deve ritornare “consistente”.
La progettazione per contratto, è appunto una tecnica di progettazione,
e non di programmazione. Essa permette per esempio di testare anche la consistenza
dell’ereditarietà. Una sottoclasse infatti, potrebbe indebolire
le pre-condizioni, e fortificare le post-condizioni e le invarianti di classe,
al fine di convalidare l’estensione. Al lettore interessato ad approfondire
le sue conoscenze sulla progettazione per contratto, consigliamo di acquistare
il libro di Bertrand Meyer, “Object Oriented Software Construction”.
-Uso delle asserzioni
Per poter sfruttare l’utilità
delle asserzioni all’interno dei nostri programmi, bisogna compilarli
e mandarli in esecuzione utilizzando particolari accorgimenti. L’introduzione
della parola chiave assert infatti,
ha per la prima volta sollevato il problema della compatibilità all’indietro
con le precedenti versioni di Java. Non è raro infatti trovare applicazioni
scritte precedentemente all’uscita della versione 1.4 di Java, che utilizzano
come nomi di variabili o metodi la parola assert.
Spesso questo è dovuto proprio alla necessità di alcuni sviluppatori
di simulare in Java il meccanismo delle asserzioni, fino ad allora mancante.
Quindi, per compilare un’applicazione che fa uso delle asserzioni, bisogna
stare attenti anche alla versione di Java che stiamo utilizzando:
- Note per la compilazione di programmi che utilizzano la parola assert
1) Se si utilizza una versione di Java precedente alla 1.4, non è possibile
utilizzare le asserzioni, e assert
non è nemmeno una parola chiave.
2) Se si utilizza la versione di Java 1.4 e si vuole sfruttare il meccanismo
delle asserzioni in un programma, allora bisogna compilarlo con il flag “–source
1.4”, come nel seguente esempio:
javac
–source 1.4 MioProgrammaConAsserzioni.java
Se non utilizziamo il flag suddetto, allora il compilatore non considererà
assert come parola chiave. Conseguentemente,
programmi che utilizzano assert
come costrutto non saranno compilati (perché il costrutto non sarà
riconosciuto), e allo sviluppatore verrà segnalato con un warning, che
dalla versione 1.4 assert è
una parola chiave del linguaggio. I programmi che invece utilizzano assert
come nome di variabili o metodi, saranno compilati correttamente, ma sarà
segnalato lo stesso warning di cui sopra.
3) Se si utilizza la versione 1.5 di Java allora la situazione cambia nuovamente.
Infatti se non si specifica il flag "-source", sarà implicitamente
utilizzato il flag "-source 1.5". Se si vuole sfruttare il meccanismo
delle asserzioni all’interno del programma, basterà quindi compilare
senza utilizzare flag, come nel seguente esempio:
javac
MioProgrammaConAsserzioni.java
che è equivalente a
javac
- source 1.5 MioProgrammaConAsserzioni.java
ed anche a
javac
- source 5 MioProgrammaConAsserzioni.java
visto che la versione 1.5 di Java è stata pubblicizzata come "Java
5".
Se invece si vuole sfruttare la parola assert
come identificatore di un metodo o di una variabile (magari perché il
codice era stato scritto antecedentemente alla versione 1.4), bisognerà
sfruttare il flag -source specificando
una versione precedente alla 1.4. Per esempio:
javac
–source 1.3 MioVecchioProgramma.java
Purtroppo la situazione è questa, e bisogna stare attenti.
- Note per l’esecuzione di programmi che utilizzano la parola assert
Come più volte detto, è possibile in fase di esecuzione abilitare
o disabilitare le asserzioni. Come al solito bisogna utilizzare dei flag, questa
volta applicandoli al comando “java”, ovvero “–enableassertions”
(o più brevemente “-ea”) per abilitare le asserzioni, e “–disableassertions”
(o “-da”) per disabilitare le asserzioni. Per esempio:
java
–ea MioProgrammaConAsserzioni
Abilita da parte della JVM la lettura dei costrutti assert.
Mentre
java
–da MioProgrammaConAsserzioni
disabilita le asserzioni, in modo tale da non rallentare in alcun modo l’applicazione.
Siccome le asserzioni sono di default disabilitate, il precedente codice è
esattamente equivalente al seguente:
java
MioProgrammaConAsserzioni
Sia per l’abilitazione sia per la disabilitazione valgono le seguenti
regole:
1) Se non si specificano argomenti dopo i flag di abilitazione o disabilitazione
delle asserzioni, allora saranno abilitate o disabilitate le asserzioni in tutte
le classi del nostro programma (ma non nelle classi della libreria standard
utilizzate). Questo è il caso dei precedenti esempi.
2) Specificando invece il nome di un package seguito da tre puntini, si abilitano
o si disabilitano le asserzioni in quel package e in tutti i sotto package.
Per esempio il comando:
java
–ea –da:miopackage... MioProgramma
abiliterà le asserzioni in tutto le classi tranne quelle del package
miopackage,
3) Specificando solo i tre puntini invece si abilitano o si disabilitano le
asserzioni nel package di default (ovvero la cartella da dove parte il comando)
4) Specificando solo un nome di una classe invece si abilitano o si disabilitano
le asserzioni in quella classe. Per esempio il comando:
java
–ea:... –da:MiaClasse MioProgramma
abiliterà le asserzioni in tutte le classi del package di default, tranne
che nella classe MiaClasse.
N.B. : è anche possibile eventualmente abilitare o disabilitare, le asserzioni
delle classi della libreria standard che si vuole utilizzare mediante i flag
“-enablesystemassertions” (o più brevemente “-esa”),
e
“-disablesystemassertions” (o “-dsa”). Anche per questi
flag valgono le regole di cui sopra.
N.B. : Per quanto riguarda la fase di esecuzione, non esistono differenze sul
come sfruttare le asserzioni tra la versione 1.4 e 1.5… fortunatamente…
N.B. : in alcuni programmi critici, è possibile che lo sviluppatore si
voglia assicurare che le asserzioni siano abilitate. Con il seguente blocco
di codice statico:
static {
boolean assertsEnabled
= false;
assert assertsEnabled
= true;
if (!assertsEnabled)
throw
new RuntimeException(“Asserts must be enabled!”);
}
è possibile garantire che il programma sia eseguibile solo se le asserzioni
sono abilitate. Il blocco infatti, prima dichiara ed inizializza la variabile
booleana assertsEnabled a false,
per poi cambiare il suo valore a true
se le asserzioni sono abilitate. Quindi se le asserzioni non sono abilitate,
il programma termina con il lancio della RuntimeException,
altrimenti continua. Ricordiamo che il blocco statico, viene eseguito un’unica
volta nel momento in cui la classe che lo contiene viene caricata. Per questa
ragione il blocco statico dovrebbe essere inserito nella classe del main
per essere sicuri di ottenere il risultato voluto.
-Quando usare le asserzioni
Non tutti gli sviluppatori
possono essere interessati all’utilizzo delle asserzioni. Un’asserzione
non può ridursi ad essere un modo coinciso di esprimere una condizione
regolare. Un’asserzione è invece il concetto fondamentale di una
metodologia di progettazione per rendere i programmi più robusti. Nel
momento in cui però, lo sviluppatore decide di utilizzare tale strumento,
dovrebbe essere suo interesse utilizzarlo correttamente. I seguenti consigli
derivano dall’esperienza e dallo studio dei testi relativi alle asserzioni
dell’autore.
1) È spesso consigliato (anche nella documentazione ufficiale Sun), non
utilizzare pre-condizioni, per testare la correttezza dei parametri di metodi
pubblici. È invece raccomandato l’utilizzo delle pre-condizioni
per testare la correttezza dei parametri di metodi privati, protetti o con visibilità
a livello di package. Questo dipende dal fatto che un metodo non pubblico, ha
la possibilità di essere chiamato da un contesto limitato, corretto e
funzionante. Ciò implica che assumiamo che le nostre chiamate al metodo
in questione sono corrette, ed è quindi lecito rinforzare tale concetto
con un’asserzione. Per esempio supponiamo di avere un metodo con visibilità
di package come il seguente:
public
class InstancesFactory {
Object getInstance(int
index) {
assert
(index == 1 || index == 2);
switch
(index) {
case
1:
return
new Instance1();
case
2:
return
new Instance2();
}
}
}
Se questo metodo può essere chiamato solo da classi cha appartengono
allo stesso package della classe InstancesFactory,
allora non deve mai accadere che il parametro index
sia diverso da 1 o 2,
perchè tale situazione rappresenterebbe un bug.
Se invece il metodo getInstance(),
fosse dichiarato public, allora
la situazione sarebbe diversa. Infatti, un eventuale controllo del parametro
index, dovrebbe essere considerato
ordinario, e quindi da gestire magari mediante il lancio di un’eccezione:
public
class InstancesFactory {
public Object
getInstance(int index) throws Exception {
if
(index == 1 || index == 2) {
throw
new Exception(“Indice errato: ” + index);
}
switch
(index) {
case
1:
return
new Instance1();
case
2:
return
new Instance2();
}
}
}
L’uso di un’asserzione in tal caso, non garantirebbe la robustezza
del programma, ma solo la sua eventuale interruzione, se fossero abilitate le
asserzioni al runtime, non potendo a priori controllare la chiamata al metodo.
In pratica una pre-condizione di questo tipo violerebbe il concetto object oriented
di metodo pubblico.
2) È sconsigliato l’utilizzo di asserzioni laddove si vuole testare
la correttezza di dati che sono inseriti da un utente. Le asserzioni dovrebbero
testare la consistenza del programma con se stesso, non la consistenza dell’utente
con il programma. L’eventuale input non corretto da parte di un utente
è giusto che sia gestito mediante eccezioni, non asserzioni. Per esempio,
condideriamo la seguente classe Data:
public
class Data {
private int
giorno;
. . .
public void
setGiorno(int g) {
assert
(g > 0 && g <= 31): “Giorno non valido”;
giorno
= g;
}
.
. .
dove il parametro g del metodo
setGiorno() viene passato da
un utente mediante un oggetto interfaccia,
che rappresenta un interfaccia grafica:
...
Data
unaData = new Data();
unaData.setGiorno(interfaccia.dammiGiornoInserito());
unaData.setMese(interfaccia.dammiMeseInserito());
unaData.setAnno(interfaccia.dammiAnnoInserito());
...
Come il lettore avrà intuito, l’utilizzo della parola chiave
assert non è corretto.
Infatti nel caso le asserzioni fossero abilitate in fase di esecuzione dell’applicazione,
e l’utente inserisse un valore errato per inizializzare la variabile giorno,
l’applicazione si interromperebbe con un AssertError!
Ovviamente se le asserzioni non fossero abilitate allora nessun controllo impedirebbe
all’utente di inserire valori errati. La soluzione ideale sarebbe quella
di gestire la situazione tramite un’eccezione, per esempio:
public
void setGiorno(int g) throws RuntimeException {
if
(!(g > 0 && g <= 31)) {
throw
new RuntimeException(“Giorno non valido”);
}
giorno
= g;
}
Ovviamente la condizione è ampiamente migliorabile…
3) L’uso delle asserzioni invece, ben si adatta alle post-condizioni ed
alle invarianti. Per post-condizione intendiamo una condizione che viene verificata
appena prima che termini l’esecuzione di un metodo (ultima istruzione).
Segue un esempio:
public
class Connection {
private
boolean isOpen = false;
public void open()
{
//
...
isOpen
= true;
//
...
assert
isOpen;
}
public
void close() throws ConnectionException {
if
(!isOpen) {
throw
new ConnectionException("Impossibile chiudere connessioni non aperte!"
);
}
//
...
isOpen
= false;
//
...
assert
!isOpen;
}
}
Dividiamo le invarianti in interne, di classe e sul flusso di controllo.
Per invarianti interne intendiamo asserzioni che testano la correttezza
dei flussi delle nostre classi. Per esempio il seguente blocco di codice:
if
(i == 0) {
...
}
else if (i == 1) {
...
}
else { // ma sicuramente (i == 2)
...
}
può diventare più robusto con l’uso di un’asserzione:
if (i == 0) {
...
}
else if (i == 1) {
...
}
else {
assert
i == 2 : “Attenzione i = ” + I + “!”;
...
}
Maggiore probabilità di utilizzo di un tale tipo di invariante, è
all’interno di una clausola default
di un costrutto switch. Spesso
lo sviluppatore sottovaluta il costrutto omettendo la clausola default,
perchè suppone che il flusso passi sicuramente per un certo case.
Per convalidare le nostre supposizioni, sono molto utili le asserzioni. Per
esempio il seguente blocco di codice:
switch(tipoAuto)
{
case
Auto.SPORTIVA:
...
break;
case
Auto.LUSSO:
...
break;
case
Auto.UTILITARIA:
...
break;
}
può diventare più robusto con l’uso di un’asserzione:
switch(tipoAuto)
{
case
Auto.SPORTIVA:
...
break;
case
Auto.LUSSO:
...
break;
case
Auto.UTILITARIA:
...
break;
default:
assert
false : “Tipo auto non previsto : ” + tipoAuto;
}
Per invarianti di classe, intendiamo particolari invarianti interne che
devono essere vere per tutte le istanze di una certa classe, in ogni momento
del loro ciclo di vita, tranne che durante l’esecuzione di alcuni metodi.
All’inizio ed al termine di ogni metodo però, lo stato dell’oggetto
deve tornare “consistente”. Per esempio un oggetto della seguente
classe:
public
class Bilancia {
private
double peso;
public
Bilancia() {
azzeraLancetta();
assert
lancettaAzzerata();
}
private
void setPeso(double grammi) {
assert
grammi > 0; // pre-condizione
peso
= grammi;
}
private
double getPeso() {
return
peso;
}
public
void pesa(double grammi) {
if
(grammi < 0) {
throw
new RuntimeException(“Grammi < 0!”);
}
setPeso(grammi);
mostraPeso();
azzeraLancetta();
assert
lancettaAzzerata(); // invariante di classe
}
private
void mostraPeso() {
System.out.println(“Il
peso è di ” + peso + “ grammi”);
}
private
void azzeraLancetta() {
setPeso(0);
}
private
boolean lancettaAzzerata () {
return
peso == 0;
}
}
potrebbe dopo ogni pesatura, azzerare la lancetta (notare che i due soli metodi
pubblici terminano con un’asserzione).
Per invarianti sul flusso di controllo intendiamo asserzioni che vengono
posizionate in posti del codice che non dovrebbero mai essere raggiunte. Per
esempio, se abbiamo un pezzo di codice che viene commentato in tal modo:
public
void metodo() {
if
(flag == true) {
return;
}
//
L’esecuzione non dovrebbe mai arrivare qui!
}
Potremmo sostituire il commento con un asserzione sicuramente false:
public
void metodo() {
if
(flag == true) {
return;
}
assert
false;
}
-Conclusioni: