Domain Model

Das Herzstück der Implementierung ist das sogenannte Domänenmodell, das ein Objektmodell der Domäne ist und sowohl Verhalten als auch Daten beinhaltet. Mit diesem ist es möglich, komplexe Fachlichkeit einfach zu implementieren, da diese als sogenannte POCOs (plain old csharp objects - simple Klassen) implementiert werden. Dabei werden Klassen- und Operationsnamen der Domäne entnommen.

Haben die Klassen des Domänenmodells keine Operationen, so ist dies ein Anti-Pattern und wird in diesem Fall anämisches Domänenmodell genannt. Dies gilt es zu vermeiden!

Jedes Team prägt dabei eine im Bounded Context gültige Ubiquitous Language (allgegenwärtige Sprache) aus. Somit verfolgen wir das Ziel, die Intention der Implementierung im Code auszudrücken und nicht, wie ein Sachverhalt implementiert ist. Lesbarkeit ist dabei ein primäres Ziel.

Lesbarkeit als primäres Ziel bedeutet nicht, dass falscher Code implementiert oder akzeptiert wird, sondern versucht die Bedeutung der Entwicklungsmethodik zu unterstreichen. Denn häufig werden Klassen für einen bestimmten Kontext implementiert. Werden diese zu einem viel späteren Zeitpunkt in einem anderen Kontext verwendet, der Code refaktorisiert oder die Anforderungen geändert, können fehlerhafte Konstellation nur schwer immer vermieden werden. Da heute Code nicht mehr starr und Änderungen unterworfen ist, ist Lesbarkeit essenziell, weil er dann verstanden und angepasst werden kann. Test-Driven Development (nach Kent Beck) unterstützt diese agilen Praktiken.

Verantwortlichkeit Domain Model

Da Klassen des Domänenmodells als POCOs implementiert werden und somit keine Abhängigkeit zu Benutzeroberflächen, Datenbanken und keine verteilte Kommunikation durchführen, ist es sehr einfach, hierfür Unittests als Grundbaustein der Testpyramide zu implementieren. Wir werden hierauf im Rahmen des Testkonzepts zurückkommen. 

POCOs

Das Domänenmodell wird also technologieneutral implementiert. Diese Technologieneutralität gilt für alle Bestandteile von Application Core. Der Application Core beinhaltet sämtliche Konstrukte, die die eigentliche Geschäftslogik implementieren.

Verantwortlichkeit von Application Core

Das nachfolgende Diagramm zeigt exemplarisch einen Teil des Domänenmodells des Schleupen.CS-Architekturbeispiels Bibliotheksverwaltung.

Domänenmodell der Bibliotheksverwaltung

Das Domänenmodell besteht im Beispiel aus den Klassen Benutzerkonto, Ausleihe, Buch, Inhaltsangabe, Seite. Diese haben Attribute und Methoden gemäß des objektorientierten Paradigmas. Abweichend von reiner Objektorientierung (OO) sind diese über Id-Klassen verknüpft, was zeigt, dass DDD nicht reines OO ist. Wir werden dies unten erläutern. 

Die Abbildung auf die Projekte erfolgt geradlinig:

Schalen der Application Core und deren Abbildung auf eine Visual Studio Solution

Entitys

Domänenmodelle bestehen im Allgemeinen aus Entitys und Value Objects. Entitys sind Objekte, die eine eindeutige Identität haben. Diese Identität wird in Schleupen.CS in erster Näherung als Guid implementiert. Das heißt im Umkehrschluss, dass Objekte, die eine unterschiedliche Identität haben, deren Attribute aber gleich sind, auch unterschiedliche Objekte sind. Das ist deshalb sinnvoll, da ein konkretes Buch-Exemplar beispielsweise nicht eindeutig durch Titel, Autor, Erscheinungsdatum etc. für alle möglichen Fälle für alle Zeit bestimmt ist (der Schlüssel darf sich nie ändern!). Ein Buch hat daher bei uns eine Id (= Identität) als Guid, was die einzelnen Buch-Exemplare dann immer unterscheidbar macht. Die Identität einer Entity ist global eindeutig und kann somit über Microservices hinweg verwendet werden.

Eine Entität implementiert die Domänenlogik in Form von Methoden, wobei diese auf den Attributen arbeiten und sich ihr Zustand dabei ändern kann. Der folgende Code zeigt ein einfaches Beispiel.

public partial class Ausleihe: Entity<AusleiheId>, ...
{
    private DateTimeOffset ausgeliehenAm;
    private DateTimeOffset ausgeliehenBis;
    private BuchId buchId;
    private BenutzerkontoId benutzerkontoId;

    public Ausleihe(AusleiheId id,BenutzerkontoId benutzerkontoId,DateTimeOffset ausgeliehenAm,BuchId buchId) { ... }

    [ExcludeFromCodeCoverage] // implizite Abhängigkeit zu NHibernate
    protected Ausleihe() { ... }

    public virtual void VerlaengereAusleiheUm(TimeSpan fristverlaengerung)
    {
        ausgeliehenBis += fristverlaengerung;
    }
    ...
}

