Services d'injection DDD sur les appels de méthodes d'entité

11

Format court de la question

Est-il conforme aux meilleures pratiques de DDD et OOP d'injecter des services sur les appels de méthode d'entité?

Exemple de format long

Disons que nous avons le cas classique Order-LineItems dans DDD, où nous avons une entité de domaine appelée une commande, qui agit également en tant que racine agrégée, et cette entité est composée non seulement de ses objets de valeur, mais également d'une collection d'éléments de ligne Entités.

Supposons que nous voulons une syntaxe fluide dans notre application, afin de pouvoir faire quelque chose comme ça (en notant la syntaxe à la ligne 2, où nous appelons la getLineItemsméthode):

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

Nous ne voulons injecter aucune sorte de LineItemRepository dans OrderEntity, car c'est une violation de plusieurs principes auxquels je peux penser. Mais, la maîtrise de la syntaxe est quelque chose que nous voulons vraiment, car il est facile à lire et à maintenir, ainsi qu'à tester.

Considérez le code suivant, en notant la méthode getLineItemsdans OrderEntity:

interface IOrderService {
    public function getOrderByID($orderID) : OrderEntity;
    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection;
}

class OrderService implements IOrderService {
    private $orderRepository;
    private $lineItemRepository;

    public function __construct(IOrderRepository $orderRepository, ILineItemRepository $lineItemRepository) {
        $this->orderRepository = $orderRepository;
        $this->lineItemRepository = $lineItemRepository;
    }

    public function getOrderByID($orderID) : OrderEntity {
        return $this->orderRepository->getByID($orderID);
    }

    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection {
        return $this->lineItemRepository->getLineItemsByOrderID($orderEntity->ID());
    }
}

class OrderEntity {
    private $ID;
    private $lineItems;

    public function getLineItems(IOrderServiceInternal $orderService) {
        if(!is_null($this->lineItems)) {
            $this->lineItems = $orderService->getLineItems($this);
        }
        return $this->lineItems;
    }
}

Est-ce la façon acceptée de mettre en œuvre une syntaxe fluide dans les entités sans violer les principes fondamentaux de DDD et OOP? Pour moi, cela semble bien, car nous exposons uniquement la couche service, pas la couche infrastructure (qui est imbriquée dans le service)

e_i_pi
la source

Réponses:

9

Il est tout à fait correct de passer un service de domaine dans un appel d'entité. Disons, nous devons calculer une somme de facture avec un algorithme compliqué qui peut dépendre, par exemple, d'un type de client. Voici à quoi cela pourrait ressembler:

class Invoice
{
    private $currency;
    private $customerId;

    public function __construct()
    {
    }

    public function sum(InvoiceCalculator $calculator)
    {
        $sum =
            new SumRecord(
                $calculator->calculate($this)
            )
        ;

        if ($sum->isZero()) {
            $this->events->add(new ZeroSumCalculated());
        }

        return $sum;
    }
}

Une autre approche consiste à séparer une logique métier située dans le service de domaine via des événements de domaine . Gardez à l'esprit que cette approche implique uniquement des services d'application différents, mais la même portée de transaction de base de données.

La troisième approche est celle que je suis en faveur: si je me retrouve à utiliser un service de domaine, cela signifie probablement que j'ai manqué un concept de domaine, car je modélise mes concepts principalement avec des noms , pas des verbes. Donc, idéalement, je n'ai pas du tout besoin d'un service de domaine et une bonne partie de toute ma logique métier réside dans les décorateurs .

Vadim Samokhin
la source
6

Je suis choqué de lire certaines des réponses ici.

Il est parfaitement valide de passer des services de domaine dans des méthodes d'entité dans DDD pour déléguer certains calculs commerciaux. Par exemple, imaginez que votre racine agrégée (une entité) a besoin d'accéder à une ressource externe via http afin de faire une logique métier et de déclencher un événement. Si vous n'injectez pas le service par la méthode commerciale de l'entité, comment le feriez-vous autrement? Souhaitez-vous instancier un client http au sein de votre entité? Cela ressemble à une idée terrible.

Ce qui est incorrect, c'est d'injecter des services dans des agrégats via son constructeur. Mais grâce à une méthode commerciale, c'est ok et parfaitement normal.

diegosasw
la source
1
Pourquoi le cas que vous avez donné ne serait-il pas la responsabilité d'un service de domaine?
e_i_pi
1
c'est un service de domaine, mais il est injecté dans la méthode métier. La couche d'application n'est qu'un orchestrateur,
diegosasw
Je ne suis pas expérimenté dans DDD, mais je ne devrais pas appeler le service de domaine à partir du service d'application et après la validation du service de domaine, continuer à appeler les méthodes d'entité via ce service d'application? Je suis confronté au même problème dans mon projet, car le service de domaine exécute l'appel de base de données via le référentiel ... Je ne sais pas si cela va.
Muflix
Le service de domaine doit orchestrer, si vous l'appelez à partir de l'application plus tard, cela signifie que vous traitez en quelque sorte la réponse, puis vous faites quelque chose avec. Cela ressemble peut-être à la logique métier. Si tel est le cas, il appartient à la couche Domaine et l'application résout plus tard simplement la dépendance et l'injecte dans l'agrégat. Le service de domaine aurait pu injecter un référentiel dont la base de données de mise en œuvre devrait appartenir à la couche infrastructure (juste la mise en œuvre, pas l'interface / le contrat). S'il décrit votre langue omniprésente, il appartient au domaine.
diegosasw
5

Est-il conforme aux meilleures pratiques de DDD et OOP d'injecter des services sur les appels de méthode d'entité?

