web-dev-qa-db-fra.com

Trouver la racine agrégée DDD

Jouons au jeu préféré de tout le monde, trouvons la racine Aggregrate. Utilisons le domaine canonique Customer/Order/OrderLines/Product problem. Traditionnellement, le client, la commande et le produit sont les AR, OrderLines étant des entités sous la commande. La logique derrière cela est que vous devez identifier les clients, les commandes et les produits, mais une ligne de commande n'existerait pas sans une commande. Ainsi, dans notre domaine problématique, nous avons une règle commerciale disant qu'un client ne peut avoir qu'une seule commande non livrée à la fois.

Est-ce que cela déplace la commande sous la racine globale du client? Je pense que oui. Mais ce faisant, l'AR client est plutôt volumineux et sujet à des problèmes de concurrence plus tard.

Ou, si nous avions une règle commerciale stipulant qu'un client ne peut commander un produit particulier qu'une seule fois dans sa vie. Il s'agit d'une preuve supplémentaire exigeant que le client soit propriétaire de la commande.

Mais quand il s'agit de l'expédition, ils font toutes leurs actions sur la Commande, pas le client. C'est un peu stupide de devoir charger tout le client pour marquer une commande individuelle comme livrée.

Voici ce que je propose:

class Customer
{
    public Guid Id {get;set;}
    public string Name { get; set; }
    public Address Address { get; set; }
    public IEnumerable<Order> Orders { get; set; }
    public void PlaceOrder(ThingsInTheOrder thingsInTheOrder)
    {
        // Make sure there aren't any pending orders already.
        // Make sure they aren't ordering a Widget if they've already ordered a Widget in the past.
        // Create an Order object and add it to the collection.  Raise a domain event to trigger emails and other stuff
    }
}

class Order
{
    public Guid Id { get; set; }
    public IEnumerable<OrderLine> OrderLines { get; set; }
    public ShippingData {get;set;}
    public void Ship(ShippedByPerson shippedByPerson, string trackingCode)
    {
         // Create a new ShippingData object and assign it from the data passed in.  
         // Publish a domain event
    }
}

Ma plus grande préoccupation est le problème de concurrence et le fait que l'Ordre lui-même possède les caractéristiques d'une racine agrégée.

10
Darthg8r

Version courte

La justification de DDD est que les objets de domaine sont des abstractions qui devraient répondre à vos exigences de domaine fonctionnel - si les objets de domaine ne sont pas en mesure de répondre facilement à ces exigences, cela suggère que vous utilisez peut-être la mauvaise abstraction.

Nommer les objets de domaine en utilisant les noms d'entité peut conduire à ce que ces objets deviennent étroitement- couplés les uns aux autres et devenant des objets "dieu" gonflés, et ils peuvent générer des problèmes tels que celui de cette question, comme "Où est le bon endroit pour mettre la méthode CreateOrder?".

Pour faciliter l'identification de la "bonne" racine d'agrégat, envisagez une approche différente où les objets de domaine sont basés sur les exigences fonctionnelles de haut niveau de l'entreprise - c'est-à-dire choisissez des noms qui font référence aux exigences fonctionnelles et/ou aux comportements que les utilisateurs du système doivent effectuer.


Version longue

DDD est une approche de OO Design qui est destinée à générer un graphique des objets de domaine dans le Couche métier de votre système - Les objets de domaine sont responsables de la satisfaction de vos exigences commerciales de haut niveau et devraient idéalement pouvoir s'appuyer sur la couche données pour des choses comme les performances et l'intégrité du magasin de données persistantes sous-jacent.

Une autre façon de voir les choses pourrait être les puces de cette liste

  • Les noms d'entité suggèrent généralement des attributs de données.
  • Les noms de domaine doivent suggérer un comportement
  • DDD et OO La modélisation concerne les abstractions basées sur les exigences fonctionnelles et le domaine principal/la logique métier.
  • La couche logique applicative est chargée de satisfaire aux exigences de domaine de haut niveau

L'une des idées fausses les plus courantes concernant DDD est que les objets de domaine doivent être basés sur une "chose" physique réelle (c'est-à-dire un nom sur lequel vous pouvez pointer dans le monde réel, attribué à toutes sortes de données/propriétés), cependant les données/Les attributs de ces choses du monde réel ne constituent pas nécessairement un bon point de départ pour essayer de définir les exigences fonctionnelles.

