DEV Community

Ludovic Fleury
Ludovic Fleury

Posted on • Edited on

Domain Driven Design avec PHP & Symfony

english version available here

Partage d'observations, considérations & recommandations concernant les développements pilotés par le métier avec PHP et le framework Symfony (version 2.x à 5.x)

note: pas l'habitude de rédiger en français sur les sujets techniques. Les sources, liens & références seront souvent en anglais

Redonner du sens au développement

DDD gagne en popularité dans la communauté Symfony/PHP. J'évoquais précédemment ma quête insatiable de sens qui motive mon évangélisation toute particulière pour le DDD.

Définition

DDD c'est un paradigme, une approche, une méthode, une façon de penser et de voir.

La valeur du DDD réside dans sa définition: "développement piloté par le métier". Si vous ne devez retenir et comprendre qu'une seule chose du DDD: c'est bien son intitulé. Si vous restez concentré sur le métier, le contexte métier, les règles métiers, le vocabulaire métiers, les acteurs métiers dès que vous lancez votre IDE pour coder: votre code sera en accord avec les principes DDD.

Le DDD n'est pas une architecture logicielle ou un style architectural. Les "Entities", les "Value Objects" ou les "Aggregates Root" ne sont que des détails techniques trompeurs, uniquement utiles dans nos univers orientés objets.

Le DDD c'est essentiellement une compréhension du métier cristallisée dans un dépôt de code par les développeurs.

Concrètement

Si les chefs de projets, les "stakeholders" ou les responsables métiers prenaient le temps de lire le contenu d'un dépôt DDD, ils devraient normalement comprendre 80% du code.

En réalité les langages de programmation de haut niveau comme PHP peuvent être considérés comme une version extrêmement appauvrie de l'anglais. En DDD, le code PHP décrit une règle métier ou un concept métier en "anglais orienté-objet".

A quoi ressemble mon code ? A des instructions métiers écrites dans un très mauvais anglais :

$cashier->checkout($cart) // $caissier->encaisse($panier)
$cashier->charge($card) // $caissier->debite($carte)
$accountant->write($payment) // $comptable->ecrit($paiement)
$compliance->verify($payment) // $controlleurFinancier->verifie($paiement)
Enter fullscreen mode Exit fullscreen mode

A quoi ressemble mes classes "domain" (métiers) ? Là encore, à des concepts métiers écrits dans un anglais particulier:

namespace Legal\Company\France;

/**
 * French unique registration number for a specific establishments of a company
 * issued by INSEE
 * SIRET: "Système d'Identification du Répertoire des ETablissements"
 * 
 * @see https://www.economie.gouv.fr/entreprises/numeros-siren-siret
 */
Class Siret implements RegistrationNumber
{
    private Siren $siren;
    private Nic $nic;

    public function __construct(string $number)
    {
        try {
            $this->siren = new Siren(mb_substr($number, 0, 9));
            $this->nic = new Nic(mb_substr($number, 9));
        } catch (\Exception $exception) {
            throw new \DomainException('Invalid SIRET number');
        }
    }

    public function getNumber(): string
    {
        return $this->siren->getNumber() . $this->nic->getNumber()
    }
}
Enter fullscreen mode Exit fullscreen mode
namespace Legal\Company\France;

/**
 * Unique registration number for a french company
 * Issued by INSEE, composing the first part of SIRET
 * Système d'Identification du Répertoire des ENtreprises
 *
 * @see https://bpifrance-creation.fr/encyclopedie/formalites-creation-dune-entreprise/formalites-generalites/numeros-didentification
 */
class Siren
{
    private string $number;

    public function __construct(string $number)
    {
         if (preg_match('/^[0-9]{9}$/', $number) !== 1) {
            throw new \DomainException('Invalid SIREN number');
         }

         $this->number = $number;
    }

    public function getNumber(): string
    {
        return $this->number;
    }
}
Enter fullscreen mode Exit fullscreen mode

Les devs français sont sûrement familiers avec les concepts métiers ci-dessus. En revanche, si vous ne connaissez pas le fonctionnement des immatriculations de société en France, en lisant simplement ce code vous devriez avoir une idée assez claire sur le sujet.

