Wie wird die Validierung von Referenzen zwischen Aggregaten behandelt?

11

Ich habe ein bisschen Probleme mit der Referenzierung zwischen Aggregaten. Nehmen wir an, das Aggregat Carhat einen Verweis auf das Aggregat Driver. Diese Referenz wird modelliert durch Car.driverId.

Mein Problem ist nun, wie weit ich gehen soll, um die Erstellung eines CarAggregats in zu validieren CarFactory. Sollte ich darauf vertrauen, dass sich das Bestehen DriverIdauf ein vorhandenes bezieht, Driveroder sollte ich diese Invariante überprüfen?

Zur Überprüfung sehe ich zwei Möglichkeiten:

  • Ich könnte die Unterschrift der Autofabrik ändern, um eine vollständige Fahrerentität zu akzeptieren. Die Fabrik würde dann einfach den Ausweis von dieser Entität auswählen und das Auto damit bauen. Hier wird die Invariante implizit überprüft.
  • Ich könnte einen Verweis auf den DriverRepositoryin der CarFactoryund explizit aufrufen driverRepository.exists(driverId).

Aber jetzt frage ich mich, ob das nicht zu viel invariante Überprüfung ist? Ich könnte mir vorstellen, dass diese Aggregate in einem separaten begrenzten Kontext leben könnten, und jetzt würde ich das Auto-BC mit Abhängigkeiten vom DriverRepository oder der Driver-Entität des Fahrer-BC verschmutzen.

Wenn ich mit Domain-Experten sprechen würde, würden sie niemals die Gültigkeit solcher Referenzen in Frage stellen. Ich spüre, dass ich mein Domain-Modell mit nicht verwandten Bedenken verschmutzte. Andererseits sollte die Benutzereingabe irgendwann überprüft werden.

Markus Malkusch
quelle

Antworten:

6

Ich könnte die Unterschrift der Autofabrik ändern, um eine vollständige Fahrerentität zu akzeptieren. Die Fabrik würde dann einfach den Ausweis von dieser Entität auswählen und das Auto damit bauen. Hier wird die Invariante implizit überprüft.

Dieser Ansatz ist ansprechend, da Sie den Scheck kostenlos erhalten und er gut auf die allgegenwärtige Sprache abgestimmt ist. A Carwird nicht von a angetrieben driverId, sondern von a Driver.

Dieser Ansatz wird von Vaughn Vernon in seinem Beispiel-Kontext mit Identitäts- und Zugriffsbeispiel verwendet, in dem er ein UserAggregat an ein GroupAggregat übergibt , das jedoch Groupnur einen Werttyp enthält GroupMember. Wie Sie sehen können, kann er auch prüfen, ob der Benutzer aktiviert ist (wir sind uns bewusst, dass die Prüfung möglicherweise veraltet ist).

    public void addUser(User aUser) {
        //original code omitted
        this.assertArgumentTrue(aUser.isEnabled(), "User is not enabled.");

        if (this.groupMembers().add(aUser.toGroupMember()) && !this.isInternalGroup()) {
            //original code omitted
        }
    }

DriverIndem Sie die Instanz übergeben, öffnen Sie sich jedoch auch einer versehentlichen Änderung des DriverInneren Car. Das Übergeben einer Wertreferenz erleichtert das Nachdenken über Änderungen aus Sicht eines Programmierers. Gleichzeitig dreht sich bei DDD alles um die Ubiquitous Language. Vielleicht ist es das Risiko wert.

Wenn Sie tatsächlich gute Namen für die Anwendung des Interface Segregation Principle (ISP) finden können , können Sie sich auf eine Schnittstelle verlassen, die nicht über die Verhaltensmethoden verfügt. Sie könnten vielleicht auch ein Wertobjektkonzept entwickeln, das eine unveränderliche Treiberreferenz darstellt und das nur von einem vorhandenen Treiber (z DriverDescriptor driver = driver.descriptor(). B. ) instanziiert werden kann .

Ich könnte mir vorstellen, dass diese Aggregate in einem separaten begrenzten Kontext leben könnten, und jetzt würde ich das Auto-BC mit Abhängigkeiten vom DriverRepository oder der Driver-Entität des Fahrer-BC verschmutzen.

