Filtrage et validation constituent dans les faits deux des plus gros sujets de programmation en informatique de gestion, et pourtant ils sont souvent très mal implémentés. C’est dommage car avec un peu d’astuce on peut fortement améliorer la situation, et c’est ce qu’on va voir maintenant…
Débloque les + belles offres tech en 10 mins
Je mets le filtrage et la validation dans le même panier car il s’agit dans les faits peu ou prou de la même chose, à savoir appliquer un ensemble de règles pour savoir si un élément est valide ou pas. Il y a dans les faits quelques différences entre les deux qu’on va voir.
Bien souvent hélas un code qui fait de la validation (ou du filtrage, je vais arrêter de radoter), ressemble à ça :
public void validate(final List<MonObjet> maListe) {
for (MonObjet obj: maListe) {
if (condition1) {
// ...
}
if (condition2) {
// ...
}
if (condition3) {
// ...
}
// ...
if (condition50) {
// ...
}
}
}
Ce code présente dans les faits de nombreux problèmes, à savoir :
L’effet de bord est qu’il deviendra de plus en plus compliqué d’ajouter de nouvelles règles sans être sûr de ne rien casser.
Pour résoudre ces problèmes il faudrait adopter l’approche suivante :
Le fait de boucler sur les éléments est très simple, on pourra déjà améliorer la situation en refactorant la boucle comme suit :
public void validate(final List<MonObjet> maListe) {
for (MonObjet obj: maListe) {
runValidation(obj);
}
}
Mais bon la situation n’est pas idéale car il reste à séparer toutes nos règles métier pour les rendre simple à gérer.
L’idée est d’implémenter les règles dans les feuilles de votre composite, à raison d’une règle par feuille, ou de plusieurs si celles-ci sont vraiment triviales. De la sorte vous n’aurez plus aucun problème à tester chaque règle séparément, et en assemblant le tout vous aurez votre ensemble complet de règles.
Vous savez quoi ? Même si on ne le voit pas bien souvent il est tout à fait possible de constituer des listes de beans avec Spring, et de les injecter. Supposons que j’ai une interface MonBean
, une classe MonBeanComposite
qui implémente MonBean
pour le composite et mes règles métier implémentées dans les classes MonBean1
, MonBean2
et MonBean3
qui implémentent l’interface MonBean
.
Si vous passez par la configuration par annotations, ne mettez pas d’annotation du type @Service
ou @Component
sur les implémentations de MonBean
. J’entends par là sur les classes en elles-même. Si celles-ci ont des dépendances vous pouvez par contre parfaitement utilisez des @Resource
ou des @Autowired
. D’autre part dans MonBeanComposite
il ne faut pas mettre d’annotation sur la liste d’instances de MonBean
en dépendance, par contre il est conseillé d’utiliser le constructeur au lieu d’un setter pour la passer en paramètre.
En fait on va instancier les beans en passant par une classe de configuration de Spring, comme suit :
@Bean
public MonBean1 monBean1() {
return new MonBean1();
}
@Bean
public MonBean2 monBean2() {
return new MonBean2();
}
@Bean
public MonBean3 monBean3() {
return new MonBean3();
}
@Bean
public List<MonBean> monBeanList() {
List<MonBean> result = new ArrayList<>();
result.add(monBean1());
result.add(monBean2());
result.add(monBean3());
return result;
}
@Bean
public MonBeanComposite monBeanComposite() {
return new MonBeanComposite(monBeanList());
}
Dès lors vous aurez accès à votre composite avec l’annotation suivante :
@Resource(name = "monBeanComposite")
private MonBean monBean;
Avec le XML la configuration se ferait comme suit :
<bean id="monBean1" class="x.y.z.MonBean1"/>
<bean id="monBean2" class="x.y.z.MonBean2"/>
<bean id="monBean3" class="x.y.z.MonBean3"/>
<bean id="monBeanComposite" class="x.y.z.MonBeanComposite">
<constructor-arg>
<list>
<ref bean="monBean1"/>
<ref bean="monBean2"/>
<ref bean="monBean3"/>
</list>
</constructor-arg>
</bean>
Maintenant passons aux éléments propres aux filtres. L’interface d’un filtre devrait toujours être comme suit :
public boolean filter(final MonObj objToTest, final ObjContext ctx);
En d’autres termes cette interface prend en paramètre l’objet à tester, et éventuellement un objet de contexte qui peut être utile dans certains cas. Elle retourne true
si l’objet à tester satisfait la condition du filtre, false
sinon… et c’est tout ! En aucun cas ce n’est la responsabilité du filtre que d’itérer sur la liste d’objets à filtrer, son rôle se limite à dire si oui ou non tel ou tel objet satisfait la règle métier..
En d’autres terme la responsabilité d’itérer sur les éléments à filtrer doit être confiée à l’appelant, avec un code comme celui-ci :
public List<MonObj> filterList(final List<MonObj> list, final Filter f) {
List<MonObj> result = new ArrayList<>();
for (MonObj obj: list) {
if (f.filter(obj)) {
result.add(obj);
}
}
return result;
}
Ainsi on peut facilement tester le code qui itère sur les objets (il suffit de créer un mock sur le filtre…) et d’autre part il est très facile de chaîner les filtres avec un composite.
Les validateurs valident des objets, et stockent les messages d’erreur dans un objet chargé de les collecter afin de les renvoyer d’un seul coup à l’utilisateur. Par conséquent l’API d’un validateur devrait être comme suit :
public void validate(final MonObj candidate, final ObjContext ctx, final MsgCollector collector);
De même que pour les filtres, l’objet de contexte est optionnel, et on peut réduire cette interface à deux paramètres en mettant le collecteur de messages d’erreur en attribut de l’objet de contexte dans le cas où celui-ci existerait.
Je ne vais pas revenir sur l’utilisation de cet objet, néanmoins il y a un point très intéressant apparu avec Java 8 qu’on voit immédiatement.
Comme vous le savez probablement Java 8 amène les lambdas, qui permettent de simplifier l’écriture de bon nombre de classes anonymes lorsque celles-ci sont très simples et qu’elles ont une interface à une seule méthode (dite « interface fonctionnelle »).
Jusqu’à présent quand on voulait tester une règle, on utilisait le pattern suivant :
if (!condition) {
monCollector.addError(new MonError(...));
}
C’était lourd et pas vraiment élégant.
Il était aussi possible de définir sur MonCollector
une méthode comme celle ci-dessous, qui n’ajoute un message d’erreur que si une assertion est à false
:
public void addError(final boolean assertion, final MonError monError) {
if (!assertion) {
errorList.add(monError);
}
}
Le souci était alors à rechercher du côté des performances étant donné qu’on instanciait beaucoup d’objets MonError
pour rien, même si au final le code était bien plus élégant (et plus simple à tester…) car on s’épargnait le if
.
Maintenant avec les lambdas on peut créer une interface fonctionnelle MonErrorInstanciator
dont la définition est ci-dessous :
@FunctionalInterface
public interface MonErrorInstanciator {
public MonError newMonError();
}
Dès lors on peut modifier la méthode addError
du collecteur d’erreurs présenté ci-dessus en :
public void addError(final boolean assertion, final MonErrorInstanciator instanciator) {
if (!assertion) {
MonError err = instanciator.newMonError();
errorList.add(monError);
}
}
Ainsi on n’instancie pas plus de MonError
que nécessaire. Et avec les lambdas l’utilisation de l’API devient :
monCollector.addError(assertion, () -> new MonError(...));
Le code est quand même plus sympa à lire et à maintenir, pas vrai ? 😉 D’autre part ici la JVM saura optimiser l’utilisation de la lambda qui ne sera pas réinstanciée à chaque appel de la méthode addError
de monCollector
.
A l’heure actuelle je suis en train d’appliquer cette méthode pour refactorer et tester automatiquement une application, et le moins qu’on puisse dire est qu’en fait on gagne énormément de temps. Au départ il est vrai qu’il y a un certain coût lié au design qui est un peu plus lourd que le « tout en vrac », mais au final dès lors qu’il faut ajouter ou retirer des règles métier tout devient très simple et très facile à maintenir.
Débloque les + belles offres tech en 10 mins
Cet article vous a plu ? Vous aimerez sûrement aussi :
Julien
Moi c’est Julien, ingénieur en informatique avec quelques années d’expérience. Je suis tombé dans la marmite étant petit, mon père avait acheté un Apple – avant même ma naissance (oui ça date !). Et maintenant je me passionne essentiellement pour tout ce qui est du monde Java et du système, les OS open source en particulier.
Au quotidien, je suis devops, bref je fais du dév, je discute avec les opérationnels, et je fais du conseil auprès des clients.
Les maladies inflammatoires chroniques de l’intestin ou "MICI" sont invisibles, mais leurs impacts sur la…
Depuis l'été, j'ai un Pixel qui intègre à la fois un TPU (Tensor Processing Unit)…
On se retrouve dans un nouvel article avec toutes les infos sur cette nouvelle saison…
Pourquoi l’inclusion numérique est essentielle : le point avec Mathieu Froidure. Dans un monde de…
Elles sont passées où les femmes dans la tech ? Entre le manque de représentation…