In Schleupen.CS wird zur Implementierung ein benutzerdefiniertes Tool für das Visual Studio namens Fagento verwendet, um auf die internen Attribute über eine generierte Schnittstelle zugreifen zu können. Die Klasse implementiert diese Schnittstelle explizit. Der Zugriff auf Interna darf nur in genau folgenden Kontexten verwendet werden, sodass die Kapselung gewahrt bleiben kann:

  • Assembler - bei der Übersetzung von Domänenobjekte zu Datenverträgen im Anticorruption-Layer muss auf die Interna zugegriffen werden
  • NHibernate-Mapping - da NHibernate über diesen Zugriff die Objekte erzeugt und / oder in die Datenbank schreibt
  • Tests - in einigen Fällen muss der innere Zustand geprüft werden. Hierzu soll kein Code nur für Tests codiert werden oder die Kapselung gebrochen werden.

Fagento dient dem typsicheren Zugriff und der Vermeidung von Reflection! Reflection ist fehleranfälliger, schlechter lesbar und langsamer. Eine andere Alternative wäre die Bereitstellung von Gettern, was allerdings das Problem das NHibernate-Mappings nicht generell löst.

Fagento generiert folgenden Code als partial class:

[System.CodeDom.Compiler.GeneratedCode("Schleupen.CS.PI.DevelopmentTools.Fagento", "3.32.1.68")]
public partial interface IAusleiheData : Schleupen.CS.PI.Framework.Entities.IEntityData<Schleupen.AS.MT.BIB.Ausleihen.AusleiheId>
{
    System.DateTimeOffset AusgeliehenAm { get; set; }
    System.DateTimeOffset AusgeliehenBis { get; set; }
    Schleupen.AS.MT.BIB.Buecher.BuchId BuchId { get; set; }
    Schleupen.AS.MT.BIB.Benutzerkonten.BenutzerkontoId BenutzerkontoId { get; set; }
    ...
}

[System.CodeDom.Compiler.GeneratedCode("Schleupen.CS.PI.DevelopmentTools.Fagento", "3.32.1.68")]
partial class Ausleihe : IAusleiheData
{
    [DebuggerNonUserCode]
    System.DateTimeOffset IAusleiheData.AusgeliehenAm
    {
        get
        {
            return AusgeliehenAm;
        }
        set
        {
            AusgeliehenAm = value;
        }
    }
    ...
}

Durch die Verwendung von Fagento wird forciert, dass eine Klasse nicht per se alle Eigenschaften offenlegt, sondern Tell-Don't-Ask angewendet wird: Im obigen Beispiel kann man Tell-Don't-Ask an der Methode VerlaengereAusleiheUm(...)  gut erkennen. Hier wird eine Methode implementiert, anhand dessen Namen die Intention erkannt werden kann (Ubiquitous Language!) statt eine Eigenschaft AusgeliehenAm von "außen" zu nutzen.

Die Klasse verwendet zudem eine sogenannte EntityId, die eine Guid kapselt. Diese Ausprägung ist typsicherer und ausdrucksstärker als eine Guid. Was diese sogenannten Value Objects sind, wird nachfolgend erklärt.

Value Objects

Value Objects sind Objekte, die keine konzeptionelle Identität haben. D.h. der Wert an sich reicht aus und man benötigt keine Unterscheidung bzgl. einzelner Instanzen.

Beispiel: Geld - hierbei ist nur der Wert 10 Euro zur Abrechnung relevant, nicht aber welcher 10 Euro-Schein verwendet worden ist. 

Value Objects sind unveränderlich (immutable) und haben keinen Lebenszyklus. Bei Änderungen wird ein neues Objekt zurückgegeben. Wichtig ist, dass ValueObjects Equals() und GetHashCode() implementieren. Das nachfolgende Beispiel zeigt eine Implementierung bei der diese Methoden über die Basisklasse unter Zuhilfenahme von GetMembers() implementiert ist:

  public partial class Isbn : ValueObject
  {
    public const string ValidNummerRegexPattern = @"(ISBN[-]*(1[03])*[ ]*(: ){0,1})*(([0-9Xx][- ]*){13}|([0-9Xx][- ]*){10})";
    private readonly Regex validNummerRegex = new Regex(ValidNummerRegexPattern);
    private string nummer;

    public Isbn(string nummer)
    {
      if(string.IsNullOrEmpty(nummer)) { throw new ArgumentNullException(nameof(nummer)); }
      if (!ValidNummerRegex.IsMatch(nummer)) { throw new ArgumentException($"{nummer} ist keine gültige ISBN."); }
      this.nummer = nummer;
    }

    protected override IEnumerable<object> GetMembers()
    {
        yield return nummer;
    }
    ...
}

Enumerationen sind ebenfalls Value Objects.

ValueObjects können mehrere Felder haben. Beispiel: Farbe mit den Feldern der RGB-Werte.

Ist ein Value Object immer ein Value Object?

Diese Suggestivfrage deutet darauf hin, dass das nicht der Fall ist. Nehmen wir das Beispiel eines 10-Euro-Scheins. In dem meisten Fällen ist es irrelevant, welcher 10-Euro-Schein verwendet wird, beim Buchen beispielsweise ist nur relevant, dass es sich um 10 Euro handelt. Für eine Bank ist das aber anders. Hier ist der konkrete Schein sehr relevant! Das heißt, dass dies eine kontextbezogene Entscheidung ist!

Cookie Consent mit Real Cookie Banner