it-swarm.com.de

Rich Domain Models - wie genau passt das Verhalten dazu?

In der Debatte zwischen Rich- und Anemic-Domain-Modellen ist das Internet voller philosophischer Ratschläge, aber es fehlen nur maßgebliche Beispiele. Ziel dieser Frage ist es, endgültige Richtlinien und konkrete Beispiele für geeignete domänengesteuerte Entwurfsmodelle zu finden. (Idealerweise in C #.)

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

Die folgenden WorkItem-Domänenmodelle sind nichts anderes als Eigenschaftstaschen, 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 Fehlwahrnehmung von Domänendiensten. Es enthält die gesamte Verhaltens-/Geschäftslogik für das WorkItem. Per Yemelyanov und andere ist es prozedural . (S. 6)

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

enter image description here

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, aber das Domänenverhalten lautete: Status aktualisieren 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, der das Video in meiner Antwort unten zusammenfasst. Bitte zögern Sie nicht, die Antwort mit Korrekturen zu kommentieren. 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 zweijährigem Studium nicht versprechen kann, dass ich den "richtigen Weg" kenne, es zu tun. Allgegenwärtige Sprache, aggregierte Wurzeln und ihr Ansatz für verhaltensorientiertes Design sind die wertvollen Beiträge von DDD für die Branche. Beharrlichkeitsunwissenheit und Event-Sourcing sorgen für Verwirrung, und ich denke, eine solche Philosophie hält sie von einer breiteren Akzeptanz ab. Aber wenn ich diesen Code mit dem, was ich gelernt habe, noch einmal machen müsste, würde er ungefähr so ​​aussehen:

(enter image description here

Ich freue mich immer noch über Antworten auf diesen (sehr aktiven) Beitrag, die Best Practices-Code für ein gültiges Domain-Modell enthalten.

89
RJB

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

Seine Antwort:

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

http://vimeo.com/4359819

Ich habe mir einige Notizen gemacht, um das Video zum Nutzen meines Teams 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 ... wissen wir nicht, dass sie zu Beginn komplex sein werden. Sie werden einfach so."
    • Die Komplexität wächst auf natürliche Weise, wenn Code und Anforderungen hinzugefügt werden. Anwendungen können als CRUD sehr einfach beginnen, aber Verhalten/Regeln können eingebrannt werden.
    • "Das Schöne ist, dass wir nicht komplex anfangen müssen. Wir können mit dem anämischen Domain-Modell beginnen, das sind nur Eigentumstaschen, und mit nur Standard-Refactoring-Techniken können wir uns einem echten Domain-Modell zuwenden."
  • Domänenmodelle = Geschäftsobjekte. Domänenverhalten = Geschäftsregeln.
  • Verhalten ist in einer Anwendung häufig verborgen - es kann in PageLoad, Button1_Click oder häufig in Hilfsklassen wie 'FooManager' oder 'FooService' sein.
  • Geschäftsregeln, die von Domänenobjekten getrennt sind, erfordern, dass wir uns an diese Regeln erinnern.
    • In meinem persönlichen Beispiel oben ist eine Geschäftsregel WorkItem.StatusHistory.Add (). Wir ändern nicht nur den Status, wir archivieren ihn für die Prüfung.
  • Domänenverhalten "Beseitigen Sie Fehler in einer Anwendung viel einfacher als nur das Schreiben einer Reihe von Tests." Tests erfordern, dass Sie wissen, um diese Tests zu schreiben. Das Domain-Verhalten bietet Ihnen die richtigen Pfade zum Testen .
  • Domänendienste sind "Hilfsklassen zum Koordinieren von Aktivitäten zwischen verschiedenen Domänenmodellentitäten".
    • Domänendienste! = Domainverhalten. Entitäten haben Verhalten, Domänendienste sind nur Vermittler zwischen den Entitäten.
  • Domänenobjekte sollten nicht über die benötigte Infrastruktur verfügen (d. H. IOfferCalculatorService). Der Infrastrukturdienst sollte an das Domänenmodell übergeben werden, das ihn verwendet.
  • Domain-Modelle sollten anbieten, Ihnen zu 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 durch sein eigenes Verhalten seine eigenen Eigenschaften festlegen kann. Ansonsten ist es "promiskuitiv".
  • Anämische Domänenmodellobjekte, die nur Eigenschaftstaschen für ein ORM sind, sind nur "ein dünnes Furnier - eine stark typisierte Version über der Datenbank".
    • "So einfach es auch ist, eine Datenbankzeile in ein Objekt zu bekommen, das haben wir."
    • 'Die meisten persistenten Objektmodelle sind genau das. Was ein anämisches Domänenmodell von einer Anwendung unterscheidet, die kein wirkliches Verhalten aufweist, ist, wenn ein Objekt Geschäftsregeln hat, diese Regeln jedoch nicht in einem Domänenmodell gefunden werden. '
  • "Für viele Anwendungen ist es nicht wirklich erforderlich, eine echte Logikschicht für Geschäftsanwendungen zu erstellen. Es ist nur etwas, das mit der Datenbank kommunizieren kann und möglicherweise eine einfache Möglichkeit darstellt, 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, mit anderen Punkten zu kommentieren, die Ihrer Meinung nach aufgenommen werden sollten, oder wenn Sie der Meinung sind, dass eine dieser Notizen falsch ist. Versucht, so viel wie möglich direkt zu zitieren oder zu paraphrasieren.

64
RJB

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 der Methode AddStatusUpdate ist keine Domänenlogik, sondern eine Logik, die diese Domäne verwendet. Diese Art von Logik ist sinnvoll, um sich in einem Dienst zu befinden, der externe Anforderungen verarbeitet.

Wenn beispielsweise erforderlich war, dass ein bestimmtes Arbeitselement nur bestimmte Status haben kann oder dass es nur N Status haben kann, ist dies eine Domänenlogik und sollte entweder Teil von WorkItem oder StatusHistory als Methode.

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 über viele komplexe Domänenlogiken verfügen. Z.B. Logik, die auf Entitäten selbst funktioniert und sich aus Anforderungen ergibt. Wenn es im Code darum geht, Entitäten von externen Daten aus 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, dann ist das Domänenlogik.

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

Schließlich ist etwas OT, was ich erwähnen möchte, dass ein echtes Domain-Modell mit echtem OOP Design) mit dem Entity Framework wirklich schwer zu erhalten wäre. Während ORMs waren Entworfen mit Zuordnung von true OOP Struktur zu relationalen, gibt es immer noch viele Probleme, und das relationale Modell wird oft in das OOP Modell) lecken. Selbst mit nHibernate, Was ich für viel leistungsfähiger halte als EF, kann ein Problem sein.

