Archives mensuelles : février 2019

PHP – Le design pattern Etat

Le design pattern Etat est un design pattern comportemental; son utilisation est préconisée dès lors que le comportement d’un objet dépend directement de l’état dans lequel il se trouve à l’instant T.

Programmeur énervé par l'absence de pattern Etat

Programmeur se mettant dans un état pas possible à la lecture d’un code écrit voilà 10 ans

Architecture du design pattern Etat

Les participants à ce design pattern Etat sont au nombre de 3:

  • Une abstraction (interface ou classe abstraite) qui liste les différents comportements devant être implémentés par des états concrets
  • Des états concrets réalisant chacune des méthodes listées dans l’abstraction évoquée ci-dessus
  • Un contexte définissant l’interface que le code client va utiliser et ayant en composition les états concrets auquel il va simplement déléguer les requêtes lui parvenant

Where’s the bill, Bill?

Prenons un exemple parlant: le commerce en ligne ! Une commande faite en ligne traverse toute une série d’états; elle peut être créée, validée, payée ou mise en recouvrement, annulée, remboursée, expédiée etc. Evidemment certaines transitions d’état ne peuvent pas se faire: quand elle est expédiée, elle ne peut pas être annulée et inversement.

Notre site de vente en ligne d’ouvrages informatique CODEBOOKS reçoit plusieurs centaines de commandes par jour.
Une commande est créée dans l’état « En Attente », ce qui signifie qu’elle est en attente d’une validation, manuelle ou automatique. Cette validation peut intervenir ou pas, si le moyen de paiement s’avère frauduleux ou s’il ne parvient pas dans les délais précisés dans les conditions générales de vente.

Une fois qu’elle est validée, elle est préparée puis expédiée afin d’être livrée. C’est notre scénario nominal, le Best Case Scenario.
D’autres scenarii peuvent se produire: l’acheteur réalise à la livraison qu’il a déjà le livre ou bien qu’il l’a commandé dans une langue qu’il ne sait pas lire; il devra être remboursé après renvoi du livre à CODEBOOKS. Une erreur de stock peut également conduire à valider une commande portant sur un livre non disponible, elle devra donc être annulée puis remboursée.

Tes états d’âme, Eric…(air bien connu)

Nos états

Chacun de nos états fera l’objet d’une classe concrète dérivant une classe abstraite (ou implémentant une interface, au choix). J’ai choisi une classe abstraite dans laquelle je factorise mon constructeur, pour éviter de le répéter bêtement dans chaque classe fille car il fait dans tous les cas la même chose. Cette classe abstraite implémente une interface qui va contraindre chacune des classes filles à implémenter l’ensemble des méthodes qu’un état pourra mettre à disposition. Notez bien que le contexte est en composition de chacun des états concrets, pour garder la trace de l’état courant d’une commande.

interface EtatInterface
{
    public function mettreEnAttente(): void;
    public function valider(): void;
    public function annuler(): void;
    public function rembourser(): void;
    public function expedier(): void;
    public function signalerLivre(): void;
}

abstract class EtatAbstract implements EtatInterface
{
    protected $contexte;
    
    public function __construct(ContexteInterface $contexte)
    {
        $this->contexte = $contexte;
    }
}

Après étude, nous avons déterminé les états possibles d’une commande:

  • En Attente: c’est l’état par défaut, celui dans lequel se trouve toute commande « atterrissant » sur notre système d’information
  • Annulé
  • Validé
  • Expédié
  • Remboursé
  • Livré

Vous allez me dire « Mais avec cette interface, l’état Annulé va se retrouver à devoir implémenter une méthode expedier alors qu’il ne doit pas pouvoir transiter vers cet état ! » et vous aurez tout à fait raison ! C’est comme ça, il nous faut une interface qui liste toutes les méthodes qu’on peut invoquer sur l’ensemble des états pour faire plaisir à ce bon vieux Liskov !

Dès qu’une méthode sera appelée sur un état qui ne doit pas l’implémenter, nous lèverons une exception maison nommée MethodNotImplementedException:

class MethodNotImplementedException extends \Exception {}

Le contexte

Notre contexte va être appelé par le code client, c’est lui qui va garder une trace de l’état courant de notre commande. Il a en composition une instance de chacun des différents types d’états disponibles dans lesquels il va s’auto-injecter et à sa construction, il mettra l’état courant à sa valeur par défaut, à savoir « en attente ». Il propose une série de getters (sauf pour enAttente car nous postulons qu’aucun état ne peut effectuer de transition vers cet état initial) ainsi que les méthodes que le code client appellera. Vous notez que ces méthodes font de la délégation bête et méchante. L’unique setter est crucial pour tenir le contexte à jour des changements d’état intervenus dans l’application.

