29 décembre 2009
Imprimer ce billet

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 :

Collection<Produit> panier = new Set<Produit>();

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 :

public class Panier {
   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 interface Specification<T> {
    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 :

  • AndSpecification
  • OrSpecification
  • NotSpecification

Par exemple la classe AndSpecification :

public class AndSpecification<T> extends AbstractCompositeSpecification<T> {

    @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 class LeafSpecification<T> extends AbstractCompositeSpecification<T> {
    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 class RegleProduitsDuPanierSontDeSaison extends LeafSpecification<Panier>{
    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 class RegleProduitsDuPanierSontOranges extends LeafSpecification<Panier> {
    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 !

public class ReglePromoJAimeLesProduitsOranges extends LeafSpecification<Panier> {
    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.