Wo sollten Sie den Status „anderer“ Aggregate überprüfen?

8

Szenario:

Ein Kunde gibt eine Bestellung auf und gibt nach Erhalt des Produkts eine Rückmeldung zum Bestellvorgang.

Nehmen Sie die folgenden aggregierten Wurzeln an:

  • Kunde
  • Auftrag
  • Feedback

Hier sind die Geschäftsregeln:

  1. Ein Kunde kann nur Feedback zu seiner eigenen Bestellung geben, nicht zu der eines anderen.
  2. Ein Kunde kann nur dann eine Rückmeldung geben, wenn die Bestellung bezahlt wurde.

    class Feedback {
        public function __construct($feedbackId,
                                    Customer $customer,
                                    Order $order,
                                    $content) {
            if ($customer->customerId() != $order->customerId()) {
                // Error
            }
            if (!$order->isPaid()) {
                // Error
            }
            $this->feedbackId = $feedbackId;
            $this->customerId = $customerId;
            $this->orderId = $orderId;
            $this->content = $content;
        }
    }
    

Nehmen wir nun an, das Unternehmen möchte eine neue Regel:

  1. Ein Kunde kann nur dann eine Rückmeldung geben, wenn die SupplierWare der Bestellung noch in Betrieb ist.

    class Feedback {
        public function __construct($feedbackId,
                                    Customer $customer,
                                    Order $order,
                                    Supplier $supplier,
                                    $content) {
            if ($customer->customerId() != $order->customerId()) {
                // Error
            }
            if (!$order->isPaid()) {
                // Error
            }
            // NEW RULE HERE
            if (!$supplier->isOperating()) {
                // Error
            }
            $this->feedbackId = $feedbackId;
            $this->customerId = $customerId;
            $this->orderId = $orderId;
            $this->content = $content;
        }
    }
    

Ich habe die Implementierung der ersten beiden Regeln im Feedback Aggregat selbst platziert. Ich fühle mich wohl dabei, insbesondere angesichts der FeedbackTatsache , dass das Aggregat alle anderen Aggregate nach Identität referenziert. Zum Beispiel zeigen die Eigenschaften der FeedbackKomponente an, dass sie von der Existenz der anderen Aggregate weiß , daher fühle ich mich wohl, wenn sie auch den schreibgeschützten Zustand dieser Aggregate kennt .

Aufgrund seiner Eigenschaften hat das FeedbackAggregat jedoch keine Kenntnis von der Existenz des SupplierAggregats. Sollte es also Kenntnis vom schreibgeschützten Zustand dieses Aggregats haben?

Die alternative Lösung zur Implementierung von Regel 3 besteht darin, diese Logik an die entsprechende Stelle zu verschieben CommandHandler. Dies scheint jedoch die Domänenlogik vom "Zentrum" meiner zwiebelbasierten Architektur zu entfernen.

Überblick über meine Zwiebelarchitektur