Bien sûr, Business Logic devrait utiliser ces données, mais les objets de domaine eux-mêmes devraient finalement être des abstractions qui représentent les exigences et les comportements fonctionnels du domaine.

Par exemple; les noms tels que Order ou Customer n'impliquent aucun comportement et sont donc généralement des abstractions inutiles pour représenter la logique métier et les objets de domaine.

Lorsque vous recherchez les types d'abstractions qui pourraient être utiles pour représenter la logique applicative, tenez compte des exigences typiques auxquelles vous pouvez vous attendre d'un système:

  • En tant que vendeur, je souhaite créer une commande pour un nouveau client afin de pouvoir générer une facture pour les produits à vendre avec leurs prix et leur quantité.
  • En tant que conseiller du service client, je souhaite annuler une commande en attente afin que la commande ne soit pas exécutée par un exploitant d'entrepôt.
  • En tant que conseiller du service client, je souhaite retourner une ligne de commande afin que le produit puisse être ajusté dans l'inventaire et que le paiement soit remboursé via la méthode de paiement d'origine du client.
  • En tant qu'opérateur d'entrepôt, je souhaite afficher tous les produits d'une commande en attente et les informations d'expédition afin de pouvoir choisir les produits et les expédier via le service de messagerie.
  • etc.

Modélisation des exigences de domaine avec une approche DDD

Sur la base de la liste ci-dessus, considérez certains objets de domaine potentiels pour un tel système de commandes:

SalesOrderCheckout
PendingOrdersStream
WarehouseOrderDespatcher
OrderRefundProcessor 

En tant qu'objets de domaine, ceux-ci représentent des abstractions qui s'approprient diverses exigences comportementales du domaine; en effet, leurs noms font allusion fortement aux exigences fonctionnelles spécifiques qu'ils remplissent.

(Il peut également y avoir une infrastructure supplémentaire comme un EventMediator pour transmettre des notifications aux observateurs qui veulent savoir quand une nouvelle commande a été créée, ou quand une commande a été expédiée, etc.).

Par exemple, SalesOrderCheckout a probablement besoin de gérer les données sur les clients, l'expédition et les produits, mais n'a rien à voir avec le comportement pour l'expédition des commandes, le tri des commandes en attente ou l'émission de remboursements.

Pour que SalesOrderCheckout satisfasse aux exigences de son domaine, il faut notamment appliquer ces règles commerciales, comme empêcher les clients de commander trop d'articles, éventuellement exécuter une certaine validation et peut-être déclencher des notifications pour d'autres parties du système - il peut faire tout cela sans ayant nécessairement besoin de dépendre de l'un des autres objets.

DDD utilisant des noms d'entité pour représenter des objets de domaine

Il existe un certain nombre de dangers potentiels lors du traitement de noms simples tels que Order, Customer et Product en tant qu'objets de domaine; parmi ces problèmes figurent ceux auxquels vous faites allusion dans la question:

  • Si une méthode gère un Order, un Customer et un Product, à quel objet de domaine appartient-il?
  • Où est la racine agrégée pour ces 3 objets?

Si vous choisissez les noms d'entité pour représenter les objets de domaine, un certain nombre de choses peuvent se produire:

  • Order, Customer et Product risquent de devenir des objets "divins"
  • Risque de se retrouver avec un seul objet divin Manager pour tout lier.
  • Ces objets risquent de devenir étroitement liés les uns aux autres - il peut être difficile de répondre aux exigences du domaine sans passer par this (ou self)
  • Un risque de développer des abstractions "qui fuient" - c'est-à-dire que les objets de domaine devraient exposer des dizaines de méthodes get/set qui affaiblissent l'encapsulation (ou, si vous ne le faites pas, alors un autre programmeur le fera probablement plus tard..).
  • Risque de gonflement des objets de domaine avec un mélange complexe de données commerciales (par exemple, saisie de données utilisateur via une interface utilisateur) et d'état transitoire (par exemple, un `` historique '' des actions de l'utilisateur lorsque la commande a été modifiée).

DDD, OO Design et modèles simples

Une idée fausse commune concernant DDD et OO Design est que les modèles "simples" sont en quelque sorte "mauvais" ou un "anti-modèle". Martin Fowler a écrit un article décrivant le modèle de domaine anémique - mais comme il l'indique clairement dans l'article, DDD lui-même ne doit pas "contredire" l'approche d'une séparation nette entre les couches

