Chapitre 11
11 - Domain-Driven Design (DDD)
11 - Domain-Driven Design (DDD)
Chapitre 11 : Domain-Driven Design (DDD)
Introduction
Le Domain-Driven Design (Eric Evans, 2003) est une approche de modélisation logicielle qui place le domaine métier au centre de la conception. Il fournit un ensemble de patterns pour capturer la complexité métier et produire un logiciel qui reflète fidèlement les processus réels.
11.1 Fondamentaux du DDD
11.1.1 Pourquoi le DDD ?
Le DDD répond à un constat simple : la complexité d'un logiciel vient du domaine métier, pas de la technique. Pourtant, la plupart des projets se concentrent sur la technique (framework, base de données, API) et négligent la modélisation du domaine.
Diagramme en cours de génération...
11.1.2 Les piliers du DDD
Diagramme en cours de génération...
11.2 Ubiquitous Language
11.2.1 Définition
L'Ubiquitous Language (langage ubiquitaire) est un langage commun partagé par tous les membres de l'équipe — développeurs, experts métier, product owners, testeurs.
Principes :
- Mêmes termes utilisés dans les conversations, les diagrammes, le code, la documentation
- Pas de traduction entre "terme métier" et "terme technique"
- Évolue avec la compréhension du domaine
Exemple :
Métier : "Un client passe une commande"
Technique (sans UL) : "L'utilisateur insère un enregistrement dans la table orders"
Ubiquitous : "client.passerCommande(panier)" — le code parle le même langage
11.2.2 Glossaire du domaine
| Terme métier | Définition | Nom dans le code |
|---|---|---|
| Client | Personne physique ou morale qui achète | Client |
| Commande | Ensemble d'articles achetés par un client | Commande |
| Panier | Commande en cours de constitution | Panier |
| Paiement | Transaction financière d'une commande | Paiement |
| Expédition | Envoi physique des articles | Expedition |
11.3 Bounded Context
11.3.1 Définition
Un Bounded Context est une limite explicite à l'intérieur de laquelle un modèle de domaine est cohérent. En dehors de cette limite, les termes peuvent avoir des significations différentes.
Diagramme en cours de génération...
11.3.2 Context Map
La Context Map montre les relations entre les bounded contexts :
Diagramme en cours de génération...
Relations entre Contextes :
| Relation | Description |
|---|---|
| Partnership | Deux contextes coopèrent pour atteindre un objectif commun |
| Shared Kernel | Partage d'un sous-ensemble du modèle |
| Customer-Supplier | Un contexte dépend de l'autre (upstream/downstream) |
| Conformist | Le downstream adopte le modèle de l'upstream sans changement |
| Anticorruption Layer (ACL) | Traduction entre deux modèles |
| Open Host Service (OHS) | API publique exposée |
| Published Language | Langage standard d'échange |
11.3.3 Exemple de Context Map avec ACL
Diagramme en cours de génération...
11.4 Entity vs Value Object
11.4.1 Entity
Une Entity est un objet qui possède une identité qui persiste dans le temps, indépendamment de ses attributs.
class Client:
def __init__(self, id: ClientId, nom: str, email: str):
self._id = id # Identité unique et immuable
self._nom = nom # Peut changer
self._email = email # Peut changer
@property
def id(self) -> ClientId:
return self._id
def changeEmail(self, nouveau_email: str):
self._email = nouveau_email
def __eq__(self, other):
if not isinstance(other, Client):
return False
return self._id == other._id
Caractéristiques :
- Identité unique (ID)
- Mutable (les attributs peuvent changer)
- L'égalité est basée sur l'ID
- Cycle de vie suivi
11.4.2 Value Object
Un Value Object est un objet défini uniquement par ses attributs, sans identité.
class Adresse:
def __init__(self, rue: str, ville: str, code_postal: str, pays: str):
self._rue = rue
self._ville = ville
self._code_postal = code_postal
self._pays = pays
def __eq__(self, other):
if not isinstance(other, Adresse):
return False
return (self._rue == other._rue and
self._ville == other._ville and
self._code_postal == other._code_postal and
self._pays == other._pays)
def __hash__(self):
return hash((self._rue, self._ville, self._code_postal, self._pays))
Caractéristiques :
- Pas d'identité
- Immuable (ne peut pas être modifié après création)
- L'égalité est basée sur les attributs
- Peut être partagé en toute sécurité
- Remplacé, pas modifié
11.4.3 Comparaison
| Critère | Entity | Value Object |
|---|---|---|
| Identité | Oui | Non |
| Mutabilité | Mutable | Immuable |
| Égalité | ID | Attributs |
| Cycle de vie | Suivi | Jetable |
| Exemple | Client, Commande | Adresse, Argent, Date |
Quand choisir ?
Diagramme en cours de génération...
11.5 Aggregate
11.5.1 Définition
Un Aggregate est un groupe d'objets (Entities et Value Objects) qui sont traités comme une unité cohérente pour les modifications de données.
class Commande(Aggregate):
def __init__(self, id: CommandeId, client_id: ClientId):
self._id = id
self._client_id = client_id
self._lignes: list[LigneCommande] = []
self._statut = StatutCommande.CREE
def ajouter_ligne(self, produit_id: ProduitId, quantite: int, prix: Argent):
if self._statut != StatutCommande.CREE:
raise CommandeException("Impossible d'ajouter une ligne à une commande confirmée")
self._lignes.append(LigneCommande(produit_id, quantite, prix))
def total(self) -> Argent:
return sum(ligne.sous_total() for ligne in self._lignes)
def confirmer(self):
if not self._lignes:
raise CommandeException("Une commande vide ne peut pas être confirmée")
self._statut = StatutCommande.CONFIRMEE
Règles des Aggregates :
- Root Aggregate : Un aggregate a une racine (l'Entity principale) — c'est le seul point d'accès
- Transaction Boundary : Les modifications à l'intérieur d'un aggregate sont atomiques
- Références : On référence les autres aggregates par leur ID, pas par référence objet
- Cohérence : L'aggregate garantit l'invariant à chaque modification
11.5.2 Exemple de structure Aggregate
Diagramme en cours de génération...
11.5.3 Tailles des Aggregates
Diagramme en cours de génération...
Règle d'or : Plus un aggregate est petit, meilleures sont la performance et la scalabilité.
11.6 Domain Events
11.6.1 Définition
Un Domain Event est un événement qui capture quelque chose de significatif qui s'est passé dans le domaine.
class CommandeConfirmée(Event):
def __init__(self, commande_id: CommandeId, date: datetime):
self.commande_id = commande_id
self.date = date
self.occured_on = datetime.now()
11.6.2 Cycle de vie d'un Domain Event
Diagramme en cours de génération...
11.6.3 Identité d'un événement
class DomainEvent:
def __init__(self):
self.event_id = str(uuid.uuid4())
self.occured_on = datetime.now()
self.version = 1 # Pour versioning des événements
11.6.4 Exemples de Domain Events
| Événement | Signification |
|---|---|
CommandeCréée | Une nouvelle commande a été créée |
PaiementEffectué | Le paiement a été validé |
StockInsuffisant | Stock trop bas pour une commande |
ClientBloqué | Client placé sur liste noire |
LivraisonExpédiée | Colis remis au transporteur |
11.7 Repository
11.7.1 Définition
Un Repository est un mécanisme de persistance qui isole le modèle de domaine de la couche infrastructure. Il agit comme un collection en mémoire pour les aggregates.
class CommandeRepository(ABC):
@abstractmethod
def save(self, commande: Commande):
pass
@abstractmethod
def get_by_id(self, id: CommandeId) -> Commande:
pass
@abstractmethod
def get_by_client(self, client_id: ClientId) -> list[Commande]:
pass
11.7.2 Implémentation
class PostgresCommandeRepository(CommandeRepository):
def __init__(self, connection):
self._conn = connection
def save(self, commande: Commande):
# Sauvegarder l'aggregate
with self._conn.transaction():
self._save_header(commande)
self._save_lines(commande)
self._save_events(commande) # Pour event sourcing
def get_by_id(self, id: CommandeId) -> Commande:
row = self._conn.execute("SELECT * FROM commandes WHERE id = ?", (id,))
if not row:
raise CommandeNotFound(id)
return self._hydrate(row)
11.8 Factory
La Factory encapsule la création d'objets complexes.
class CommandeFactory:
@staticmethod
def creer_commande(client: Client, adresse_livraison: Adresse) -> Commande:
commande = Commande(
id=CommandeId(generate_uuid()),
client_id=client.id,
adresse_livraison=adresse_livraison
)
# Enregistrer l'événement de création
commande.add_domain_event(CommandeCréée(commande.id))
return commande
@staticmethod
def creer_commande_express(client: Client, produits: list[Produit]) -> Commande:
commande = CommandeFactory.creer_commande(client, client.adresse_principale)
for produit in produits:
commande.ajouter_ligne(
produit_id=produit.id,
quantite=1,
prix=produit.prix
)
return commande
11.9 Architecture en couches
11.9.1 Couches DDD
Diagramme en cours de génération...
11.9.2 Responsabilités par couche
| Couche | Responsabilité | Dépendances |
|---|---|---|
| Application | Cas d'utilisation, transactions, sécurité | Domaine |
| Domain | Logique métier, règles, invariants | Aucune |
| Infrastructure | Persistance, messagerie, APIs | Domaine (ports) |
| Interface | UI, API REST, CLI | Application |
11.9.3 Principe de dépendance
Diagramme en cours de génération...
Règle : Les dépendances vont de l'extérieur vers l'intérieur. Le domaine ne dépend de rien.
11.10 Exemple complet : Commande
11.10.1 Diagramme de classes
Diagramme en cours de génération...
11.10.2 Cas d'utilisation : Confirmer commande
class ConfirmerCommandeUseCase:
def __init__(self, repo: CommandeRepository, event_bus: EventBus):
self._repo = repo
self._event_bus = event_bus
def execute(self, commande_id: CommandeId) -> Commande:
commande = self._repo.get_by_id(commande_id)
# Validation métier
if commande.statut != StatutCommande.CREE:
raise InvalidStatutException(
f"Impossible de confirmer une commande en statut {commande.statut}"
)
# Exécution
commande.confirmer()
# Persistance
self._repo.save(commande)
# Événements
for event in commande.domain_events:
self._event_bus.publish(event)
return commande
11.11 Event Storming introduction
L'Event Storming (Brandolini) est une technique de modélisation collaborative qui sera détaillée au chapitre 14. En DDD, elle sert à :
- Découvrir les Domain Events
- Identifier les Bounded Contexts
- Trouver les Aggregates
- Définir l'Ubiquitous Language
Diagramme en cours de génération...
Références
- Eric Evans — Domain-Driven Design: Tackling Complexity in the Heart of Software (2003)
- Vaughn Vernon — Implementing Domain-Driven Design (2013)
- Alberto Brandolini — Introducing EventStorming (2014)
- Martin Fowler — DDD community resources (martinfowler.com)
- DDD Crew — DDD Reference (dddreference.com)