Magnus
quelle
Repository-Schnittstellen sind Teil der Domäne. Daher kann eine Konstruktionslogik (die für sich genommen als Service im DDD-Buch betrachtet wird) das Repository eines Auftrags aufrufen, um zu fragen, ob der Lieferant des Auftrags noch in Betrieb ist.
Euphorisch
Erstens Supplierwürde der Betriebszustand eines Aggregats nicht über ein OrderRepository abgefragt . Supplierund Ordersind zwei separate Aggregate. Zweitens gab es in der DDD / CQRS-Mailingliste eine Frage zum Übergeben von aggregierten Roots und Repositorys an andere aggregierte Root-Methoden (einschließlich des Konstruktors). Es gab verschiedene Meinungen, aber Greg Young erwähnte, dass es üblich ist, aggregierte Wurzeln als Parameter zu übergeben, während eine andere Person sagte, dass Repositories enger mit der Infrastruktur als mit der Domäne verbunden sind. ZB Repositorys "abstrakt in Speichersammlungen" und haben keine Logik.
Magnus
Ist der Lieferant nicht mit der Bestellung verbunden? Was passiert, wenn ein Lieferant übergeben wird, der nichts mit der Bestellung zu tun hat? Nun, "arbeitet der Lieferant?" Ist keine Logik. Es ist eine einfache Abfrage. Es gibt auch einen Grund, warum dies häufig vorkommt: Ohne diesen Code wird Ihr Code viel komplexer und erfordert die Weitergabe von Informationen, bei denen Fehler auftreten können. Außerdem ist "Repository-Schnittstelle" keine Infrastruktur. Die Repository-Implementierung ist.
Euphorisch
Du hast recht. So wie a Customernur zu einer eigenen Bestellung Feedback geben kann ( $order->customerId() == $customer->customerId()), müssen wir auch die Lieferanten-ID ( $order->supplierId() == $supplier->supplierId()) vergleichen. Die erste Regel schützt vor dem Benutzer, der falsche Werte angibt. Die zweite Regel schützt vor dem Programmierer, der falsche Werte liefert. Die Prüfung, ob der Lieferant tätig ist, muss jedoch entweder in der FeedbackEntität oder im Befehlshandler erfolgen. Wo ist die Frage?
Magnus
2
Zwei Kommentare, die nicht direkt mit der Frage zusammenhängen. Erstens sieht es falsch aus, Aggregatwurzeln als Argumente an ein anderes Aggregat zu übergeben - das sollten IDs sein - es gibt nichts Nützliches, was ein Aggregat mit einem anderen Aggregat tun kann. Zweitens sind Kunde und Lieferant ... schwierig. In beiden Fällen ist das Buch der Aufzeichnung die reale Welt: Sie können den Lieferanten in der realen Welt nicht stoppen, indem Sie einen CeaseOperations-Befehl an Ihr Domain-Modell senden.
VoiceOfUnreason

Antworten:

1

Wenn für die Transaktionskorrektheit ein Aggregat den aktuellen Status eines anderen Aggregats kennt, ist Ihr Modell falsch.

In den meisten Fällen ist keine Transaktionskorrektheit erforderlich . Unternehmen neigen dazu, Latenz und veraltete Daten zu tolerieren. Dies gilt insbesondere für Inkonsistenzen, die leicht zu erkennen und leicht zu beheben sind.

Der Befehl wird also von dem Aggregat ausgeführt, das den Status ändert. Um die nicht unbedingt korrekte Prüfung durchzuführen, ist nicht unbedingt die neueste Kopie des Status des anderen Aggregats erforderlich .

Bei Befehlen für ein vorhandenes Aggregat besteht das übliche Muster darin, ein Repository an das Aggregat zu übergeben, und das Aggregat übergibt seinen Status an das Repository, das eine Abfrage bereitstellt, die einen unveränderlichen Status / eine unveränderliche Projektion des anderen Aggregats zurückgibt

class Feedback {
    void downvote(Repository<Supplier.State> query) {
        Supplier.State supplier = query.getById(this->supplierId);
        boolean isOperating = state.isOperating();
        ....
    }
}

Aber Konstruktionsmuster sind seltsam - wenn Sie das Objekt erstellen, kennt der Aufrufer den internen Status bereits, weil er ihn bereitstellt. Das gleiche Muster funktioniert, es sieht einfach sinnlos aus

class Feedback {
    __construct(SupplierId supplierId, SupplierOperatingQuery query ...) {
        Supplier.State supplier = query.getById(this->supplierId);
        boolean isOperating = state.isOperating();
        ....
    }
}

Wir befolgen die Regeln, indem wir die gesamte Domänenlogik in den Domänenobjekten beibehalten, schützen die Geschäftsinvariante jedoch nicht wirklich auf nützliche Weise (da der Anwendungskomponente dieselben Informationen zur Verfügung stehen). Für das Erstellungsmuster wäre es genauso gut zu schreiben