Non, vous ne devez rien injecter dans votre couche de domaine (cela inclut les entités, les objets de valeur, les usines et les services de domaine). Cette couche doit être indépendante de tout framework, bibliothèque ou technologie tierce et ne doit effectuer aucun appel d'E / S.

$order->getLineItems($orderService)

C'est faux car l'agrégat ne devrait avoir besoin que d'autre chose que lui-même pour retourner les articles de la commande. L' ensemble de l' agrégat doit être déjà chargé avant son appel de méthode. Si vous pensez que cela devrait être paresseux, deux possibilités s'offrent à vous:

  1. Vos limites d'agrégats sont erronées, elles sont trop grandes.

  2. Dans ce cas d'utilisation, vous utilisez l'agrégat uniquement pour la lecture. La meilleure solution consiste à séparer le modèle d'écriture du modèle de lecture (c'est-à-dire utiliser CQRS ). Dans cette architecture plus propre , vous n'êtes pas autorisé à interroger l'agrégat mais un modèle de lecture.

Constantin Galbenu
la source
Si j'ai besoin d'un appel à la base de données pour la validation, je dois l'appeler dans le service d'application et transmettre un résultat au service de domaine ou directement dans la racine agrégée plutôt qu'injecter le référentiel dans le service de domaine?
Muflix
1
@Muflix oui, c'est vrai
Constantin Galbenu
3

L'idée clé des schémas tactiques DDD: l'application accède à toutes les données de l'application en agissant sur une racine agrégée. Cela implique que les seules entités accessibles en dehors du modèle de domaine sont les racines agrégées.

La racine agrégée Order ne fournirait jamais une référence à sa collection d'élément de ligne qui vous permettrait de modifier la collection, pas plus qu'elle ne générerait une collection de références à un élément de campagne qui vous permettrait de la modifier. Si vous souhaitez modifier l'agrégat Order, le principe hollywoodien s'applique: "Dites, ne demandez pas".

Renvoyer des valeurs à partir de l'agrégat est très bien, car les valeurs sont intrinsèquement immuables; vous ne pouvez pas modifier mes données en changeant votre copie.

L'utilisation d'un service de domaine comme argument pour aider l'agrégat à fournir les valeurs correctes est une chose parfaitement raisonnable à faire.

Normalement, vous n'utiliseriez pas un service de domaine pour fournir un accès aux données qui se trouvent à l'intérieur de l'agrégat, car l'agrégat devrait déjà y avoir accès.

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

L'orthographe est donc bizarre si nous essayons d'accéder à la collection de valeurs de l'élément de campagne de cette campagne. L'orthographe la plus naturelle serait

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

Bien sûr, cela présuppose que les éléments de campagne ont déjà été chargés.

Le schéma habituel est que la charge de l'agrégat comprendra tout l'état requis pour le cas d'utilisation particulier. En d' autres termes, vous pouvez avoir plusieurs différents façons de charger le même agrégat; vos méthodes de référentiel sont adaptées à l'usage .

Cette approche n'est pas quelque chose que vous trouverez dans l'Evans original, où il supposait qu'un agrégat aurait un seul modèle de données associé. Il sort plus naturellement du CQRS.

VoiceOfUnreason
la source
Merci pour cela. J'ai maintenant lu environ la moitié du "livre rouge", et j'ai eu mon premier goût d'appliquer correctement le principe d'Hollywood dans la couche d'infrastructure. En relisant toutes ces réponses, elles font toutes de bons points, mais je pense que la vôtre a des points très importants concernant la portée lineItems()et le préchargement lors de la première récupération de la racine agrégée.
e_i_pi
3

De manière générale, les objets de valeur appartenant à l'agrégat n'ont pas de référentiel par eux-mêmes. C'est la responsabilité globale de la racine de les remplir. Dans votre cas, il est de la responsabilité de votre OrderRepository de remplir à la fois les objets d'entité Order et de valeur OrderLine.

En ce qui concerne l'implémentation de l'infrastructure du OrderRepository, dans le cas ORM, c'est une relation un-à-plusieurs, et vous pouvez choisir de charger la OrderLine avec impatience ou paresseux.

Je ne sais pas exactement ce que signifient vos services. C'est assez proche de "Application Service". Si tel est le cas, ce n'est généralement pas une bonne idée d'injecter les services dans la racine agrégée / l'entité / l'objet valeur. Le service d'application doit être le client du service racine / entité / objet de valeur agrégée et du service de domaine. Une autre chose à propos de vos services est que l'exposition d'objets de valeur dans Application Service n'est pas non plus une bonne idée. Ils doivent être accessibles par racine agrégée.

ivenxu
la source
2

La réponse est: certainement NON, évitez de passer des services dans les méthodes d'entité.

La solution est simple: laissez simplement le référentiel de commandes renvoyer la commande avec tous ses éléments de ligne. Dans votre cas, l'agrégat est Order + LineItems, donc si le référentiel ne renvoie pas un agrégat complet, il ne fait pas son travail.

Le principe plus large est le suivant: garder les bits fonctionnels (par exemple, la logique de domaine) séparés des bits non fonctionnels (par exemple, la persistance).

Encore une chose: si vous le pouvez, essayez d'éviter de faire cela:

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

Faites cela à la place

$order = $orderService->getOrderByID($orderID);
$order->doSomethingSignificant();

Dans la conception orientée objet, nous essayons d'éviter de pêcher dans les données d'un objet. Nous préférons demander à l'objet de faire ce que nous voulons.

xpmatteo
la source