DDD avec Broadway et le Design pattern State

Par @jbnahan69
Attention, cet article est ancien. Les informations ou instructions qu'il contient peuvent ne plus fonctionner.

[English version]

Au cours d’un petit sprint de refactorisation, j’ai constaté que la classe de mon principal agrégat prenait beaucoup d’embonpoint.

J’avais dépassé les 750 lignes de code avec, dans beaucoup d’actions, soit un “switch" soit une petite dizaine de “IF”. Cela ne me plaisait pas beaucoup car, si une modification était demandée à ce niveau-là, la modification serait délicate.

En prenant un peu de recul sur mon code (c’est toujours une bonne idée de prendre du recul ou faire relire son code par quelqu’un d’autre) je me suis aperçu que ma classe avait 5 constantes définies et une propriété “state” utilisée dans tous les «switch» ou «if». En cherchant un peu et en relisant le livre “Design Pattern - Tête la première”, je suis retombé sur le patron de conception “État”.

En y réfléchissant et en regardant du code implémentant ce patron, je me suis décidé à l’implémenter sur mon agrégat.

Commence alors une réflexion intense !

Étant donné que les méthodes liées à l’état sont redirigées directement à l’objet «état» courant, comment émettre les évènements depuis l’objet «état» ? Comment changer d’état dans mon agrégat ? Comment ne pas exposer la méthode `setState()` pour éviter qu’un élément extérieur ne change l’état de l’agrégat ?

Toutes ces questions ont eu une réponse simple. Mais pour cela, reprenons-les une à une.
Comment émettre les évènements depuis l’objet «état» ?
En les stockant dans une variable d’instance de type “tableau”. L’agrégat les récupèrera après l’exécution de l’action afin de les émettre.

Comment changer l’état dans mon agrégat ?
Ça ne change pas de place… C’est toujours dans les méthodes protégées `apply…()`. Elles sont les seules habilitées à modifier l’état interne de l’agrégat.

Comment ne pas exposer la méthode `setState()` pour éviter qu’un élément extérieur ne change l’état de l’agrégat ?
La réponse précédente y répond également ! Il n’y a pas de fonction `setState()`. Les objets «état» sont instanciés et affectés directement dans les méthodes d’application des évènements.

Au final, les deux patrons de conception sont parfaitement compatibles et réalisent une composition très efficace.

L’ajout d’un état est maintenant possible grâce à l’application du «pattern State». Maintenant la classe de mon agrégat a perdu plus de 250 lignes de code et tous les “if” et "switch" ont disparu.

Voici l'implémentation avec Broadway :

<?php

interface State
{
    public function change();

    public function getEvents();
}

abstract class BaseState implements State
{
    private $events;

    private $aggregate;

    /**
     * The aggregate link is for acces to property of aggegate.
     */
    public function __construct($aggregate)
    {
        $this->aggregate = $aggregate;
    }

    /**
     * return all events not sent
     */
    public function getEvents()
    {
        $events = $this->events;

        $this->events = [];

        return $events;
    }

    private function addEvent($event)
    {
        $this->events[] = $event;
    }
}

class FirstState extends BaseState
{
    public function change()
    {
        // Store event into state object
        $this->addEvent(new ChangeEvent($this->aggregate->getAggregateRootId()));
    }
}

class SecondState extends BaseState
{
    public function change()
    {
        //No change
        throw new \Exception("Unable to change the state !");
    }
}

L'agrégat et les événements :

<?php

use Broadway\EventSourcing\EventSourcedAggregateRoot;
use FirstState;
use SecondState;

class Agregat extends EventSourcedAggregateRoot
{
    private $id;

    private $state;

    public function getAggregateRootId()
    {
        return $this->id;
    }


    public static function make($id)
    {
        //...
        $this->apply(new MakeEvent($this->getAggregateRootId()));
    }

    protected function applyMakeEvent(MakeEvent $event)
    {
        $this->id = $event->getId();
        $this->state = new FirstState($this);
    }

    public function change()
    {
        $this->state->change();
        $this->applyMany($this->state->getEvents());
    }

    protected function applyChange(ChangeEvent $event)
    {
        $this->state = new SecondState($this);
    }

    private function applyMany(array $events)
    {
        foreach ($events as $event) {
            $this->apply($event);
        }
    }

}

class ChangeEvent {
    private $id;

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

    public function getId()
    {
        return $this->id;
    }
}

class MakeEvent {
    private $id;

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

    public function getId()
    {
        return $this->id;
    }
}

Cette branche de mon dépôt test-broadway applique ce patron. Dite moi ce que vous en pensez dans les commentaires.

Lire aussi:

Author avatar
Jean-Baptiste Nahan

Consultant Expert Web, j'aide les entreprises ayant des difficultés avec leur projet Web (PHP, Symfony, Sylius).

@jbnahan69 | Macintoshplus | Linkedin | JB Dev Labs