class Feedback {
    __construct(Supplier.State supplier, ...) {
        boolean isOperating = state.isOperating();
        ....
    }
}
VoiceOfUnreason
quelle
1. Ist die SupplierOperatingQueryAbfrage des Lesemodells oder "Abfrage" im Namen irreführend? 2. Transaktionskonsistenz ist nicht erforderlich. Es spielt keine Rolle, ob der Lieferant den Betrieb eine Sekunde vor dem Feedback eines Kunden einstellt, aber heißt das, wir sollten es trotzdem nicht überprüfen? 3. Erzwingt die Bereitstellung eines "Abfragedienstes" anstelle des Objekts in Ihrem Beispiel die Transaktionskonsistenz? Wenn das so ist, wie? 4. Wie wirkt sich die Verwendung solcher Abfragedienste auf Unit-Tests aus?
Magnus
1. Abfrage in dem Sinne, dass das Aufrufen den Status von nichts ändert. 3. Es gibt keine Transaktionskonsistenz mit dem Abfragedienst, es gibt keine Interaktion zwischen ihm und dem gleichzeitig ausgeführten Befehl, der das andere Aggregat ändert. 4. In diesem Fall ist es Teil des SPI des Domänenmodells. Stellen Sie daher einfach eine Testimplementierung bereit. Hmm, das ist allerdings etwas seltsam - DomainService ist möglicherweise nicht der beste Begriff.
VoiceOfUnreason
2. Denken Sie daran, dass Ihre Prüfung möglicherweise die falsche Antwort liefert, da die Daten, die Sie hier verwenden, über eine Aggregatgrenze hinausgehen (Beispiel: Ihre Prüfung besagt, dass sie nicht in Ordnung ist, sollte aber daran liegen, dass sich das andere Aggregat ändert). Daher ist es möglicherweise besser, diese Prüfung in das Lesemodell zu verschieben (akzeptieren Sie immer den Befehl, erstellen Sie jedoch einen Ausnahmebericht, wenn das Modell inkonsistent ist). Sie können auch festlegen, dass der Client nur Befehle sendet, die erfolgreich sein sollen. Das heißt, der Client sollte keine Befehle senden, von denen er erwartet, dass sie fehlschlagen, basierend auf seinem Verständnis des aktuellen Status.
VoiceOfUnreason
1. Es ist im Allgemeinen verpönt, wenn die "Schreibseite" die "Leseseite" abfragt (z. B. Projektionen mit Ereignisquellen). "... in dem Sinne, dass das Aufrufen den Status von nichts ändert" - und auch nicht einfach einen unveränderlichen Accessor zu verwenden, was meiner Meinung nach viel einfacher ist. 2. Es wäre schön , zu duplizieren die Prüfung in dem Lesemodell, aber wenn Sie bewegen sie (sprich: Entfernen Sie es aus dem Server), sind Sie Probleme für sich selbst zu schaffen. Erstens muss Ihre Geschäftsregel in jedem Client (Webbrowser und mobile Clients) dupliziert werden. Zweitens ist es einfach, diese Prüfung zu umgehen:
Magnus
3. "... es gibt keine Interaktion zwischen ihm und dem gleichzeitig ausgeführten Befehl, der das andere Aggregat ändert" - und das Laden des Lieferantenaggregats selbst auch nicht, da nur das Feedback-Aggregat geändert wird. 4. SupplierOperatingQuery ist also eine Schnittstelle, die eine konkrete Implementierung erfordert. Das bedeutet, dass Sie in Ihrem Komponententest eine Scheinimplementierung erstellen müssen, um einfach den True / False-Wert einer einzelnen Variablen zu testen, die bereits im anderen Objekt vorhanden ist. Riecht nach Overkill. Warum nicht auch eine CustomerOwnsOrderQuery und eine OrderIsPaidQuery erstellen?
Magnus
-1

Ich weiß, dass dies eine alte Frage ist, aber ich möchte darauf hinweisen, dass das Problem direkt auf einer falschen Prämisse beruht. Das heißt, die aggregierten Wurzeln, von denen wir annehmen sollen, dass sie existieren, sind einfach falsch.