interface ContexteInterface
{
    public function etatValide(): EtatInterface;
    public function etatAnnule(): EtatInterface;
    public function etatExpedie(): EtatInterface;
    public function etatLivre(): EtatInterface;
    public function etatActuel(): EtatInterface;
    public function changerEtat(EtatInterface $etat): void;
}

class Contexte implements ContexteInterface
{
    private $enAttente;
    private $valide;
    private $expedie;
    private $livre;
    private $rembourse;
    private $annule;
    private $etatActuel;

    public function __construct()
    {
        $this->enAttente = new EnAttente($this);
        $this->valide = new Valide($this);
        $this->expedie = new Expedie($this);
        $this->livre = new Livre($this);
        $this->rembourse = new Rembourse($this);
        $this->annule = new Annule($this);
        
        $this->etatActuel = $this->enAttente;
    }
    
    public function etatValide(): EtatInterface
    {
        return $this->valide;
    }
    
    public function etatAnnule(): EtatInterface
    {
        return $this->annule;
    }
    
    public function etatExpedie(): EtatInterface
    {
        return $this->expedie;
    }
    
    public function etatLivre(): EtatInterface
    {
        return $this->livre;
    }
    
    public function etatRembourse(): EtatInterface
    {
        return $this->rembourse;
    }
    
    public function etatActuel(): EtatInterface
    {
        return $this->etatActuel;
    }
    
    public function changerEtat(EtatInterface $etat): void
    {
        $this->etatActuel = $etat;
    }
    
    public function valider(): void
    {
        $this->etatActuel->valider();
    }
    
    public function expedier(): void
    {
        $this->etatActuel->expedier();
    }
    
    public function signalerCommeLivre(): void
    {
        $this->etatActuel->signalerLivre();
    }
    
    public function effectuerRemboursement(): void
    {
        $this->etatActuel->rembourser();
    }
    
    public function effectuerAnnulation(): void
    {
        $this->etatActuel->annuler();
    }
}

Nos états…concrets !

EnAttente

Prenons notre première classe concrète: EnAttente. Cet état peut effectuer des transitions vers les états Validé (si le paiement est valide) ou Annulé (dans le cas contraire). Il ne peut pas le faire vers lui-même ainsi que vers les états Remboursé, Expédié et Livré car à ce stade le paiement de la commande n’est pas une certitude.

class EnAttente extends EtatAbstract
{   
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Déjà en attente');
    }
    
    public function valider(): void
    {
        $this->contexte->changerEtat($this->contexte->etatValide());
    }
    
    public function annuler(): void
    {
        $this->contexte->changerEtat($this->contexte->etatAnnule());
    }
    
    public function rembourser(): void
    {
        throw new MethodNotImplementedException('En attente');
    }
    
    public function expedier(): void
    {
        throw new MethodNotImplementedException('En attente');
    }
    
    public function signalerLivre(): void
    {
        throw new MethodNotImplementedException('En attente');
    }
}

Annulé

Comme tous les états, Annule ne peut aller vers lui-même et il n’effectue de transition qu’en direction de Remboursé.

class Annule extends EtatAbstract
{
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Annulé');
    }
    
    public function valider(): void
    {
        throw new MethodNotImplementedException('Annulé');
    }
    
    public function annuler(): void
    {
        throw new MethodNotImplementedException('Déjà annulé');
    }
    
    public function rembourser(): void
    {
        $this->contexte->changerEtat($this->contexte->etatRembourse());
    }
    
    public function expedier(): void
    {
        throw new MethodNotImplementedException('Annulé');
    }
    
    public function signalerLivre(): void
    {
        throw new MethodNotImplementedException('Annulé');
    }
}

Validé

Valide peut évoluer en Annulé (opposition bancaire ou autres) ou en Expédié, si tout se déroule normalement.

class Valide extends EtatAbstract
{    
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Validé');
    }
    
    public function valider(): void
    {
        throw new MethodNotImplementedException('Déjà validé');
    }
    
    public function annuler(): void
    {
        $this->contexte->changerEtat($this->contexte->etatAnnule());
    }
    
    public function rembourser(): void
    {
        throw new MethodNotImplementedException('Validé');
    }
    
    public function expedier(): void
    {
        $this->contexte->changerEtat($this->contexte->etatExpedie());
    }
    
    public function signalerLivre(): void
    {
        throw new MethodNotImplementedException('Validé');
    }
}