Nein, das würdest du eigentlich nicht. Es gibt immer eine Antikorruptionsschicht, um sicherzustellen, dass die Konzepte eines Kontexts nicht in einen anderen übergehen. Es ist tatsächlich viel einfacher, wenn Sie einen BC für Autofahrerverbände haben, da Sie vorhandene Konzepte wie Carund Driverspeziell für diesen Kontext modellieren können .

Daher haben Sie möglicherweise einen DriverLookupServiceim BC definierten Verantwortlichen für die Verwaltung von Autofahrerverbänden. Dieser Dienst ruft möglicherweise einen Webdienst auf, der vom Treiberverwaltungskontext verfügbar gemacht wird und DriverInstanzen zurückgibt , bei denen es sich höchstwahrscheinlich um Wertobjekte in diesem Kontext handelt.

Beachten Sie, dass Webdienste nicht unbedingt die beste Integrationsmethode zwischen BCs sind. Sie können sich auch auf Nachrichten verlassen, bei denen beispielsweise eine UserCreatedNachricht aus dem Treiberverwaltungskontext in einem Remotekontext verwendet wird, in dem eine Darstellung des Treibers in der eigenen Datenbank gespeichert wird. Der DriverLookupServicekönnte dann diese DB verwenden und die Treiberdaten würden mit weiteren Meldungen (zB DriverLicenceRevoked) auf dem neuesten Stand gehalten .

Ich kann Ihnen nicht wirklich sagen, welcher Ansatz für Ihre Domain besser ist, aber hoffentlich erhalten Sie dadurch genügend Einblicke, um eine Entscheidung zu treffen.

Plalx
quelle
3

Die Art und Weise, wie Sie die Frage stellen (und zwei Alternativen vorschlagen), ist, als ob die einzige Sorge darin besteht, dass die Fahrer-ID zum Zeitpunkt der Erstellung des Autos noch gültig ist.

Sie müssen sich jedoch auch Sorgen machen, dass der mit driverId verknüpfte Fahrer nicht gelöscht wird, bevor das Auto entweder gelöscht oder einem anderen Fahrer zugewiesen wurde (und möglicherweise auch, dass der Fahrer keinem anderen Auto zugewiesen ist (dies, wenn die Domäne einen Fahrer nur darauf beschränkt) mit einem Auto verbunden sein)).

Ich schlage vor, dass Sie anstelle der Validierung zuweisen (was die Validierung der Anwesenheit einschließen würde). Sie werden dann Löschungen verbieten, solange sie noch zugewiesen sind, und so den Race-Zustand veralteter Daten während der Erstellung sowie das andere längerfristige Problem schützen. (Beachten Sie, dass die Zuordnung sowohl validiert als auch zugewiesene Markierungen enthält und atomar arbeitet.)

Übrigens stimme ich @PriceJones zu, dass die Verbindung zwischen Auto und Fahrer wahrscheinlich eine Verantwortung ist, die entweder vom Auto oder vom Fahrer getrennt ist. Diese Art der Zuordnung wird mit der Zeit immer komplexer, da es sich um ein Planungsproblem handelt (Fahrer, Autos, Zeitfenster / Fenster, Ersatz usw.). Auch wenn es sich eher um ein Registrierungsproblem handelt, möchte man möglicherweise ein historisches Problem Registrierungen sowie aktuelle Registrierungen. Somit kann es sehr wohl sein eigenes BC direkt verdienen.

Sie können ein Zuordnungsschema (z. B. einen Booleschen Wert oder einen Referenzzähler) innerhalb des BC der zugewiesenen Aggregatentitäten oder innerhalb eines separaten BC bereitstellen, z. B. derjenigen, die für die Zuordnung zwischen Auto und Fahrer verantwortlich ist. Wenn Sie das erstere tun, können Sie (gültige) Löschvorgänge zulassen, die an das Auto oder den Fahrer BC ausgegeben werden. Wenn Sie Letzteres tun, müssen Sie Löschungen aus den Auto- und Fahrer-BCs verhindern und diese stattdessen über den Planer der Auto- und Fahrerassoziation senden.

Sie können auch einige der Zuweisungsverantwortlichkeiten wie folgt auf die BCs aufteilen. Der Auto- und Fahrer-BC stellt jeweils ein "Zuordnungsschema" bereit, das den zugewiesenen Booleschen Wert mit diesem BC validiert und festlegt. Wenn ihr Zuordnungs-Boolescher Wert festgelegt ist, verhindert der BC das Löschen der entsprechenden Entitäten. (Und das System ist so eingerichtet, dass die Auto- und Fahrer-BC nur die Zuweisung und Freigabe von der Auto- / Fahrerassoziationsplanung BC zulässt.)

