Gibt es eine elegante Möglichkeit, eindeutige Einschränkungen für Domänenobjektattribute zu überprüfen, ohne die Geschäftslogik in die Serviceschicht zu verschieben?

10

Ich passe das domänengetriebene Design seit ungefähr 8 Jahren an und selbst nach all den Jahren gibt es immer noch eine Sache, die mich nervt. Das heißt, es wird nach einem eindeutigen Datensatz im Datenspeicher für ein Domänenobjekt gesucht.

Im September 2013 erwähnte Martin Fowler das TellDon'tAsk-Prinzip , das nach Möglichkeit auf alle Domänenobjekte angewendet werden sollte, die dann eine Nachricht über den Ablauf der Operation zurückgeben sollten (im objektorientierten Design erfolgt dies meistens durch Ausnahmen, wenn die Operation war nicht erfolgreich).

Meine Projekte sind normalerweise in viele Teile unterteilt, von denen zwei Domain (mit Geschäftsregeln und nichts anderem, die Domain ist völlig persistenzunabhängig) und Services sind. Dienste, die sich mit der Repository-Schicht auskennen, die für CRUD-Daten verwendet wird.

Da die Eindeutigkeit eines zu einem Objekt gehörenden Attributs eine Domänen- / Geschäftsregel ist, sollte das Domänenmodul lang sein, damit die Regel genau dort ist, wo sie sein soll.

Um die Eindeutigkeit eines Datensatzes überprüfen zu können, müssen Sie den aktuellen Datensatz, normalerweise eine Datenbank, abfragen, um herauszufinden, ob bereits ein anderer Datensatz mit einem Beispiel Namevorhanden ist.

Wenn man bedenkt, dass die Domänenschicht die Persistenz nicht kennt und keine Ahnung hat, wie die Daten abgerufen werden sollen, sondern nur, wie sie verarbeitet werden sollen, kann sie die Repositorys selbst nicht wirklich berühren.

Das Design, das ich damals angepasst habe, sieht folgendermaßen aus:

class ProductRepository
{
    // throws Repository.RecordNotFoundException
    public Product GetBySKU(string sku);
}

class ProductCrudService
{
    private ProductRepository pr;

    public ProductCrudService(ProductRepository repository)
    {
        pr = repository;
    }

    public void SaveProduct(Domain.Product product)
    {
        try {
            pr.GetBySKU(product.SKU);

            throw Service.ProductWithSKUAlreadyExistsException("msg");
        } catch (Repository.RecordNotFoundException e) {
            // suppress/log exception
        }

        pr.MarkFresh(product);
        pr.ProcessChanges();
    }
}

Dies führt dazu, dass Dienste Domänenregeln definieren und nicht die Domänenschicht selbst, und dass die Regeln auf mehrere Abschnitte Ihres Codes verteilt sind.

Ich habe das TellDon'tAsk-Prinzip erwähnt, da der Dienst, wie Sie deutlich sehen können, eine Aktion anbietet (er speichert entweder die ProductAusnahme oder löst eine Ausnahme aus), aber innerhalb der Methode arbeiten Sie Objekte mithilfe eines prozeduralen Ansatzes.

Die naheliegende Lösung besteht darin, eine Domain.ProductCollectionKlasse mit einer Add(Domain.Product)Methode zu erstellen , die ProductWithSKUAlreadyExistsExceptiondas auslöst. Die Leistung ist jedoch sehr gering, da Sie alle Produkte aus dem Datenspeicher abrufen müssten, um im Code herauszufinden, ob ein Produkt bereits dieselbe SKU hat als das Produkt, das Sie hinzufügen möchten.

Wie löst ihr dieses spezielle Problem? Dies ist an sich kein wirkliches Problem. Ich habe die Service-Schicht jahrelang bestimmte Domain-Regeln darstellen lassen. Die Service-Schicht dient normalerweise auch komplexeren Domain-Operationen. Ich frage mich nur, ob Sie während Ihrer Karriere auf eine bessere, zentralere Lösung gestoßen sind.

