I. Introduction

I-A. 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 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 :

 
Sélectionnez
    /**
     * 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.

 
Sélectionnez
    /**
     * 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.

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 (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, 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 :

 
Sélectionnez
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 :

 
Sélectionnez
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é 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 « 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 ? Ça vous parait compliqué ? Donc, comme toujours, un petit exemple et vous allez tout de suite comprendre :

 
Sélectionnez
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";

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 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 :

 
Sélectionnez
        /**
         * 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";
        }

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

 
Sélectionnez
        /**
         * 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.

III-C-2. Postcondition

 
Sélectionnez
        /**
         * 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

 
Sélectionnez
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 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. É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ébrayage (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 :

 
Sélectionnez
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 :

 
Sélectionnez
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'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 auto-documenté ;
  • 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'extrême programming.