Les vertus

Le code est "expressif" pas seulement "lisible". Il traduit mot pour mot les concepts et le contexte métier.

Le code n'est pas seulement "expressif", il est également "immutable". Je ne me suis pas imposé un design pattern ou une "best practice". J'ai simplement respecté les règles des immatriculations d'entreprises françaises (SIRET, SIREN & NIC), ces numéros sont immuables donc mon code est immuable... et donc ces objets s'apparentent naturellement à des "Value Objects", chouette. Le fait qu'ils soient des VO n'est donc qu'une simple conséquence de l'implémentation des spécifications métiers!

La cuillère n'existe pas

Oubliez les "best practices", les "design patterns", les "fluent interface", "event dispatching", MVC, SoC, SRP, SOA, RAD, CRUD, DRY... Oubliez la tech, seul le métier pilote l'implémentation, le métier est l'unique priorité.

Utilisez votre langage (ici PHP) pour modéliser le métier le plus simplement possible. Tel l'expert qui explique son métier à un junior, le dev "explique" explicitement et clairement dans son code un process métier.

Si vous implémentez des Entités avec des setters & getters, des "Value Object" dans le namespace \ValueObject\, vous êtes en train de manquer l'essentiel du DDD.

J'insiste: le DDD ne s'intéresse pas à l'implémentation, DDD c'est le partage d'un langage commun entre le métier et le code.

Dernière remarque au sujet de la valeur du DDD:
Toutes les bases de code ont de la valeur au moment de l'exécution par la machine. Concernant les bases de code DDD, elles ont une très forte valeur même non-exécutées. Ce sont des centres de connaissance, des patrimoines métiers pour les personnes qui travaillent avec.

DDD et Symfony

J'ai découvert le DDD autour de 2014, il m'a fallu une année complète, intensive de lecture et d'implémentation pour me sentir à l'aise et "efficient" avec mes outils (PHP, Symfony & Doctrine). Car comme Evans le mentionne:

"Don't fight your framework".

-- Domain-driven Design: Tackling Complexity in the Heart of Software

En fonction des projets et de leur complexité, j'adapte mes implémentations. Mais généralement lorsque j'amorce un nouveau projet ou que je coach un nouveau dev sur le DDD, je suis ces grandes lignes directrices:

  • J'utilise & j'abuse du command pattern (Symfony Messenger est parfait)

  • Je n'utilise pas le pattern CQRS

  • Je n'utilise pas le pattern ES

  • J'utilise Behat & le BDD au niveau de mes commandes/handlers, couverture de test intégrale

  • Je n'utilise que très rarement les tests unitaires, 20-30% de couverture en général.

Pourquoi ni CQRS ni d'ES ?

Parce que ça coûte un rein! (voir l'article suivant qui présente la stack soft & hard d'un ES.)
Ce sont des solutions ultra puissantes. Mais à déployer uniquement lorsque cela fait sens (rapport coût & besoin).

ES

Je pense qu'il n'y a pas beaucoup de dev php qui sont familiers avec l'event sourcing, encore moins qui sont expérimentés pour créer, maintenir ou modéliser un event store correctement dans le temps. Souvent, au début d'un projet on manque de "maturité" modèle, on apprend, on refacto et on migre. Mais sur un event store, aucune migration. L'historique reflète directement les erreurs d'implémentation passées, ce qui multiplie drastiquement les coût de maintenance logicielle.

CQRS

Le CQRS c'est vraiment puissant quand on passe sur des problématiques/volumes de très grande échelle. Dans ce type de contexte, le CQRS apporte directement la complexité de la "Cohérence à terme" aka eventual consistency. Je vous laisse envisager le (sur)coût de cette spécificité.

j'ai eu des expériences avec les 2 patterns, séparément et simultanément, dans des projets DDD, avec Symfony, parfois Doctrine et SQL/MongoDB

