Rich Domain Models - wie genau passt das Verhalten dazu?

84

In der Debatte von Rich vs. Anemic Domain Models steckt das Internet voller philosophischer Ratschläge, aber es fehlen maßgebliche Beispiele. Ziel dieser Frage ist es, definitive Richtlinien und konkrete Beispiele für geeignete domänengetriebene Designmodelle zu finden. (Idealerweise in C #.)

Für ein reales Beispiel scheint diese Implementierung von DDD falsch zu sein:

Bei den folgenden WorkItem-Domänenmodellen handelt es sich nur um Eigenschaftensäcke, die von Entity Framework für eine Code-First-Datenbank verwendet werden. Per Fowler ist es anämisch .

Die WorkItemService-Schicht ist anscheinend eine häufige Fehleinschätzung von Domain Services. Es enthält die gesamte Verhaltens- / Geschäftslogik für das WorkItem. Per Yemelyanov und andere ist es prozedural . (S. 6)

Also, wenn das unten stehende falsch ist, wie kann ich es richtig machen?
Das Verhalten, dh AddStatusUpdate oder Checkout , sollte zur WorkItem-Klasse gehören.
Welche Abhängigkeiten sollte das WorkItem-Modell haben?

Bildbeschreibung hier eingeben

public class WorkItemService : IWorkItemService {
    private IUnitOfWorkFactory _unitOfWorkFactory;

    //using Unity for dependency injection
    public WorkItemService(IUnitOfWorkFactory unitOfWorkFactory) {
        _unitOfWorkFactory = unitOfWorkFactory;
    }

    public void AddStatusUpdate(int workItemId, int statusId) {

        using (var unitOfWork = _unitOfWorkFactory.GetUnitOfWork<IWorkItemUnitOfWork>()) {
            var workItemRepo = unitOfWork.WorkItemRepository;
            var workItemStatusRepo = unitOfWork.WorkItemStatusRepository;

            var workItem = workItemRepo.Read(wi => wi.Id == workItemId).FirstOrDefault();
            if (workItem == null)
                throw new ArgumentException(string.Format(@"The provided WorkItem Id '{0}' is not recognized", workItemId), "workItemId");

            var status = workItemStatusRepo.Read(s => s.Id == statusId).FirstOrDefault();
            if (status == null)
                throw new ArgumentException(string.Format(@"The provided Status Id '{0}' is not recognized", statusId), "statusId");

            workItem.StatusHistory.Add(status);

            workItemRepo.Update(workItem);
            unitOfWork.Save();
        }
    }
}

(Dieses Beispiel wurde vereinfacht, um besser lesbar zu sein. Der Code ist definitiv immer noch klobig, da es sich um einen verwirrten Versuch handelt. Das Verhalten der Domäne war jedoch: Aktualisierungsstatus durch Hinzufügen des neuen Status zum Archivverlauf. Letztendlich stimme ich den anderen Antworten zu könnte nur von CRUD gehandhabt werden.)

Aktualisieren

@AlexeyZimarev gab die beste Antwort, ein perfektes Video zu diesem Thema in C # von Jimmy Bogard, aber es wurde anscheinend in einen Kommentar unten verschoben, weil es nicht genügend Informationen über den Link hinaus gab. Ich habe einen groben Entwurf meiner Notizen, die das Video in meiner Antwort unten zusammenfassen. Bitte kommentieren Sie die Antwort mit eventuellen Korrekturen. Das Video ist eine Stunde lang, aber sehr sehenswert.

Update - 2 Jahre später

Ich denke, es ist ein Zeichen für die beginnende Reife von DDD, dass ich auch nach 2-jährigem Studium nicht versprechen kann, dass ich den "richtigen Weg" dazu kenne. Allgegenwärtige Sprache, aggregierte Wurzeln und sein Ansatz für verhaltensorientiertes Design sind die wertvollen Beiträge von DDD für die Branche. Beharrlichkeit, Ignoranz und Event-Sourcing sorgen für Verwirrung, und ich denke, dass eine solche Philosophie sie von einer breiteren Akzeptanz abhält. Aber wenn ich diesen Code mit dem, was ich gelernt habe, noch einmal machen müsste, würde er ungefähr so ​​aussehen:

Bildbeschreibung hier eingeben

Ich freue mich immer noch über Antworten auf diesen (sehr aktiven) Beitrag, der einen Code für bewährte Methoden für ein gültiges Domain-Modell enthält.

RJB
quelle
6
Alle philosophischen Theorien fallen zu Boden, wenn Sie sie erzählen "I don't want to duplicate all my entities into DTOs simply because I don't need it and it violates DRY, and I also don't want my client application to take a dependency on EntityFramework.dll". "Entities" im Entity Framework-Jargon ist nicht dasselbe wie "Entities" im "Domain Model"
Federico Berasategui
Es ist in Ordnung, meine Domain-Entitäten mit einem automatisierten Tool wie Automapper in ein DTO zu duplizieren, sofern dies erforderlich ist. Ich bin mir nur nicht sicher, wie das am Ende des Tages aussehen soll.
RJB
16
Ich würde Ihnen empfehlen, Jimmy Bogards NDC 2012-Session "Crafting Wicked Domain Models" auf Vimeo anzusehen . Er erklärt, was eine reiche Domäne sein sollte und wie man sie im wirklichen Leben umsetzt, indem man sich in Ihren Entitäten verhält. Beispiele sind sehr praktisch und alle in C #.
Alexey Zimarev
Danke, ich bin auf halbem Weg durch das Video und das ist soweit perfekt. Ich wusste, dass es irgendwo eine "richtige" Antwort geben musste, wenn dies falsch war ...
RJB
2
Ich verlange auch Liebe für Java: /
uylmz

Antworten:

59

Die hilfreichste Antwort wurde von Alexey Zimarev gegeben und erhielt mindestens 7 positive Stimmen, bevor ein Moderator sie in einen Kommentar unterhalb meiner ursprünglichen Frage verschob ....

Seine Antwort:

Ich würde Ihnen empfehlen, Jimmy Bogards NDC 2012-Session "Crafting Wicked Domain Models" auf Vimeo anzusehen. Er erklärt, was eine reiche Domäne sein sollte und wie man sie im wirklichen Leben umsetzt, indem man Verhalten in Ihren Entitäten hat. Beispiele sind sehr praktisch und alle in C #.

http://vimeo.com/43598193

Ich habe einige Notizen gemacht, um das Video für mein Team zusammenzufassen und um in diesem Beitrag ein wenig mehr Details zu liefern. (Das Video ist eine Stunde lang, aber wirklich jede Minute wert, wenn Sie Zeit haben. Jimmy Bogard verdient viel Anerkennung für seine Erklärung.)

  • "Für die meisten Anwendungen ... wir wissen nicht, dass sie zu Beginn komplex sein werden. Sie werden einfach so."
    • Die Komplexität wächst natürlich, wenn Code und Anforderungen hinzugefügt werden. Anwendungen können sehr einfach beginnen, wie CRUD, aber Verhalten / Regeln können eingebrannt werden.
    • "Das Schöne ist, dass wir nicht erst komplex anfangen müssen. Wir können mit dem anämischen Domain-Modell beginnen, das sind nur Eigentumstaschen, und wir können mit Standard-Refactoring-Techniken zu einem echten Domain-Modell übergehen."
  • Domänenmodelle = Geschäftsobjekte. Domainverhalten = Geschäftsregeln.
  • Verhalten ist in einer Anwendung häufig verborgen - dies kann in PageLoad, Button1_Click oder in Hilfsklassen wie 'FooManager' oder 'FooService' der Fall sein.
  • Geschäftsregeln, die von Domänenobjekten getrennt sind, erfordern, dass wir uns an diese Regeln erinnern.
    • In meinem obigen persönlichen Beispiel ist eine Geschäftsregel WorkItem.StatusHistory.Add (). Wir ändern nicht nur den Status, wir archivieren ihn für die Prüfung.
  • Domain-Verhalten "Beseitigen Sie Fehler in einer Anwendung viel einfacher als nur eine Reihe von Tests." Für Tests müssen Sie wissen, wie diese Tests geschrieben werden. Das Domain - Verhalten bietet Ihnen die richtigen Pfade zu testen .
  • Domänendienste sind "Hilfsklassen zum Koordinieren von Aktivitäten zwischen verschiedenen Domänenmodellentitäten".
    • Domain Services! = Domainverhalten. Entitäten haben Verhalten, Domänendienste sind nur Vermittler zwischen den Entitäten.
  • Domänenobjekte sollten nicht über die erforderliche Infrastruktur verfügen (z. B. IOfferCalculatorService). Der Infrastrukturdienst sollte an das Domänenmodell übergeben werden, das ihn verwendet.
  • Domain-Modelle sollten Ihnen sagen, was sie können, und sie sollten nur in der Lage sein, diese Dinge zu tun.
  • Die Eigenschaften von Domänenmodellen sollten mit privaten Setzern geschützt werden, damit nur das Modell seine eigenen Eigenschaften durch sein eigenes Verhalten festlegen kann . Ansonsten ist es "promiskuitiv".
  • Anämische Domänenmodellobjekte, bei denen es sich nur um Eigentumssäcke für ein ORM handelt, sind nur "ein dünnes Furnier - eine stark typisierte Version über die Datenbank".
    • "So einfach es auch ist, eine Datenbankzeile in ein Objekt zu bekommen, das haben wir."
    • „Die meisten beständigen Objektmodelle sind genau das. Ein anämisches Domänenmodell unterscheidet sich von einer Anwendung, die sich nicht wirklich verhält, wenn ein Objekt Geschäftsregeln enthält, diese Regeln jedoch in einem Domänenmodell nicht gefunden werden. '
  • "Für viele Anwendungen ist es nicht unbedingt erforderlich, eine echte Geschäftsanwendungslogikebene zu erstellen. Sie kann lediglich mit der Datenbank kommunizieren und bietet möglicherweise eine einfache Möglichkeit, die darin enthaltenen Daten darzustellen."
    • Mit anderen Worten, wenn Sie nur CRUD ohne spezielle Geschäftsobjekte oder Verhaltensregeln ausführen, benötigen Sie kein DDD.

Bitte zögern Sie nicht, andere Punkte zu kommentieren, die Ihrer Meinung nach enthalten sein sollten, oder wenn Sie der Meinung sind, dass eine dieser Anmerkungen falsch ist. Versucht, so viel wie möglich direkt zu zitieren oder zu paraphrasieren.

RJB
quelle
Tolles Video, um zu sehen, wie das Refactoring in einem Tool funktioniert. Es geht viel um die ordnungsgemäße Kapselung von Domänenobjekten (um sicherzustellen, dass sie konsistent sind). Er macht einen großartigen Job, indem er die Geschäftsregeln für Angebote, Mitglieder usw. erklärt. Er erwähnt das Wort ein paarmal invariant (das ist eine vertragsbasierte Domain-Modellierung). Ich wünschte, der .net-Code würde besser kommunizieren, was eine formale Geschäftsregel ist, da sich diese ändern und Sie sie pflegen müssen.
Fuhrmanator
6

Ihre Frage kann nicht beantwortet werden, da Ihr Beispiel falsch ist. Insbesondere, weil es kein Verhalten gibt. Zumindest nicht im Bereich Ihrer Domain. Das Beispiel einer AddStatusUpdateMethode ist keine Domänenlogik, sondern eine Logik, die diese Domäne verwendet. Diese Art von Logik ist sinnvoll, wenn Sie sich in einer Art Service befinden, der externe Anforderungen verarbeitet.

Wenn zum Beispiel die Anforderung bestand, dass ein bestimmtes Arbeitselement nur bestimmte Status haben darf oder dass es nur N Status haben darf, dann ist dies Domänenlogik und sollte Teil einer WorkItemoder StatusHistoryals Methode sein.

Der Grund für Ihre Verwirrung ist, dass Sie versuchen, eine Richtlinie auf Code anzuwenden, der sie nicht benötigt. Domänenmodelle sind nur relevant, wenn Sie viele komplexe Domänenlogiken haben. Z.B. Logik, die an Entitäten selbst arbeitet und sich aus Anforderungen ergibt. Wenn es im Code darum geht, Entitäten von außen zu manipulieren, handelt es sich höchstwahrscheinlich nicht um eine Domänenlogik. Aber in dem Moment, in dem Sie viele ifs erhalten, basierend auf den Daten und Entitäten, mit denen Sie arbeiten, ist dies Domänenlogik.

Eines der Probleme der echten Domänenmodellierung besteht darin, komplexe Anforderungen zu verwalten. Und als solches können seine wahre Kraft und Vorteile nicht in einfachem Code dargestellt werden. Sie benötigen Dutzende von Unternehmen mit einer Vielzahl von Anforderungen, um die Vorteile wirklich zu erkennen. Auch hier ist Ihr Beispiel zu einfach, als dass das Domain-Modell wirklich glänzen könnte.

Abschließend möchte ich noch erwähnen, dass ein echtes Domain-Modell mit echtem OOP-Design mit dem Entity Framework nur schwer zu erhalten ist. Während ORMs mit der Abbildung einer echten OOP-Struktur auf relationale entworfen wurden, gibt es immer noch viele Probleme, und das relationale Modell wird häufig in das OOP-Modell übernommen. Selbst mit nHibernate, das ich für viel leistungsfähiger als EF halte, kann dies ein Problem sein.

Euphorisch
quelle
Gute Argumente. Wohin würde die AddStatusUpdate-Methode dann in Data oder einem anderen Projekt in der Infrastruktur gehören? Was ist ein Beispiel für ein Verhalten, das theoretisch zu WorkItem gehören könnte? Jeder Pseudocode oder Mock-up wäre sehr dankbar. Mein Beispiel wurde vereinfacht, um besser lesbar zu sein. Es gibt andere Entitäten, und beispielsweise hat das AddStatusUpdate ein zusätzliches Verhalten - es nimmt tatsächlich einen Statuskategorienamen an, und wenn diese Kategorie nicht vorhanden ist, wird die Kategorie erstellt.
RJB
@RJB Wie gesagt, AddStatusUpdate ist Code, der die Domäne verwendet. Also entweder eine Art Webservice oder eine Anwendung, die die Domänenklassen verwendet. Und wie ich bereits sagte, können Sie keine Art von Mockup oder Pseudocode erwarten, da Sie das gesamte Projekt so komplex gestalten müssten, dass der wahre Vorteil des OOP-Domänenmodells sichtbar wird.
Euphoric
5

Ihre Annahme, dass das Einkapseln Ihrer mit WorkItem verbundenen Geschäftslogik in einen "Fat Service" ein inhärentes Anti-Pattern ist, das ich argumentieren würde, ist nicht unbedingt.

Unabhängig von Ihren Überlegungen zum anämischen Domänenmodell fördern die für eine Branchen-.NET-Anwendung typischen Standardmuster und -methoden einen mehrschichtigen Transaktionsansatz, der aus verschiedenen Komponenten besteht. Sie fördern die Trennung der Geschäftslogik vom Domänenmodell, um die Kommunikation eines gemeinsamen Domänenmodells zwischen anderen .NET-Komponenten sowie zwischen Komponenten auf verschiedenen Technologie-Stacks oder über physische Ebenen hinweg zu erleichtern.

Ein Beispiel hierfür ist ein .NET-basierter SOAP-Webdienst, der mit einer Silverlight-Clientanwendung kommuniziert, die zufällig eine DLL mit einfachen Datentypen enthält. Dieses Domänenentitätsprojekt kann in eine .NET-Assembly oder eine Silverlight-Assembly integriert werden, in der interessierte Silverlight-Komponenten mit dieser DLL keinem Objektverhalten ausgesetzt sind, das möglicherweise von Komponenten abhängt, die nur für den Dienst verfügbar sind.

Unabhängig von Ihrer Haltung zu dieser Debatte ist dies das von Microsoft angenommene und akzeptierte Muster, und meiner Meinung nach ist es kein falscher Ansatz, aber ein Objektmodell, das sein eigenes Verhalten definiert, ist auch nicht unbedingt ein Anti-Muster. Wenn Sie mit diesem Entwurf fortfahren, ist es am besten, einige der Einschränkungen und Schwachstellen zu erkennen und zu verstehen, auf die Sie stoßen könnten, wenn Sie andere Komponenten integrieren müssen, die Ihr Domänenmodell anzeigen müssen. In diesem speziellen Fall möchten Sie vielleicht, dass ein Übersetzer Ihr objektorientiertes Domänenmodell in einfache Datenobjekte konvertiert, die bestimmte Verhaltensmethoden nicht verfügbar machen.

maple_shaft
quelle
1
1) Wie können Sie die Geschäftslogik vom Domänenmodell trennen? Es ist der Bereich, in dem diese Geschäftslogik lebt; Die Entitäten in dieser Domäne führen das mit dieser Geschäftslogik verknüpfte Verhalten aus. Die reale Welt hat keine Dienste, noch existieren sie in den Köpfen von Domain-Experten. 2) Eine Komponente , die integrieren möge mit Ihnen ein eigenen Domain - Modell bauen muss, weil seine Bedürfnisse werden unterschiedlich sein und es wird eine andere Sicht auf Ihrem Domain - Modell hat. Es ist eine lange Tradition, dass Sie ein Domänenmodell erstellen können, das gemeinsam genutzt werden kann.
Stefan Billiet
1
@StefanBilliet Das sind gute Argumente für den Irrtum eines universellen Domänenmodells, aber es ist in einfacheren Komponenten und Komponenteninteraktionen möglich, wie ich dies zuvor getan habe. Meiner Meinung nach kann die Übersetzungslogik zwischen Domänenmodellen zu einer Menge langwierigem Code führen. Wenn dies sicher vermieden werden kann, ist dies eine gute Wahl für das Design.
maple_shaft
1
Um ehrlich zu sein, ich denke, die einzige gute Wahl für das Design ist ein Modell, über das ein Geschäftsexperte nachdenken kann. Sie erstellen ein Modell einer Domäne, mit dem ein Unternehmen bestimmte Probleme innerhalb dieser Domäne lösen kann. Das Aufteilen des Verhaltens von Domain-Entitäten in Dienste macht es für alle Beteiligten schwieriger, da Sie ständig abbilden müssen, was ein Domain-Experte zu Service-Code sagt, der kaum Ähnlichkeit mit der aktuellen Konversation hat. Nach meiner Erfahrung verlieren Sie viel mehr Zeit damit, als Boilerplate zu tippen. Das heißt nicht, dass es keinen Weg gibt, um den Kesselplatz-Code zu umgehen.
Stefan Billiet
@StefanBilliet In einer perfekten Welt stimme ich Ihnen zu, in der ein Geschäftsexperte die Zeit hat, sich mit Entwicklern zusammenzusetzen. Die Realität in der Software-Branche ist, dass der Business-Experte keine Zeit oder kein Interesse daran hat, auf dieser oder einer schlechteren Ebene involviert zu sein. Von den Entwicklern wird jedoch erwartet, dass sie es nur mit einer vagen Anleitung herausfinden.
maple_shaft
Stimmt, aber das ist kein Grund, diese Realität zu akzeptieren. Solch ein Streben fortzusetzen bedeutet, die Zeit (und möglicherweise den Ruf) der Entwickler und das Geld des Kunden zu verschwenden. Der von mir beschriebene Prozess ist eine Beziehung, die im Laufe der Zeit aufgebaut werden muss. Es ist sehr anstrengend, liefert aber viel bessere Ergebnisse. Es gibt einen Grund, warum "Ubiquitous Language" oft als der wichtigste Aspekt von DDD angesehen wird.
Stefan Billiet
5

