Domain Driven Design : Partie 1

24.04.2017  • Gilles Gauthier

Domain Driven Design c’est essentiellement une question de nommage. On nomme énormément de choses dans notre code, mais de quelle manière ? et pourquoi ?

Pour ceux qui aiment la programmation orientée objet, on modélise très vite notre model en un diagramme de classe. On donne un nom générique à chaque classe et c’est plié.

Pourtant quand on commence un nouveau projet, on nous donne un contexte, un domaine sur lequel nous reposer. Si on doit proposer un catalogue de vélo ou de voyages, on va utiliser des mots et des termes différents. Cependant, notre code lui va rester sensiblement le même.

“Tu cherches la classe pour le panier ? Cart bien sûr. On ajoute des voyages dans la classe Cart et on fait le paiement. C’est logique”. Donc on aura sûrement une action AddToCart. C’est logique…

Le DDD nous suggère de nommer les choses en fonction de notre contexte. Personnellement je préférerais voir une action nommée BookATrip. Gardons-la sous le coude et jetons un oeil à l’arborescence ci-dessous proposée par le cosmos du DDD :

└── Booking
    └── Domain
        ├── Command
        │   └── BookATrip
        ├── Handler
        │   └── BookATripHandler
        └── Model
            ├── Booking
            └── Trip

Une personne extérieure à votre projet, voire même votre client, pourrait lire tout cela et comprendre rapidement le domaine (voyage) que vous traitez.

En anglais on utilise le Ubiquitous Language, c’est le champ lexical de notre projet.

La limitation de notre contexte, c’est la réservation (Booking).

En anglais on parle de Bounded Context ou contexte borné en français.

Notre dossier Command contiendra l’ensemble des classes de notre domaine qui nécessiteront une action spécifique. Elles seront traitées par un et un seul service, une classe avec une seule responsabilité, nommée Handler.

Notre classe BookATrip va nous permettre de créer notre action. Pour cela on se laisse le choix de travailler avec ce qui nous convient. Par exemple, on peut choisir une infrastructure classique, Symfony, et un contrôleur. On peut aussi créer une commande console, une API REST etc. Choisissez l’infrastructure qui vous plaît.

Pourquoi je parle d’infrastructure ? Simplement parce que notre voyage peut passer par n’importe quel tuyau de votre usine (ce n’est peut-être pas le terme qui convient le mieux !), au final il passera toujours par le même point de traitement avant de continuer sa route. Nos informations sont renseignées dans la commande BookATrip et sont traitées par BookATripHandler. Tant que ces deux classes se tiennent la main dans leur promenade, on est bon. Regardez l’exemple d’infrastructure :

└── Booking
    ├── Domain
    │   ├── Command
    │   │   └── BookATrip
    │   ├── Handler
    │   │   └── BookATripHandler
    │   └── Model
    │       ├── Booking
    │       └── Trip
    └── Infrastructure
        ├── API
        ├── CLI
        ├── SYMFONY
        └── ZEND_EXPRESSIVE

Il n’en faut pas plus ! Si vous savez faire du Symfony c’est déjà terminé :

└── Booking
    ├── Domain
    │   ├── Command
    │   │   └── BookATrip
    │   ├── Handler
    │   │   └── BookATripHandler
    │   └── Model
    │       ├── Booking
    │       └── Trip
    └── Infrastructure
        ├── API
        ├── CLI
        ├── SYMFONY
        │   ├── Action
        │   │   └── Booking
        │   │       └── BookATrip
        │   ├── routes.yml
        │   └── services.yml
        └── ZEND_EXPRESSIVE

Il manque quelque chose à notre contexte. On fait la réservation d’un voyage, mais pour qui ? L’utilisateur de votre application. On aurait pu dire voyageur. On pourrait, mais le contexte c’est la réservation d’un voyage, c’est un utilisateur qui fait la réservation. Le voyageur sera la personne avec le billet d’avion entre les mains !

En parlant d’avion, notre client nous dit que le voyage est pour deux personnes maximum et que l’utilisateur peut choisir quelques options pour le vol pour chaque voyageur. Concrètement, on tombe dans le cas où notre simple action de réserver un voyage se transforme en plusieurs étapes de réservation. Notre client nous a fait une petite liste justement :

  • réserver le voyage
  • spécifier le nombre de voyageur
  • choisir des options pour l’avion (hublot, classe éco, parachute..)
  • spécifier le nombre de chambre à l’hôtel
  • donner ses coordonnées (téléphone, adresse..)
  • prendre un guide

La liste s’allonge et les actions aussi. On prend la peine de nommer les actions de manière explicite (peut-être que vous n’auriez pas fait les mêmes) :

