Backend

le pattern command dispatcher

Le pattern command dispatcher sert à découpler l'exécution d'une commande de son commandant. Le commandant n'a pas besoin de savoir comment l'exécution de la commande est mise en œuvre, mais seulement que la commande existe.

7 min de lecture
le pattern command dispatcher

Le pattern command dispatcher sert à découpler l'exécution d'une commande de son commandant. Le commandant n'a pas besoin de savoir comment l'exécution de la commande est mise en œuvre, mais seulement que la commande existe.

Ce pattern est particulièrement utile pour éliminer les problèmes d'entrée dans l'application. Qu'il s'agisse d'une requête HTTP, d'une commande de console ou d'un message provenant d'une file d'attente, ils sont tous mis en correspondance avec la même commande, qui est exécutée exactement de la même manière.

En découplant la commande de son exécution, il est facile de remplacer ou de remanier l'implémentation, sans affecter la commande. De plus, comme l'implémentation est isolée, nous pouvons instancier des commandes avec n'importe quels paramètres et confirmer les résultats.

Ce pattern est également parfaitement adapté à la conception pilotée par le domaine, car les commandes expriment les problèmes du domaine et leur mise en œuvre peut être répartie de manière ordonnée entre les différentes couches.

Enfin, comme il fournit un emplacement unique pour l'exécution des commandes, il facilite la mise en œuvre de la mise en cache et du sourcing d'événements. Le dispatcher peut également être enrichi d'événements before et after pour des implémentations plus performantes.

Concepts

Trois termes sont toujours associés au pattern command dispatcher : command, dispatcher et handler. La commande est un simple DTO, généralement dépourvu de comportement, qui porte les paramètres de la commande. Le dispatcher fait correspondre la commande avec le handler, en utilisant généralement l'identité de la commande, son nom de classe par exemple. Enfin, le handler reçoit la commande en paramètre et se charge de son exécution.

Différenciation

Bien que similaire, car la commande encapsule toutes les informations nécessaires à l'exécution d'une action, le pattern command dispatcher est différent du pattern commande, pour lequel l'objet command connaît le receiver.

Bien que complémentaire, le pattern du dispatcher de commande est également différent du command query responsibility segregation pattern, dont l'idée est d'utiliser différents modèles pour lire et écrire des informations.

Enfin, le pattern diffère du event dispatcher pattern, qui a plusieurs handlers, alors que le command dispatcher pattern fait correspondre une commande avec un seul handler.

Exemple d'implémentation

Les exemples de code suivants montrent comment l'idée de base du pattern command dispatcher peut être implémentée en PHP.

Le dispatcher utilise un handler resolver qui, à partir d'une commande, détermine le handler à utiliser. Le résolveur de handlers utilise généralement un conteneur dependency injection pour instancier le handler.

class Dispatcher {
    private $handlerResolver ;
    public function __constructor(array $handlerResolver) {
        $this->handlerResolver = $handlerResolver ;
    }
    public function dispatch(object $command) {
        $handler = $this->handlerResolver->resolve($commande) ;
        return $handler($command) ;
    }
}

La commande suivante peut être utilisée pour créer une recette, elle contient l'identifiant de la recette à créer et sa charge utile initiale.

final class CreateRecipe {
    public $id ;
    public $payload ;
    public function __construct(string $id, array $payload) {
        $this->id = $id ;
        $this->payload = $payload ;
    }
}

Le gestionnaire suivant gère la commande CreateRecipe. C'est une bonne idée de nommer le gestionnaire d'après la commande pour que le lien soit facile à établir. L'exemple de code est assez superficiel, mais il est facile d'imaginer une forme de validation pour la charge utile et l'utilisation d'un repository pour conserver la recette.

final class CreateRecipeHandler {
    // ...
    public function __invoke(CreateRecipe $command) {
        // ...
    }
}

La vie d'une commande