Andy
quelle
Warum eine ganze Liste von Namen ziehen, wenn Sie nur nach einem fragen und die Logik darauf stützen können, ob gefunden wurde oder nicht? Sie sagen der Persistenzschicht, was zu tun ist und nicht, wie es zu tun ist, solange eine Übereinstimmung mit der Benutzeroberfläche besteht.
JeffO
1
Bei der Verwendung des Begriffs Service sollten wir uns um mehr Klarheit bemühen, z. B. um den Unterschied zwischen Domänendiensten in der Domänenschicht und Anwendungsdiensten in der Anwendungsschicht. Siehe gorodinski.com/blog/2012/04/14/…
Erik Eidt
Hervorragender Artikel, @ErikEidt, danke. Ich denke, mein Design ist dann nicht falsch. Wenn ich Herrn Gorodinski vertrauen kann, sagt er so ziemlich dasselbe: Eine bessere Lösung besteht darin, dass ein Anwendungsdienst die von einer Entität benötigten Informationen abruft, die Ausführungsumgebung effektiv einrichtet und bereitstellt es an die Entität und hat den gleichen Einwand gegen das direkte Einfügen von Repository in das Domänenmodell wie ich, hauptsächlich durch das Unterbrechen der SRP.
Andy
Ein Zitat aus der Referenz "Tell, Don't Ask": "Aber ich persönlich benutze Tell-Dont-Ask nicht."
Radarbob

Antworten:

7

Wenn man bedenkt, dass die Domänenschicht die Persistenz nicht kennt und keine Ahnung hat, wie die Daten abgerufen werden sollen, sondern nur, wie sie verarbeitet werden sollen, kann sie die Repositorys selbst nicht wirklich berühren.

Ich würde diesem Teil nicht zustimmen. Besonders der letzte Satz.

Es ist zwar richtig, dass die Domäne persistenzunabhängig sein sollte, sie weiß jedoch, dass es eine "Sammlung von Domänenentitäten" gibt. Und dass es Domain-Regeln gibt, die diese Sammlung als Ganzes betreffen. Einzigartigkeit ist einer von ihnen. Und da die Implementierung der eigentlichen Logik stark vom spezifischen Persistenzmodus abhängt, muss es in der Domäne eine Art Abstraktion geben, die die Notwendigkeit dieser Logik spezifiziert.

Es ist also so einfach wie das Erstellen einer Schnittstelle, die abfragen kann, ob der Name bereits vorhanden ist. Diese wird dann in Ihrem Datenspeicher implementiert und von jedem aufgerufen, der wissen muss, ob der Name eindeutig ist.

Und ich möchte betonen, dass Repositories DOMAIN-Dienste sind. Sie sind Abstraktionen rund um die Beharrlichkeit. Es ist die Implementierung des Repositorys, die von der Domäne getrennt sein sollte. Es ist absolut nichts Falsches daran, wenn eine Domänenentität einen Domänendienst aufruft. Es ist nichts Falsches daran, dass eine Entität das Repository verwenden kann, um eine andere Entität abzurufen oder bestimmte Informationen abzurufen, die nicht ohne weiteres im Speicher gespeichert werden können. Dies ist ein Grund, warum Repository das Schlüsselkonzept in Evans 'Buch ist .