"Il convient également de souligner que le fait de mettre le comportement dans les objets du domaine ne doit pas contredire l'approche solide consistant à utiliser la superposition pour séparer la logique du domaine de choses telles que la persistance et les responsabilités de présentation. La logique qui devrait se trouver dans un l'objet de domaine est une logique de domaine - validations, calculs, règles métier - peu importe comment vous l'appelez. "

En d'autres termes, l'utilisation de modèles simples pour conserver les données commerciales transférées entre d'autres couches (par exemple, un modèle de commande transmis par une application utilisateur lorsque l'utilisateur souhaite créer une nouvelle commande) n'est pas la même chose qu'un "modèle de domaine anémique". Les modèles de données `` simples '' sont souvent le meilleur moyen de suivre les données et de transférer des données entre les couches (comme un service Web REST, un magasin de persistance, une application ou une interface utilisateur, etc.).

La logique métier peut traiter les données de ces modèles et les suivre dans le cadre de l'état de l'entreprise - mais ne s'appropriera pas nécessairement ces modèles.

La racine agrégée

En regardant de nouveau les exemples d'objets de domaine - SalesOrderCheckout, PendingOrdersStream, WarehouseOrderDespatcher, OrderRefundProcessor il n'y a toujours pas de racine d'agrégat évidente; mais cela n'a pas vraiment d'importance parce que ces objets de domaine ont des responsabilités très distinctes qui ne semblent pas se chevaucher.

Fonctionnellement, il n'est pas nécessaire que le SalesOrderCheckout parle au PendingOrdersStream car le travail du premier est terminé lorsqu'il a ajouté un nouvel ordre à la base de données; d'autre part, le PendingOrdersStream peut récupérer de nouvelles commandes à partir de la base de données. Ces objets n'ont pas besoin d'interagir directement entre eux (peut-être qu'un médiateur d'événements pourrait fournir des notifications entre les deux, mais je m'attendrais à ce que tout couplage entre ces objets soit très lâche)

Peut-être que la racine agrégée sera un conteneur IoC qui injecte un ou plusieurs de ces objets de domaine dans un contrôleur d'interface utilisateur, fournissant également d'autres infrastructures comme EventMediator et Repository. Ou peut-être que ce sera une sorte de service d'orchestrateur léger assis au-dessus de la couche métier.

La racine agrégée n'a pas nécessairement un objet de domaine. Dans un souci de garder la séparation des préoccupations entre les objets de domaine, c'est généralement une bonne chose lorsque la racine agrégée est un objet séparé sans logique métier.

12
Ben Cottrell

Quels sont les critères de définition d'un agrégat?

Revenons aux bases du grand livre bleu:

Agrégat: Un cluster d'objets associés qui sont traités comme une unité dans le but de modifier les données =. Les références externes sont limitées à un membre de l'AGGREGATE, désigné comme racine. Un ensemble de règles de cohérence s’applique à l’intérieur des limites de l’AGRÉGAT.

Le but est de maintenir les invariants. Mais c'est aussi gérer correctement l'identité locale, c'est-à-dire identifier des objets qui n'ont pas de sens à eux seuls.

Order et Order line appartiennent définitivement à un tel cluster. Par exemple:

  • Supprimer un Order, nécessitera la suppression de toutes ses lignes.
  • La suppression d'une ligne peut nécessiter une renumérotation des lignes suivantes
  • L'ajout d'une nouvelle ligne nécessiterait de déterminer la ligne nulle sur la base de toutes les autres lignes du même ordre.
  • La modification de certaines informations de commande, comme par exemple la devise, peut affecter la signification du prix dans les éléments de ligne (ou nécessiter un recalcul des prix).

Donc, ici, l'agrégat complet est nécessaire pour garantir les règles de cohérence et les invariants.

Quand arrêter?

Maintenant, vous décrivez certaines règles métier et soutenez que pour les garantir, vous devez considérer le client comme faisant partie de l'agrégat:

Nous avons une règle commerciale stipulant qu'un client ne peut recevoir qu'une seule commande non livrée à la fois.

Bien sûr pourquoi pas. Voyons les implications: la commande serait toujours accessible via le client. Est-ce que c'est la réalité ? Lorsque les travailleurs remplissent les cases pour livrer la commande, devront-ils lire le code-barres du client et le code-barres de la commande pour accéder à la commande? En effet, en général, l'identité d'une Commande est globale et non locale pour un client, et cette relative indépendance suggère de le garder en dehors de l'agrégat.

