27/09/2012

Gérer des règles métiers complexes et/ou changeantes

Désolé d'avance pour la longueur de ce billet ; comme ça fait longtemps que mon blog n'a pas été mis à jour, j'en profite pour faire un mini-tutoriel sur un sujet qui me tient à coeur : comment gérer les règles métier, autrement dit les Spécifications fonctionnelles, dans un projet php ?

Je pense qu'on a tous (enfin, on devrait :-) ) avoir en tête les principes SOLID. Ces principes sont étroitement liées à la notion de Complexité Cyclomatique, qui elle, est moins connue.

Derrière ce terme barbare, que vous connaissez bien si vous faites du test unitaire ou si vous utilisez des outils tels que PHPDepend, se cache en réalité quelque chose de simple : si chaque bloc conditionnel de votre projet est un noeud, la complexité cyclomatique est la somme de l'ensemble des chemins empruntable dans votre projet.

De cette manière, le code suivant :

<?php
<p>if c1</p>
    a()
<p>else</p>
    b()

<p>if c2</p>
    c()
<p>else</p>
    d()

a une complexité cyclomatique plus élevée que

<?php
<p>if c1</p>
    a()
<p>else</p>
    b()

Un programme maintenable est en général un programme dont la complexité cyclomatique est la plus faible possible. Et c'est justement l'enjeu de la POO de nous fournir un moyen de concevoir des applications de Complexité cyclomatique faible.

Bon, ça c'est pour comprendre le principe. Après on me dit souvent : "oui, mais moi comment je fais pour gérer mes différents cas possibles si je dois limiter mes if() ?"

Prenez cet exemple :

  • on a un panier de produits
  • on ne peut ajouter que des produits en stock
  • on ne peut ajouter que 20 produits maximum
  • la règle ci-dessus ne s'applique pas en période de fêtes
  • les règles ci-dessus sont susceptibles de changer souvent

Générallement on imbrique des if(), du coup on se retrouve avec un arbre applicatif assez large, c'est-à-dire de nombreux chemins possibles. A terme :

  • on a un risque de changement du code source très important (à chaque fois qu'on ajoute une règle métier)
  • on gère mal l'ajout de nouvelles règles
  • très vite l'algorithme devient imbuvable car trop complexe

C'est là qu'intervient la notion Specification (bon, je sais il est temps, l'intro était longue ;-) )

Ce pattern répond à : "Comment gérer mes règles métier dans mon projet". Il est généralement associé au DDD (Domain Driven Design), mais on peut l'appliquer dans n'importe quel contexte qui s'y prête.

L'idée est la suivante : chaque règle métier va être représentée par un objet (une Spécification), à qui l'on va demander si la règle est respectée :

$anyObject = new StdClass;
$specification = new MySpecification;
$isOk = $specification->isSatisfedBy($anyObject);

Là où ça devient puissant, c'est qu'on va pouvoir créer des Spécifications composites pour créer des règles métiers complexes à partir d'un ensemble de règles simples :

$anyObject = new StdClass;
$specification =
    new MySpecification1()
    ->and(new MySpecification2())
    ->and(
        new MySpecification3()
        ->or(new MySpecification4())
    );
;
$isOk = $specification->isSatisfedBy($anyObject);

Vous voyez les avantages : vous pouvez désormais appliquer n'importe quelle règle métier sans avoir à imbriquer plein de if() ; si vous souhaitez tester unitairement une règle, vous pouvez mocker les autres ; vos règles sont facilement évolutives...

Bon concrètement comment ça se passe ? Il faut commencer par créer notre contrat pour le fonctionnement de nos Spécifications :

interface SpecificationInterface {

    public function isSatisfiedBy($object);

    public function andSpec(SpecificationInterface $specification);

    public function orSpec(SpecificationInterface $specification);

    public function notSpec(SpecificationInterface $specification);
}

Ensuite, pour permettre la création de Spécification composite il faut créer une classe abstraite générique pour nos spécifications.

abstract class Specification implements SpecificationInterface {

    public function andSpec(SpecificationInterface $specification) {
        return new AndSpecification($this, $specification);
    }

    public function orSpec(SpecificationInterface $specification) {
        return new OrSpecification($this, $specification);
    }

    public function notSpec(SpecificationInterface $specification) {
        return new NotSpecification($this);
    }

}

Il ne nous reste plus qu'à déterminer le comportement de chacunes de nos structures de contrôle :

Pour le "et":

class AndSpecification extends Specification implements SpecificationInterface {

    private $specification1;
    private $specification2;

    function __construct(SpecificationInterface $specification1, SpecificationInterface $specification2) {
        $this->specification1 = $specification1;
        $this->specification2 = $specification2;
    }

    public function isSatisfiedBy($object) {
        return $this->specification1->isSatisfiedBy($object)
                && $this->specification2->isSatisfiedBy($object);
    }

}

Pour le "ou" :

class OrSpecification extends Specification implements SpecificationInterface {

    private $specification1;
    private $specification2;

    function __construct(SpecificationInterface $specification1, SpecificationInterface $specification2) {
        $this->specification1 = $specification1;
        $this->specification2 = $specification2;
    }

    public function isSatisfiedBy($object) {
        return $this->specification1->isSatisfiedBy($object)
                ||  $this->specification2->isSatisfiedBy($object);
    }
}

Et enfin pour le "non" :

class NotSpecification extends Specification implements SpecificationInterface {

    private $specification;

    public function __construct($specification) {
        $this->specification = $specification;
    }

    public function isSatisfiedBy($object) {
        return !$this->specification->isSatisfiedBy($object);
    }
}

Ca y est, on vient de se créer le minimum vital pour gérer nos règles métiers. Ca, c'est fait une bonne fois pour toute...

Maintenant dans notre projet il suffit de faire hériter nos règles de la classe Specification. Par exemple :

class SpecLePannierPeutEtreRempli extends Specification {
    public function isSatisfiedBy($customer) {
        return(boolean)  $x; // la condition de notre règle ici
    }
}

class SpecOnEstEnPeriodeDeFetes extends Specification {
    public function isSatisfiedBy($customer) {
        return (boolean) $x; // la condition de notre règle ici
    }
}

class SpecLeProduitEstEnStock extends Specification {
    public function isSatisfiedBy($customer) {
        return (boolean) $x; // la condition de notre règle ici
    }
}

Et après il suffit simplement d'utiliser nos règles. Depuis PHP 5.4 on peut utiliser une interface fluide sur nos constructeurs, nous voici donc avec :

$specification =
    new SpecLeProduitEstEnStock()
    ->and(SpecLePannierPeutEtreRempli())
    ->and(
        new SpecIlResteDeLaPlaceDansLePannier()
        ->or(new SpecOnEstEnPeriodeDeFetes())
    );

if($specification->isSatisfedBy(specification)) {
    $panier->ajouterProduit($produit);
} else {
    throw new ArticleNePeutPasEtreMisDansLePannierException('...');
}

Pour synthétiser :

  • la Spécification permet de gérer des règles métiers
  • elle permet de combiner n règles métiers dynamiquement
  • elle facilite la gestion du changement fonctionnel
  • elle facile la lisibilité des règles
  • elle vous permet de mocker certaines parties des règles métier
  • ce n'est pas un remède miracle, mais elle mérite d'être plus utilisé ^^

N'hésitez pas à laisser vos retours sur ce pattern Specification, je suis curieux de savoir s'il est utilisé massivement ou non ? Ou peut-être utilisez-vous déjà un framework de gestion de règles métier, comme ceux qu'il existe dans le monde du Java ?

blog comments powered by Disqus