Euphorisch
quelle
Vielen Dank für die Eingabe. Könnte das Repository tatsächlich das darstellen, was Domain.ProductCollectionich mir vorgestellt habe, wenn man bedenkt, dass sie für das Abrufen von Objekten aus der Domänenschicht verantwortlich sind?
Andy
@ DavidPacker Ich verstehe nicht wirklich, was du meinst. Weil es nicht nötig sein sollte, alle Elemente im Speicher zu behalten. Die Implementierung der Methode "DoesNameExist" sollte (höchstwahrscheinlich) eine SQL-Abfrage auf der Datenspeicherseite sein.
Euphorisch
Was gemein ist, ist, anstatt die Daten in einer Sammlung im Speicher zu speichern, wenn ich alle Produkte wissen möchte, von denen ich sie nicht erhalten muss, Domain.ProductCollectionsondern stattdessen das Repository zu fragen, ebenso wie zu fragen, ob das Domain.ProductCollectionein Produkt mit dem enthält Diesmal wurde die SKU übergeben und stattdessen das Repository gefragt (dies ist eigentlich das Beispiel), das die zugrunde liegende Datenbank abfragt, anstatt über die vorinstallierten Produkte zu iterieren. Ich möchte nicht alle Produkte im Speicher speichern, es sei denn, ich muss, dies wäre ein völliger Unsinn.
Andy
Was zu einer anderen Frage führt, sollte das Repository dann wissen, ob ein Attribut eindeutig sein sollte? Ich habe Repositorys immer als ziemlich dumme Komponenten implementiert, gespeichert, was Sie übergeben, um sie zu speichern, und versucht, Daten basierend auf den übergebenen Bedingungen abzurufen und die Entscheidung in Dienste umzuwandeln.
Andy
@ DavidPacker Nun, das "Domain-Wissen" ist in der Schnittstelle. Die Schnittstelle sagt "Domäne benötigt diese Funktionalität", und der Datenspeicher stellt diese Funktionalität dann bereit. Wenn Sie eine IProductRepositorySchnittstelle mit haben DoesNameExist, ist es offensichtlich, was die Methode tun sollte. Es sollte jedoch irgendwo in der Domäne sein, sicherzustellen, dass das Produkt nicht denselben Namen wie das vorhandene Produkt haben kann.
Euphorisch
3

Sie müssen Greg Young bei der Set-Validierung lesen .

Kurze Antwort: Bevor Sie zu weit in das Rattennest vordringen, müssen Sie sicherstellen, dass Sie den Wert der Anforderung aus geschäftlicher Sicht verstehen. Wie teuer ist es wirklich, die Duplizierung zu erkennen und zu mindern, anstatt sie zu verhindern?

Das Problem mit den Anforderungen an die „Einzigartigkeit“ ist, dass es sehr oft einen tieferen Grund gibt, warum die Leute sie wollen - Yves Reynhout

Längere Antwort: Ich habe eine Reihe von Möglichkeiten gesehen, aber alle haben Kompromisse.

Sie können nach Duplikaten suchen, bevor Sie den Befehl an die Domäne senden. Dies kann im Client oder im Service erfolgen (Ihr Beispiel zeigt die Technik). Wenn Sie mit der Logik, die aus der Domänenschicht austritt, nicht zufrieden sind, können Sie mit a das gleiche Ergebnis erzielen DomainService.