Expedié

Cet état peut effectuer des transitions uniquement vers Livré.

class Expedie extends EtatAbstract
{   
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Expédié');
    }
    
    public function valider(): void
    {
        throw new MethodNotImplementedException('Expédié');
    }
    
    public function annuler(): void
    {
        throw new MethodNotImplementedException('Expédié');
    }
    
    public function rembourser(): void
    {
        throw new MethodNotImplementedException('Expédié');
    }
    
    public function expedier(): void
    {
        throw new MethodNotImplementedException('Déjà expédié');
    }
    
    public function signalerLivre(): void
    {
        $this->contexte->changerEtat($this->contexte->etatLivre());
    }
}

Remboursé

C’est un état terminal.

class Rembourse extends EtatAbstract
{
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Remboursé');
    }
    
    public function valider(): void
    {
        throw new MethodNotImplementedException('Remboursé');
    }
    
    public function annuler(): void
    {
        throw new MethodNotImplementedException('Remboursé');
    }
    
    public function rembourser(): void
    {
        throw new MethodNotImplementedException('Déjà Remboursé');
    }
    
    public function expedier(): void
    {
        throw new MethodNotImplementedException('Remboursé');
    }
    
    public function signalerLivre(): void
    {
        throw new MethodNotImplementedException('Remboursé');
    }
}

Livré

Cet état ne peut transiter que vers Annulé, quand le client reçoit un produit qui ne lui convient pas. L’annulation donnera ensuite lieu à un remboursement.

class Livre extends EtatAbstract
{
    public function mettreEnAttente(): void
    {
        throw new MethodNotImplementedException('Livré');
    }
    
    public function valider(): void
    {
        throw new MethodNotImplementedException('Livré');
    }
    
    public function annuler(): void
    {
        $this->contexte->changerEtat($this->contexte->etatAnnule());
    }
    
    public function rembourser(): void
    {
        throw new MethodNotImplementedException('Livré');
    }
    
    public function expedier(): void
    {
        throw new MethodNotImplementedException('Livré');
    }
    
    public function signalerLivre(): void
    {
        throw new MethodNotImplementedException('Déjà Livré');
    }
}

Le code client

Nous avons à notre disposition une classe Commande; elle possède une référence vers le contexte, qui va ainsi garder trace des différents états qu’elle traverse. C’est elle que notre code client va appeler.

class Commande
{
    private $contexte;
    
    public function __construct(ContexteInterface $contexte)
    {
        $this->contexte = $contexte;
    }
    
    public function valider(): void
    {
        $this->contexte->valider();
        echo "Etat = ".get_class($this->contexte->etatActuel()).PHP_EOL;
    }

    public function expedier(): void
    {
        $this->contexte->expedier();
        echo "Etat = ".get_class($this->contexte->etatActuel()).PHP_EOL;
    }
    
    public function signalerCommeEtantLivre(): void
    {
        $this->contexte->signalerCommeLivre();
        echo "Etat = ".get_class($this->contexte->etatActuel()).PHP_EOL;
    }
    
    public function procederAuRemboursement(): void
    {
        $this->contexte->effectuerRemboursement();
        echo "Etat = ".get_class($this->contexte->etatActuel()).PHP_EOL;
    }
 
    public function effectuerUneAnnulation(): void
    {
        $this->contexte->effectuerAnnulation();
        echo "Etat = ".get_class($this->contexte->etatActuel()).PHP_EOL;
    }
    
}

$contexte = new Contexte();
$commande = new Commande($contexte);

Voici notre scénario nominal, ou best case scenario (fingaz crossed!) :

$commande->valider();
$commande->expedier();
$commande->signalerCommeEtantLivre();

La commande va traverser 4 états: elle va passer de « en attente » à « validée », puis à « expédiée » et enfin à « livrée ».

Voici le scénario où la commande est validée mais une erreur de stock conduit à devoir l’annuler et la rembourser:

$commande->valider();
$commande->effectuerUneAnnulation();
$commande->procederAuRemboursement(); // état TERMINAL

Dans le scénario suivant, le produit est livré mais l’acheteur réalise qu’il l’a déjà ou bien qu’il s’est trompé lors de sa commande et le produit de remplacement n’est plus disponible dans le stock:

