6 Domain Driven Design
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
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
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.
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.