Das Auto- und Fahrerplanungs-BC verwaltet dann einen Kalender mit Fahrern, die für einige Zeiträume / Zeiträume jetzt und in Zukunft mit dem Auto verbunden sind, und benachrichtigt die anderen BCs über die Freigabe nur bei der letzten Verwendung eines geplanten Autos oder Fahrers.


Als radikalere Lösung können Sie die Auto- und Fahrer-BCs als reine Anhänge-Fabriken für historische Aufzeichnungen behandeln und das Eigentum dem Planer der Auto- / Fahrervereinigung überlassen. Das Auto BC kann ein neues Auto mit allen Details des Autos zusammen mit seiner Fahrgestellnummer generieren. Das Eigentum an dem Auto wird vom Planer der Auto- / Fahrervereinigung übernommen. Selbst wenn eine Auto- / Fahrervereinigung gelöscht und das Auto selbst zerstört wird, sind die Autoaufzeichnungen per Definition im Auto-BC vorhanden, und wir können das Auto-BC verwenden, um historische Daten nachzuschlagen. während Auto- / Fahrerverbände / -eigentümer (Vergangenheit, Gegenwart und möglicherweise zukünftige geplante) von einem anderen BC verwaltet werden.

Erik Eidt
quelle
2

Nehmen wir an, das aggregierte Auto hat einen Verweis auf den aggregierten Fahrer. Diese Referenz wird mit Car.driverId modelliert.

Ja, das ist der richtige Weg, um ein Aggregat mit einem anderen zu koppeln.

Wenn ich mit Domain-Experten sprechen würde, würden sie niemals die Gültigkeit solcher Referenzen in Frage stellen

Nicht ganz die richtige Frage, die Sie Ihren Domain-Experten stellen sollten. Versuchen Sie "Was kostet das Unternehmen, wenn der Treiber nicht vorhanden ist?"

Ich würde DriverRepository wahrscheinlich nicht verwenden, um die driverId zu überprüfen. Stattdessen würde ich einen Domänendienst verwenden, um dies zu tun. Ich denke, es ist besser, die Absicht auszudrücken - unter dem Deckmantel überprüft der Domänendienst immer noch das Aufzeichnungssystem.

Also so etwas wie

class DriverService {
    private final DriverRepository driverRepository;

    boolean doesDriverExist(DriverId driverId) {
        return driverRepository.exists(driverId);
    }
}

Sie fragen die Domain tatsächlich an verschiedenen Stellen nach der driverId ab

  • Vom Client vor dem Senden des Befehls
  • In der Anwendung, bevor Sie den Befehl an das Modell übergeben
  • Innerhalb des Domänenmodells während der Befehlsverarbeitung

Einige oder alle dieser Überprüfungen können Fehler bei der Benutzereingabe reduzieren. Aber sie arbeiten alle mit veralteten Daten. Das andere Aggregat kann sich sofort ändern, nachdem wir die Frage gestellt haben. Es besteht also immer die Gefahr falsch negativer / positiver Ergebnisse.

  • Führen Sie in einem Ausnahmebericht aus, nachdem der Befehl ausgeführt wurde

Hier arbeiten Sie immer noch mit veralteten Daten (die Aggregate führen möglicherweise Befehle aus, während Sie den Bericht ausführen. Möglicherweise können Sie nicht die neuesten Schreibvorgänge für alle Aggregate anzeigen). Die Überprüfungen zwischen Aggregaten werden jedoch niemals perfekt sein (Car.create (Treiber: 7) wird gleichzeitig mit Driver.delete (Treiber: 7) ausgeführt). Dies bietet Ihnen also eine zusätzliche Schutzschicht gegen Risiken.

VoiceOfUnreason
quelle
1
Driver.deletesollte nicht existieren. Ich habe nie wirklich eine Domain gesehen, in der Aggregate zerstört werden. Wenn Sie ARs in der Nähe halten, können Sie niemals mit Waisenkindern enden.
Plalx
1