$commande->valider();
$commande->expedier();
$commande->signalerCommeEtantLivre();
$commande->effectuerUneAnnulation();
$commande->procederAuRemboursement(); // état TERMINAL

Enfin, dans notre dernier scénario, le paiement n’est pas arrivé à temps (mandat, virement bancaire, chèque…) et il faut annuler la commande ! C’est le plus simple:

$commande->effectuerUneAnnulation();

PHP – Le design pattern Strategy

Le design pattern Strategy fait partie de la famille des design patterns comportementaux; il facilite l’utilisation d’algorithmes interchangeables à l’exécution, c’est à dire dynamiquement. Il obéit au bon vieux principe de la programmation orientée objet : Encapsuler ce qui varie.

Design pattern Strategy

Les algorithmes à encapsuler

Pour un comportement identique (reagir) nous avons trois implémentations différentes. Ce sont elles que nous souhaitons isoler pour réduire au maximum l’impact du changement qui tôt ou tard se produira dans notre code. Ce comportement commun attend en paramètre un objet se conformant à l’interface StrategieInterface et ses implémentations variables consistent à ici à appliquer un traitement rudimentaire à la méthode donnerPhrase de cet objet en paramètre.

interface StrategieInterface
{
    public function reagir(PersonneInterface $personne): string;
}
 
class Enerve implements StrategieInterface
{
    public function reagir(PersonneInterface $personne): string
    {
        return strtoupper($personne->donnerPhrase().' !!!').PHP_EOL;
    }
}

class Geek implements StrategieInterface
{
    public function reagir(PersonneInterface $personne): string
    {
        return str_replace('o', '0', $personne->donnerPhrase()).PHP_EOL;
    }
}

class Jovial implements StrategieInterface
{
    public function reagir(PersonneInterface $personne): string
    {
        return ucfirst($personne->donnerPhrase()).' :)'.PHP_EOL;
    }
}

Voici notre classe Personne et l’interface qu’elle implémente. La seule chose qu’elle fait est de retourner la chaîne de caractères bonjour !

interface PersonneInterface
{
    public function donnerPhrase(): string;
}

class Personne implements PersonneInterface
{
    public function donnerPhrase(): string
    {
        return 'bonjour';
    }
}

L’objet Contexte

Voici enfin notre objet Contexte, partie intégrante de notre pattern. Il gère simplement une référence à la stratégie, à laquelle il transmet les requêtes des clients via la délégation à la méthode reagir. C’est en quelque sorte le liant entre le code client et nos différentes stratégies concrètes.

class Contexte
{
    private $strategie;

    public function __construct(StrategieInterface $strategie)
    {
        $this->strategie = $strategie;
    }
    
    public function exprimeReaction(PersonneInterface $personne): string
    {
      return $this->strategie->reagir($personne);
    }
}

Pour finir, écrivons le code client qui va utiliser notre design pattern. Evidemment, nous ne nous adressons pas directement aux stratégies mais nous passons par l’objet Contexte, que nous initialiserons successivement avec des instances de chacune des stratégies concrètes.

$personne = new Personne();
$humeurs = [new Enerve(), new Geek(), new Jovial()];

foreach ($humeurs as $humeur) {
    $contexte = new Contexte($humeur);
    echo $contexte->exprimeReaction($personne);
}

A garder en tête:

  • Un trop grand nombre de if (syndrome appelé la forêt d’ifs) est souvent le signe que Strategy doit être envisagé
  • Gare à la multiplication intempestive des stratégies concrètes, leur nombre peut vite augmenter !
  • Toutes les stratégies concrètes implémentent la même abstraction, il peut parfois arriver qu’elles reçoivent des informations qui ne leur seront pas utiles

Le design pattern Prototype en PHP

Prototype est un design pattern de création: son but est de répliquer des instances dites « prototypes » via un mécanisme de clonage.
Il est prescrit lorsqu’il convient d’éviter de trop nombreuses instanciations d’une classe, notamment lorsque celle-ci possède une logique assez complexe dans son constructeur, ce qui idéalement ne devrait jamais arriver.

Il est important d’économiser les ressources car le coût d’un clonage d’objet reste bien inférieur à celui d’une instanciation. Evidemment, vous ne verrez pas la différence si vous travaillez avec une dizaine d’objets mais ce ne sera sans doute pas le cas si votre application doit gérer 100 000 objets simultanément le jour d’une facturation par exemple.

