Utiliser les assertions

Sébastien MERIC

Dans ce document, je vous présente un cadre de développement particulier : Les assertions. Les assertions sont la traduction en code interprétable par un ordinateur de vos certitudes. Vous traduisez souvent celles-ci à l'aide de commentaires dans le code. À 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'essaie de vous donner des réflexes pour cette utilisation. Pour une fois, la lisibilité du code n'est pas au cœur du sujet, cette fois on cherche à être plus lisible par l'ordinateur plutôt que par l'être humain.

- Titre : Utiliser les assertions

- Auteur : Sébastien MERIC

- Parution : 23 novembre 2002

- Mise à jour : 14 novembre 2019

- Licence : Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright ® 2013 Sébastien MERIC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction

- I-A. Qu'est-ce qu'une assertion ?

- I-B. Un mot de la programmation par contrats

- II. Syntaxe

- III. Utilisation

- III-A. Invariants logiques

- III-B. Invariants de contrôle des flux

- III-C. Pré/postconditions et invariants fonctionnels

- III-C-1. Précondition

- III-C-2. Postcondition

- III-C-3. Invariants fonctionnels

- IV. Mauvaises pratiques

- V. Du développement au déploiement (Java uniquement)

- VI. Conclusion

I - Introduction

I-A - Qu'est-ce 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és, 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 cela 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 sont 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 vient 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 sont 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 mutateurs 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 bogue ! De plus, j'ai gagné en lisibilité puisque les assertions sont plus faciles à lire que la phrase de trois lignes.

I-B - 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 (i.e. si telle classe implémente MonInterface, elle sera capable de répondre à telle et telle requête) et définit 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 puissent pas renvoyer la valeur null. La postcondition, quant à 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'étends 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…

II - 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 objet est n'importe quel objet contenant les informations concernant l'échec 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.

III - Utilisation

Alors, comment et surtout où utiliser ces fameux asserts ? Eh 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ée en introduction. Mais reprenons dans le détail.

III-A - 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 « tautologie » 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 ? Ça 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 coefficient compris entre 0 et 1";
assert 0 <= coef : "coef est un coefficient compris entre 0 et 1";

III-B - 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 contrô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 peut 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 euros
* @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 connus";
}

III-C - 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 quatre roues, est un invariant fonctionnel de la classe Voiture. Le nombre de connexions est limité à dix est aussi un invariant fonctionnel. Je reprends donc chacune de ces explications et je les illustre avec du code.

III-C-1 - 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é par ordre croissant</i></p>
* @param prix le prix en euros
* @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 {
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.

III-C-2 - 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;
}

III-C-3 - 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.";
...
}
...

IV - 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 ferai 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. À ce propos je vous demanderai 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 mauvais augure de tester les paramètres de vos méthodes publiques à l'aide d'assertions. Éviter 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 limiter 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ée 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éunies plusieurs instructions, et le manque de lisibilité est à l'opposé de la robustesse. La deuxième raison est tout 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 connexion 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";

V - Du développement au déploiement (Java uniquement)

Le développement à l'aide d'assertions nécessite le jdk1.4. La compilation doit spécifier 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.

VI - Conclusion

Nous voilà donc d'ores 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 frameworks de tests, dans lequel j'utiliserai le framework JUnit livré en open source par la communauté des adeptes de l'extrême programming.