class Product {
    void register(SKU sku, DuplicationService skuLookup) {
        if (skuLookup.isKnownSku(sku) {
            throw ProductWithSKUAlreadyExistsException(...)
        }
        ...
    }
}

Auf diese Weise muss die Implementierung des DeduplicationService natürlich etwas über das Nachschlagen des vorhandenen Skus wissen. Während also ein Teil der Arbeit in die Domäne zurückgedrängt wird, stehen Sie immer noch vor denselben grundlegenden Problemen (Sie benötigen eine Antwort für die Satzvalidierung, Probleme mit den Rennbedingungen).

Sie können die Validierung in Ihrer Persistenzschicht selbst durchführen. Relationale Datenbanken sind wirklich gut in der Satzvalidierung. Legen Sie eine Einschränkung für die Eindeutigkeit der SKU-Spalte Ihres Produkts fest, und Sie können loslegen. Die Anwendung speichert das Produkt nur im Repository, und bei einem Problem tritt eine Einschränkungsverletzung auf. Der Anwendungscode sieht also gut aus und Ihre Rennbedingung ist beseitigt, aber es sind "Domain" -Regeln durchgesickert.

Sie können in Ihrer Domain ein separates Aggregat erstellen, das den Satz bekannter Skus darstellt. Ich kann mir hier zwei Variationen vorstellen.

Eines ist so etwas wie ein ProductCatalog; Produkte existieren woanders, aber die Beziehung zwischen Produkten und Skus wird durch einen Katalog aufrechterhalten, der die Einzigartigkeit von Skus garantiert. Nicht, dass dies impliziert, dass Produkte keinen Skus haben. Skus werden von einem ProductCatalog zugewiesen (wenn Skus eindeutig sein soll, erreichen Sie dies, indem Sie nur ein einziges ProductCatalog-Aggregat haben). Überprüfen Sie die allgegenwärtige Sprache mit Ihren Domain-Experten. Wenn so etwas existiert, könnte dies der richtige Ansatz sein.

Eine Alternative ist eher ein Sku-Reservierungsservice. Der grundlegende Mechanismus ist der gleiche: Ein Aggregat kennt alle Skus und kann so die Einführung von Duplikaten verhindern. Der Mechanismus ist jedoch etwas anders: Sie erwerben einen Leasingvertrag für einen Sku, bevor Sie ihn einem Produkt zuweisen. Wenn Sie das Produkt erstellen, geben Sie den Mietvertrag an die SKU weiter. Es gibt immer noch eine Rennbedingung im Spiel (verschiedene Aggregate, daher unterschiedliche Transaktionen), aber es hat einen anderen Geschmack. Der eigentliche Nachteil hierbei ist, dass Sie einen Leasingdienst in das Domain-Modell projizieren, ohne wirklich eine Begründung in der Domain-Sprache zu haben.

Sie können alle Produktentitäten in einem einzigen Aggregat zusammenfassen, dh im oben beschriebenen Produktkatalog. Sie erhalten absolut die Einzigartigkeit des Skus, wenn Sie dies tun, aber die Kosten sind zusätzliche Streitigkeiten. Das Ändern eines Produkts bedeutet wirklich das Ändern des gesamten Katalogs.

Ich mag es nicht, alle Produkt-SKUs aus der Datenbank zu ziehen, um den Vorgang im Arbeitsspeicher auszuführen.

Vielleicht musst du nicht. Wenn Sie Ihren Sku mit einem Bloom-Filter testen , können Sie viele einzigartige Skus entdecken, ohne das Set zu laden.

Wenn Ihr Anwendungsfall es Ihnen erlaubt, willkürlich zu entscheiden, welchen Skus Sie ablehnen, können Sie alle Fehlalarme entfernen (keine große Sache, wenn Sie den Clients erlauben, den von ihnen vorgeschlagenen Skus zu testen, bevor Sie den Befehl senden). Auf diese Weise können Sie vermeiden, dass das Gerät in den Speicher geladen wird.

(Wenn Sie mehr Akzeptanz wünschen, können Sie in Betracht ziehen, den Skus im Falle einer Übereinstimmung im Bloom-Filter faul zu laden. Manchmal riskieren Sie immer noch, den gesamten Skus in den Speicher zu laden, aber dies sollte nicht der Fall sein, wenn Sie dies zulassen den Client-Code, um den Befehl vor dem Senden auf Fehler zu überprüfen).

VoiceOfUnreason
quelle
Ich mag es nicht, alle Produkt-SKUs aus der Datenbank zu ziehen, um den Vorgang im Arbeitsspeicher auszuführen. Es ist die offensichtliche Lösung, die ich in der ersten Frage vorgeschlagen und deren Leistung in Frage gestellt habe. Außerdem ist es schlecht, wenn IMO sich auf die Einschränkung in Ihrer Datenbank stützt, um für die Eindeutigkeit verantwortlich zu sein. Wenn Sie zu einem neuen Datenbankmodul wechseln und die eindeutige Einschränkung während einer Transformation verloren gegangen ist, haben Sie den Code beschädigt, da die zuvor vorhandenen Datenbankinformationen nicht mehr vorhanden sind. Trotzdem danke für den Link, interessante Lektüre.
Andy
Vielleicht ein Bloom-Filter (siehe Bearbeiten).
VoiceOfUnreason