In dem von Ihnen beschriebenen System gibt es nur einen aggregierten Stamm: Kunde. Sowohl eine Bestellung als auch eine Rückmeldung sind zwar eigenständige Aggregate, hängen jedoch vom Kunden ab, sind also selbst keine aggregierten Wurzeln. Die Logik, die Sie in Ihrem Feedback-Konstruktor angeben, scheint darauf hinzudeuten, dass eine Bestellung eine Kunden-ID haben muss und Feedback auch mit einem Kunden verknüpft sein muss. Das macht Sinn. Wie kann eine Bestellung oder ein Feedback nicht mit einem Kunden in Verbindung gebracht werden? Darüber hinaus scheint der Lieferant logisch mit der Bestellung verbunden zu sein (würde sich also in diesem Aggregat befinden).

Vor diesem Hintergrund sind alle gewünschten Informationen bereits im Stammverzeichnis des Kunden verfügbar, und es wird deutlich, dass Sie Ihre Regeln an der falschen Stelle durchsetzen. Konstruktoren sind schreckliche Orte zur Durchsetzung von Geschäftsregeln und sollten unter allen Umständen vermieden werden. So sollte es aussehen (Hinweis: Ich werde keine Konstruktoren für Kunde und Auftrag einschließen, da wahrscheinlich Fabriken verwendet werden sollten. Außerdem werden nicht alle Schnittstellenmethoden angezeigt).

/*******************************\
   Interfaces, explained below 
\*******************************/

interface ICustomer
{
    public function getId() : int;
}

interface IUser extends ICustomer
{
    public function getUsername() : string;

    public function getPassword() : string;

    public function changeUsername( string $new ) : void;

    public function resetPassword( string $new ) : void;

}

interface IReviewer extends ICustomer
{
    public function provideFeedback( IOrder $order, string $content ) : void;
}

interface IBuyer extends ICustomer
{
    public function placeOrder( IOrder $order ) : void;
}

interface IOrder
{
    public function getCustomerId() : int;

    public function addFeedback( string $content ) : void;
}


interface IFeedback
{
    public function addContent( string $content ) : void;

    public function isValidContent( string $content ) : void;
}



/*******************************\
   Implentation
\*******************************/



class Customer implements IReviewer, IBuyer
{
    protected $id;

    protected $orders = [];

    public function provideFeedback( IOrder $order, string $content ) : void
    {
        if( $order->getCustomerId() !== $this->getId() )
            throw new \InvalidArgumentException('Customers can only provide feedback on their own orders');

        $order->addFeedback( $content );
    }
}


class Order implements IOrder
{
    protected $supplier;

    protected $feedbacks = [];

    public function addFeedback( string $content ) : void
    {
        if( false === $this->supplier->isOperating() )
            throw new \Exception('Feedback can only be added to orders if the supplier is still operating.');

        // could be any IFeedback
        $feedback = new Feedback( $this );

        $feedback->addContent( $content );

        $this->feedbacks[] = $feedback;
    }
}


class Feedback implements IFeedback
{
    protected $order;

    protected $content;

    public function __construct( IOrder $order )
    {    
         // we don't carry our business rules in constructors
         $this->order = $order;
    }

    public function addContent( string $content ) : void
    {
        if( false === $this->isValidContent($content) )
            throw new \Exception("Content contains offensive language.");

        $this->content = $content;
    }
}

Okay. Lassen Sie uns das ein wenig zusammenfassen. Das erste, was Sie bemerken werden, ist, wie viel aussagekräftiger dieses Modell ist. Alles ist eine Aktion, es wird klar, wo Geschäftsregeln gelten sollten. Das obige Design "macht" nicht nur das Richtige, es "sagt" das Richtige.

Was würde jemanden dazu bringen anzunehmen, dass Regeln in der folgenden Zeile ausgeführt werden?

// this is a BAD place for rules to execute
$feedback = new Feedback( $id, $customerId, $order, $supplier, $content);

Zweitens können Sie sehen, dass die gesamte Logik zur Validierung von Geschäftsregeln so nah wie möglich an den Modellen ausgeführt wird, auf die sie sich beziehen. In Ihrem Beispiel führt der Konstruktor (eine einzelne Methode) mehrere Validierungen für verschiedene Modelle durch. Das bricht das SOLID-Design. Wo würden wir eine Überprüfung hinzufügen, um sicherzustellen, dass der Feedback-Inhalt keine schlechten Wörter enthält? Noch eine Überprüfung im Konstruktor? Was ist, wenn verschiedene Arten von Feedback unterschiedliche Inhaltsprüfungen erfordern? Hässlich.