La structure

  • src/Controller/: Symfony controller
  • src/Command/: Symfony CLI commands

  • src/Action/: Command + CommandHandler

  • src/Domain/: AR, Entity, VO, Repository Interface

  • src/*: Infrastructure, other stuff.

Loin d'être idéal, le répertoire nommé /Domain/ est plutôt pratique. On peut remplacer le mot "Domain" par le nom spécifique du métier qu'on est en train d'adresser, par exemple: /Ecommerce/.

Le débat sur la pluralité des "domains"

Les questions existentielles apparaissent dès qu'on manipule plusieurs domaines dans son projet. Il y a des écoles & courants de pensées sur la question de l'existence du Core Domain et la séparation des Supporting Domains. De mon côté, c'est tranché, ces derniers sont organisés sans distinction particulière dans /Domain/:

  • /Domain/Payment
  • /Domain/Compliance
  • /Domain/Delivery

Problème réglé, c'est cohérent (consistent), ça gagne à tous les coups.

Le contenu du "Domain"?

A l'intérieur de src/Domain/, j'ai l'ensemble des concepts métiers utiles à l'implémentation. Il y a certaines classes qui sont persistantes (en base de données), d'autres non. On y trouve également les services métiers.

Comme src/Entity n'existe plus, la configuration de Doctrine est modifiée pour scanner le répertoire src/Domain/ dans son ensemble. Les annotations sont utilisées pour définir le mapping en base, car la localité, c'est également valable pour mon cerveau.

Evidemment, lorsque le projet s'alourdi, il ne faut pas hésiter à ré-organiser /Domain/ en reflétant les besoins métiers:

/Domain/Legal/Company/France/RegistrationNumber/Siret.php
/Domain/Legal/Company/France/RegistrationNumber/Siren.php
/Domain/Legal/Company/France/RegistrationNumber/Nic.php
Enter fullscreen mode Exit fullscreen mode

Commande qui conquiert

L'usage

J'utilise systématiquement le command pattern pour les raisons suivantes:

  • les command handler s'inscrivent parfaitement dans les architectures hexagonales

  • les command handler offrent de la cohérence sur la manipulation de ma couche "modèle“, ils en sont les uniques points d'entrée.

  • les command handlers clarifient les frontières transactionnelles d'une opération métier.

  • les command handlers normalisent la couche de service applicative

  • les command handler soulagent les prises de tête sur les Aggregate Root

J'abuse pas mal de ces command handlers car je les détourne parfois de leur but premier:

L'abus

  • Lorsque j'ai beaucoup d'inconnus, d'incertitudes, un manque de compréhension, ou simplement lorsque je suis fainéant ou pressé: je les utilise pour stocker du code métier temporairement, à vocation à être déplacé ultérieurement dans des services métiers ou des objets métiers.

  • Lorsque je dois synchroniser plusieurs process métier entre eux: je les utilise au lieu d'implémenter des films d'horreur nommés SAGA

  • Lorsque l'implémentation d'un AR est particulièrement subtile, difficile ou coûteuse avec Symfony/Doctrine, je code une partie de cette logique dans un handler (ça craint, vraiment à éviter).

Cela doit sonner très faux aux oreilles des puristes, mea maxima culpa. J'ai fait mon deuil & je suis complètement dans l'acceptation car ça fonctionne très bien pour moi.

Le naming

Les commandes sont nommées d'après les processus métiers, les actions métiers qu'elles représentent. L'organisation s'adapte évidemment selon les besoins spécifiques:

src/Action/Checkout.php
src/Action/CheckoutHandler.php

src/Action/FraudulentPaymentDeclaration.php
src/Action/FraudulentPaymentDeclarationHandler.php

src/Action/Compliance/Registration.php
src/Action/Compliance/RegistrationHandler.php
src/Action/Compliance/FrenchEstablishmentRegistration.php
src/Action/Compliance/FrenchEstablishmentRegistrationHandler.php
Enter fullscreen mode Exit fullscreen mode

Maybe I would have used an imperative form for the Command: Register instead of Registration

-- Florian Klein

Un process métier, une action métier porte généralement un nom. Il est important de prendre le temps d'identifier clairement une action métier par son nom. Ce n'est pas toujours possible.

Un verbe est un indicateur d'une potentielle faiblesse de compréhension du métier, exemple:

  • MarkAsRead
  • ValidateStatus

Attention, c'est une direction, dans mes dépôts il arrive très souvent d'avoir des commandes débutant par un verbe.

Détail d'implémentation des commandes

Les commandes sont donc les expressions d'une intention (métier). Elles sont modélisées & implémentées comme de simple DTO qui n'acceptent que des primitives facilitant l'architecture hexagonale ainsi que l'application port/adapter. Les commandes représentent la frontière entre les primitives et les concepts métiers dans mon application:

namespace Action\Compliance;

use Domain\Geo\Address;
use Domain\Geo\Country;

class Registration
{
  private string $userId;
  private string $name;
  private string $addressLine1;
  private string $addressLine2;
  private string $addressZip;
  private string $addressLocality;
  private string $addressCountry;

  public function __construct(
    string $userId,
    string $name, 
    string $addressLine1, 
    string $addressLine2,
    string $addressZip,
    string $addressLocality,
    string $addressCountry
  ) {
    $this->userId = $userId
    // ... boring stuff
  }

  public function getUserId(): Uuid
  {
    return new Uuid($this->userId);
  }

  public function getName(): string
  {
    return $this->name;
  }

  public function getAddress(): Address
  {
    return new Address(
      $this->addressLine1,
      $this->addressLine2,
      $this->addressZip,
      $this->addressLocality,
      new Country($this->addressCountry)
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

J'ajoute généralement de la validation Symfony sur les commandes pour gérer les erreurs "de forme" et avertir l'utilisateur. Là encore, via des annotations. En revanche, les règles métiers fortes sont toujours exprimées & implémentées dans les objets métiers (domain), une violation de ces règles entraîne systématiquement une levée d'exception.

Avec cette approche, l'implémentation de l'architecture hexagonale est enfantine:

  • Les Symfony/Form peuvent déclarer le DTO commande comme model, la fameuse data-class

  • Les commandes (CLI) de Symfony/Console peuvent initialiser le DTO commande avec ces arguments

  • Un Worker/Consumer RabbitMQ peut désérialiser un message et le transformer en DTO commande

  • Un Symfony/EventListener peut transformer un Event en DTO commande

  • Web Socket, etc...

En revanche, une fois que ma commande est transmise à son Handler, ce dernier manipule essentiellement des objets métiers (VO, Entity). Un objet métier fournit des garanties fortes:

  • Un état systématiquement valide (d'après les règles métiers)

  • Un état quasi-systématiquement immuable (immutable).

  • Une API publique autorisant uniquement des changements d'état prévus & spécifiés par le métier

Manipuler ce type d'objet métier est un réel bonheur, un soulagement: la charge cognitive est très faible, inversement à la confiance dans le système, très forte.

Quelques remarques :

  • Pas de typage spécifique pour les ID des AR et des entités (UserId, BookId, ...). Pénible à maintenir avec Doctrine. Dans certains projets très complexes, il m'est arrivé d'en utiliser.

  • Comment arbitrer entre VO (objet métier) et primitive?
    Si le métier porte de l'importance à un concept, si le métier à des règles spécifiques liées à un concept : VO. Sinon, les primitives sont suffisantes. Cela reste du PHP, pas du Java.

Les command handlers ?

Le rôle des command handlers est de coordonner AR, les Entités, en bref, de coordonner les concepts métiers entre eux.

Très souvent ils utilisent un repository pour hydrater l'AR concerné par l'action, la commande métier.

Le handler appelle ensuite des méthodes sur les objets métiers (AR/Entité). Doctrine implémentant le data-mapping, le handler gère également les cycles de la couche de persistance. En sus, certains handlers coordonnent d'autres détails d'infrastructure. Très souvent relatifs aux IO nécessaires à la composition d'un état métier correct (satisfaisant) répondant au pré-requis d'une action métier souhaitée.

Symfony/Messenger prend entièrement à sa charge la gestion des commandes et des handlers. Depuis un controlleur HTTP, une Symfony/Commande ou encore un Worker RabitMQ, il suffit d'injecter le bus et de dispatch votre commande.

En cas de process métiers (trop) longs, passer les traitements des commandes en asynchrones ne nécessite quasiment aucun effort d'implémentation. Une simple modification de la configuration de Symfony/Messenger:

  • la commande est envoyée via un RabbitMQ.
  • Le handler connecté en tant que "consumer" de la queue.

Comme nous n'utilisons pas CQRS, une forte majorité des exécutions de commandes est synchrone, il suffit d'utiliser les fonctionnalités de Symfony/Messenger pour récupérer le résultat d'un handler, ajouter un peu de sérialisation pour obtenir une belle représentation REST pour une réponse HTTP.

Les Bounded Contexts (BC)

Par défaut, je considère qu'un dépôt git représente 1 BC. Ma compréhension évolue souvent lors d'un projet. Ou tout simplement, le projet grandi, grossi et se découvre également de nouveaux BC's.

Il m'arrive très souvent de séparer des contextes a posteriori. La présence de plusieurs contextes peut signifier:

  • une incompréhension du métier
  • de fortes incertitudes sur le périmètre métier
  • une extrême complexité

Organiser ses BC's

Les micro-services, c'est comme le CQRS: ça coûte un rein, si c'est mal orchestré: ça en coute deux. Si votre équipe dispose d'une armée de devops, vous avez de la chance. Sinon, le principe de localité, ça marche aussi:

src/BC1/Controller/
src/BC1/Command/
src/BC1/Domain/

src/BC2/Controller/
src/BC2/Command/
src/BC2/Domain/
Enter fullscreen mode Exit fullscreen mode

Pourquoi? Séparer ou délimiter des BC's est une opération très définissante. Très risquée également. Synchroniser des BC's entre eux est relativement coûteux.

De cette façon, vous pouvez faire évoluer vos BC's, itérer, comprendre, refactoriser et déplacer du code de domain en domain, de BC en BC. Lorsque vous atteignez la "maturité modèle" et maitrisez la compréhension métier. Vous avez toute la liberté de séparer vos BC sur plusieurs dépôts... et de jouer avec K8s.

Conclusion

Il est temps de prendre du recul à nouveau après cette plongée parfois très détaillée des techniques adoptées pour DDD & Symfony. Rappelez-vous le but de cet article et son message: "Oubliez la tech & concentrez vous sur le métier"

Ces détails techniques sont un condensé de connaissances, d'expériences, d'astuces ou de compromis rassemblés au fil des années pour gérer simplement une approche DDD en synergie avec l'écosystème PHP et Symfony. Mais encore une fois: c'est le moins important du DDD.

Ces méthodes sont à disposition dans l'espoir de ne plus s'attarder sur le "comment" du code. Mais de concentrer les efforts & la valeur sur le "quoi" et le "pourquoi".

EDIT JUNE 2nd
Lucas Courot a réagi via twitter sur 3 points:

  • les commandes asynchrones
  • le coût de gherkin (Behat)
  • les notifications en place des exceptions

Un échange de plusieurs tweets résume nos idées, opinions, divergentes ou convergentes, toujours intéressantes.

Top comments (10)

Collapse
 
spike31 profile image
Gilles Gauthier

Merci pour ton retour d'expérience !

Tu n'as pas parlé des events dispatchés dans un command handler, c'est volontaire ?

Par exemple moi j'aime bien dans un command handler "UploadImageHandler" pouvoir dispatcher un event "ImageUploaded" et avoir un subscriber qui écoute cet event et qui va créer un nouvel handler selon les besoins.

Exemple créer une notification pour informer qu'une image a été uploadée.

public function onImageUploaded(ImageUploaded $event) {
  $this->messenger->dispatch(new Notification(Type::Image));
}
Enter fullscreen mode Exit fullscreen mode

ça permets de ne pas avoir un EventBus qui puisse créer des "actions" mais toujours des commands handlers

C'est quelque chose que tu fais ?

:)

Collapse
 
ludofleury profile image
Ludovic Fleury • Edited

Merci Gilles pour ton commentaire (et ton boulot sur Lexik)

Je ne suis pas sur de bien comprendre le détail de ta question:

un command handler "UploadImageHandler" pouvoir dispatcher un event "ImageUploaded" et avoir un subscriber qui écoute cet event et qui va créer un nouvel handler selon les besoins.

Si je lis cette phrase j'en comprends la chronologie suivante:

  1. Dispatch command: UploadImage
  2. Handle command: UploadImageHandler
  3. Event: ImageUploaded
  4. Handle command: CropThumbnailHandler

Car je lis subscriber qui écoute cet event et qui va créer un nouvel handler ou encore pas avoir un EventBus qui puisse créer des "actions" mais toujours des commands handlers)

J'ai du mal à articuler cette solution, dans tes listeners/subscribers tu instancies la commande + directement le handler?

Il m'arrive d'implémenter des Events bien entendu, je n'en parle pas pour éviter les confusions entre Event domain et Event application.

Event domain

Pour être un peu plus technique: lorsqu'on fait de l'ES (event sourcing), on parle d'event domain. Ce sont des Events utilisés pour gérer le cycle de vie des concepts métiers. Prenons l'exemple de la mise à jour d'un email utilisateur. C'est un évènement du domaine qui a des conséquences en cascade souvent sur d'autres concepts du métier. Les changements d'état sur les entités, les AR's sont gérés via un stream d'events .

Note: il n'est pas obligatoire de faire de l'event sourcing pour dispatcher des events dans son domain. Mais c'est tout de même beaucoup plus rare.

Event applicatif

On se souvient que sur une implem DDD "classique" ou "hexagonale" , on identifie 3 couches: application, domain et infrastructure.

Les commandes / handlers appartiennent à la couche applicative. La couche qui manipule les concepts métiers (la couche domaine). Cette couche peut bien entendu émettre des évènements. Ce sont exactement ceux que tu mentionnes dans ton commentaire: un command handler qui dispatch un event. L'articulation est la suivante:

  1. Dispatch command: UploadImage
  2. Handle command: UploadImageHandler
  3. Dispatch Event: ImageUploaded

  4. Handle Event: ImageUploadedListener

  5. Dispatch command: CropPreview

  6. Handle command: CropPreviewHandler

  7. Dispatch event: PreviewCropped

A la lecture de ton exemple de code, je pense que c'est ce que tu as voulu dire (?)
Et j'essaie d'interpréter:

ne pas avoir un EventBus qui puisse créer des "actions" mais toujours des commands handlers

En "J'évite d'avoir des command handlers qui instancie/dispatch directement de nouvelles commandes, je découple la séquentialité des commandes grâces à des évènements." (?)

Collapse
 
spike31 profile image
Gilles Gauthier

C'est bien ça Ludovic ! (J'avoue que n'ai pas fais l'effort d'être clair en me relisant..!) Je parlais d'Event Applicatif.

Si je reprends ton point 4. ImageUploadedListener

Ce que je fais c'est que j'injecte le CommandBus dans le listener, pour pouvoir dispatcher la command CropPreview, et ainsi recommencer une nouvelle boucle

command -> handler -> event -> listener -> command -> handler ...

Et le tout est enveloppé, quand c'est possible, dans une transaction Mysql. Je dis quand c'est possible, car si j'ai une action tournée vers l'extérieure comme "envoyer un email", il n'y a pas de rollback/annulation possible.

C'est une mise en place que j'avais lu dans un article CQRS Journey il y a plusieurs années (rechercher sur la page "Approach 3: Using a process manager")

En suivant ce principe j'arrive à découpler mes handlers et à pouvoir "enchaîner" les commandes qui en découlent, et mon code est organisé de manière plus "lisible".

J'ai souvent vu des exemples de ce genre :

  1. Dispatch command: CreateUser
  2. Handle command: CreateUserHandler
  3. Dispatch Event: UserCreated
  4. Handle Event: UserListener
  5. Et dans le listener le code complet pour envoyer un email

Je trouve que c'est une erreur parce qu'on donne de la responsabilité à un listener, on lui donne de la logique métier, alors que (pour moi) il est juste là que pour "router" vers la prochaine command à exécuter. Comme un ProcessManager..

En tout cas j'ai hâte de lire tes prochains articles !

Thread Thread
 
ludofleury profile image
Ludovic Fleury

Super, merci pour ton retour, je vais essayer de compléter la réponse dans ce cas avec tes éléments.

Concernant les "events applicatif", du coup je suis tout à fait aligné avec ton implémentation.

c'est une erreur parce qu'on donne de la responsabilité à un listener, on lui donne de la logique métier

Ce point est très important et je soutiens l'approche. Les listeners ne doivent pas contenir de logique métier. Il y a plusieurs options pour éviter cela: soit le dispatch d'une autre commande (comme tu le fais) soit l'appel à un service applicatif autre qu'une commande (mais je trouve qu'on perd immédiatement en consistence "DX": plusieurs façon de manipuler la couche applicative).

En revanche:

Et le tout est enveloppé, quand c'est possible, dans une transaction Mysql

Là, je suis plus réservé. J'éviterai au maximum. Parce que selon les approches "tactiques" du DDD, comprendre les reco d'implémentation:

  • Les AR garantissent l'atomicité d'une opération
  • J'étends cela au niveau des commandes pour soulager le design des AR (concepts plutôt difficiles) et également éviter les SAGA

Mais, je délimite la transactionnalité d'une opération métier à 1 unique commande. Le plus difficile à maintenir dans une application, c'est surement l'intégrité des changements d'état, la diffusion de la transaction au travers de plusieurs commandes amène du risque, de la complexité de maintenance.

Comme tu peux le lire dans l'article, je fais également beaucoup de compromis ou d'économie dans mes implémentations (command handler parfois bof-bof, commande async). Et il n'y a pas de place à la pureté dans la réalité (des contextes de nos projets). Donc ce n'est pas "mauvais" et nous maitrisons nos risques dans ces décisions tech un peu "touchy", je me méfierai juste de banaliser des transactions "plus large".

Cette article est en venu spontanément en réaction à l'excellente conf de @lilobase . Je ne pensais pas rédiger plus, mais si tu as des sujets, cela peut m'inspirer?

Dernier point, dans la traduction anglaise de cet article j'ai une partie "Bonus Track" à la fin, qui parle de la synchronisation de BC's et évoque une mécanique events similaire.

Collapse
 
alexsoyes profile image
Alex so yes

Merci Ludovic pour cet article super et pour avoir répondu aussi longuement aux commentaires.

Chaque lecture m'amène d'autant plus de questions, c'est génial, merci encore pour ta bienveillance et ton envie de transmettre !

Collapse
 
alanpoulain profile image
Alan Poulain

Super article !
Je m'y retrouve à peu près partout excepté quelques détails.
En particulier pourquoi ne pas séparer aussi les commandes par domaine métier et les avoir toutes dans Action ?
Petite remarque : totalement d'accord avec la couverture de test (tests fonctionnels en priorité, tests unitaires si besoin), mais ça aurait été intéressant d'avoir une explication supplémentaire sur ce choix.

Collapse
 
ludofleury profile image
Ludovic Fleury • Edited

Merci beaucoup pour ton feedback Alan (et ton boulot sur API Platform!)

Structuration des commandes au plus près de l'opérationnel

En particulier pourquoi ne pas séparer aussi les commandes par domaine métier et les avoir toutes dans Action ?

Les commandes sont sur une limite floue parfois entre la couche application et la couche domain
Rien n'est parfait, ni même le métier. Néanmoins les commande handlers sont des actions métiers, des process métiers. Hors ces process, ces actions, sont amenés à travailler sur plusieurs domaines. En détail:

Cela est étroitement lié à mon paradigme très personnel qui ne fait absolument aucun discernement de la qualité des domains (Core, Supporting etc). Je prends l'exemple des coordonnées (address, phone, coordinates) qui sont très souvent, dans un "supporting domain" partagées dans les applications métiers. Certes nous n'avons pas besoin de respecter toujours le même niveau de règle métier, mais le niveau de "générisation" rend l'abstraction/concrétion dans chaque "domain" assez.... futile?

Ce que j'ai observé c'est que la tech offre de nouvelles possibilités au métier. On ne fait pas qu'automatiser (totalement ou partiellement) des process métiers existants. On permet de réinventer (assembler, enlever, fusionner) des process métiers grâce à la technologie. En conséquence, les domaines identifiés du métiers sont très souvent amenés à collaborer/coexister dans des nouvelles actions.

Autre point, je n'utilise pas de SAGA, et j'évite les AR "fumeux/couteux" trop puristes parfois. Je préfère synchroniser 2 AR (cross-domain) dans certains cas. Donc j'utilise la couche command handlers à ces fins économiques/pragmatiques.

La couche /Action/ coordonne la couche /Domain/ sans se conformer à la structuration du domaine. En d'autre terme: la couche "Application" est "au dessus" de la couche "Domain". Je ne sais pas si je suis très clair?

Lorsque j'ai beaucoup "d'actions", je peux les regrouper par catégorie d'actions, famille d'actions. Mais cette structuration est au plus proche de l'opérationnel du métier (donc plus loin de la taxonomie "domain").

Avec cette approche, la structuration de /Domain/ est tellement plus "facile". Dès que j'ai un partage de concept métier avec énormément de similitude au sein du même BC: je soulage l'implémentation en re-positionnant le concept partagé & partageable.

Je n'aime pas le terme "Action", mais je voulais éviter le conflit avec command-cli... En revanche je ne travaille pas avec API platform, donc je n'ai pas ce conflit "ADR" dans mon "framework". Je pense qu'il faudrait l'adapter dans le cas d'API platform (don't fight your framework)

Choisir la couche de tests en fonction de l'usage & de l'audience cible

Petite remarque : totalement d'accord avec la couverture de test (tests fonctionnels en priorité, tests unitaires si besoin), mais ça aurait été intéressant d'avoir une explication supplémentaire sur ce choix.

Il y a plusieurs valeurs sur un harnais de tests. La première, déjà très forte, est la stabilité du software/application. La seconde est la garantie des coûts de développement. De manière générale, sur une qualité de code acceptable, la problématique du cout du code réside dans la maîtrise & la connaissance de ce dernier (turn over des ressources humaines, mise jour de la documentation, cohérence des spécifications dans le temps, disponibilité des stakeholders? etc...)

En d'autres termes, les coûts de dev dans un dépot avec une qualité "ok": c'est une formule qui doit avoir comme base forte: le reverse engineering.

Je crois très fortement que les harnais de tests doivent couvrir cet aspect. En bref: un harnais de test doit répondre à ces deux questions:

  • Est-ce que ça fonctionne (encore) ?
  • Qu'est-ce que le code est capable de faire?

Le deuxième point est important: Car en lisant le "how/comment" (les tests) on doit voir surgir le "why/pourquoi". Pourquoi a-t-on codé cette classe, cette commande, ce projet.

J'ai été dans des boites "produits" et le DDD a été appliqué sur la construction de ces produits dont la source du code est propriétaire. Avec ces 2 critères, le harnais de test est naturellement orienté Behat sur la commande. Car la valeur des tests est de décrire le process métier et les différents scénario métiers pour lesquels le code a été prévu.

Dans le cadre d'open source, ou de projet a destination de développeurs, la couche unitaire devrait être très solide (là encore, question d'usage et d'audience pour valoriser le harnais de test). Si mon produit était essentiellement une API (Stripe?), il faudrait, dans cet autres cas, prioriser des tests à ce niveau.

Remarque: le DDD ne formalise aucune méthode organisationnelle. i.e: a aucun moment il n'est mention de la structuration ou du workflow de production. Le BDD (Behat est orienté BDD) lui structure l'approche collaborative métier, PO, Stackholder, ux, dev etc... j'utilise donc le BDD pour poser un cadre d'execution favorisant le DDD.

Collapse
 
qamarh profile image
qamar-h

Bonjour,

Merci pour votre article.

Je voulais savoir si vous aviez un projet sur github par exemple qui implémenterait le DDD.

Qamar.

Collapse
 
ludofleury profile image
Ludovic Fleury

Hello Qamar, merci pour ton retour.
Je n’ai pas de projet ouvert sur GitHub en DDD. Je n’ai que ce dépôt partiel et daté qui est un début d’implémentation d’évent sourcing github.com/ludofleury/blackflag/tr...

Si j’ai l’opportunité de travailler sur un projet ouvert nécessitant une implémentation DDD, je ne manquerai pas de te donner le lien.

Tu peux toujours m’envoyer tes dépôts et questions :)

Collapse
 
qamarh profile image
qamar-h

Super. Merci pour ta réponse.