Es könnte hilfreich sein zu fragen: Sind Sie sicher, dass Autos mit Fahrern gebaut werden? Ich habe noch nie von einem Auto gehört, das aus einem Fahrer in der realen Welt besteht. Der Grund, warum diese Frage wichtig ist, liegt darin, dass sie Sie möglicherweise in die Richtung weist, Autos und Fahrer unabhängig voneinander zu erstellen und dann einen externen Mechanismus zu erstellen, der einen Fahrer einem Auto zuweist. Ein Auto kann ohne Fahrerreferenz existieren und dennoch ein gültiges Auto sein.

Wenn ein Auto unbedingt einen Fahrer in Ihrem Kontext haben muss, sollten Sie das Builder-Muster berücksichtigen. Dieses Muster wird dafür verantwortlich sein, dass Autos mit vorhandenen Fahrern gebaut werden. Die Fabriken werden unabhängig validierte Autos und Fahrer bedienen, aber der Hersteller wird sicherstellen, dass das Auto die Referenz hat, die es benötigt, bevor es das Auto bedient.

Preis Jones
quelle
Ich habe auch über die Beziehung zwischen Auto und Fahrer nachgedacht - aber die Einführung eines DriverAssignment-Aggregats bewegt nur die Referenz, die validiert werden muss.
VoiceOfUnreason
1

Aber jetzt frage ich mich, ob das nicht zu viel invariante Überprüfung ist?

Ich glaube schon. Das Abrufen einer bestimmten Treiber-ID aus der Datenbank gibt einen leeren Satz zurück, wenn dieser nicht vorhanden ist. Wenn Sie also das Rückgabeergebnis überprüfen, müssen Sie nicht mehr fragen, ob es vorhanden ist (und dann abrufen).

Dann macht es das Klassendesign auch unnötig

  • Wenn es eine Anforderung gibt "ein geparktes Auto kann einen Fahrer haben oder nicht"
  • Wenn ein Treiberobjekt ein benötigt DriverIdund im Konstruktor festgelegt ist.
  • Wenn das Carnur das braucht DriverId, hab einen Driver.IdGetter. Kein Setter.

Das Repository ist nicht der Ort für Geschäftsregeln

  • A Carkümmert es, ob es eine Driver(oder zumindest seine ID) hat. A Driverkümmert es, wenn es eine hat DriverId. Das Repositorykümmert sich um die Datenintegrität und könnte sich nicht weniger um fahrerlose Autos kümmern.
  • Die Datenbank verfügt über Datenintegritätsregeln. Nicht-Null-Schlüssel, Nicht-Null-Einschränkungen usw. Bei der Datenintegrität geht es jedoch um Daten- / Tabellenschemata, nicht um Geschäftsregeln. Wir haben in diesem Fall eine stark korrelierte, symbiotische Beziehung, aber mischen Sie nicht beide.
  • Die Tatsache, dass a DriverIdeine Geschäftsdomänensache ist, wird in den entsprechenden Klassen behandelt.

Trennung von Bedenken Verletzung

... passiert, wenn Repository.DriverIdExists()die Frage gestellt wird.

Erstellen Sie ein Domänenobjekt. Wenn nicht, Driverdann vielleicht ein DriverInfo(nur ein DriverIdund Name, sagen wir) Objekt. Das DriverIdwird beim Bau validiert. Es muss existieren und der richtige Typ sein, und was auch immer. Dann ist es ein Problem beim Entwurf von Clientklassen, wie mit einem nicht vorhandenen Treiber / einer nicht vorhandenen Treiber-ID umgegangen werden soll.

Vielleicht Carist a ohne Fahrer in Ordnung, bis Sie anrufen Car.Drive(). In diesem Fall stellt das CarObjekt natürlich seinen eigenen Zustand sicher. Ich kann nicht ohne fahren Driver- na ja, noch nicht ganz.

Es ist schlecht, eine Eigenschaft von ihrer Klasse zu trennen

Sicher, haben Sie eine, Car.DriverIdwenn Sie es wünschen. Aber es sollte ungefähr so ​​aussehen:

public class Car {
    // Non-null driver has a driverId by definition/contract.
    protected DriverInfo myDriver;
    public DriverId {get { return myDriver.Id; }}

    public void Drive() {
       if (myDriver == null ) return errorMessage; // or something
       // ... continue driving
    }
}

Nicht das:

public class Car {
    public int DriverId {get; protected set;}
}

Jetzt Carmüssen sie sich mit allen DriverIdGültigkeitsfragen befassen - ein Verstoß gegen das Prinzip der einzigen Verantwortung; und redundanter Code wahrscheinlich.

Radarbob
quelle