De plus, ces règles commerciales ressemblent davantage à des politiques: c'est une décision arbitraire de la société d'exécuter son processus avec ces règles. Si les règles ne sont pas respectées, le patron peut être mécontent, mais les données ne sont pas vraiment incohérentes. De plus, du jour au lendemain "par client, une commande non livrée à la fois" pourrait devenir "dix commandes non livrées par client" ou même "indépendamment du client, cent commandes non livrées par entrepôt", de sorte que le total pourrait ne plus être justifié.

10
Christophe

Vous avez mis en place un exemple de personne de paille. C'est trop simpliste et je doute qu'il reflète un système du monde réel. Je ne modéliserais pas ces entités et leur comportement associé de la manière que vous avez spécifiée à cause de cela.

Vos classes doivent modéliser l'état d'une commande d'une manière qui se reflète dans plusieurs agrégats. Par exemple, lorsque le client met le système dans l'état où sa demande de commande doit être traitée, je peux créer un agrégat d'objet d'entité de domaine appelé CustomerOrderRequest ou PendingCustomerOrder ou même simplement CustomerOrder, ou quel que soit le langage utilisé par l'entreprise, et il pourrait contenir un pointeur vers le client et les lignes de commande, puis avoir une méthode comme canCustomerCompleteOrder() qui est appelée à partir de la couche de service.

Cet objet de domaine contiendrait la logique métier pour déterminer si la commande était valide ou non.

Si la commande était valide et traitée, j'aurais un moyen de faire passer cet objet à un autre objet qui représentait la commande traitée.

Je pense que le problème avec votre compréhension est que vous utilisez un exemple d'agrégats trop simplifié. Un PendingOrder peut être son propre agrégat séparé d'un UndeliveredOrder et à nouveau séparé d'un DeliveredOrder ou d'un CancelledOrder ou autre.

1
RibaldEddie

dans notre domaine problématique, nous avons une règle commerciale disant qu'un client ne peut avoir qu'une seule commande non livrée à la fois.

Avant d'aller trop loin dans ce terrier de lapin, vous devriez revoir la discussion de Greg Young sur définir la cohérence , et en particulier:

Quel est l'impact commercial d'une défaillance?

Parce que dans de nombreux cas, la bonne réponse n'est pas d'essayer d'empêcher la mauvaise chose de se produire, mais plutôt de générer des rapports d'exception en cas de problème.

Mais, en supposant que plusieurs commandes non livrées constituent un passif important pour votre entreprise ...

Oui, si vous voulez vous assurer qu'il n'y a qu'une seule commande non livrée, il doit y avoir un agrégat capable de voir toutes les commandes d'un client.

Cet agrégat n'est pas nécessairement l'agrégat client .

Il peut s'agir d'une file d'attente de commandes ou d'un historique de commandes, où toutes les commandes d'un client spécifique vont dans la même file d'attente. D'après ce que vous avez dit, il n'a pas besoin de toutes les données de profil du client, donc cela ne devrait pas faire partie de cet agrégat.

Mais quand il s'agit de l'expédition, ils font toutes leurs actions sur la Commande, pas le client.

Oui, lorsque vous travaillez réellement avec des feuilles de traitement et de tirage, la vue d'historique n'est pas particulièrement pertinente.

La vue historique, pour appliquer votre invariant, n'a besoin que de l'ID de commande et de son état de traitement actuel. Cela ne doit pas nécessairement faire partie du même agrégat que l'ordre - rappelez-vous, les limites des agrégats concernent la gestion du changement, pas la structuration des vues.

Il se peut donc que vous traitiez la commande comme un agrégat et l'historique des commandes comme un agrégat distinct et que vous coordonniez l'activité entre les deux.

1
VoiceOfUnreason

Vaughn Vernon le mentionne dans son livre "Implementation of Domain-Driven Design" au début du chapitre 7 (Services):

"Souvent, la meilleure indication que vous devez créer un service dans le modèle de domaine est lorsque l'opération que vous devez effectuer semble déplacée en tant que méthode sur un agrégat ou un objet de valeur".

Ainsi, dans ce cas, il pourrait y avoir un service de domaine appelé "CreateOrderService" qui prend une instance du client et la liste des articles pour la commande.

class CreateOrderService
{
    public Order CreateOrder(Customer customer, ThingsInTheOrder thingsInTheOrder)
    {
        // Get all the orders for the customer
        // Check if any of the things to be ordered exist in previous orders   
        // If none have been previously ordered, create the order and return it
        // Otherwise return null 
    }
}
1
claudius