La notion d'agrégat en DDD

La notion d'agrégat est un principe essentiel de la conception pilotée par le domaine (Domain Driven Design). L'agrégat désigne un groupe d'objets du domaine qui doit être manipulé comme une seule unité métier et dont les relations enfants ne peuvent être utilisées de manière indépendante.

Prenons par exemple le cas d'une facture. Une facture est composée de différentes propriétés: un émetteur, un destinataire, une date, un identifiant ainsi qu'une liste de produits ou prestations. Dans une modélisation orientée DDD, la facture est un agrégat car les différents éléments qui la composent n'ont pas de raison d'exister unitairement.

De ce fait, toute modification de notre facture doit se dérouler au sein de notre agrégat. Il ne devrait par exemple, pas être possible d'ajouter une nouvelle ligne sans passer pas notre objet de base Facture. De la même façon, il ne devrait pas être possible de récupérer des éléments de notre facture sans passer cette dernière. Cela implique que lorsque l'on récupère ou enregistre un agrégat, toutes les données de ce dernier sont traités en un seul bloc.

Ci-dessous un exemple de comment nous pourrions modéliser l'ajout de ligne à notre Facture:

class Facture
{
    /** @var FactureLigne[] */
    private array $lignes;

    public function lignes(): array
    {
        return $this->lignes;
    }

    public function ajouterLigne(FactureLigne $ligne): self
    {
        // règles de validations
        // ...

        $this->lignes[] = $ligne;

        return $this;
    }
}

// utilisation dans le code applicatif
$facture = new Facture(/* ... */);
foreach ($produits as $produit) {
    $facture->ajouterLigne(/* ... */);
}

$entityManager->save($facture);

L'un des principaux avantages de ce mode de fonctionnement est que l'on garantie ainsi la cohérence de notre objet tout au long de son utilisation. Il est seul responsable des règles de validation, qui sont alors centralisées au sein de notre agrégat et assure ainsi la cohérence des données.

Si cette modélisation peut paraître logique pour certains, on trouve de nombreuses applications CRUD qui ne modélisent pas les données de cette manière. La facture et les lignes associées étant parfois gérées indépendamment.

class Facture
{ /* ... */ }

class FactureLigne
{ /* ... */ }


// utilisation dans le code applicatif
$facture = new Facture(/* ... */);
$entityManager->save($facture);

foreach ($produits as $produit) {
    $ligne = new FactureLigne(/* ... */);
    $entityManager->save($ligne);
}

Dans l'exemple précédent, les lignes de facture étant ajoutées en dehors du contexte de notre facture, rien n'assure que les données des lignes soient cohérentes avec l'état de notre facture. Par exemple, si la facture est payée, il ne devrait pas être possible de pouvoir la modifier. Or si les actions de modification se déroulent en dehors de notre agrégat, il sera difficile de garantir ces règles métiers (cela ne sera pas impossible, mais les règles de validation risque de se retrouver éparpiller dans le code au lieu d'être centraliser dans l'objet qui a la connaissance de son contexte).

Dans son article sur le sujet, Martin Fowler ajoute:

Les agrégats sont parfois confondus avec les classes de collection (telles que les listes). Les agrégats sont des concepts du domaine, tandis que les collections sont génériques. Un agrégat contient souvent des collections et des champs simples. Le terme "agrégat" est commun et est utilisé dans différents contextes, ils ne font pas forcément référence au même concept que les agrégats dans un contexte de conception pilotée par le métier.

Les agrégats permettent de garantir la consistance des objets de votre application. Que vous développiez une application DDD ou CRUD, je ne peux que vous conseillez d'utiliser ce pattern.