La vie d'une commande peut être résumée comme suit :

  1. L'application reçoit une entrée
  2. Une commande est instanciée en utilisant les informations de cette entrée
  3. La commande est transmise à un dispatcher
  4. Le dispatcher trouve le handler pour cette commande
  5. La commande est transmise au handler
  6. Le handler exécute la commande
  7. Un résultat est renvoyé (en fonction de la rigueur de l'implémentation)

Test

Étant donné qu'en utilisant le pattern command dispatcher, l'entrée est découplée de l'exécution, il est facile de tester l'implémentation de manière isolée, sans avoir à s'occuper de la logique particulière d'un contrôleur/console/consommateur. De plus, comme il est très facile d'instancier une commande, de nombreux cas peuvent être vérifiés pour couvrir entièrement l'implémentation.

Les handlers sont généralement assez petits et dépendent de plusieurs services pour leur exécution. Souvent, ces services sont simulés pour gérer différents cas.

L'exemple suivant montre comment tester un DeleteRecipeHandler :

final class DeleteRecipeHandler {
    private $repository ;
    public function __construct(RecipeRepository $repository) {
        $this->repository = $repository ;
    }
    public function __invoke(DeleteRecipe $command) : void {
        $this->repository->delete($command->id) ;
    }
}
class DeleteRecipeHandlerTest extends TestCase {
    public function testHandler() {
        $command = new DeleteRecipe($id = uniqid()) ;
        $repository = $this->prophesize(RecipeRepository::class) ;
        $repository->delete($id)->shouldBeCalled() ;
        $handler = new DeleteEntityHandler($repository->reveal()) ;
        $handler($command) ;
    }
}

Une fois que tous les cas particuliers du handler sont testés, le test d'intégration peut simplement vérifier que l'exécution d'une commande produit le résultat attendu.

L'exemple suivant montre comment une commande est utilisée

class RecipeIntegrationTest extends TestCase {
    // ...
    public function testDelete() {
        $id = $this->fixtures['recipe']->getId() ;
        $command = new DeleteRecipe($id) ;
        $this->dispatchCommand($command) ;
        $this-assertFalse($this->repository->exists($id)) ;
    }
}

Le pattern command dispatcher et le DDD

Le pattern du dispatcher de commandes est parfaitement adapté à la conception pilotée par le domaine car les commandes expriment des problèmes de domaine tels que "créer une recette", "renommer une recette", "publier une recette".

Les commandes sont définies dans la couche application, les handlers dans la couche domaine, les services qu'elles utilisent dans la couche infrastructure, et les adaptateurs d'entrée/sortie sont définis dans la couche présentation.

Les classes suivantes peuvent être utilisées pour créer une recette :

  • Application\Command\CreateRecipe
  • Application\Domain\Recipe\Handler\CreateRecipeHandler
  • ApplicationDomaine\Recipe\RecipeRepository (interface)
  • Application\Infrastructure\Persistance\Repository\RecipeRepository (implémentation)
  • Présentation\HTTP\Action\CreateRecipeAction

Utilisation avancée du pattern Command Dispatcher.

Implémentation de CQS

L'idée centrale du pattern Command Dispatcher étant avant tout le découplage de l'exécution, la même idée peut être utilisée pour implémenter un dispatcher de requêtes. Avoir des dispatcheurs différents pour les requêtes et les commandes permet d'implémenter une logique spécifique, comme la mise en cache par exemple.

Implémentation de la mise en cache

Avec un dispatcher de requêtes - variante du dispatcher de commandes - qui déclenche des événements avant et après la répartition des requêtes, et parce qu'une commande (ou une requête) contient toutes les informations nécessaires pour effectuer une requête, il est très simple d'implémenter une couche de mise en cache. L'événement before peut être utilisé pour fournir une réponse en cache, tandis que l'événement after peut être utilisé pour rafraîchir le cache. Enfin, la commande command peut être utilisée comme clé de cache.

Implémentation de l'event sourcing

Event sourcing garantit que toutes les modifications de l'état de l'application sont stockées sous la forme d'une séquence d'événements. Non seulement nous pouvons interroger ces événements, mais nous pouvons également utiliser le journal des événements pour reconstruire les états passés, et comme base pour ajuster automatiquement l'état pour faire face aux changements rétroactifs. Parce que les commandes contiennent toutes les informations nécessaires à leur exécution, elles sont les blocs de construction parfaits pour l'event sourcing. Elles peuvent être stockées dans un journal et rejouées plus tard en passant en séquence au dispatcher de commandes.

Implémentation de restrictions

En émettant un événement before ou en décorant le dispatcher de commandes, on peut mettre en œuvre des restrictions. Par exemple, un contrôleur HTTP pourrait utiliser un dispatcher de commande décoré qui exigerait un jeton d'utilisateur et vérifierait sa portée avant d'envoyer la commande.

En Bref

Parce qu'il dissocie la commande de son exécution, le pattern command dispatcher augmente la flexibilité des applications en permettant de modifier leurs services à tout moment sans avoir à modifier l'application elle-même. Il élimine les problèmes de réception de données et facilite les tests. Il permet des implémentations avancées telles que la mise en cache des réponses ou le sourcing d'événements.

CC: Merci à @olvlvl pour la permission de traduction de cette article