Il Design by
Contract (DBC nel seguito dell'articolo) rappresenta un'interessante soluzione;
si tratta di una metodologia che consiste, ad una prima approssimazione,
nel considerare un'interfaccia come un contratto e nel definire i termini
del contratto, ovvero
-
le condizioni che
il client deve rispettare per poter chiamare un metodo (precondizioni);
-
le condizioni che
il metodo si impegna a rispettare, purché le precondizioni siano
soddisfatte (postcondizioni);
-
le condizioni che
devono essere sempre valide (invarianti).
Nel seguito dell'articolo
vedremo di approfondire il DBC quindi vedremo come sviluppare un supporto
minimale per le asserzioni in Java.
Il DBC in
dettaglio
La programmazione
OO permette di sviluppare software flessibile, riutilizzabile ed
estensibile ma molto spesso questo non basta per avere software veramente
affidabile. Poter riutilizzare codice esistente ha molti vantaggi purché
quel codice faccia veramente ciò che noi ci aspettiamo. Ciò
che è necessario per fare un salto di qualità nello sviluppo
del codice è una metodologia che permetta di specificare in modo
rigoroso il comportamento di un oggetto.
Questo è
proprio quello che permettono di fare i metodi di specifica formale ma
il tempo richiesto per il loro utilizzo è eccessivo e quindi si
tratta di una soluzione non conveniente. Il DBC è stato sviluppato
da B. Meyer e integrato nel linguaggio Eiffel [2] come possibile soluzione
a questi problemi.
Come accennato
nell'introduzione il DBC si basa sul concetto di contratto software: i
contraenti sono un oggetto O e i client che lo utilizzano. Ognuno
di essi ha degli obblighi
-
quando un client
invoca un metodo m() dell'oggetto O deve garantire che lo
stato dell'oggetto sia consistente prima dell'esecuzione del metodo (precondizioni);
-
l'oggetto da parte
sua deve garantire che al termine dell'esecuzione del metodo il client
veda soddisfatte le proprie richieste (postcondizioni);
-
inoltre vi sono
delle condizioni che devono sempre essere rispettate durante l'esecuzione
del metodo e durante tutto il ciclo di vita dell'oggetto (invarianti);
Se una delle condizioni
non è verificata allora il contratto non è più valido
e l'esecuzione deve essere interrotta con un'eccezione, o comunque occorre
segnalare il problema in un qualche modo ad esempio utilizzando un file
di log. Le condizioni specificate sono espresse in generale con i classici
operatori relazionali (=, <, >, ...). Strumenti come iContract [7] supportano
invece una sintassi più potente che comprende ad esempio i quantificatori
(forall, exists) e che rappresenta un sottoinsieme di OCL, l'Object Constraint
Language di UML [9].
Vediamo ora
come si integra il DBC con l'ereditarietà, un aspetto molto importante
per l'utilizzo di questa metodologia con la programmazione a oggetti. Se
una classe derivata ridefinisce un metodo della classe base allora
-
le precondizioni
del metodo nella classe derivata dovranno essere le stesse della classe
base oppure più deboli;
-
le postcondizioni
dovranno essere le stesse della classe base oppure le stesse.
In questo modo è
possibile garantire che un client possa utilizzare la classe derivata in
luogo di quella base. Il contratto della classe derivata è cioè
più "rigido" di quella della classe base.
Un uso interessante
del DBC è il supporto alla documentazione. Molto spesso infatti
la documentazione è ambigua, poco precisa oppure del tutto mancante.
Inserire nella documentazione di un metodo le sue pre e postcondizioni
ne aumenta di gran lunga la qualità e aiuta ad utilizzare e comprendere
il metodo in modo più sicuro ed efficace.
Usare il DBC
in Java
Iniziamo subito
con una nota dolente: Java non supporta il DBC e simularlo è abbastanza
complicato. Occorre infatti inserire manualmente il controllo su precondizioni
e postcondizioni nel codice dei metodi, prestando attenzione ad esempio
a tutte le istruzioni di ritorno presenti. Se all'interno di un metodo
troviamo cioè più istruzioni di return occorre verificare
prima di ognuna di esse le postcondizioni. Inoltre la gestione dell'ereditarietà
delle condizioni complica ulteriormente le cose. Dovranno infatti essere
ancora i programmatori a verificare la consistenza dei controlli sui metodi
ridefiniti nelle classi derivate. L'ultimo macigno è la mancanza
di compilazione condizionale in Java, che è molto importante per
eliminare le asserzioni dal codice rilasciato.
Una curiosità:
uno dei primi documenti di specifica del linguaggio prevedeva il supporto
al DBC, ma Gosling non lo ha poi inserito per problemi di scadenze [8].
Nonostante i
problemi descritti prima vediamo ora come sviluppare un semplice tool per
usare le asserzioni in Java. Non arriveremo a supportare appieno il DBC
ma avremo comunque uno strumento utile per cominciare a ragionare in termini
di contratto software a livello di singolo metodo, rinunciando però
al supporto per l'ereditarietà. Come osservato poi in [3] l'uso
delle semplici asserzioni "è uno dei cardini della programmazione
professionale".
Il seguente
listato mostra l'implementazione completa del tool:
public
class Contract {
public static void require(boolean condition, String message) {
if ( !
condition) {
throw
new RuntimeException("Precondition violated: " + message);
}
}
public
static void ensure(boolean condition, String message) {
if
( ! condition) {
throw new RuntimeException("Postcondition violated: " +
message);
}
}
public
static void invariant(boolean condition, String message) {
if
( ! condition) {
throw new RuntimeException("Invariant violated: " +
message);
}
}
}
Per chi volesse
approfondire l'uso del DBC in Java vi sono diverse librerie, anche commerciali,
che offrono tale supporto anche utilizzando l'ereditarietà. Alcuni
esempi si possono trovare in [8].
Esempio
Vediamo ora
di mettere in pratica i concetti del Design by Contract e di testare la
nostra classe Contract con un esempio. Supponiamo di voler realizzare una
classe che rappresenta i dati di una persona. Questa classe avrà
attributi che rappresentano dati come nome, data di nascita e professione.
Vi sarà quindi una serie di metodi per modificare ed accedere a
questi attributi, con alcuni vincoli di integrità come ad esempio
il fatto che il nome debba essere una stringa di lunghezza superiore a
0. Segue ora il listato della classe Person
import
java.util.Date;
public
class Person {
private String name;
private Date birthdate;
private String job;
public Person() {
name = "";
birthdate = new Date();
job = "";
}
public void setName(String name) {
Contract.require(name.length() > 0, "invalid name");
this.name = name;
}
public String getName() {
Contract.ensure(name.length() > 0, "invalid name");
return name;
}
// ...
}
Ho tralasciato il
codice degli altri metodi poiché è analogo. Possiamo vedere
come il metodo setName() richieda nella precondizione che il parametro
stringa debba avere una lunghezza maggiore di zero. Un controllo più
sofisticato potrebbe verificare che la stringa contenga almeno uno spazio
ed essere quindi della forma "cognome nome". Il metodo getName() verifica
la stessa condizione sul risultato, usando quindi una postcondizione. Vediamo
un possibile codice client che usa la classe precedente
...
Person p1 = new
Person(),
p2 = new Person(),
p3 = new Person();
String name;
p1.setName("Giovannini
Andrea"); // OK
name = p1.getName();
// OK
p2.setName("");
// precondizione violata
name = p3.getName();
// postcondizione violata
...
E' importante osservare
che i problemi del codice precedente non si sarebbero verificati durante
la compilazione. L'esempio presentato è molto semplice ma è
facile immaginare la portata dei vantaggi del DBC in programmi più
complessi con molte righe di codice. Un altro esempio di utilizzo del DBC
in Java può essere trovato in [6].
Conclusioni
In questo articolo
abbiamo introdotto una metodologia di progettazione e sviluppo poco conosciuta
ma molto potente che permette di aumentare la qualità del software.
Questo è fondamentale se si pensa alla programmazione per componenti
che devono essere riutilizzati magari su vasta scala.
Il Design by
Contract può essere applicato in linea di principio ad ogni linguaggio
a oggetti e in questo articolo abbiamo visto come poter utilizzarlo in
Java. Il tool qui presentato non vuole essere un completo supporto al DBC
ma un semplice punto d'inizio.
Un aspetto che
non abbiamo approfondito è l'utilizzo del Design by Contract in
un ambiente multi-threaded. Ad esempio le precondizioni non rappresentano
più vincoli sul chiamante perchè altri thread potrebbero
intervenire per modificare lo stato dell'oggetto chiamato. La soluzione
proposta sempre da Meyer consiste nel far diventare le precondizioni delle
condizioni di sincronizzazione. Ulteriori informazioni possono essere trovate
in [5].
Riferimenti
-
Andrea Giovannini,
"Sviluppare con le interfacce in Java", MokaByte N. 34, Ottobre 1999
-
Il sito di Eiffel,
http://www.eiffel.com
-
Carlo Pescio, "Debugging:
tecniche e tool", Computer Programming N. 43, Gennaio 1996
-
Carlo Pescio, "Oggetti
ed Interfacce", Computer Programming N. 63, Novembre 1997
-
Carlo Pescio, "Oggetti
e Thread", Computer Programming N. 65, Gennaio 1998
-
Jim Weirich, "Design
by Contract for Java", http://w3.one.net/~jweirich/java/javadbc/java-dbc.html
-
Il sito di iContract,
http://www.reliable-systems.com/tools/iContract/iContract.htm
-
Una raccolta di
link su DBC e Java, http://www.elj.com/eiffel/feature/dbc/java/ge/
-
Il sito di Rational,
la società che ha sviluppato UML, http://www.rational.com
|