Link Search Menu Expand Document

6 Domain Driven Design

Inhalt

Ubiquitous Language

Vier Beispiele fĂŒr die Ubiquitous Language; jeweils Bezeichnung, Bedeutung und kurze BegrĂŒndung, warum es zur Ubiquitous Language gehört

Die Ubiquitous Language ist die allgegenwĂ€rtige Fachsprache, die in der DomĂ€ne gesprochen wird und daher auch im Sourcecode verwendet werden soll. Auf diese Weise kann die “VerstĂ€ndniskluft” zwischen Entwickler:innen und DomĂ€nenexpert:innen verringert werden und es wird einfacher, im Modell der RealitĂ€t (Code) die DomĂ€ne gut abzubilden.

Auch fĂŒr Dominion wird eine eigene Sprache mit besonderem, spiel-spezifischem Vokabular gesprochen. Dies wird besonders in der Spielanleitung deutlich, wo man beispielsweise in der Sektion HĂ€ufige Anweisungen auf den Aktionskarten fast schon eine Art Glossar angelegt findet.

Hier fĂŒnf Beispiele:

Bezeichnung Bedeutung BegrĂŒndung
Ablegen “Karten werden immer von der Hand abgelegt, sofern nicht anders auf der Karte angegeben. Abgelegte Karten kommen offen auf den eigenen Ablagestapel. [
] Lediglich die oberste Karte des Ablagestapels muss immer sichtbar sein.” Im Code wurde Ablegen als void discard(Card card) modelliert. Die Bezeichnung wird durchgĂ€ngig durch die gesamte Spielanleitung hinweg verwendet und ist Bestandteil des Grundvokabulars, um das Spiel verstehen zu können. Daher gehört es zur Ubiquitous Language.
Entsorgen “Entsorgt der Spieler Karten, legt er sie offen auf den MĂŒllstapel bzw. auf das MĂŒll-Tableau, falls noch keine Karte entsorgt wurde. Entsorgte Karten können nicht wieder gekauft oder genommen werden.” Im Code wurde Entsorgen als void dispose(Card card) modelliert. Die Bezeichnung wird durchgĂ€ngig durch die gesamte Spielanleitung hinweg verwendet und ist Bestandteil des Grundvokabulars, um das Spiel verstehen zu können. Daher gehört es zur Ubiquitous Language.
Nehmen “Karten werden aus dem Vorrat genommen und ungenutzt auf den Ablagestapel gelegt – dies ist kein Kauf. Karten, die ohne Angabe der Herkunft genommen werden mĂŒssen oder dĂŒrfen, werden vom Vorrat genommen.” Im Code wurde Nehmen als void take(Card card) modelliert. Die Bezeichnung wird durchgĂ€ngig durch die gesamte Spielanleitung hinweg verwendet und ist Bestandteil des Grundvokabulars, um das Spiel verstehen zu können. Daher gehört es zur Ubiquitous Language.
+ X Aktionen “Der Spieler darf X weitere Aktionskarten ausspielen. ZunĂ€chst muss er jedoch alle Anweisungen der aktuellen Aktionskarte (soweit möglich) erfĂŒllen. Darf er weitere Aktionen ausfĂŒhren (z.B. durch das Ausspielen von mehreren Aktionskarten), sollte die Anzahl der Aktionen der Übersichtlichkeit halber laut mitgezĂ€hlt werden.” Die Bezeichnung wird durchgĂ€ngig durch die gesamte Spielanleitung hinweg verwendet, insbesondere ist sie auf vielen Aktionskarten aufgedruckt. In der Anleitung wird bei solchen Anweisungen klar darauf geachtet wird, “mĂŒssen” und “dĂŒrfen” korrekt zu verwenden (diese Bezeichnungen sind sogar fett gedruckt). Da die Bezeichnung Bestandteil des Grundvokabular ist, um erfolgreich Aktionskarten auslegen und spielen zu können, gehört + X Aktionen zur Ubiquitous Language.
Deck Ein Deck ist ein Kartenstapel im Spiel, auf den Karten abgelegt und von dem Karten wieder gezogen werden können. Nicht nur in Dominion ist die Idee des Kartenstapels bekannt. Im Englischen habe ich mich, unter anderem nach diesem Post, entschieden, den Kartenstapel als Deck zu bezeichnen. Im Code ist das Kartendeck in der DomĂ€ne als Deck realisiert. In der Spielanleitung ist dieses Wort allgegenwĂ€rtig, zum Beispiel auch bei den Begriffen “Nachziehstapel” oder “Ablagestapel” (siehe Seite 5 unten). Der Begriff ist Bestandteil des Grundvokabulars, um das Spiel verstehen zu können und ist daher Teil der Ubiquitous Language.

Entities

UML, Beschreibung und BegrĂŒndung des Einsatzes einer Entity; falls keine Entity vorhanden: ausfĂŒhrliche BegrĂŒndung, warum es keines geben kann/hier nicht sinnvoll ist

Entity Player (reused from mock section)

