|
|
|
|
Utiliser les assertions |
|
|
 |
|
|
|
|
|
23 Novembre 2002 |
|
|
Version 1.0 |
|
|
Par Sébastien MERIC |
|
|
Remerciements : Stefan Bertholon |
|
Dans ce documement, je vous présente un cadre de développement particulier :
Les assertions. Les assertions sont la traduction en code interpratble par
un ordinateur de vos certitudes. Vous traduisez souvant celles-ci à l'aide
de commentaires dans le code. A partir d'aujourd'hui, vous pourrez le faire sous
la forme d'un code interprétable.
Je vous donne comme d'habitude quelques bonnes pratiques, en effet, j'essaye de vous donner
des reflexes pour cette utilisation. Pour une fois, la lisibilité du code n'est pas
au coeur du sujet, cette fois on cherche à être plus lisible par l'ordinateur plutôt
que par l'être humain.
Introduction
Qu'est qu'une assertion
D'abord un petit tour du côté du dictionnaire ne peut
pas faire de mal :
assertion n. f. Proposition
que l'on avance comme vraie. Des assertions mensongères.
(
Dictionnaire Universel Francophone © 1997
HACHETTE/EDICEF)
Nous voilà donc fixé, c'est assez clair comme définition,
il nous reste quand même à le traduire en termes de
langage formel comme java, et de comprendre à quoi celà
peut nous servir.
Dans l'article vérifier
la validité de vos paramètres, vous trouverez
dans le code du dernier paragraphe les lignes de code suivantes :
/** * Vérification de l'égalité de deux intervalles.<p> * Vérifie que les deux intervalles soient bien équivalents en comparant simplement * la date de début et la date de fin. * @return vrai, si les deux intervalles sont équivalents. */ public boolean equals(java.lang.Object obj) { if (null == obj) { // rien à comparer return false; } if ( ! (obj instanceof DateIntervalle)) { // pas le bon type d'objet return false; } DateIntervalle intervalle = (DateIntervalle) obj;
// à partir de ce point, on sait que l'on peut commencer quelques tests // comme de plus, on prend soin des setters, il n'y a plus qu'à reprendre // le code de départ return getBeginAt().equals(intervalle.getBeginAt()) && getEndAt().equals(intervalle.getEndAt()); }
Il y a là trois lignes qui attirent mon attention, elles ne sont
d'ailleurs là que pour ça : les commentaires au milieu du
code. Dans ces commentaires je présume en français et sous
la forme de commentaires que getBeginAt() et getEndAt() ne renvoient jamais
null. Bien, parfait, mais s'il pouvait y avoir une syntaxe qui vienne
préciser la même chose, et que le compilateur comprenne, j'enrichirais
de fait la sémantique de mon code. Vous vous en doutez, c'est
le but de cet article ! Le mot clef assert est là pour ça
depuis le jdk1.4, profitons en.
/** * Vérification de l'égalité de deux intervalles.<p> * Vérifie que les deux intervalles soient bien équivalents en comparant simplement * la date de début et la date de fin. * @return vrai, si les deux intervalles sont équivalents. */ public boolean equals(java.lang.Object obj) { if (null == obj) { // rien à comparer return false; } if ( ! (obj instanceof DateIntervalle)) { // pas le bon type d'objet return false; } DateIntervalle intervalle = (DateIntervalle) obj;
assert null != getBeginAt(); assert null != getEndAt();
return getBeginAt().equals(intervalle.getBeginAt()) && getEndAt().equals(intervalle.getEndAt()); }
Nous y voilà, j'ai dit la même chose qu'avec les commentaires,
mais cette fois le compilateur est capable de l'interpréter
et surtout, si quelqu'un venait à modifier le code des
muttateurs ou des accesseurs et qu'il oublie ou ignore l'existence
de cette contrainte, il sera prévenu de son erreur très
rapidement. Il ne va pas chercher pendant des heures d'où
vient son bug ! De plus, j'ai gagné en lisibilité
puisque les assertions sont plus faciles à lire que la phrase
de trois lignes.
Un mot de la programmation par contrats
Difficile de vous parler des assertions sans au préalable
vous faire un tout petit topo sur la programmation par contrats. Des
contrats entre classes vous en passez dès que vous définissez
une interface en java. La programmation par contrats va plus
loin que la simple définition d'un contrat statique (ie.
si telle classe implémente MonInterface, elle sera capable
de répondre à telle et telle requêtes) et
définie aussi une partie du contrat dynamique. En effet,
en programmation par contrats, en plus de préciser les signatures
des diverses méthodes à implémenter, on précise
aussi les pré et postconditions liées à l'utilisation
de ces méthodes. En clair, on précise qu'avant d'appeler
la méthode equals (dans l'exemple précédent),
on s'attend à ce que getBeginAt() et getEndAt() ne peuvent
pas renvoyer la valeur null. La postcondition, quand à elle,
précise l'état de votre classe après l'utilisation
d'une méthode. On peut alors remanier l'exemple précédent
pour le rendre encore plus lisible : la postcondition de setBeginAt()
: que getBeginAt() ne soit pas null ! Il devient donc inutile de le
répéter dans la méthode equals, ce qui améliore
encore la lisibilité de votre code ! Je ne m'étend pas
plus sur ce vaste sujet dans cet article, il n'y en a pas lieu, mais
je vais quand même vous donner un pointeur vers un framework
de programmation par contrat pour java : jContractor. La lecture de
la documentation est intéressante, pleine de bonnes idées,
et mérite que vous vous y attardiez, même si vous ne
pensez pas utiliser la notion de programmation par contrats dans vos
futurs développements.
jContractor
: A Reflective Java Library to Support Design By ...
Syntaxe
Revenons donc à nos moutons, les assertions et leur utilisation
en java. (pour d'autres langages, n'hésitez pas à me
contacter je serais heureux comme pour tous les articles se trouvant
dans cette rubrique, de pouvoir diversifier les langages pour les
exemples). Pour le coup, il n'y a pas grand chose à apprendre,
ce qui est une bonne nouvelle. Le mot clef assert s'utilise donc
de la manière suivante :
assert condition [: objet] ;
où condition est une expression conditionnelle, c'est à
dire n'importe quoi dont la valeur de retour est un booléen,
et object est n'importe quel objet contenant les informations concernant
l'echec de l'assertion.
La plupart du temps, vous utiliserez donc la syntaxe suivante
:
assert isMaConditionVraie() : "Ma condition devrait toujours être vérifiée avant de ...."; assert size() < 5 : "La voiture ne peut pas avoir plus de 4 roues";
Simplissime, non ? Et grâce à ce petit bout de code
de rien du tout, vos développements vont vraiment être améliorés
! Mais attention, c'est comme tout, ne résolvez pas tout,
à coup de assert, ou vous allez tomber dans un excès
regrettable.
Utilisation
Alors comment et surtout où utiliser ces fameux asserts ?
Et bien, pour commencer, chaque fois que vous présumez d'une
vérité sans pour autant qu'elle soit triviale, c'est à
dire pratiquement chaque fois que vous présumez de quelque
chose dans le code, écrivez le dans un assert. Pour faire
plus clair, je veux dire, chaque fois que vous vous trouvez dans
la situation que j'ai présenté en introduction. Mais
reprenons dans le détail :
Invariants logiques
Les invariants logiques sont tous les invariants pour lesquels la
logique sémantique du code précédent mène
à ce qui semble être une totologie au moment où
on l'écrit mais pourrait fort bien être détruit
par la modification future de ce même code. Comme vous avez
de fait présumé d'une vérité qui n'est
pas traduite par du code, il est bon de le spécifier par
une assertion. Comment ? Cà vous parait compliqué ?
Donc, comme toujours, un petit exemple et vous allez tout de suite
comprendre :
coef = (x * x) / (1 + (x * x)); assert coef < 1 : "coef est un coeficient compris entre 0 et 1"; assert 0 <= coef : "coef est un coeficient compris entre 0 et 1";
Invariants de contrôle des flux
Les contrôles de flux sont essentiels au bon déroulement
de vos programmes. Sans assertion, c'est vous qui en relisant votre code,
êtes capable de dire si le flux est bien suivi, ou s'il est de mauvaise
qualité. Grâce à la notion d'assertion, vous pouvez
demander à votre ordinateur de faire le contôle des flux
pour vous, et je peux vous assurer que quand ces flux deviennent nombreux,
il est plus efficace que vous pour ce travail. Mais essayons quand même
de comprendre ce qu'est en effet le contrôle de flux, puis je complèterais
par un exemple de code pour illustrer ma pensée. Dans votre code,
il y a des points par lesquels vous avez la certitude de ne pas devoir
passer ou alors sous conditions particulières. Si par exemple je
m'assure que l'utilisateur ne peux pas choisir d'option hors des valeurs
que je lui présente grâce à une combo-box, et que
je suis au moment où je code, certain de retrouver, dans un tableau,
un objet correspondant à l'une de ces valeurs, je n'ai plus qu'à
faire une boucle qui cherche la valeur et fait un return dès qu'elle
l'a trouvée. Je suis donc sûr, enfin sur le papier, que je
ne peux pas sortir de cette boucle sans avoir trouvé ce que je cherche.
Je fais une assertion, je l'écris donc dans le code :
/** * Estime le prix passé en argument.<p> * <font color="red">Attention ce code ne peut pas être considéré comme * robuste, il est épuré de tout ce qui ne concerne pas directement la * démonstration en cours : contrôle des flux à l'aide d'assertion. L'utilisation * de tableaux plutôt que de collections n'est pas non plus une bonne pratique * mais elle permet de concentrer l'attention sur le but de la démonstration</font><p> * Se base sur deux tableaux de même longueur contenant les jalons de prix, et les * estimations associées. * @param prix le prix en euro * @return Une chaîne de caractères contenant l'estimation de l'ordinateur * @throw IllegalArgumentException si le prix est négatif */ public String controleDeFlux(int prix) throws IllegalArgumentException {
if (prix < 0) { // le prix ne peut pas être négatif throw IllegalArgumentException("Le prix est négatif"); }
int minimum = 0;
for(int i=0; i < tableauPrix.length; i++) { if ((minimum <= prix) && (prix < tableauPrix[i])) { // dans le bon intervalle return tableauEstimation[i]; } minimum = tableauPrix[i]; }
assert false : "Le prix ne correspond à aucun des intervalles connu"; }
Pré/postconditions et invariants fonctionnels
Les préconditions, je vous en ai parlé en introduction,
ce sont tout simplement les conditions nécessaires au bon fonctionnement
du code inclus dans la méthode que vous êtes sur le point
d'écrire. Les postconditions sont les conditions sur l'état
du système tel qu'il devrait être laissé à
la fin de l'appel de la méthode. Enfin ,les invariants fonctionnels
sont toutes les contraintes que doit respecter votre classe, ou
plutôt ses instances les objets, pour être en conformité
avec la définition de celle-ci. C'est à dire par exemple,
une voiture doit avoir 4 roues, est un invariant fonctionnel de
la classe Voiture. Le nombre de connexions est limité à
10 est aussi un invariant fonctionnel. Je reprends donc chacune
de ces explications et je les illustre avec du code :
Précondition
/** * Estime le prix passé en argument.<p> * <font color="red">Attention ce code ne peut pas être considéré comme * robuste, il est épuré de tout ce qui ne concerne pas directement la * démonstration en cours : des préconditions à l'aide d'assertions. L'utilisation * de tableaux plutôt que de collections n'est pas non plus une bonne pratique * mais elle permet de concentrer l'attention sur le but de la démonstration</font><p> * Se base sur deux tableaux de même longueur contenant les jalons de prix, et les * estimations associées. * <p><i>Vérifie que les tableaux sont de même longueur, avec au moins deux valeurs et que * le tableau des prix est rangés par ordre croissant</i></p> * @param prix le prix en euro * @return Une chaîne de caractère contenant l'estimation de l'ordinateur * @throw IllegalArgumentException si le prix est négatif */ public String controleDeFlux(int prix) throws IllegalArgumentException {
assert tableauPrix.length == tableauEstimations.length : "Le nombre d'intervalles ne correspond pas au nombre d'estimations"; assert tableauxPrix.length > 1 : "Impossible de définir des intervalles sans valeur ou avec une seule valeur";
color="#a52a2a">for (int i = 0; i < tableau.length - 1; i++) { assert tableauPrix[i] <= tableauPrix[i+1] : "Les valeurs du tableau des prix ne sont pas rangées dans l'ordre"; }
if (prix < 0) { // le prix ne peut pas être négatif throw IllegalArgumentException("Le prix est négatif"); } ... }
Remarquez bien ici que le prix négatif n'est pas traité
à l'aide d'une assertion ! Ceci vient du fait qu'en aucun cas
je ne peux présumer de ce que les classes clientes de ma classe
me passeront comme argument. Voir le paragraphe mauvaises pratiques pour en savoir plus.
Postcondition
/** * Ajoute un objet dans une collection et vérifie que l'objet a bien été ajouté.<p> * <i>Ce code est encore une fois sans intérêt hors de son contexte : * démontrer l'intérêt et expliquer la notion de postcondition</i> * @param o l'objet à stocker. */ public void add(Object o) { int _old_size = maCollection.size(); maCollection.add(o);
assert maCollection.size() == _old_size + 1; }
Invariants fonctionnels
public class Voiture { public void roule() { assert 4 == getRoues.size() : "La voiture ne peut pas rouler si elle n'a pas exactement 4 roues."; ... } ...
Mauvaises pratiques
Comme pour chaque nouvelle technologie apprise, le premier piège
auquel le développeur se heurte est bien entendu son envie
de l'utiliser à tout prix, donc dans toutes situations.
La tentation d'utiliser les assertions pour lever des exceptions
est donc forte. Pourtant, une assertion non vérifiée
ne lance pas une exception mais une erreur. (Si vous êtes
assez nombreux à m'envoyer à ce propos, un mp, je
vous ferais un article sur les Throwable, les Exception
et les Error
Il est donc important de ne pas mélanger, la vérification
des paramètres dont je parlais dans un article précédent,
avec les assertions. A ce propos je vous demanderais de refaire
un tour sur la définition du terme... Nous y voilà
donc, nous ne pouvons pas présumer de ce que l'utilisateur
de notre code va nous passer en paramètre. Il est donc de
très mauvaise augure de tester les paramètres de
vos méthodes publiques à l'aide d'assertions. Eviter
aussi de les utiliser pour les méthodes protégées,
leur portée étant plus étendue que le simple
périmètre de votre développement en cours.
Vous pouvez par contre sans crainte définir des assertions
sur les arguments de vos méthodes privées.
Pourtant l'assertion a un net avantage sur la vérification if
() { throw }, elle est débrayable (voir : Du développement
au déploiement), on cherchera donc l'utiliser de préférence
à cette vérification "hardcodée". Pour ce faire, il
faudra limité absolument les déclarations de méthodes
publiques, et utiliser au maximum la notion de composant. On limitera ainsi
l'interface publique du développement, ce qui nous permettra d'utiliser
le maximum d'assertions.
De plus, la couche applicative, ou votre application complète
si vous ne développez pas en couches, peut aussi être considérée
comme privée (elle ne fait référence qu'à elle
même et ne s'attend pas à être utilisé autrement
que par elle même). Dans ce cas, vous pouvez utiliser les assertions
sur les méthodes publiques, mais vous devez alors considérer
comme non-assertable toutes les
interactions avec l'utilisateur.
Une autre pratique très mauvaise, serait d'appeler une méthode
ayant un effet sur l'état de votre classe. Il y a deux raisons
fondamentales à cette remarque : on ne le répètera
jamais assez, il est très difficile de lire du code dans
lequel sur une seule ligne sont réunis plusieurs instructions,
et le manque de lisibilité est à l'opposé
de la robustesse. La deuxième raison est toute aussi importante,
dans le prochain paragraphe, je vous explique qu'il est possible
une fois la phase de développement terminée, de débrayer
les assertions pour des raisons évidentes d'optimisation
des performances. Donc, si vous modifiez l'état de votre
système pendant une assertion, au moment du déploiement,
votre programme ne fonctionnera
plus ! Ainsi n'écrivez jamais quelque chose dans ce goût
:
assert connect() : "La connection est impossible"; // A NE JAMAIS FAIRE
Enfin, une pratique qui n'est pas mauvaise que dans les assertions, mais
qui est particulièrement redoutable à cet endroit : ne considérez
jamais l'égalité entre deux réels ou entre deux dates
comme possible ! En effet, il est pratiquement impossible d'obtenir l'égalité
parfaite entre deux réels ou entre deux dates. Si vous devez vérifier
l'égalité de deux réels, vérifiez plutôt
que la distance entre les deux est faible :
assert abs(r1 - r2) < 0.0001 : "r1="+r1+" et r2="+r2+" devraient être égaux";
Du développement
au déploiement (Java uniquement)
Le développement à l'aide d'assertions nécessite
le jdk1.4. La compilation doit spécifiée que vous faites
usage du mot clef assert à l'aide de l'argument en ligne
de commande -source 1.4.
Enfin, au moment de lancer votre application, par défaut,
les assertions sont débrayées (ceci afin que votre utilisateur
ne soit pas obligé de le faire), il faut donc spécifier
à la jvm de les utiliser à l'aide de l'argument
en ligne de commande : -ea ou -enableassertions.
Il reste des subtilités pour l'utilisation des assertions,
vous pouvez en effet les activer uniquement sur certaines classes, mais
je vous laisse lire le document
de chez sun pour en savoir plus.
Conclusion
Nous voilà donc d'ors et déjà à la tête
de bonnes pratiques et d'outils permettant de rendre le code intrinsèquement
robuste, je vous rappelle rapidement ces pratiques :
- Vérifier la validité des paramètres,
- Documenter, commenter et rendre le code lisible donc
autodocumenté,
- Utiliser les assertions, en particulier spécifier
les préconditions, les postconditions et les invariants de
classes.
Il ne nous reste plus qu'une chose à faire, s'assurer que
le code répond aux critères de robustesse en le mettant
volontairement dans les situations détectées
comme étant difficiles. Pour ce faire, il vous faut un moyen
simple de tester le code. Il existe des frameworks de tests unitaires
très efficaces pour le faire. Je vous encourage donc maintenant
à lire l'article sur les framework de tests, dans lequel
j'utiliserais le framework JUnit livré en open source par la
communauté des adeptes de l'extreme programming.
|