Passer au contenu principal

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…

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 :

  • On mélange à la fois le fait de boucler sur les éléments et les règles métier.
  • Il y a de nombreuses règles métier, autrement dit cette méthode a autant de responsabilités que de règles métier.
  • Un tel code n’est que difficilement testable et maintenable.

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 :

  • D’un côté boucler sur les éléments.
  • De l’autre séparer les règles métier en de nombreuses entités.

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.

Le design pattern idéal pour faire la séparation : le composite

Le design pattern Composite fait partie des grands principe de la programmation. On ne va pas le redétailler ici, mais juste mettre son diagramme UML qui est le suivant :

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.

Et avec Spring ?

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.

La configuration par annotations

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;

La configuration par XML

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>

Les filtres

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

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.

Java 8 et les lambdas

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.

Retour d’expérience

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.

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.

Son Twitter Son LinkedIn

Laisser un commentaire