Mir ist klar, dass diese Frage ziemlich alt ist, daher ist diese Antwort für die Nachwelt bestimmt. Ich möchte mit einem konkreten Beispiel antworten, anstatt mit einem theoretischen.

Kapsele die "Änderung des Workitem-Status" in der WorkItemKlasse wie folgt:

public SomeStatusUpdateType Status { get; private set; }

public void ChangeStatus(SomeStatusUpdateType status)
{
    // Maybe we designed this badly at first ;-)
    Status = status;       
}

Jetzt ist Ihre WorkItemKlasse dafür verantwortlich, sich in einem legalen Zustand zu halten. Die Implementierung ist jedoch ziemlich schwach. Der Product Owner möchte einen Verlauf aller Statusaktualisierungen, die an der vorgenommen wurden WorkItem.

Wir ändern es in etwa so:

private ICollection<SomeStatusUpdateType> StatusUpdates { get; private set; }
public SomeStatusUpdateType Status => StatusUpdates.OrderByDescending(s => s.CreatedOn).FirstOrDefault();

public void ChangeStatus(SomeStatusUpdateType status)
{
    // Better...
    StatusUpdates.Add(status);       
}

Die Implementierung hat sich drastisch geändert, aber der Aufrufer der ChangeStatusMethode kennt die zugrunde liegenden Implementierungsdetails nicht und hat keinen Grund, sich selbst zu ändern.

Dies ist ein Beispiel für eine Rich Domain Model-Entität, IMHO.

Don
quelle