Le pattern Specification pour la gestion de vos règles métier
Souvent lorsque l'on parle de gérer les règles métiers, on pense à moteur de règle, pas forcement ...
Le design pattern Specification est une solution de gestion de vos règles métiers.
Ce pattern a été formalisé par Eric Evans, père du DDD, et Martin Fowler que l'on ne présente plus.
Ce pattern est simple mais très puissant. Il permet de :
- marquer et identifier les règles métiers,
- les centraliser,
- les réutiliser,
- communiquer entre développeurs et fonctionnels sur ces règles métiers.
Cette solution a récemment été mise en place sur un gros site d'eCommerce en France. Je partage ici avec vous mon retour d'expérience, donc laissez vous convaincre !
Allo Houston ? On a un problème
Prenons un exemple simple mais significatif, un site d'eCommerce de fruits et légumes.
Supposons que l'on parte d'un besoin simple, exprimé par Ted, notre expert fonctionnel : "En tant qu'internaute, je veux ajouter des produits (donc des fruits ou/et légumes) à mon panier".
Bob, Le valeureux développeur, n'hésite pas un seul instant et réalise la besogne de la manière suivante :
panier.add(new FraiseDePlougastel(5, Unite.BARQUETTE));
panier.add(new PommeDeTerreBinch(2, Unite.KILO));
Le développeur se dit "Trop facile pour que se soit ça ...", réfléchit, " Je sais, on peut pas ajouter à l'infini ! On doit de se limiter à une valeur réaliste, par exemple 10 Kilogrammes de produit par panier"
Bob aime le travail bien fait et décide de refactorer son code de la manière suivante afin de s'adapter au mieux à une solution réaliste :
private static final double LIMITE_DE_POIDS = 10.0;
private Collection<Produit> contenu = new Set<Produit>();
public double getPoidsEnKilo() {
...
}
public void ajouter(Produit produit) {
// TODO Bob : gerer pre condition suivant besoin de Ted
if (getPoidsEnKilo() + produit.getPoidsEnKilo() <LIMITE_DE_POIDS) {
contenu.add(produit);
}
}
}
Une fois le refactoring effectué, Bob s'empresse de remonter le problème à son expert fonctionnel préféré. Ted, ne niant pas la faille, indique à Bob qu'il va revoir son besoin. Mais que pour cela il a besoin d'un peu de réflexion.
Le temps de la réflexion pris, Ted revient vers Bob avec un besoin affiné :
"En tant qu'internaute habitant en Ile de France, je veux ajouter jusqu'à 100 kilos de produit à mon panier"
"En tant qu'internaute de France métropolitaine, je veux ajouter jusqu'à 25 kilos de produit à mon panier"
"En tant qu'internaute des DOM/TOM, je veux ajouter jusqu'à 10 kilos de produit supportant le transport en avion à mon panier"
Bob, devenu tout blanc : "Ça va être compliqué à implémenter"
Ted : "Oui, mais j'ai un super transporteur en ile de france, commercialement je ne peux pas m'en passer"
Ted, pour rassurer Bob : "T'inquiètes pas dans 3 mois, on changera tout ca, on aura de nouveaux transporteurs"
Bob : "Ah, mais on sera déjà en production ..."
Les affaires se compliquent pour le pauvre Bob, comment va t'on pouvoir l'aider?
Description du problème à résoudre
Tout d'abord, une application "n'est qu'une" succession d'évaluations de règles métiers qui sont structurées et hiérarchisées.
Ces règles sont de complexité et de durée de vie très variable.
En discutant avec des référents fonctionnels, j'ai pu sentir une certaine frustration sur la gestion des règles métiers. Cette frustration se ressent encore plus sur les micros règles que l'on change fréquemment.
En effet, ils aimeraient avoir une forte visibilité sur ces règles métiers : "est ce que la documentation décrit correctement le code?". De plus, ils aimeraient avoir une plus grande réactivité sur l'évolution de ces règles.
Sans formalisme pour gérer toutes ces règles mouvantes, il y a une probabilité importante pour que l'on introduise une dette technique dans l'application :
- règles perdues et dissimulées dans le code,
- implémentation de règles dupliquées,
- règles difficilement modifiables.
Tout le monde a autour de lui un exemple d'abandon de code. C'est un cercle vicieux. En effet, moins on connait un code et moins on va essayer de le modifier ; moins on va essayer de le modifier et moins on connaîtra le code ... (bouclez ! on se retrouve à la réécriture complète de ce code).
Pour les règles métiers qui mettent en avant une stratégie commerciale cela n'est pas acceptable.
Il faut avoir une grande réactivité et une très bonne gestion du changement. Il faut donc une excellente connaissance de l'implémentation de la part des développeurs et des fonctionnels.
Description de la solution
Ainsi, idéalement un expert fonctionnel aimerait :
- définir une règle métier,
- composer plusieurs règles métiers,
- avoir l'état exact d'implémentation des règles métiers tel que développé dans le code source (aux valeurs près !), et
- activer/désactiver une règle rapidement, souvent sans redémarrage des serveurs d'application.
Pour faciliter la maintenance d'un tel modèle, la solution suivante peut être mise en place : l'utilisation du Pattern Specification.
L'implémentation d'une règle métier suit le contrat d'utilisation suivant :
public boolean isSatisfiedBy(T candidate);
public Specification<T> or(Specification<T> specification);
public Specification<T> and(Specification<T> specification);
public Specification<T> not();
}
une méthode issatisfiedby détermine si la règle métier est respectée. les trois autres méthodes, or, and et not permettent de combiner les règles métiers entre elles.
Il y a trois classes utilitaires permettant d'implémenter les opérateurs :
AndSpecificationOrSpecificationNotSpecification
Par exemple la classe AndSpecification :
@Override
public boolean isSatisfiedBy(final T candidate) {
boolean result = true;
for (Specification<T> specification : this.specifications) {
result &= specification.isSatisfiedBy(candidate);
}
return result;
}
public AndSpecification(Specification<T>... specifications) {
super(specifications);
}
}
Les règles métiers étendent la classe LeafSpecification :
public abstract boolean isSatisfiedBy(T candidate);
}
Exemples de règles
La règle RegleProduitsDuPanierSontDeSaison permet de déterminer si tous les produits du panier sont de saison.
public boolean isSatisfiedBy(Panier panier) {
for (Produit produit : panier.getProduits()) {
if (!Mois.estDeSaison(produit.getMoisDeSaison())) {
return false;
}
}
return true;
}
}
La règle RegleProduitsDuPanierSontOranges permet de déterminer si tous les produits du panier sont oranges.
public boolean isSatisfiedBy(Panier panier) {
for (Produit produit : panier.getProduits()) {
if ( ! (produit instanceof Abricot
|| produit instanceof Carotte
|| produit instanceof Citrouille
|| produit instanceof Mandarine
|| produit instanceof Orange)) {
return false;
}
}
return true;
}
}
La règle ReglePromoJAimeLesProduitsOranges se repose sur la composition des deux règles précédentes. Si les deux règles précédentes sont respectées alors la commande du client sera éligible à la promotion J'aime les produits oranges !
RegleProduitsDuPanierSontOranges regleProduitsDuPanierSontOranges = new RegleProduitsDuPanierSontOranges();
RegleProduitsDuPanierSontDeSaison regleProduitsDuPanierSontDeSaison = new RegleProduitsDuPanierSontDeSaison();
public boolean isSatisfiedBy(Panier panier) {
return regleProduitsDuPanierSontDeSaison.and(regleProduitsDuPanierSontOranges).isSatisfiedBy(panier);
}
}
Documentation
La solution est fiable, si et seulement si les développeurs et les fonctionnels connaissent précisément l'implémentation de ces règles.
Il faut donc une solution rigoureuse. La solution ne sera rigoureuse que si le développeur est rigoureux dans sa documentation : une modification dans le code implique une modification dans la documentation. Les règles métiers sont volontairement limitées à un nombre de traitements restreints afin d'alléger ce travail de documentation. Par la même occasion, la documentation sera moins longue et donc plus abordable pour un humain.
Voici un exemple de patron de documentation pour une règle métier :
| Nom | Nom de la classe qui correspond à la classe en Java (donc sans d'accent, sans espace, et qui ne commence pas par un chiffre) |
| Cas d'utilisation | Où on utilise cette règle |
| Dépendance | Il est possible de combiner les règles. Une règle peut être simplement une combinaison d'autres règles |
| Données en entrée | Quelles sont les données analysées |
| Description & algorithme métier | Décrire le plus précisément possible la règle métier (à la valeur près) |
| Configurabilité | Indiquer si la règle est modifiable, activable, desactivable à chaud |
Les règles métiers ont intérêt à être classées par package, un package par domaine fonctionnel.
Retour d'expérience
Cette solution a été mise en place sur un projet d'eCommerce à des fins de maintenance et de réactivité aux demandes d'évolution. Après 6 mois, l'équipe est satisfaite du résultat voire même un peu bluffée.
En effet, les équipes fonctionnels avaient émis un besoin fort de pouvoir gérer les règles.
Pour cela, l'équipe technique a entrepris une étude. Cette étude avait une double fonction :
- spécifier le besoin, et
- comparer les différentes solutions pour répondre à ce besoin.
Lors de l'étude, différents moteurs de règle Drools, Jess, Java Rules Engine, Groovy Rules, etc., ont été comparés. Aucune solution, hormis Drools, ne nous semblait fiable. Drools était quant à lui trop complexe pour notre besoin.
La solution pattern est intéressante car conceptuellement et techniquement très simple.
Si les développeurs et les fonctionnels jouent le jeu de la documentation, alors on a une vraie maitrise des règles métiers. Ce dernier point reste le plus risqué mais cela ne dépend que des acteurs du projet.
De plus, cette solution nous a permis de gagner du temps :
- Le développement du code du pattern Specification est rapide (quelques jours) par rapport à l'intégration et de la mise en place de bonnes pratiques sur un framework. Par exemple avec Drools, il faut plusieurs mois pour avoir une bonne maitrise technique.
- Cout de formation des autres développeurs très peu élévé toujours par rapport à Drools.
- Le temps et les efforts de développement et documentation sont peu élévés, on ne fait que du Java sans artifice et un peu de documentation.
Conclusion
Le pattern Specification est une alternative sérieuse au choix d'un moteur de règle. En effet, bien souvent l'utilisation de ce dernier est une solution démesurée par rapport à la complexité du besoin exprimé. Elle est moins risquée technologiquement (pas de formation, pas d'inconnue technique), moins couteuse et plus simple.
En agrémentant avec un peu de code utilitaire, il est facilement possible de rendre plus fonctionnelle notre modeste solution de gestion de règles métiers :
- rechargement de règle à chaud,
- activation et désactivation des règles à chaud,
- monitoring et log des règles métiers.









Bon article sur un pattern intéressant - et que l'on applique parfois sans le savoir. Ne va pas assez loin sur le rechargement à chaud mais bon, pas de souci particulier.
Drools n'est pas si compliqué que cela, en tout cas ce sont des gens qui le maîtrisent bien qui m'ont dit ça. Une belle solution, mais ajouter encore un outil? A chacun de juger selon ses besoins et ses contraintes.
Autre solution, que j'ai vu appliquée : au lieu d'avoir des des gens qui veulent n'importe quoi et qui oublient d'un jour sur l'autre ce qu'ils voulaient - Et oui.... ça peut amener à ça aussi, de donner la main sur les règles métier à des gens pas totalement férus de logique... - avoir une cellule archi/développement stable qui est garante des règles métier.
Autre solution, le dsl. Sorte de sur-mesure de la gestion des règles métier. La création d'objets qui obéissent à un pattern aidera tout de même pas mal à la création d'un dsl. On le voit, dans le code présenté ici en exemple
Bonne année, les xebia boys!
Juste pour pinailler, il semble bien que la règle RegleProduitsDuPanierSontOranges qui "permet de déterminer si tous les produits du panier sont oranges", ne détermine plutôt que la couleur du premier produit du panier.
Très bon article et très bonne idée de pattern. Le résultat fait assez DSL.
Le pattern Specification semble le mix des patterns Command et Composite.
Je suis curieux de savoir pourquoi Drools semblait trop complexe...
Sinon, à mi-chemin, comme mentionné dans un commentaire précédent, l'usage d'un DSL semble effectivement une option.
Très intéressant.
Nous utilisons ce pattern depuis plusieurs années pour vérifier les "invariants" d’une classe. C'est simple et efficace.
Nos classes métier encapsulent bien sûr tous ces "invariants" (qui sont une partie des règles métier), et exposent uniquement la méthode estCoherent() au reste du monde. Ainsi on peut à tout moment vérifier la cohérence d’un objet ou d’un aggregat (Aggregate). Vive le DDD!!!
Par contre, comment remonter -simplement- la bonne information en cas d’échec d’une des spécifications? Ceci est encore plus important si vous enchaînez plusieurs spécifications. C’est, à ce jour, la seule limite que nous avons trouvée à ce pattern...
Au final, le plus important est de ce concentrer sur votre modèle Objet (anémique ou riche?). Le pattern Specification convient à un certain type de "règles métier", mais peut-être pas pour toutes (attention au marteau...).
Merci pour vos articles et bonne année 2010!
Pattern très simple, souple pour vérifier des règles métiers ou les invariants d'un objet.
Cependant je voudrais aller plus loin et retourner la ou les raisons de l'échec de l'application d'une règle métier afin que l'utilisateur sache où corriger son erreur. J'ai fait en sorte que la spécification retourne une paire contenant le boolean classique et la raison de l'échec si false.
Là, j'ai un problème...
En effet les combinaisons de spécifications peuvent être complexes, et par conséquent la raison de l'échec est difficilement exprimable. Peut être est-ce normal; ou alors je m'y prends mal.
Quelqu'un aurait une idée?
Moi ça me fait penser au pattern décorateur (un café crème, un café crème sucré, un café sucré...), avec des règles de composition mieux précisées, et un test d'accord.
Je trouve ça pas mal, peut être faudrait-il y adjoindre un contexte d'opération ? Les règles sont souvent intitulées xxxDuPanierYyy, cela ne serait pas mieux que de formaliser ce DuPanier, histoire de ne pas appliquer des règles DuPanier à des objets DeLivraison, par exemple ?
Moi aussi j'ai beaucoup lorgné du coté de drools, toujours pour conclure avec grand regret que c'était trop compliqué pour moi. Si quelqu'un peut me démontrer que je me suis trompé, j'en serais fort content. J'en suis arrivé à penser qu'on pouvait peut être traiter les règles par un jeu de routes à la mode Camel, mais j'en suis aux supputations des considérations.
Bonjour, merci pour vos commentaires
- gestion de règle à chaud -
@Ga.K
Concernant la gestion de règle à chaud c'est assez simple, en fait notre solution finale combine une solution Java avec ce pattern et du Groovy, pour sa syntaxe puissante. De plus on utilise le module Spring pour le support dynamique de langage : http://static.springsource.org/spring/docs/2.0.x/reference/dynamic-language.html.
. Après il faut trouver un bon compris entre développement en Java et Groovy.
Donc la modification de certaines règles se résume à un redéploiement des fichiers Groovy, après Spring fait le reste
- Drools -
Petite exclusivité, la semaine prochaine, il y aura un prochain article sur Drools
. Selon moi ce pattern peut rapidement être mis en place avec un cout et temps minimum dans les équipes de développement, contrairement à Drools. Drools est un framework assez complexe mais pas insurmontable. Ce framework a de nombreux avantages, mais les fonctionnalités doivent bien être maitrisées afin de se lancer (DSL, table de décision via fichier Excel, IHM, langage MVEL (drools), ... ).
Drools demande de former les développeurs et de faire idéalement un prototype. Cependant Drools est relativement facilement intégrable dans un projet existant. On peut envisager d'intégrer en douceur Drools c'est à dire algorithme par algorithme.
@ym
Merci tu as détecté un bug
. J'ai corrigé mais je suis par forcement satisfait du résultat :
{
public class RegleProduitsDuPanierSontOranges extends LeafSpecification
public boolean isSatisfiedBy(Panier panier) {
for (Produit produit : panier.getProduits()) {
if ( ! (produit instanceof Abricot
|| produit instanceof Carotte
|| produit instanceof Citrouille
|| produit instanceof Mandarine
|| produit instanceof Orange)) {
return false;
}
}
return true;
}
}
Je ne voulais pas en parler dans l'article mais on peut aussi utiliser une sorte de pattern visiteur, par exemple un parcours en "et" sur les produits d'un panier.
Par exemple la classe qui s'occupe du parcours :
{
public abstract class ParcoursEnEtSurProduitsDuPanier extends LeafSpecification
abstract protected boolean isProduitSatisfiedBy(Produit produit);
public boolean isSatisfiedBy(Panier panier) {
for (Produit produit : panier.getProduits()) {
if ( ! isProduitSatisfiedBy(produit)) {
return false;
}
}
return true;
}
}
La classe qui implémente la règle sur un produit :
public class RegleProduitEstOrange extends ParcoursEnEtSurProduitsDuPanier {
protected boolean isProduitSatisfiedBy(Produit produit) {
if (produit instanceof Abricot
|| produit instanceof Carotte
|| produit instanceof Citrouille
|| produit instanceof Mandarine
|| produit instanceof Orange) {
return true;
}
return false;
}
}
Voila je pense que c'est un peu plus lisible.
Le pattern me semble plutôt intéressant. Il permet de formaliser les règles et de les isoler.
Par contre je ne suis pas sûr de comprendre tout : Dans l'exemple cité, hormis le fait de savoir si le client est en France métropolitaine, en Ile de France ou pas, il faut aussi gérer les conséquence des ces règles càd la limite de poids de 10 kg de 100kg ou de 25kg.
Le pattern semble gérer le "SI" mais pas le "ALORS" contrairement à ce que ferait un Drools. Comment gérer de façon propre et isolé ces "constantes" de poids qui finalement risque de changer plus rapidement que les règles définissant si le client et de France métropolitaine ou pas ?
Ca n'existerait pas déjà sous le nom de Closures et Predicates tout ça ? hmm ?
Merci pour le travail de rédaction.
- Contexte -
@Dun, pour gérer un contexte il y a deux possibilités. 1 - soit tu gardes le modèle de l'article avec les règles qui prennent en argument des objets du modèle. Tu géres le "ALORS" en dehors des règles.
2 - Sinon tu peux te rapprocher d'un modèle à la Drools. Tu as des règles fines qui utilisent les objets du domaine. Tu as un (ou des) règle de plus haut niveau qui eux utilisent un objet contexte. Dans cet objet contexte on peut injecter un service à appeler dans le "ALORS",
@Herve A. ce contexte peut aussi servir avoir une trace de l'information ou un état d'exécution.
Par exemple je prends un cas concret. Prenons un algorithme métier pour l'éligibilité des moyens de livraison en fonction du panier. Il faut donc créer un objet contexte à cet algorithme qui va contenir les données d'entrée (le panier, ou autre) et les données de sortis, les moyens de livraison et le prix associé. On a donc maintenant des règles qui sont capables de calculer un resultat plus évolué que "oui" / "non".
Le 21 janvier à Lille au Ch’ti JUG nous avons l’honneur de recevoir Mark Proctor et Geoffrey De Smeet de JBoss. Mark est le projet leader du projet drools. Il vient d’être nommé directeur de RuleML. Geoffrey est le créateur de drools planner.
Plus d’informations : http://chtijug.org/rendez-vous-le-21-janvier-drools-avec-cylande-et-luniversite-de-lille-1/
Bonjour,
Merci pour cet article très intéressant qui permet de mettre un nom et une spécification sur une conception que nous utilisons souvent sans le savoir.
J'aurai une petite question à vous posez.
Ce pattern - comme de nombreux autres - s'applique sur la couche métier de nos architectures.
Comment peut-on l'utiliser au niveau de la couche de persistance ?
Au sein de mon entreprise, nous avons des règles métier qui vont surement évoluées dans le temps.
Ces règles nous permettent de savoir quelles données nous devons remonter de la base de données vers la présentation utilisateur.
Par exemple, nous avons une contrainte sur l'appartenance à un pays. Nous effectuons donc une requête SQL en ajoutant une condition sur une jointure.
Sachant que nous utilisons cette condition sur l'ensemble des requêtes, si les règles métiers changent, nous devrons repasser sur l'ensemble de ces requêtes pour ajouter/enlever des conditions.
On retrouve le pourquoi du comment du pattern Specification au niveau métier.
Il est possible d'implémenter ce pattern pour l'accès aux données persistantes. Le soucis, c'est que chaque règle supplémentaire a un cout important car il s'agit d'un aller/retour vers la base de données. 3 milles produits x 4 règles, on explose le compteur rapidement avec 200 demandes simultanées.
Avez-vous une solution implémentation du pattern Specification pour l'accès aux données ?
Cordialement,
Thomas Tourlourat - Armetiz.info