Le pattern de specification et les lambdas
Introduction : le pattern de spécification
J’ai récemment mis en place le pattern de spécification pour gérer des règles métier au sein d’une de nos application. Je me suis inspiré du très bon article suivant trouvé sur le bog de Xebia : http://blog.xebia.fr/2009/12/29/le-pattern-specification-pour-la-gestion-de-vos-regles-metier/ cet article, bien qu’ancien (7 ans!) est toujours d’actualité. Il est lui-même inspiré du pattern tel que décrit par Martin Fowler ici : http://www.martinfowler.com/apsupp/spec.pdf
Je ne vais pas vous faire la description complète du pattern (si vous ne le connaissez pas je vous conseille viement de lire l’article de Xebia), son principe est de séparer chaque règle métier dans une classe données (une spécification) et de pouvoir les composer avec des and, or et not pour améliorer la lisibilité des règles métiers.
Mais l’article de Xebia est très ancien, il date d’un monde avant Java 8, avant les lambdas et la programation fonctionnelle qu’elles permettent.
Je vous propose donc ici de revisiter ce pattern avec l’introduction des lambdas!
Voici un exemple de règles métier basique que nous allons transformer via ce pattern pour en accroitre la lisibilité et la réutilisabilité :
Obj1 obj1 = new Obj1();//init boolean isSatisfied = obj.getText() != null && (obj.getNb() > 10 || obj.getNb() < 1);
Le pattern de spécification sans lambda
Voici un diagramme du pattern de spécification originel tel que défini dans l’article du blog de Xebia et qui présente les classes suivantes :
- Une interface Specification qui définit la méthode isSatisfiedBy(T obj) qui contient la règle de gestion en question et les méthodes and(), or(), not() permettant la composition de spécification
- Une classe abstraire AbstractCompositeSpecification qui implémente l’interface Specification et ses méthodes and(), or(), not() via les classes concrètes suivantes :
- AndSpecification : composition de spécification via ET logique
- OrSpecification : composition de spécification via OU logique
- NotSpecification : composition de spécification via NON logique
- Une class abstraite LeafSpecification qui sert de marqueur pour l’implémentation de nos spécifications. Chaque spécification que nous implémenterons devra étendre cette classe (ex par exemple de la classe MySpecification dans le schema) et implémenter la méthode isSatisfiedBy(T obj)
Voici un exemple d’implémentation en Java : https://github.com/loicmathieu/dojo/tree/master/src/main/java/fr/loicmathieu/dojo/pattern/specification
L’implémentation du pattern avec trois spécifications basiques :
public class Obj1HasNotNullText extends LeafSpecification<Obj1>{ @Override public boolean isSatisfiedBy(Obj1 obj) { return obj.getText() != null; } } public class Obj1NbLessThan1 extends LeafSpecification<Obj1> { @Override public boolean isSatisfiedBy(Obj1 obj) { return obj.getNb() < 1; } } public class Obj1NbMoreThan10 extends LeafSpecification<Obj1> { @Override public boolean isSatisfiedBy(Obj1 obj) { return obj.getNb() > 10; } }
Et leurs utilisation :
Obj1 obj1 = new Obj1();//init Obj1HasNotNullText spec1 = new Obj1HasNotNullText(); Obj1NbMoreThan10 spec2 = new Obj1NbMoreThan10(); Obj1NbLessThan1 spec3 = new Obj1NbLessThan1(); boolean isSatisfied = spec1.and(spec2.or(spec3)).isSatisfiedBy(obj1);
Le pattern de spécification avec lambda
Dans l’exemple précédent, on voit bien que lorsque les règles sont basiques, la création de classes dédiées est fastitieuse. Essayons donc de modifier le pattern afin que l’on puisse utiliser des lambdas pour l’implémentation de nos spécifications.
Pour pouvoir définir une spécification via une lambda, il faut que l’interface Specification deviennent une interface fonctionnelle : une interface avec uniquement une seule méthode (voir https://docs.oracle.com/javase/8/docs/api/java/lang/FunctionalInterface.html pour plus d’informations). On va donc extraire dans une interface CompositeSpecification les méthodes de compositions de spécification (and(), or(), not()) et ne laisser uniquement que dans l’interface Specification la méthode isSatisfiedy(T obj) qui pourra maintenant donc être implémentée via une lambda.
Mais la question est : comment définir cette lambda, il nous faudrait une sorte de factory quelque part? J’ai choisit d’ajouter une méthode statique from(Specification<T> spec) à la classe abstraite LeafSpecification qui pemet de créer une spécification depuis une lambda, c’est là que tout se passe et voici comment ça marche :
public abstract class LeafSpecification<T> extends AbstractCompositeSpecification<T>{ public static <T> LeafSpecification<T> from(Specification<T> spec){ return new LambdaSpecification<>(spec); } static class LambdaSpecification<T> extends LeafSpecification<T> { private Specification<T> s; LambdaSpecification(Specification<T> s){ this.s = s; } public boolean isSatisfiedBy(T obj) { return s.isSatisfiedBy(obj); } } }
J’ai donc créé une classe interne LambdaSpecification qui implèmente isSatisfiedBy(T obj) via une spécification passé lors de la construction de la classe … et voila ! On peut maintenant créer des spécification facilement depuis une lambda via :
Specification<Obj> spec = LeafSpecification.from(obj -> obj.size() > 10);
Voici le diagramme de classe modifié :
Voici un exemple d’implémentation en Java : https://github.com/loicmathieu/dojo/tree/master/src/main/java/fr/loicmathieu/dojo/pattern/lambdaspec
L’implémentation du pattern avec trois spécifications basiques et son utilisation (ici, plus besoin de créer de classes séparés pour les spécifications):
Obj1 obj1 = new Obj1();//init LeafSpecification<Obj1> spec1 = LeafSpecification.from(obj -> obj.getText() != null); LeafSpecification<Obj1> spec2 = LeafSpecification.from(obj -> obj.getNb() > 10); LeafSpecification<Obj1> spec3 = LeafSpecification.from(obj -> obj.getNb() < 1); boolean isSatisfied = spec1.and(spec2.or(spec3)).isSatisfiedBy(obj1);
Conclusion
L’utilisation des lambdas pour implémenter le pattern de spécification semble un ajout intéréssant. Mais attention, il est intéréssant dans le cadre de spécifications basiques tenant sur un ensemble de lignes réduites (voir uniquement pour des spécifications à une ligne?). Le mieux étant de mixer des spécifications en lambda avec des spécifications implémentée de manière classiques quand elles représentent des règles métier complexes pour ne pas aller à l’encontre de pourquoi le pattern de spécification a été créer à la base (lisibilité du code entre autre).
On peut d’ailleur se poser la question de savoir si l’implémentation en lambda du pattern de spécification ne serait pas en elle-même un anti-pattern car on ré-écrit alors toutes les règles métier directement dans la classe les utilisant sans pouvoir les ré-utiliser dans d’autre classes … je vous laisse le soin de juger ;).