I. 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.
II. 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 incorrectes variable par variable (ici variable s'applique aux arguments et aux attributs) ;
- recensement des actions à entreprendre si l'une des valeurs incorrectes survient ;
- écriture 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.)
III. 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 mutateurs 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éé le 13 octobre 2002, 10:02
*
*/
package
com.developpez.sme.samples;
import
java.util.*;
/**
* Gestion d'intervalles de dates, permettant entre autres 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 dates
*/
public
DateIntervalle
(
) {
this
(
null
, null
);
}
/**
* Constructeur d'intervalles de dates plus complet
*
@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 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
"["
+
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 exécuté)
}
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'œil. Elle est pourtant la cause de nombre de bogues 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 aboutit, 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 débogage.
IV. 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 bogues 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és 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 ?
Eh 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. 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'entrée 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éé le 13 octobre 2002, 10:02
*
*/
package
com.developpez.sme.samples;
import
java.util.*;
/**
* Gestion d'intervalles de dates, permettant entre autres 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 dates
*/
public
DateIntervalle
(
) {
this
(
null
, null
);
}
/**
* Constructeur d'intervalles de dates plus complet
*
@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 dates.
<
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 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
(
));
}
// Liste des getters et des setters
public
Calendar getBeginAt
(
) {
return
beginAt;
}
/**
* Définit 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éfinit 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 pour date le "long" 0
* qui a peu de chances 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 passé 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;
}
VI. 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. Évidemment, 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.