prototype de voiture

Protype attendant un clonage incognito

Les composants de Protoype

  • une abstraction (classe abstraite ou interface) qui force la présence d’une méthode dédiée au clonage dans les classes concrètes
  • une ou plusieurs classes concrètes dériveront l’abstraction, implémentant de fait la méthode de clonage. Les instanciations seront faites sur ces classes
  • des clones de ces classes seront effectués par le code client, qui participe à ce pattern

Voici notre abstraction; elle force l’implémentation d’une méthode __clone() et par chance, PHP en fourni une « out of the box » comme on dit (oui, on peut aussi dire « nativement ») ! Le reste est très basique: un constructeur sans aucune logique métier, qui prend ce qu’on lui donne et le range consciencieusement, un setter et deux getters.

L’abstraction

abstract class HumainAbstract
{   
    protected $prenom;
    
    protected $sexe;
        
    public function __construct(string $prenom)
    {
        $this->prenom = $prenom;
    }
    
    public function donnerSexe(): string
    {
        return $this->sexe; 
    }
    
    public function changerPrenom(string $prenom)
    {
        $this->prenom = $prenom;
    }
    
    public function donnerPrenom(): string
    {
        return $this->prenom; 
    }
    
    abstract public function __clone();
}

Les classes concrètes

Les classes concrètes – les prototypes – vont dériver cette classe abstraite et, pour rester simples, ne feront absolument rien dans leur méthode __clone() ! Elles ont une variable d’instance qu’il est juste possible de lire mais pas de modifier. Seul le prénom est modifiable (regardez dans la classe abstraite !).

class Male extends HumainAbstract
{
    protected $sexe = 'M';

    public function __clone()
    {
    }
}

class Femelle extends HumainAbstract
{
    protected $sexe = 'F';
    
    public function __clone()
    {
    }
}

Le code client

Le client est un acteur à part entière du design pattern prototype, c’est lui qui va cloner les prototypes.
Ici nous décidons que, les premiers humains ayant sans doute communiqué entre eux par un langage très primitif, notre prototype de mâle s’appellera « Rrrnnngrrwggl », tandis que la femelle portera le doux sobriquet de « Nyyyynyaaa ».

Tous les êtres humains descendant de ces respectables parents à la pilosité prononcée ne seront que des clones dont nous changerons uniquement le prénom. Vous noterez que nous utilisons l’affectation dynamique, c’est à dire que chaque clone sera rangé dans une variable dont le nom sera incrémenté à chaque itération de notre boucle.

$prenoms = [
        'René', 'Eric', 'Jean', 'Robert', 'Marius',
        'Kevin', 'Léo', 'Jacques', 'Loïc', 'John',
        'Alexis', 'Kenneth', 'Nathanaël', 'Christophe'
    ];
    
$male1 = new Male('Rrrnnngrrwggl');

$numeroClone = 0;
$clones = [];

foreach ($prenoms as $prenom) {
    ++$numeroClone;
    
    $nomClone = 'clone'.$numeroClone;
    $$nomClone = clone $male1;
    $$nomClone->changerPrenom($prenom);
    $clones[] = $$nomClone;
}

$prenoms = [
        'Lise', 'Marie', 'Ninon', 'Rachida', 'Ana',
        'Martine', 'Svetlana', 'Eve', 'Carole',
        'Sylvie', 'Laurie', 'Zhang', 'Fatoumata'
    ];

$femelle1 = new Femelle('Nyyyynyaaa');
    
foreach ($prenoms as $prenom) {
    ++$numeroClone;
    
    $nomClone = 'clone'.$numeroClone;
    $$nomClone = clone $femelle1;
    $$nomClone->changerPrenom($prenom);
    $clones[] = $$nomClone;
}

foreach ($clones as $clone) {
    echo $clone->donnerSexe().'/'.$clone->donnerPrenom().PHP_EOL;
}

A retenir

  • N’implémentez pas Prototype si vous devez gérer un petit nombre de copies, ce serait tuer une mouche au lance-roquettes
  • Lors d’un appel à __clone(), le constructeur n’est pas appelé (c’est le but)
  • Si votre objet à cloner a des objets en compositions, pensez à les cloner eux aussi pour éviter de pointer vers une référence qui n’est pas la bonne…
  • Prenez garde aux références circulaires (A dépend de B qui dépend de C qui dépend de A…) entre les objets