Domain
│   ├── Command
│   │   ├── BookATrip
│   │   ├── ChoseNumberOfRoomsForTheHotel
│   │   ├── SelectFlightOptions
│   │   └── UserFillsHisAddress
│   ├── Handler
│   │   └── BookATripHandler
│   └── Model
│       ├── Booking
│       ├── Traveller
│       ├── Trip
│       ├── BookingUser
│       └── ValueObject
│           ├── BookingGuide
│           ├── BookingHotelOption
│           ├── BookingPlaneOption
│           └── UserAddress

Rappel :

Une entité est un objet défini par son identité, non par ses données.

Un Value Object est un objet défini par ses données et non par son identité.

La réservation se fait en plusieurs étapes. Mais on peut imaginer un seul formulaire pour remplir le tout également. Du coup, est-ce que notre formulaire va influencer nos actions ? Est-ce qu’on a trop anticipé la logique métier ?

On peut très bien avoir un formulaire basé sur son propre model avec tous les champs nécessaires, et lors de la soumission du formulaire, passer les données à chaque action. La seule donnée importante c’est celle de notre réservation, son ID unique.

e4eaaaf2-d142-11e1-b3e4-080027620cdd // UUID

Booking est une entité que l’on a choisie pour être un aggregate root, c’est lui qui est à la base des autres classes. Il est le noyau autour duquel gravite les électrons. Et pour cela on va le définir via un ID unique.

Les aggregate root se référencent entre eux uniquement par leur ID, jamais via des instances d’objet ou de jointure sql.

On peut aussi envisager de présenter cet UUID d’une autre manière :

BK-T-19–05–2018-E4EAAAF2

Cette notation est sûrement plus lisible pour un humain. On pourrait le traduire par “Booking Trip Context” “le 09–05–2018”. Le dernier segment corresponds au premier morceau de l’UUID. On obtient ainsi un ID de 25 caractères unique.

Il faut également penser à encapsuler ces informations, par exemple si on veut récupérer la date de création :

// ValueObject BookingId
$rawId = "BK-T-19–05–2018-E4EAAAF2";
$bookingId = new BookingId($rawId);
...
$bookingCreationDate = $bookingId->creationDate();
// Aggregate Root Booking.php
public function creationDate()
{
    return $this->bookingId()->creationDate();
}

Source d’inspiration : Vaughn Vernon

Pour revenir à l’aggregate root, concrètement, les autres entités qui lui sont reliées peuvent avoir un id auto-incrémenté, ils sont de moindre importance. Seule la classe BookingUser comportera un id auto-incrémenté et un ID unique du vrai aggregate root User généré quelque part dans un autre bounded context, et le value object UserAddress.

Pourquoi n’a-t-on pas directement une relation entre Booking et User ? Comme expliqué plus haut, Booking référence est un aggregate root User seulement via son ID.

Plus tard on aura besoin des autres informations concernant l’utilisateur pour un affichage en frontend par exemple, mais le cas d’utilisation serait celui de la lecture du model, alors qu’ici on est préoccupé par l’écriture du model.

L’autre point important est que l’on évite que la classe Booking puisse modifier les informations d’un User. Lors de l’action BookATrip, si on s’amuse à modifier des informations de l’utilisateur, on tombe dans le piège où notre code devient fortement couplé. Laissons le Bounded Context Booking et le Bounded Context User séparés. Diviser pour mieux régner !

Résumé

On a vu que le DDD était en grande partie une histoire de nommage, mais aussi d’architecture du code. Le domaine est très important, sans lui on ne saurait pas comment délimiter nos contextes. On vient de voir le bounded context Booking, mais il faut préciser qu’on était sur la partie publique du site. On aura très certainement une partie privée, pour notre client. Le contexte sera différent, c’est là qu’on va créer des voyages et les proposer à la vente.

Communiquer via des ID unique entre les aggregates root renforce le principe de responsabilité unique.

On effleure la question des micro-services, c’est-à-dire que nos voyages pourraient très bien être créés soit dans notre projet avec des sous-projets bien distincts, soit dans un ERP quelconque. La question importante à se poser est de savoir si votre projet à vraiment besoin de telles frontières entre le frontend et votre backend.

Dans la partie 2 nous verrons comment notre terme voyage peut-être utilisé dans plusieurs Bounded Contexts, mais dans le même domaine.

Lead développeur Symfony chez Spiriit.
Adepte de la satisfaction client, j'accompagne les clients dans la conception technique avancée et développe des projets souple et évolutif. Un seul mot d'ordre : Les tests.
Voir l’étude de cas
Lire l’article
Lire l’actualité
En savoir plus
En savoir plus
Voir le témoignage
Fermer