MFormations
Modern Information Systems Engineering

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étierDéfinitionNom dans le code
ClientPersonne physique ou morale qui achèteClient
CommandeEnsemble d'articles achetés par un clientCommande
PanierCommande en cours de constitutionPanier
PaiementTransaction financière d'une commandePaiement
ExpéditionEnvoi physique des articlesExpedition

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 :

RelationDescription
PartnershipDeux contextes coopèrent pour atteindre un objectif commun
Shared KernelPartage d'un sous-ensemble du modèle
Customer-SupplierUn contexte dépend de l'autre (upstream/downstream)
ConformistLe 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 LanguageLangage 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èreEntityValue Object
IdentitéOuiNon
MutabilitéMutableImmuable
ÉgalitéIDAttributs
Cycle de vieSuiviJetable
ExempleClient, CommandeAdresse, 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 :

  1. Root Aggregate : Un aggregate a une racine (l'Entity principale) — c'est le seul point d'accès
  2. Transaction Boundary : Les modifications à l'intérieur d'un aggregate sont atomiques
  3. Références : On référence les autres aggregates par leur ID, pas par référence objet
  4. 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énementSignification
CommandeCrééeUne nouvelle commande a été créée
PaiementEffectuéLe paiement a été validé
StockInsuffisantStock trop bas pour une commande
ClientBloquéClient placé sur liste noire
LivraisonExpédiéeColis 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

CoucheResponsabilitéDépendances
ApplicationCas d'utilisation, transactions, sécuritéDomaine
DomainLogique métier, règles, invariantsAucune
InfrastructurePersistance, messagerie, APIsDomaine (ports)
InterfaceUI, API REST, CLIApplication

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 EvansDomain-Driven Design: Tackling Complexity in the Heart of Software (2003)
  • Vaughn VernonImplementing Domain-Driven Design (2013)
  • Alberto BrandoliniIntroducing EventStorming (2014)
  • Martin FowlerDDD community resources (martinfowler.com)
  • DDD CrewDDD Reference (dddreference.com)