Die Klasse Player bzw. GamePlayer kann als Entity angesehen werden und modelliert die tatsĂ€chlichen Mitspieler von Dominion. Dabei werden die geltenden DomĂ€nenregeln forciert, indem nach außen Methoden zum VerĂ€ndern des Zustands (Lebenszyklus) einer Spielerin bereitgestellt werden, mit denen es nicht möglich ist, die Entity nach der Konstruktion in einen ungĂŒltigen Zustand zu versetzen. Beispiele fĂŒr solche Methoden sind void takeToHand(Card card), void discard(Card card) oder void dispose(Card card). Hier ist auch zu erkennen, dass das Value Object Card bzw. konkrete AusprĂ€gungen davon (wie z.B. bei List<ActionCard> getActionCardsOnHand()) eingesetzt werden, um so viel Verhalten wie möglich auszulagern und die Entity (trotz ihres Umfangs) möglichst schlank zu halten.

Die Klasse Player hat zudem eine eigene IdentitĂ€t in der DomĂ€ne: der Name eines Spielers ist eindeutig. Wir forcieren diese Regel im Code allerdings nicht und ĂŒberlassen es ĂŒber das CLI dem Nutzer, die Namen frei zu wĂ€hlen. Da zwei Player in der DomĂ€ne nie auf Gleichheit ĂŒberprĂŒft werden mĂŒssen, ist es aus Sicht des Codes unproblematisch, wenn zwei Spielerinnen denselben Namen haben. Nur fĂŒr die Nutzer unseres Programms könnte es dann verwirrend werden, weil man eventuell nicht mehr weiß, wer nun an der Reihe ist. Dies ist jedoch fĂŒr uns nicht weiter relevant und kann einfach behoben werden, indem die Benutzer eindeutige Namen vergeben. Sollten sich Benutzer ĂŒber dieses “Feature” beschweren, ist die Anpassung in wenigen Handgriffen erledigt (ĂŒberschreiben der equals()-Methode und beim Anlegen der Player ĂŒberprĂŒfen, dass es keine zwei gleiche Spieler gibt).

Value Objects

UML, Beschreibung und BegrĂŒndung des Einsatzes eines Value Objects; falls kein Value Object vorhanden: ausfĂŒhrliche BegrĂŒndung, warum es keines geben kann/hier nicht sinnvoll ist

Value Object Card

Die abstrakte Klasse Card modelliert eine beliebige Karte im Spiel. Alle Karten von Dominion haben gemeinsam, dass sie einen Namen (z.B. “Jahrmarkt”), einen Kartentyp (z.B. “AKTION”) und Kartenkosten (z.B. Karte kostet 5 “Geld”) haben. Die Karte ist unverĂ€nderlich, das heißt es gibt beispielsweise keine Setter in der Klasse und alle Felder sind als “blank final” markiert. Jedoch ist die Klasse selbst nicht final, sondern abstract, sodass genau genommen die Unterklassen (wie MoneyCard, PointCard und ActionCard) als eigentliche Value Objects bezeichnet werden mĂŒssten.

In der Klasse Card wurde zudem die hashCode() und equals()-Methode ĂŒberschrieben, denn da das Value Object ein Wertkonzept kapselt, sollten zwei Value Objects gleich sein, wenn sie die selben Werte haben. Diese Forderung wurde jedoch gelockert, da es keine zwei Karten in Dominion gibt, die denselben Namen haben. Dementsprechend wurde nur auf Namensgleichheit ĂŒberprĂŒft, obwohl streng genommen eigentlich neben Name, auch Typ und Kosten sowie bei Subklassen auch auf deren zusĂ€tzliche Attribute mit einfließen mĂŒssten. In diesem Zusammenhang könnte man die Card also auch als Entity bezeichnen, weil sie mit dem Namen eine eindeutige ID innerhalb der DomĂ€ne hat. Nichtsdestotrotz hat eine Karten keinen eigenen Lebenszyklus und verĂ€ndert sich nie wĂ€hrend ihrer Lebenszeit (immutable), weshalb wir sie trotzdem als Value Object klassifizieren.

Repositories

UML, Beschreibung und BegrĂŒndung des Einsatzes eines Repositories; falls kein Repository vorhanden: ausfĂŒhrliche BegrĂŒndung, warum es keines geben kann/hier nicht sinnvoll ist

Repositories bieten eine Schnittstelle, um von der DomĂ€ne aus Daten aus dem Persistenzspeicher, z.B. einer Datenbank zu lesen, ohne die technischen Details einer Datenbankverbindung kennen zu mĂŒssen. Damit vermitteln Repositories zwischen der DomĂ€ne und dem Datenmodell. Im Kern wird dazu ein Interface mit Methoden wie findById(...) definiert, das dann in Ă€ußeren Schichten (z.B. der Plugin-Schicht) implementiert werden kann. Auf diese Weise wird der Kern nicht mit unnötiger “accidental complexity” belastet.

In diesem Projekt kamen keine Repositories zum Einsatz, da bislang noch kein Zugriff auf Persistenzspeicher benötigt wurde. Das Programm agiert mit dem Benutzer ĂŒber die Konsole, wobei alle Daten im Hauptspeicher vorgehalten sind und nicht von persistentem Speicher gelesen werden mĂŒssen. Daher wurde auch kein expliziter Zugriff auf ein Datenmodell benötigt.

