DDD Injecting Services für Aufrufe von Entitätsmethoden

11

Kurzes Fragenformat

Gehört es zu den Best Practices von DDD und OOP, Dienste in Entitätsmethodenaufrufe einzufügen?

Langformat-Beispiel

Angenommen, wir haben den klassischen Order-LineItems-Fall in DDD, in dem wir eine Domänenentität namens Order haben, die auch als Aggregatstamm fungiert, und diese Entität besteht nicht nur aus ihren Wertobjekten, sondern auch aus einer Sammlung von Werbebuchungen Entitäten.

Angenommen, wir möchten eine fließende Syntax in unserer Anwendung, damit wir so etwas tun können (beachten Sie die Syntax in Zeile 2, in der wir die getLineItemsMethode aufrufen ):

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

Wir möchten keine LineItemRepository in die OrderEntity einfügen, da dies eine Verletzung mehrerer Prinzipien darstellt, die mir einfallen. Aber die fließende Syntax ist etwas, das wir wirklich wollen, weil es einfach zu lesen und zu warten sowie zu testen ist.

Betrachten Sie den folgenden Code und beachten Sie die Methode getLineItemsin 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;
    }
}

Ist dies die akzeptierte Methode zur Implementierung einer fließenden Syntax in Entities, ohne die Kernprinzipien von DDD und OOP zu verletzen? Für mich scheint es in Ordnung zu sein, da wir nur die Service-Schicht verfügbar machen, nicht die Infrastruktur-Schicht (die im Service verschachtelt ist).

e_i_pi
quelle

Antworten:

9

Es ist völlig in Ordnung , einen Domänendienst in einem Entitätsaufruf zu übergeben. Angenommen, wir müssen eine Rechnungssumme mit einem komplizierten Algorithmus berechnen, der beispielsweise von einem Kundentyp abhängen kann. So könnte es aussehen:

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;
    }
}

Ein anderer Ansatz besteht jedoch darin, eine Geschäftslogik, die sich im Domänendienst befindet, über Domänenereignisse zu trennen . Beachten Sie, dass dieser Ansatz nur unterschiedliche Anwendungsdienste, aber denselben Datenbanktransaktionsbereich impliziert.

Der dritte Ansatz ist der, für den ich bin: Wenn ich einen Domain-Service benutze, bedeutet dies wahrscheinlich, dass ich ein Domain-Konzept verpasst habe, da ich meine Konzepte hauptsächlich mit Substantiven und nicht mit Verben modelliere . Im Idealfall brauche ich also überhaupt keinen Domain-Service und ein großer Teil meiner Geschäftslogik liegt in Dekorateuren .

Zapadlo
quelle
6

Ich bin schockiert, einige der Antworten hier zu lesen.

Es ist absolut gültig, Domänendienste in Entitätsmethoden in DDD zu übergeben, um einige Geschäftsberechnungen zu delegieren. Stellen Sie sich beispielsweise vor, Ihr aggregierter Stamm (eine Entität) muss über http auf eine externe Ressource zugreifen, um Geschäftslogik zu erstellen und ein Ereignis auszulösen. Wie würden Sie es sonst tun, wenn Sie den Service nicht über die Geschäftsmethode des Unternehmens einspeisen? Würden Sie einen http-Client in Ihrer Entität instanziieren? Das klingt nach einer schrecklichen Idee.

Was falsch ist, ist das Einfügen von Diensten in Aggregate über den Konstruktor. Aber durch eine Geschäftsmethode ist es in Ordnung und völlig normal.