8
Euphoric

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

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

Ein Beispiel hierfür wäre ein .NET-basierter SOAP Webdienst, der mit einer Silverlight-Clientanwendung kommuniziert, die zufällig eine DLL mit einfachen Datentypen enthält) Das Domänenentitätsprojekt kann in eine .NET-Assembly oder eine Silverlight-Assembly integriert werden, wobei interessierte Silverlight-Komponenten mit diesem DLL) keinen Objektverhalten ausgesetzt sind, die möglicherweise von Komponenten abhängen, die nur für die verfügbar sind Bedienung.

Unabhängig von Ihrer Haltung zu dieser Debatte ist dies das von Microsoft angenommene und akzeptierte Muster, und meiner professionellen 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 Design fortfahren, ist es am besten, einige der Einschränkungen und Schwachstellen zu erkennen und zu verstehen, auf die Sie möglicherweise stoßen, wenn Sie sich in andere Komponenten integrieren müssen, die Ihr Domänenmodell sehen müssen. In diesem speziellen Fall möchten Sie möglicherweise, dass ein Übersetzer Ihr objektorientiertes Stildomänenmodell in einfache Datenobjekte konvertiert, die bestimmte Verhaltensmethoden nicht verfügbar machen.

5
maple_shaft

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

Kapseln Sie die "Änderung des Workitem-Status" in der Klasse WorkItem 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 WorkItem Klasse dafür verantwortlich, sich in einem Rechtszustand zu halten. Die Implementierung ist jedoch ziemlich schwach. Der Product Owner möchte einen Verlauf aller Statusaktualisierungen, die an WorkItem vorgenommen wurden.

Wir ändern es in so etwas:

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 Methode ChangeStatus kennt die zugrunde liegenden Implementierungsdetails nicht und hat keinen Grund, sich selbst zu ändern.

Dies ist ein Beispiel für eine Rich Domain Model Entity, IMHO.

5
Don