Drittens können Sie anhand der Schnittstellen erkennen, dass es natürliche Orte gibt, an denen die Regeln durch Komposition erweitert / geändert werden können. Beispielsweise können für verschiedene Arten von Bestellungen unterschiedliche Regeln gelten, wann Feedback gegeben werden kann. Die Bestellung kann auch verschiedene Arten von Rückmeldungen liefern, die wiederum unterschiedliche Regeln für die Validierung haben können.

Sie können auch eine Reihe von ICustomer * -Schnittstellen sehen. Diese werden verwendet, um das Kundenaggregat zusammenzustellen, das wir hier benötigen (wahrscheinlich nicht nur als Kunde bezeichnet). Der Grund dafür ist einfach. Es ist SEHR wahrscheinlich, dass ein Kunde ein RIESIGES aggregiertes Stammverzeichnis ist, das sich über Ihre gesamte Domain / Datenbank verteilt. Durch die Verwendung von Schnittstellen können wir dieses eine Aggregat (das wahrscheinlich zu groß zum Laden ist) in mehrere Aggregatwurzeln zerlegen, die nur bestimmte Aktionen (z. B. Bestellen oder Feedback) bereitstellen. Sie können sehen, dass das Aggregat in meiner Implementierung BEIDE Bestellungen aufgeben UND Feedback geben kann, aber nicht zum Zurücksetzen eines Passworts oder zum Ändern eines Benutzernamens verwendet werden kann.

Die Antwort auf Ihre Frage lautet also, dass sich Aggregate selbst validieren sollten. Wenn sie es nicht können, haben Sie wahrscheinlich ein mangelhaftes Modell.

King-Side-Slide
quelle
1
Während die Aggregatgrenzen je nachdem, wer das System entwirft, unterschiedlich sind, halte ich „ein Aggregat“, das aus der Reihenfolge stammt, einfach für dumm. Ihr Beispiel, dass ein Lieferant Teil einer Bestellung ist, ist ein gutes Beispiel dafür - kann ein Lieferant erst existieren, nachdem eine Bestellung erstellt wurde? Was ist mit doppelten Lieferanten:
Magnus
@ user1420752 Ich denke, Sie können es rückwärts haben. Das obige Modell impliziert das Gegenteil. Dass eine Bestellung ohne einen Lieferanten nicht existieren kann. In meinem Beispiel werden einfach die Informationen / Regeln / Beziehungen verwendet, die ich aus dem bereitgestellten Code entnehmen kann. Ich würde zustimmen, dass Order, ähnlich wie Customer, wahrscheinlich ein großes, komplexes Aggregat für sich ist (wenn auch keine Wurzel). Eine, die je nach Kontext auch eine Zerlegung in eine Handvoll konkreter Implementierungen erfordern kann. Der Punkt, den ich illustriere, ist, dass Entitäten sich selbst validieren MÜSSEN. Wie Sie sehen können, ist es auf diese Weise sauberer.
King-Side-Slide
@ user1420752 Ich möchte hinzufügen, dass Methoden / Konstruktoren, die viele Argumente erfordern, häufig ein Zeichen für ein anämisches Modell sind, bei dem die Daten vom Verhalten getrennt sind (und daher in großen Spannfuttern in die Teile injiziert werden müssen, die auf die Daten einwirken ). Der von Ihnen bereitgestellte Feedback-Konstruktor ist ein Beispiel dafür. Anämische Modelle neigen dazu, die Kohäsion zu verringern und zusätzliche Kopplungssemantik hinzuzufügen (wie das mehrfache Überprüfen von IDs). Hohe Kohäsion bedeutet im Allgemeinen, dass jede Methode in einer Entität alle Instanzvariablen verwendet. Dies führt natürlich zur Zersetzung großer Aggregate wie Kunde oder Bestellung
King-Side-Slide