diegosasw
quelle
1
Warum liegt der von Ihnen angegebene Fall nicht in der Verantwortung eines Domain-Dienstes?
e_i_pi
1
Es handelt sich um einen Domänendienst, der jedoch in die Geschäftsmethode integriert ist. Die Anwendungsschicht ist nur ein Orchestrator,
diegosasw
Ich habe keine Erfahrung mit DDD, sollte aber nicht Domain Application vom Application Service aufgerufen werden und nach der Validierung des Domain Service weiterhin Entity-Methoden über diesen Application Service aufrufen? Ich habe das gleiche Problem in meinem Projekt, weil der Domänendienst einen Datenbankaufruf über das Repository ausführt ... Ich weiß nicht, ob dies in Ordnung ist.
Muflix
Der Domänendienst sollte orchestrieren. Wenn Sie ihn später aus der Anwendung aufrufen, bedeutet dies, dass Sie die Antwort irgendwie verarbeiten und dann etwas damit tun. Vielleicht klingt das nach Geschäftslogik. Wenn ja, gehört es in die Domänenschicht, und die Anwendung löst später einfach die Abhängigkeit auf und fügt sie in das Aggregat ein. Der Domänendienst könnte ein Repository injiziert haben, dessen Implementierung, die auf die Datenbank trifft, zur Infrastrukturschicht gehören sollte (nur die Implementierung, nicht die Schnittstelle / der Vertrag). Wenn es Ihre allgegenwärtige Sprache beschreibt, gehört es in die Domäne.
Diegosasw
5

Gehört es zu den Best Practices von DDD und OOP, Dienste in Entitätsmethodenaufrufe einzufügen?

Nein, Sie sollten nichts in Ihre Domänenschicht einfügen (dies schließt Entitäten, Wertobjekte, Fabriken und Domänendienste ein). Diese Schicht sollte unabhängig von Frameworks, Bibliotheken oder Technologien von Drittanbietern sein und keine E / A-Aufrufe tätigen.

$order->getLineItems($orderService)

Dies ist falsch, da das Aggregat nichts anderes als sich selbst benötigen sollte, um die Bestellpositionen zurückzugeben. Das gesamte Aggregat sollte bereits vor seinem Methodenaufruf geladen sein. Wenn Sie der Meinung sind, dass dies faul geladen werden sollte, gibt es zwei Möglichkeiten:

  1. Ihre Aggregatgrenzen sind falsch, sie sind zu groß.

  2. In diesem Fall verwenden Sie das Aggregat nur zum Lesen. Die beste Lösung besteht darin, das Schreibmodell vom Lesemodell zu trennen (dh CQRS zu verwenden ). In dieser übersichtlichen Architektur dürfen Sie nicht das Aggregat, sondern ein Lesemodell abfragen.

Constantin Galbenu
quelle
Wenn ich einen Datenbankaufruf zur Validierung benötige, muss ich ihn im Anwendungsdienst aufrufen und ein Ergebnis an den Domänendienst oder direkt an das aggregierte Stammverzeichnis übergeben, anstatt das Repository in den Domänendienst einzuschleusen.
Muflix
1
@ Muflix ja, das stimmt
Constantin Galbenu
3

Die Schlüsselidee in taktischen DDD-Mustern: Die Anwendung greift auf alle Daten in der Anwendung zu, indem sie auf einen aggregierten Stamm einwirkt. Dies bedeutet, dass die einzigen Entitäten, auf die außerhalb des Domänenmodells zugegriffen werden kann, die aggregierten Wurzeln sind.

Das Order-Aggregat-Stammverzeichnis würde niemals einen Verweis auf seine Lineitem-Auflistung liefern, mit dem Sie die Auflistung ändern könnten, und es würde auch keine Auflistung von Verweisen auf eine Werbebuchung liefern, mit der Sie sie ändern könnten. Wenn Sie das Auftragsaggregat ändern möchten, gilt das Hollywood-Prinzip: "Sagen, nicht fragen".

Die Rückgabe von Werten aus dem Aggregat ist in Ordnung, da Werte von Natur aus unveränderlich sind. Sie können meine Daten nicht ändern, indem Sie Ihre Kopie davon ändern.

Die Verwendung eines Domänendienstes als Argument, um das Aggregat bei der Bereitstellung der richtigen Werte zu unterstützen, ist durchaus sinnvoll.