Allerdings hat sich bereits ein zukĂŒnftiger Einsatzbereich fĂŒr Repositories aufgetan: der CardPool definiert zurzeit alle sich im Spiel befindlichen Karten als public static final Member der Klasse:

// Action cards
public static final List<ActionCard> actionCards = List.of(

    new ActionCardBuilder("Jahrmarkt", 5).with(
            new Action(
                    new EarnActionsInstruction(2),
                    new EarnBuyingsInstruction(1),
                    new EarnMoneyInstruction(2)))
            .build(),

    new ActionCardBuilder("Markt", 5).with(
            new Action(
                    new DrawCardsInstruction(1),
                    new EarnActionsInstruction(1),
                    new EarnBuyingsInstruction(1),
                    new EarnMoneyInstruction(1)))
            .build(),

    ...
);

// Money cards
public static final MoneyCard copperCard = new MoneyCard("Kupfer", 0, 1);
public static final MoneyCard silverCard = new MoneyCard("Silber", 3, 2);
public static final MoneyCard goldCard = new MoneyCard("Gold", 6, 3);
public static final List<MoneyCard> moneyCards = List.of(copperCard, silverCard, goldCard);

...

Da das Spiel Dominion von zahlreichen Erweiterungen lebt und individuell Aktionskarten fĂŒr ein Spiel kombiniert werden können (bei Kombination aller Schachteln können ĂŒber 10 Trillionen Kombinationen gezogen werden), wĂ€re eine Erweiterung des Programms sinnvoll, damit Spieler selbst die Spielkarten zu Beginn auswĂ€hlen können. Dies könnte beispielsweise ĂŒber eine Textdatei geschehen, in denen die Namen der zu verwendenden Karten aufgelistet werden. Aber auch ohne eine solche persistente Datei könnte dann ein Repository sinnvoll sein, um die Definition der Karten besser vom Rest des Codes auszulagern. Negativ sticht zum Beispiel der GameStock heraus, der sowohl Methoden zur Verwaltung des Kartenvorrats implementiert, als auch die Karten auf diesem Vorrat selbst. Mit einem Repository könnte dies verbessert werden.

Aggregates

UML, Beschreibung und BegrĂŒndung des Einsatzes eines Aggregates; falls kein Aggregate vorhanden: ausfĂŒhrliche BegrĂŒndung, warum es keines geben kann/hier nicht sinnvoll ist

Aggregate gruppieren die Entities und Value Objects zu gemeinsam verwalteten Einheiten. Dadurch können die Objektbeziehungen untereinander gekapselt werden, von außerhalb wird dann nur mit dem Aggregate gearbeitet, indem ausschließlich auf das Aggregate Root zugegriffen wird. Dadurch kann besser die Einhaltung von DomĂ€nenregeln kontrolliert werden.

Im Code findet sich in diesem Projekt kein klassisches Aggregate, das Entities oder Value Objects kapselt. Am ehesten könnte man noch die Klasse PlayerInteraction als Aggregate bezeichnen.

Aggregate PlayerInteraction

Wie im UML-Diagramm zu erkennen, hat die PlayerInteraction jeweils eine Member-Variable fĂŒr ein Objekt vom Typ PlacerDecision bzw. PlayerInformation. Die Klasse PlayerInteraction wurde jedoch nur aus BequemlichkeitsgrĂŒnden angelegt und nicht, um DomĂ€nenregeln zu forcieren. PlayerDecision und PlayerInformation wurden hĂ€ufig zusammen in Parametern ĂŒbergeben, sodass es Sinn ergab, sie mithilfe der PlayerInteraction zu kapseln. Der Konstruktur von Player erwartet beispielsweise eine PlayerInteraction:

protected Player(String name, PlayerInteraction playerInteraction, Deck drawDeck, Stock stock) {
    ...
}

Die Klasse Player wiederum stellt dann Ă€hnlich zu PlayerInteraction die Methoden PlayerDecision decide() sowie PlayerDecision inform() zur VerfĂŒgung. Dadurch sind [Aufrufe der folgenden Art] möglich (siehe PlayerMove):

player.inform().startActionPhase();
player.decide().chooseOptionalActionCard(...);

Nicht zuletzt ist die PlayerInteraction auch deshalb kein klassisches Aggregate, weil gar keine Entities oder Value Objects zusammengefasst werden, sondern konkrete Implementierungen der Interfaces PlayerDecision und PlayerInformation. Im allgemeinen Sprachgebrauch könnte man aber trotzdem von einem “Aggregat” sprechen. Abseits davon ist der Einsatz von Aggregates fĂŒr dieses Projekt nicht erforderlich, wahrscheinlich auch deshalb, weil die Beziehungen zwischen Entities und Value Objects nicht komplex genug sind, um eine Gruppierung als Aggregate zu rechtfertigen bzw. diese KomplexitĂ€t bereits durch geeignete Abstraktion reduziert wurde.