|
|
|
|
|
Vérifier la validité de vos paramètres |
|
|
 |
|
|
|
|
|
6 Novembre 2002 |
|
|
Version 1.0 |
|
|
Par Sébastien MERIC |
|
|
Remerciements : Johann Heymes, Clément Cunin, Stefan Bertholon et avtonio |
|
Introduction
La méthode est très simple, mais mérite d'être appliquée systématiquement
dans vos développements. En effet, vérifier l'intégrité des
paramètres avant tout traitement permet d'une part de rendre
votre code beaucoup plus lisible, et d'autre part, de s'assurer
de sa robustesse. Rien ne devrait plus arriver qui n'ai été prévu.
Voici trois exemples de code, le premier sans vérification, le second
avec vérification en cours de code et le dernier avec validation des
paramètres, que vous utiliserez dorénavant.
Présentation
Premièrement, il devient urgent de présenter le concept de cet article.
Qu'est-ce que j'entends par "Vérifier
la validité de vos paramètres" ?
La validation des paramètres revient tout
simplement à mettre en entrée de toutes vos méthodes l'ensemble
des tests qui permettront de vérifier que les variables avec lesquelles
vous allez travailler sont conformes à ce que l'on en attend pour
le traitement à venir ; éventuellement à les remettre en conformité
et plus probablement, à lancer une exception si elles ne sont pas
conformes. C'est à dire en décrypté :
- Recensement
des arguments. Facile à faire, il suffit de lire la liste
dans la déclaration de la méthode
- Recensement
des attributs requis. Au pire il faudra
relire le code après avoir implémenté la méthode pour mettre à jour
la liste
- Recensement
des valeurs incorectes variable par variable (ici variable
s'applique aux arguments et aux attributs)
- Recensement
des actions à entreprendre si l'une des valeurs incorectes
survient
- Ecriture
du code de validation avant le code fonctionnel
Le document se présentera en trois parties, l'exemple valant mieux
que de longs discours, je vous présenterai le code correspondant
pour chaque partie :
- Première partie, présentation d'un
code de type " débutant " pour lequel aucune vérification de
paramètre, ni de variable n'est réalisée. On part du principe
que tout va bien se passer, on teste avec en tête l'idée que tout
ce passera bien, et plus tard, on ne comprend pas pourquoi le code
que l'on a écrit ailleurs refuse de marcher !
- Deuxième partie, présentation d'un
code avec une vérification mélangée au code. Je soulignerai
dans cette partie l'ensemble des désagréments relatifs à ce
concept (manque de lisibilité qui entraîne une difficulté de maintenance
et l'oubli de cas particuliers).
- Enfin, dans la dernière partie, je
vous présente un code qui n'a pas la prétention d'être parfait,
mais qui met en évidence l'amélioration de la robustesse et de
la lisibilité de votre développement. (Retenez que dans cet article
comme dans les prochains, la lisibilité est toujours liée à la
robustesse).
Sans vérification
Ici, je vous présente une classe assez pauvre mais qui illustrera
parfaitement mon propos. La classe prétend gérer des intervalles
de temps, elle s'appuie sur la classe Calendar du package java.util.
Les deux méthodes fondamentales pour illustrer ce propos sont
toString() et equals(Object). Par la suite, on verra aussi que
les deux muttateurs présentés dans cette première version peuvent
aussi être grandement améliorés par la vérification de paramètres.
La méthode toString() surcharge son ancêtre pour l'affichage en
clair de l'intervalle. La méthode equals() surcharge elle aussi son
ancêtre mais pour vérifier l'égalité de deux intervalles, c'est à
dire l'égalité de la date et de l'heure du début de l'intervalle d'une
part, et celle de la date et de l'heure de fin de l'intervalle d'autre
part.
/* * DateIntervalle.java * * Crée le 13 octobre 2002, 10:02 * */
package com.developpez.sme.samples;
import java.util.*;
/** * Gestion d'intervalles de dates, permettant entre autre de vérifier l'égalité de * deux intervalles.<p> * Cette classe ne sert que de démonstration de l'utilité de "vérifier les paramètres". * Elle n'a aucune autre finalité. * @author <a href=http://www.developpez.net/forums/profile.php?mode=viewprofile&u=5561>Sébastien</a> * @version 1.0 */ public class DateIntervalle {
/** * Constructeur par défaut d'intervalles de date */ public DateIntervalle() { this(null, null); } /** * Constructeur d'intervallse de date plus complets * @param begin le début de l'intervalle * @param end la fin de l'intervalle */ public DateIntervalle(Calendar begin, Calendar end) { setBeginAt(begin); setEndAt(end); }
/** * Affichage d'un intervalle de date.<p> * exprime l'intervalle sous la forme [date heure début, date heure fin]. * @returns l'intervalle sous la forme de chaîne de caractère. */ public java.lang.String toString() { return "[" + getBeginAt() + ", " + getEndAt() +"]"; } /** * 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. * @returns vrai si les deux intervalles sont équivalents. */ public boolean equals(java.lang.Object obj) { boolean result; result = getBeginAt().equals(((DateIntervalle)obj).getBeginAt()); result = result && getEndAt().equals(((DateIntervalle)obj).getEndAt()); return result; }
// Liste des getters et des setters
public Calendar getBeginAt() { return beginAt; }
public void setBeginAt(Calendar value) { this.beginAt = value; }
public Calendar getEndAt() { return endAt; }
public void setEndAt(Calendar value) { this.endAt = value; }
/** début de l'intervalle */ private Calendar beginAt; /** fin de l'intervalle */ private Calendar endAt;
}
Dans ce premier code, il faut peu d’habitude pour se rendre compte
à quel point il est fragile. En effet, voici un cas classique
d’utilisation qui va générer un comportement inattendu :
DateIntervalle monIntervalle = new DateIntervalle() if (monIntervalle.equals(null)) { // traitement (qui dans ce cas ne sera pas executer) }
L’erreur apparaît ici tellement évidente que l’on peut dire que
le code n’est absolument pas robuste en un seul coup d’oeil. Elle est pourtant
la cause de nombre de bugs dans les logiciels que nous développons.
Toutefois reprenons le en détail pour bien souligner le problème ici
:
- La création de monIntervalle
aboutie, puisque j'utilise le constructeur par défaut, à un intervalle
dont les deux bornes sont nulles.
- L'appel de
la méthode equals nous amène donc à la ligne de code
result = getBeginAt(). et trop tard, l'opérateur
point ne peut pas s'appliquer à null !
En effet, sans prendre le temps de vérifier ses paramètres de manière
exhaustive dès l’entrée en matière, c’est à dire en tête de
la méthode, il est fort à parier que viendra se glisser un oubli
qui, la plupart du temps, représente le cas particulier quasi improbable.
C’est justement le pire de tous, il va vous faire perdre des heures
en debug.
Vérification au fil du code
Nous voilà donc enfin arrivés à la seconde partie, la vérification
au fil du code et des besoins. Cette méthode, va vous paraître
quelque peu plus robuste que la première puisqu'un minimum de
bugs sembleront avoir été supprimés. On peut même probablement,
en faisant de gros efforts, traiter tous les cas se présentant
ou pouvant être amenés à se présenter. Pourtant, je trouve que la
lisibilité de votre code est plus importante encore que la pseudo
robustesse apportée ici par quelques tests conditionnels. Voici
donc le code modifié, et j'explique ensuite pourquoi ce problème de
lisibilité de code me paraît tellement plus fondamental. Je ne vous
livre que les deux méthodes modifiées, c'est déjà bien assez difficile
à lire, vous allez voir :
/** * Affichage d'un intervalle de dates.<p> * exprime l'intervalle sous la forme [date heure début, date heure fin]. * @returns l'intervalle sous la forme de chaîne de caractères. */ public java.lang.String toString() { return "[" + ((null == getBeginAt()) ? "no bound" : getBeginAt().toString()) + ", " + ((null==getEndAt()) ? "no bound" : getEndAt().toString()) +"]"; }
/** * 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. * @returns vrai si les deux intervalles sont équivalents. */ public boolean equals(java.lang.Object obj) { boolean result; if (null != obj) { if (obj instanceof DateIntervalle) { // Sinon pas de test possible DateIntervalle intervalle = (DateIntervalle) obj; if ((null != getBeginAt()) && (null != intervalle.getBeginAt())) { // tout va bien, somme toute result = getBeginAt().equals(intervalle.getBeginAt()); } else { // on traite de manière simple sinon on va continuer à imbriquer les if then else // donc si un des arguments testés est null, on répond non ! ouf :-) result = false; } if ((null != getEndAt()) && (null != intervalle.getEndAt())) { // tout va bien, somme toute result = result && getEndAt().equals(intervalle.getEndAt()); } else { // on traite de manière simple sinon on va continuer à imbriquer les if then else // donc si un des arguments testé est null, on répond non ! ouf :-) result = false; } } else { // l'objet à vérifier n'est pas un intervalle result = false; } } else { // et cette fois, c'est tout l'intervalle en comparaison qui est null result = false; } return result; }
Je veux bien essayer de croire que certains d'entre vous aient lu
le pavé ci-dessus, mais à la vérité, je doute que quiconque
prenne vraiment la peine de le faire. Alors qu'a-t-on gagné dans
cette version ? Une robustesse relative, en effet si les dates
ou l'objet passés ne correspondent pas ou sont nuls, on sait alors
répondre non à la question d'égalité entre les deux. Pourtant, et
dans ce cas j'ai complètement simplifié le traitement, qui pourra
alors dire de manière certaine dans quel cas la méthode répond oui
à la question, et dans quel cas elle répond non ? Et dans le cas où
tout serait parfaitement expliqué dans la javadoc que je vous fournis,
comment vérifier que le comportement est bien celui qui est décrit
?
Et bien du temps et de la patience vont s'imposer. Je vous laisse
imaginer le fourbi d'imbrications if then else qu'il va falloir
mettre en place pour obtenir de cet objet qu'il considère que
beginAt = null correspond réellement à moins l'infini, soit pas
de borne, et endAt correspond à plus l'infini, soit pas de borne
non plus ! Je ne vous le laisse pas en exercice, mais une chose est
certaine, je ne vais pas le faire.
Dans ce cas, je ne suis pas certain finalement, que la méthode que
j'annonce être pour débutant dans le premier paragraphe,
ne soit pas meilleure. Elle a au moins le mérite d'être utilisable
et donc d'être adaptable, ce qui n'est absolument pas le cas du
code ci-dessus. Je dois pourtant me résoudre à lire des codes de
ce type assez régulièrement.
Vérifier la validité des paramètres
Alors, alors, finalement vais-je vous proposer mieux ? Ma foi, je
l'espère bien ! En effet, je vais tenter de vous montrer à
présent que tout en conservant un code lisible, je peux faire
non seulement aussi bien que pour l'exemple précédent, mais encore
que je peux améliorer son traitement. Le concept est de plus très
simple, je vérifie d'entrer tous les cas simples (c'est en général
ceux pour lesquels les paramètres sont hors de leur domaine). Que
signifie vérifier d'entrée ? Tout simplement avant d'écrire un code
fonctionnel, souvent appelé métier, je me contente de faire tous les
tests " hors domaine de traitement " et de les traiter un par un !
Attention le un par un est essentiel, car il est important pour les
lecteurs futurs (dont vous êtes, ne l'oubliez pas) de pouvoir lister
en moins de quelques secondes les cas hors domaine !
Mais avant de se
lancer dans le code, appliquons les quelques règles présentées
au début de ce document sur la méthode equals :
|
Recensement des arguments |
|
|
obj |
|
|
Recensement des attributs |
|
|
beginAt et endAt |
|
|
Recensement des valeurs indésirables |
|
|
|
|
|
obj |
|
|
null |
|
|
obj |
|
|
pas un DateIntervalle |
|
|
beginAt |
|
|
null résolu par getter et setter |
|
|
endAt |
|
|
null résolu par getter et setter |
|
|
Recensement des traitements |
|
|
|
|
|
obj est null |
|
|
Renvoie false puisque l'égalité ne peut pas exister entre "un objet DateIntervalle non null" et "null" |
|
|
obj n'est pas un DateIntervalle |
|
|
Renvoie false aussi bien entendu |
|
Voici donc le code final, en entier puisque beaucoup de choses ont
été manipulées. Le fichier est long, mais il contient de nombreux
commentaires pour rendre la lecture plus aisée. Passez du temps
à lire une ou deux méthodes, en particulier, bien entendu, celles
qui sont pointées du doigt ci-dessus, et vous verrez que la lecture
est grandement simplifiée.
/* * DateIntervalle.java * * Crée le 13 octobre 2002, 10:02 * */
package com.developpez.sme.samples;
import java.util.*;
/** * Gestion d'intervalles de date, permettant entre autre de vérifier l'égalité de * deux intervalles.<p> * Cette classe ne sert que de démonstration de l'utilité de "vérifier les paramètres". * Elle n'a aucune autre finalité. * @author <a href=http://www.developpez.net/forums/profile.php?mode=viewprofile&u=5561>Sébastien</a> * @version 1.1 - vérification de paramètres */ public class DateIntervalle {
/** * Constructeur par défaut d'intervalles de date */ public DateIntervalle() { this(null, null); } /** * Constructeur d'intervalles de date plus complets * @param begin le début de l'intervalle * @param end la fin de l'intervalle */ public DateIntervalle(Calendar begin, Calendar end) { setBeginAt(begin); setEndAt(end); }
/** * Affichage d'un intervalle de date.<p> * exprime l'intervalle sous la forme [date heure début, date heure fin]. * @return l'intervalle sous la forme de chaîne de caractères. */ public java.lang.String toString() { // test String begining = "no bound"; String end = "no bound"; if (getInfini() != getBeginAt()) { // début spécifié begining = getBeginAt().toString(); } if (getInfini() != getEndAt()) { // fin spécifiée end = getEndAt().toString(); } return "[" + begining + ", " + end +"]"; }
/** * 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()); }
// Liste des getters et des setters
public Calendar getBeginAt() { return beginAt; }
/** * Définie la nouvelle valeur de début d'intervalle.<p> * Afin de permettre de gérer des intervalles infinis, on accepte de recevoir * une valeur "null" comme point de départ. Ceci correspond alors à un intervalle * ayant commencé dans la nuit des temps. * @param value une date de début ou null pour la nuit des temps. */ public void setBeginAt(Calendar value) { if (null == value) { // null est equivalent à "a toujours existé" this.beginAt = getInfini(); return; } this.beginAt = beginAt; }
public Calendar getEndAt() { return endAt; }
/** * Définie la nouvelle valeur de fin d'intervalle.<p> * Afin de permettre de gérer des intervalles infinis, on accepte de recevoir * une valeur "null" comme fin d'intervalle. Ceci correspond alors à un intervalle * se terminant au fin fond de l'éternité. * @param value une date de fin ou null pour l'éternité. */ public void setEndAt(Calendar value) { if (null == value) { // null est équivalent à "existera toujours" this.endAt = getInfini(); return; } this.endAt = value; } /** * Prépare une date infinie pour l'utilisation des intervalles.<p> * Fabrique ou renvoie un objet Calendar ayant p our date le "long" 0 * qui a peu de chance d'être utilisé pour les intervalles normaux. * Ceci est la seule restriction de cette classe. On aurait pu faire mieux * mais pour la démonstration, le concept est suffisant. * @return un Calendar représentant la date infinie, vers le futur ou vers * l'avenir. */ private static Calendar getInfini() { if (null == infini) { infini = Calendar.getInstance(); infini.clear(); } return infini; }
private static Calendar infini; /** début de l'intervalle */ private Calendar beginAt; /** fin de l'intervalle */ private Calendar endAt; }
Conclusion
J'ai tenté dans ce premier article de vous donner un conseil très
simple, d'ailleurs certains d'entre vous le pratiquent sûrement
déjà, mais qui devrait apporter une grande robustesse à vos développements
futurs. N'hésitez pas à l'utiliser, et à m'envoyer
un message si vous avez des questions. Evidemment, si vous
voulez proposer des améliorations, envoyez moi un message, pour
faire la mise à jour. Je le rappelle encore une fois parce que ça
me paraît primordial : la lisibilité de votre code est essentielle
à la robustesse de celui-ci. Si vous ne pouvez lire votre code, vous
ne savez pas ce qu'il fait, donc vous ne pouvez pas vérifier que ce
qu'il fait est ce que vous lui demandez de faire.
|