Normalerweise würden Sie keinen Domänendienst verwenden, um Zugriff auf Daten innerhalb des Aggregats zu gewähren, da das Aggregat bereits Zugriff darauf haben sollte.

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

Diese Schreibweise ist also seltsam, wenn wir versuchen, auf die Sammlung von Werbebuchungswerten dieser Bestellung zuzugreifen. Die natürlichere Schreibweise wäre

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

Dies setzt natürlich voraus, dass die Werbebuchungen bereits geladen wurden.

Das übliche Muster ist, dass die Last des Aggregats den gesamten für den jeweiligen Anwendungsfall erforderlichen Status umfasst. Mit anderen Worten, Sie haben möglicherweise verschiedene Möglichkeiten, dasselbe Aggregat zu laden. Ihre Repository-Methoden sind zweckmäßig .

Diesen Ansatz finden Sie nicht im ursprünglichen Evans, wo er davon ausging, dass einem Aggregat ein einzelnes Datenmodell zugeordnet ist. Es fällt natürlicher aus CQRS heraus.

VoiceOfUnreason
quelle
Danke dafür. Ich habe jetzt ungefähr die Hälfte des "roten Buches" gelesen und hatte meinen ersten Eindruck von der richtigen Anwendung des Hollywood-Prinzips in der Infrastrukturschicht. Wenn Sie all diese Antworten noch einmal lesen, sind sie alle gute Punkte, aber ich denke, Ihre haben einige sehr wichtige Punkte in Bezug auf den Umfang lineItems()und das Vorladen beim ersten Abrufen der aggregierten Wurzel.
e_i_pi
3

Im Allgemeinen haben Wertobjekte, die zum Aggregat gehören, kein eigenes Repository. Es liegt in der Gesamtverantwortung von root, sie zu füllen. In Ihrem Fall liegt es in der Verantwortung Ihres OrderRepository, sowohl die Order-Entity- als auch die OrderLine-Werteobjekte zu füllen.

In Bezug auf die Infrastrukturimplementierung des OrderRepository ist ORM eine Eins-zu-Viele-Beziehung, und Sie können die OrderLine entweder eifrig oder faul laden.

Ich bin mir nicht sicher, was Ihre Dienste genau bedeuten. Es ist ziemlich nah an "Application Service". Wenn dies der Fall ist, ist es im Allgemeinen keine gute Idee, die Dienste in das aggregierte Stamm- / Entitäts- / Wertobjekt einzufügen. Der Anwendungsdienst sollte der Client des aggregierten Stamm- / Entitäts- / Wertobjekt- und Domänendienstes sein. Eine andere Sache bei Ihren Diensten ist, dass es auch keine gute Idee ist, Wertobjekte im Anwendungsdienst verfügbar zu machen. Auf sie sollte über das aggregierte Stammverzeichnis zugegriffen werden.

ivenxu
quelle
2

Die Antwort lautet: definitiv NEIN, vermeiden Sie die Übergabe von Diensten in Entitätsmethoden.

Die Lösung ist einfach: Lassen Sie das Order-Repository die Order mit all ihren LineItems zurückgeben. In Ihrem Fall lautet das Aggregat Order + LineItems. Wenn das Repository also kein vollständiges Aggregat zurückgibt, erledigt es seine Aufgabe nicht.

Das allgemeinere Prinzip lautet: Halten Sie funktionale Bits (z. B. Domänenlogik) von nicht funktionalen Bits (z. B. Persistenz) getrennt.

Noch etwas: Wenn Sie können, versuchen Sie dies zu vermeiden:

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

Tun Sie dies stattdessen

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

Beim objektorientierten Design versuchen wir zu vermeiden, in Objektdaten herumzufischen. Wir ziehen es vor, das Objekt zu bitten, das zu tun, was wir wollen.

xpmatteo
quelle