19. Jan 2017
Lesedauer 11 Min.
Kniffliges Kniffel
dojoLösung: Ein Spiel Marke Eigenbau
Spiele stellen den Entwickler immer wieder vor spannende Herausforderungen. In diesem Fall ist es ein Würfelspiel: Kniffel, auch bekannt als Yazee.

In diesem Monat ging es darum, das Würfelspiel Kniffel [1] zu implementieren. Die Aufgabe ist vermutlich zu umfangreich, um sie vollständig innerhalb der für Entwickler empfohlenen Übungszeit zu lösen. Wir empfehlen unseren Workshop-Teilnehmern, zwei Stunden pro Woche ins Üben zu investieren [2]. Bei der monatlichen Erscheinungsweise der dotnetpro ergibt sich damit eine Übungszeit von acht Stunden für die jeweilige Aufgabe. Sicherlich knapp bemessen, um ein vollständiges Kniffelspiel zu implementieren. Für mich, und vielleicht auch für Sie, bestand somit die erste Herausforderung darin, einen ausreichend kleinen Ausschnitt der Anforderungen auszuwählen, um mit diesem zu beginnen.Und ich kann gar nicht oft genug betonen, dass schon allein diese Übung nicht oft genug praktiziert werden kann: Anforderungen so weit zu zerlegen, bis schließlich ein „Häppchen“ übrig bleibt, das in vier bis acht Stunden umgesetzt ist. Üben Sie lieber, die Anforderungen noch kleiner zu zerlegen. Zu klein gibt es eigentlich nicht, solange Sie vertikal schneiden, also jeweils einen Durchstich, ein Inkrement, produzieren.
Bottom-up vs. top-down
Ich habe bei dieser Aufgabe mit der Frage experimentiert, was passiert, wenn ich bottom-up beginne statt top-down. Die Empfehlung für Softwareentwicklungsprozesse lautet immer wieder, top-down vorzugehen, also bei konkreten Anforderungen des Product Owners zu beginnen. Bottom-up würde bedeuten, mit Funktionalität „von unten“ zu beginnen, von der wir vermuten, dass sie benötigt wird. An der Formulierung merken Sie vielleicht schon, welches Problem sich bei der Bottom-up-Vorgehensweise ergibt: Das wichtige Wort war „vermuten“.Wenn wir top-down vorgehen, ergibt sich der benötigte Funktionsumfang einer Funktionseinheit (Methode, Klasse, Bibliothek, Komponente, Microservice) von oben nach unten aus den Anforderungen des Product Owners. Bei sorgfältiger Herangehensweise ist sichergestellt, dass genau die Funktionalität implementiert wird, die sich zuvor aus dem Entwurf ergeben hat, der sich wiederum aus den Anforderungen ergeben hat. Arbeite ich dagegen bottom-up, ist die Wahrscheinlichkeit groß, dass der vermutete(!) Funktionsumfang nicht passgenau den Anforderungen entspricht.Dazu ein Beispiel: Eine Anwendung soll realisiert werden, die ihre Daten persistieren muss, damit diese vom einen zum nächsten Programmlauf erhalten bleiben. Wenn Sie nun nicht von den Anforderungen ausgehen und planen, welchen Funktionsumfang Sie von einer Persistenzklasse erwarten, bleibt Ihnen eigentlich nichts anderes übrig, als CRUD zu implementieren. CRUD steht für Create, Retrieve, Update und Delete. Und da zu diesem Zeitpunkt auch keine Datentypen klar entworfen sind, läuft es vielleicht sogar auf einen generischen Persistenzmechanismus hinaus.Endlich mal wieder Infrastruktur basteln, wird mancher Entwickler vielleicht denken. Tatsächlich verschwendet er jedoch sehr wahrscheinlich etwas Zeit, da Funktionalität erstellt wird, die niemand benötigt. Kommen Sie dagegen von „oben“, von den Anforderungen, ergibt sich durch den Entwurf eine klare Schnittstelle für die Persistenz. Vielleicht stellen Sie auf diese Weise fest, dass Delete gar nicht benötigt wird, weil Datensätze stattdessen als „gelöscht“ markiert werden, aber erhalten bleiben. Wir halten also fest: Top-down, von den Anforderungen über den Entwurf zum Code, ist effizienter als bottom-up.Kniffel bottom-up
Mein Versuch, das Kniffelspiel trotzdem, wenigstens in Teilen, bottom-up zu realisieren, führte dann tatsächlich zu Mehrarbeit. Ich hatte mir nämlich eine Klasse Würfelbecher ausgedacht, die fünf Würfel per Zufallszahlengenerator würfeln kann. Das Besondere: Man konnte null bis fünf Würfel zurücklegen und dann erneut würfeln. Im realen Spiel macht man das tatsächlich: Mit jedem Wurf kann man entscheiden, welche Würfel liegen bleiben sollen und welche man neu würfeln möchte.Bei meiner Implementation stellte sich jedoch heraus, dass meine Vorgehensweise eine Vermischung von Aspekten darstellte und die Sache zudem noch verkompliziert hat. Also habe ich meine ursprüngliche Implementation mitsamt der Unit-Tests modifiziert. Mehraufwand, der nicht angefallen wäre, hätte ich konsequent anforderungsgetrieben entworfen und dann implementiert. Eine wichtige Erkenntnis!Jetzt mal ordentlich
Mein nächster Versuch bestand daher darin, wieder „ordentlich“ vorzugehen. Ich habe also mit den Anforderungen begonnen und diese in Dialog und Interaktionen zerlegt. Bild 1 zeigt das Ergebnis meiner Analyse.
Ich habe ein Konsolen-UI entworfen und an dieses die Interaktionen als Pfeile gezeichnet. Für ein Konsolen-UI habe ich mich entschieden, weil es schneller zu implementieren ist als ein GUI. Der Anwender kann in diesem Dialog folgende Interaktionen auslösen:
- Würfeln: Bis zu fünf Würfel werden gewürfelt. Das Ergebnis wird unterhalb der Tabelle angezeigt. Jeder Würfel ist in eckigen Klammern dargestellt.
- Würfel umschalten: Mit den Tasten 1 bis 5 kann der Benutzer entscheiden, ob ein Würfel auf dem Tisch liegen bleiben oder in den Würfelbecher für den nächsten Wurf zurückgelegt werden soll.
- Setzen: Der Anwender kann die fünf Würfel in die Tabelle übernehmen. Es erfolgt dann eine Abfrage der Koordinaten.
Beenden ist daher als Interaktion nicht relevant, weil lediglich die Anwendung beendet wird. Bei einer Konsolenanwendung muss dazu im technischen Sinne Logik implementiert werden. Allerdings handelt es sich dabei nicht um Domänenlogik, da es beim Beenden nicht speziell um das Thema dieser Anwendung geht. Die Domänenlogik ist es aber, die eine Interaktion begründet. Sollten sich die Anforderungen im Hinblick auf das Beenden der Anwendung ändern, kommt die Interaktion Beenden möglicherweise hinzu.Nachdem ich die Interaktionen identifiziert hatte, habe
ich die Entwürfe auf der obersten Ebene erstellt. Bild 2 zeigt das Ergebnis.Zum Würfeln betätigt der Anwender die Taste W. Das UI löst daraufhin einen Event aus, der keine Daten trägt. Der Event landet bei der Funktionseinheit Würfeln. Diese ist dafür zuständig, beim ersten Wurf des Spiels fünf Würfel zu würfeln. Das Ergebnis sind dann zwei Aufzählungen:
ich die Entwürfe auf der obersten Ebene erstellt. Bild 2 zeigt das Ergebnis.Zum Würfeln betätigt der Anwender die Taste W. Das UI löst daraufhin einen Event aus, der keine Daten trägt. Der Event landet bei der Funktionseinheit Würfeln. Diese ist dafür zuständig, beim ersten Wurf des Spiels fünf Würfel zu würfeln. Das Ergebnis sind dann zwei Aufzählungen:
- tisch*: Eine Liste der Würfel, die auf dem Tisch liegen. Initial ist diese Liste leer.
- becher*: Eine Liste von Würfeln, die gerade gefallen sind. Initial enthält diese Liste nach dem Würfeln fünf Würfel.
Listing 1: Die Klasse Program
<span class="hljs-keyword">using</span> System; <br/><span class="hljs-keyword">using</span> kniffel.contracts; <br/><span class="hljs-keyword">using</span> kniffel.ui; <br/><br/><span class="hljs-keyword">namespace</span> <span class="hljs-title">kniffel</span> <br/>{ <br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Program</span> <br/> { <br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">static</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Main</span>(<span class="hljs-params"><span class="hljs-keyword">string</span>[] args</span>) </span>{ <br/> <span class="hljs-keyword">var</span> consoleUi = <span class="hljs-keyword">new</span> ConsoleUi(); <br/> <span class="hljs-keyword">var</span> interactors = <span class="hljs-keyword">new</span> Interactors(); <br/><br/><br/> consoleUi.Würfeln += () =&gt; { <br/> <span class="hljs-keyword">int</span>[] drinnen; <br/> <span class="hljs-keyword">int</span>[] draußen; <br/> interactors.Würfeln(<span class="hljs-keyword">out</span> drinnen, <span class="hljs-keyword">out</span> draußen); <br/> consoleUi.Wurf_anzeigen(drinnen, draußen); <br/> }; <br/><br/> consoleUi.Würfel_umschalten += i =&gt; { <br/> <span class="hljs-keyword">int</span>[] drinnen; <br/> <span class="hljs-keyword">int</span>[] draußen; <br/> interactors.Würfel_umschalten(<br/> i, <span class="hljs-keyword">out</span> drinnen, <span class="hljs-keyword">out</span> draußen); <br/> consoleUi.Wurf_anzeigen(drinnen, draußen); <br/> }; <br/><br/> consoleUi.Setzen += koordinate =&gt; { <br/> Tabelle tabelle; <br/> <span class="hljs-keyword">int</span> spieler; <br/> interactors.Setzen(<br/> koordinate, <span class="hljs-keyword">out</span> tabelle, <span class="hljs-keyword">out</span> spieler); <br/> consoleUi.Spielstand_anzeigen(<br/> tabelle, spieler); <br/> }; <br/><br/> Action start = () =&gt; { <br/> Tabelle tabelle; <br/> <span class="hljs-keyword">int</span> spieler; <br/> interactors.Start(<span class="hljs-keyword">out</span> tabelle, <span class="hljs-keyword">out</span> spieler); <br/> consoleUi.Spielstand_anzeigen(<br/> tabelle, spieler); <br/> consoleUi.MessageLoop(); <br/> }; <br/><br/> start(); <br/> } <br/> } <br/>} <br/><br/>
Kann der Wurf nicht in das angegebene Feld übernommen werden, muss der Benutzer erneut das Setzen auslösen. Dies ist der Fall, wenn das Feld bereits verwendet wurde oder wenn der Wurf aufgrund der Regeln nicht in das Feld übernommen werden kann. Konnte das Setzen durchgeführt werden, wird der Spieler gewechselt. Die Interaktion Setzen liefert dazu neben der Tabelle auch einen String, der den Spieler identifiziert.
Top-down
Mit der Umsetzung des Entwurfs bin ich von oben nach unten vorgegangen. Listing 1 zeigt die Klasse Program, die mit Main den Einstiegspunkt in die Anwendung enthält.In der Main-Methode wird jeweils das Ui mit dem Interactor verbunden. Mit Interactor bezeichnen wir eine Methode, die eine Interaktion realisiert. Die benötigten Datentypen zeigt folgendes Listing:<span class="hljs-keyword">namespace</span> <span class="hljs-title">kniffel.contracts</span>
{
<span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Tabelle</span>
{
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Tabelle</span>(<span class="hljs-params"></span>) </span>{
Zeilen = <span class="hljs-keyword">new</span> <span class="hljs-keyword">int</span>[<span class="hljs-number">15</span>][];
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">int</span> i = <span class="hljs-number">0</span>; i < <span class="hljs-number">15</span>; i++) {
Zeilen[i] = <span class="hljs-keyword">new</span> <span class="hljs-keyword">int</span>[<span class="hljs-number">6</span>];
}
}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span>[][] Zeilen { <span class="hljs-keyword">get</span>; }
}
}
<span class="hljs-keyword">namespace</span> <span class="hljs-title">kniffel.contracts</span>
{
<span class="hljs-keyword">public</span> <span class="hljs-keyword">struct</span> Koordinate
{
<span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-title">Koordinate</span>(<span class="hljs-params"><span class="hljs-keyword">int</span> spalte, <span class="hljs-keyword">int</span> zeile</span>) </span>{
Spalte = spalte;
Zeile = zeile;
}
<span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> Spalte { <span class="hljs-keyword">get</span>; }
<span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span> Zeile { <span class="hljs-keyword">get</span>; }
}
}
Die Klasse ConsoleUi realisiert die Ein-/Ausgabe. Sie ist vom Umfang her etwas länger geraten, enthält aber lediglich trivialen Code zur Formatierung und Ausgabe der Werte und fällt in die Kategorie „Fleißarbeit“.In der Klasse Interactors sind die Methoden zusammengefasst, die jeweils eine Interaktion realisieren.
Listing 2: Die Klasse Interactors
<span class="hljs-keyword">using</span> kniffel.contracts; <br/><span class="hljs-keyword">using</span> kniffel.game; <br/><br/><span class="hljs-keyword">namespace</span> <span class="hljs-title">kniffel</span> <br/>{ <br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Interactors</span> <br/> { <br/> <span class="hljs-keyword">private</span> Kniffel kniffel; <br/><br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Start</span>(<span class="hljs-params"><span class="hljs-keyword">out</span> Tabelle tabelle, </span></span><br/><span class="hljs-function"><span class="hljs-params"> <span class="hljs-keyword">out</span> <span class="hljs-keyword">int</span> spieler</span>) </span>{ <br/> kniffel = <span class="hljs-keyword">new</span> Kniffel(); <br/> kniffel.Start(<span class="hljs-keyword">out</span> tabelle, <span class="hljs-keyword">out</span> spieler); <br/> } <br/><br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> Würfeln(<span class="hljs-keyword">out</span> <span class="hljs-keyword">int</span>[] becher, <span class="hljs-keyword">out</span> <span class="hljs-keyword">int</span>[] <br/> tisch) { <br/> kniffel.Würfeln(<span class="hljs-keyword">out</span> becher, <span class="hljs-keyword">out</span> tisch); <br/> } <br/><br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> Würfel_umschalten(<br/> <span class="hljs-keyword">int</span> i, <span class="hljs-keyword">out</span> <span class="hljs-keyword">int</span>[] becher, <span class="hljs-keyword">out</span> <span class="hljs-keyword">int</span>[] tisch) { <br/> kniffel.Würfel_umschalten(<br/> i, <span class="hljs-keyword">out</span> becher, <span class="hljs-keyword">out</span> tisch); <br/> } <br/><br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Setzen</span>(<span class="hljs-params">Koordinate koordinate, </span></span><br/><span class="hljs-function"><span class="hljs-params"> <span class="hljs-keyword">out</span> Tabelle tabelle, <span class="hljs-keyword">out</span> <span class="hljs-keyword">int</span> spieler</span>) </span>{ <br/> kniffel.Setzen(koordinate, <span class="hljs-keyword">out</span> tabelle, <br/> <span class="hljs-keyword">out</span> spieler); <br/> } <br/> } <br/>} <br/><br/>
Zurzeit wird hier lediglich die Klasse Kniffel integriert, in der die Domänenlogik realisiert ist. Ich rechne damit, dass später weitere Klassen zu integrieren sind, daher habe ich die Klasse Interactors angelegt. Ferner ergibt sich so eine Struktur, die über viele Anwendungen und Beispiele hinweg immer wieder die gleiche ist. So wird es leichter, sich in unterschiedlichen Projekten zurechtzufinden, da sich die wichtigsten Strukturelemente in jedem Projekt wiederholen.
Logik
Die Domänenlogik des Spiels ist in der Klasse Kniffel realisiert; sie ist in Listing 3 zu sehen.Der Funktionsumfang der Anwendung ist bislang unvollständig. Man kann würfeln und die Würfel zwischen Becher und Tisch hin und her wechseln. Ferner kann man den Wurf in die Tabelle übernehmen. Allerdings wird dabei lediglich die Summe der fünf Würfel berechnet und übernommen.Listing 3: Die Klasse Kniffel
<span class="hljs-keyword">using</span> System.Linq; <br/><span class="hljs-keyword">using</span> kniffel.contracts; <br/><br/><span class="hljs-keyword">namespace</span> <span class="hljs-title">kniffel.game</span> <br/>{ <br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> <span class="hljs-title">Kniffel</span> <br/> { <br/> <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span>[] _becher = { <span class="hljs-number">7</span>, <span class="hljs-number">7</span>, <span class="hljs-number">7</span>, <span class="hljs-number">7</span>, <span class="hljs-number">7</span> }; <br/> <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span>[] _tisch = { <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span> }; <br/> <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> _wurf; <br/><br/> <span class="hljs-keyword">private</span> <span class="hljs-keyword">readonly</span> Würfelbecher würfelbecher = <br/> <span class="hljs-keyword">new</span> Würfelbecher(); <br/> <span class="hljs-keyword">private</span> Tabelle[] _tabelle; <br/> <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> _spieler; <br/><br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Start</span>(<span class="hljs-params"><span class="hljs-keyword">out</span> Tabelle tabelle, </span></span><br/><span class="hljs-function"><span class="hljs-params"> <span class="hljs-keyword">out</span> <span class="hljs-keyword">int</span> spieler</span>) </span>{ <br/> _tabelle = <span class="hljs-keyword">new</span> Tabelle[<span class="hljs-number">2</span>]; <br/> _tabelle[<span class="hljs-number">0</span>] = <span class="hljs-keyword">new</span> Tabelle(); <br/> _tabelle[<span class="hljs-number">1</span>] = <span class="hljs-keyword">new</span> Tabelle(); <br/> _spieler = <span class="hljs-number">0</span>; <br/> _wurf = <span class="hljs-number">0</span>; <br/><br/> tabelle = _tabelle[_spieler]; <br/> spieler = _spieler + <span class="hljs-number">1</span>; <br/> } <br/><br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> Würfeln(<span class="hljs-keyword">out</span> <span class="hljs-keyword">int</span>[] becher, <br/> <span class="hljs-keyword">out</span> <span class="hljs-keyword">int</span>[] tisch) { <br/> <span class="hljs-keyword">if</span> (_wurf == <span class="hljs-number">3</span>) { <br/> becher = _becher; <br/> tisch = _tisch; <br/> <span class="hljs-keyword">return</span>; <br/> } <br/> _wurf += <span class="hljs-number">1</span>; <br/><br/> <span class="hljs-keyword">var</span> anzahl_Würfel = _becher.Count(i =&gt; i != <span class="hljs-number">0</span>); <br/> <span class="hljs-keyword">var</span> wurf = würfelbecher.Würfeln(anzahl_Würfel); <br/><br/> Würfel_übernehmen(wurf, _becher); <br/><br/> becher = _becher; <br/> tisch = _tisch; <br/> } <br/><br/> <span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> Würfel_übernehmen(<span class="hljs-keyword">int</span>[] wurf, <br/> <span class="hljs-keyword">int</span>[] becher) { <br/> <span class="hljs-keyword">var</span> j = <span class="hljs-number">0</span>; <br/> <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> x = <span class="hljs-number">0</span>; x &lt; <span class="hljs-number">5</span>; x++) { <br/> <span class="hljs-keyword">if</span> (becher[x] != <span class="hljs-number">0</span>) { <br/> becher[x] = wurf[j]; <br/> j += <span class="hljs-number">1</span>; <br/> } <br/> } <br/> } <br/><br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> Würfel_umschalten(<span class="hljs-keyword">int</span> i, <span class="hljs-keyword">out</span> <span class="hljs-keyword">int</span>[] <br/> becher, <span class="hljs-keyword">out</span> <span class="hljs-keyword">int</span>[] tisch) { <br/> <span class="hljs-keyword">if</span> (_becher[i] == <span class="hljs-number">0</span>) { <br/> _becher[i] = _tisch[i]; <br/> _tisch[i] = <span class="hljs-number">0</span>; <br/> } <br/> <span class="hljs-keyword">else</span> { <br/> _tisch[i] = _becher[i]; <br/> _becher[i] = <span class="hljs-number">0</span>; <br/> } <br/> becher = _becher; <br/> tisch = _tisch; <br/> } <br/><br/> <span class="hljs-function"><span class="hljs-keyword">public</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Setzen</span>(<span class="hljs-params">Koordinate koordinate, </span></span><br/><span class="hljs-function"><span class="hljs-params"> <span class="hljs-keyword">out</span> Tabelle tabelle, <span class="hljs-keyword">out</span> <span class="hljs-keyword">int</span> spieler</span>) </span>{ <br/> <span class="hljs-keyword">var</span> wert = Wert_ermitteln(_becher, _tisch); <br/> _tabelle[_spieler].Zeilen[koordinate.Zeile]<br/> [koordinate.Spalte] = wert; <br/><br/> Spieler_wechseln(); <br/><br/> tabelle = _tabelle[_spieler]; <br/> spieler = _spieler + <span class="hljs-number">1</span>; <br/> } <br/><br/> <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">void</span> <span class="hljs-title">Spieler_wechseln</span>(<span class="hljs-params"></span>) </span>{ <br/> _spieler = _spieler == <span class="hljs-number">0</span> ? <span class="hljs-number">1</span> : <span class="hljs-number">0</span>; <br/> _wurf = <span class="hljs-number">0</span>; <br/> _becher = <span class="hljs-keyword">new</span>[] { <span class="hljs-number">7</span>, <span class="hljs-number">7</span>, <span class="hljs-number">7</span>, <span class="hljs-number">7</span>, <span class="hljs-number">7</span> }; <br/> _tisch = <span class="hljs-keyword">new</span>[] { <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span>, <span class="hljs-number">0</span> }; <br/> } <br/><br/> <span class="hljs-function"><span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> <span class="hljs-title">Wert_ermitteln</span>(<span class="hljs-params"><span class="hljs-keyword">int</span>[] becher, </span></span><br/><span class="hljs-function"><span class="hljs-params"> <span class="hljs-keyword">int</span>[] tisch</span>) </span>{ <br/> <span class="hljs-keyword">var</span> würfel = becher.Where(x =&gt; x != <span class="hljs-number">0</span>).Union(<br/> tisch.Where(x =&gt; x != <span class="hljs-number">0</span>)).ToArray(); <br/> <span class="hljs-keyword">return</span> würfel.Sum(); <br/> } <br/> } <br/>} <br/><br/>
Es erfolgt keine inhaltliche Prüfung beim Setzen und die Summe wird noch nicht nach den Kniffelregeln berechnet. Als erstes Inkrement ist die Lösung dennoch brauchbar, weil der Product Owner bereits sein Feedback geben kann: Die Benutzerführung ist so weit implementiert, dass man das Spiel bedienen kann. Auch das Würfeln funktioniert bereits. Lediglich die Eingabe der Koordinaten beim Setzen des Wurfs wird bislang nicht überprüft. Gibt man hier fehlerhafte Werte ein, stürzt die Anwendung ab. Der Product Owner kann an dieser Stelle entscheiden, ob als Nächstes die Benutzerschnittstelle robust gemacht werden soll oder ob die Spielregeln weiter implementiert werden sollen.
Tests
Für die Klasse Kniffel habe ich Tests ergänzt. Allerdings zeigt sich hier eine weitere Schwachstelle meiner Bottom-up-Vorgehensweise: Dadurch, dass ich mich zunächst um das Würfeln gekümmert habe, hatte ich diese Funktionalität bereits, als ich begann, mir über den „Rest“ Gedanken zu machen. Das führte dazu, dass die Klasse Kniffel eine Abhängigkeit zur Klasse Würfelbecher hat. Das bedeutet allerdings, dass die Ergebnisse vom Zufallszahlengenerator abhängig sind, der in der Klasse Würfelbecher verwendet wird. Die Abhängigkeiten zeigt Bild 3.
Beim automatisierten Testen der Klasse Kniffel stellt sich das Problem, dass für einige Tests das Ergebnis des Würfelbechers stabil bleiben müsste. Die Würfel dürften im Test eben nicht zufällig fallen. Bei den Tests für den Würfelbecher habe ich dies berücksichtigt: Hier kann für die Tests eine Instanz von Random in den Konstruktor reingereicht werden. Listing 4 zeigt die Tests für den Würfelbecher.
Listing 4: Tests für den Würfelbecher
using System; <br/>using System.Collections.Generic; <br/>using FluentAssertions; <br/>using NUnit.Framework; <br/><br/>namespace kniffel.game.tests <br/>{ <br/> [TestFixture] <br/> public class WürfelbecherTests <br/> { <br/> private Würfelbecher sut; <br/><br/> [SetUp] <br/> public void Setup() { <br/> sut = new Würfelbecher(new Random(<span class="hljs-number">42</span>)); <br/> } <br/><br/> [Test] <br/> public void Random_42_liefert_immer_folgende_<br/> Werte() { <br/> var random = new Random(<span class="hljs-number">42</span>); <br/> var result = new List&lt;int&gt;(); <br/> for (int i = <span class="hljs-number">0</span>; i &lt; <span class="hljs-number">10</span>; i++) { <br/> result.Add(random.Next(<span class="hljs-number">1</span>, <span class="hljs-number">6</span> + <span class="hljs-number">1</span>)); <br/> } <br/><br/> result.Should().Equal(<span class="hljs-number">5</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">4</span>, <span class="hljs-number">2</span>, <span class="hljs-number">2</span>, <span class="hljs-number">5</span>, <br/> <span class="hljs-number">4</span>, <span class="hljs-number">2</span>, <span class="hljs-number">5</span>); <br/> } <br/><br/> [Test] <br/> public void Wurf_besteht_aus_n_Würfeln() { <br/> var result = sut.Würfeln(<span class="hljs-number">5</span>); <br/> result.Should().HaveCount(<span class="hljs-number">5</span>); <br/> } <br/><br/> [Test] <br/> public void Wurf_liefert_n_zufällig_Zahlen() { <br/> var result = sut.Würfeln(<span class="hljs-number">5</span>); <br/> result.Should().Equal(<span class="hljs-number">5</span>, <span class="hljs-number">1</span>, <span class="hljs-number">1</span>, <span class="hljs-number">4</span>, <span class="hljs-number">2</span>); <br/> } <br/><br/> [Test] <br/> public void Es_kann_mehrfach_gewürfelt_werden() <br/> { <br/> var result = sut.Würfeln(<span class="hljs-number">5</span>); <br/> result = sut.Würfeln(<span class="hljs-number">1</span>); <br/> result.Should().Equal(<span class="hljs-number">2</span>); <br/> } <br/> } <br/>} <br/><br/>
Hier wird in der Setup-Methode eine Instanz der Klasse Random reingereicht, die immer die gleichen pseudozufälligen Werte liefert. Die zugehörige Implementation zeigt Listing 5.Nun könnte ich das gleiche Verfahren auf die Klasse Kniffel anwenden und für Tests eine Instanz von Random oder Würfelbecher reinreichen. Das beruht dann allerdings auf der überkommenen Vorgehensweise, die solche Abhängigkeiten akzeptiert und dann nach Lösungen sucht, die Abhängigkeitsstruktur testbar zu machen. Viel besser ist es dagegen, die Abhängigkeit zu eliminieren, dann stellt sich die Herausforderung beim Testen erst gar nicht.
Listing 5: Pseudozufallswerte über die Klasse Random liefern
<span class="hljs-keyword">using</span> System; <br/><span class="hljs-keyword">using</span> System.Collections.Generic; <br/><br/><span class="hljs-keyword">namespace</span> kniffel.game <br/>{ <br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">class</span> Würfelbecher <br/> { <br/> <span class="hljs-keyword">private</span> readonly IEnumerator&lt;<span class="hljs-keyword">int</span>&gt; enumerator; <br/><br/> <span class="hljs-keyword">public</span> Würfelbecher() <br/> : <span class="hljs-keyword">this</span>(<span class="hljs-keyword">new</span> Random(DateTime.Now.Millisecond)) { <br/> } <br/><br/> internal Würfelbecher(Random <span class="hljs-built_in">random</span>) { <br/> enumerator = NächsterWurfEnumerable(<span class="hljs-built_in">random</span>).<br/> GetEnumerator(); <br/> } <br/><br/> <span class="hljs-keyword">private</span> IEnumerable&lt;<span class="hljs-keyword">int</span>&gt; NächsterWurfEnumerable(<br/> Random <span class="hljs-built_in">random</span>) { <br/><br/> <span class="hljs-built_in">while</span> (true) { <br/> <span class="hljs-built_in">yield</span> <span class="hljs-built_in">return</span> <span class="hljs-built_in">random</span>.Next(<span class="hljs-number">1</span>, <span class="hljs-number">6</span> + <span class="hljs-number">1</span>); <br/> } <br/> } <br/><br/> <span class="hljs-keyword">private</span> <span class="hljs-keyword">int</span> NächsterWurf() { <br/> enumerator.MoveNext(); <br/> <span class="hljs-built_in">return</span> enumerator.Current; <br/> } <br/><br/> <span class="hljs-keyword">public</span> <span class="hljs-keyword">int</span>[] Würfeln(<span class="hljs-keyword">int</span> AnzahlWürfel) { <br/> var result = <span class="hljs-keyword">new</span> List&lt;<span class="hljs-keyword">int</span>&gt;(); <br/><br/> <span class="hljs-built_in">for</span> (var i = <span class="hljs-number">0</span>; i &lt; AnzahlWürfel; i++) { <br/> result.Add(NächsterWurf()); <br/> } <br/> <span class="hljs-built_in">return</span> result.ToArray(); <br/> } <br/> } <br/>} <br/><br/>
Für die Aufgabenverteilung der Klassen Kniffel und Würfelbecher kann die Abhängigkeit auch gelöst werden, wie in Bild 4 zu sehen ist.In dieser Abhängigkeitsstruktur sind das Integration Operation Segregation Principle (IOSP) sowie das Principle of Mutual Oblivion (PoMO) eingehalten. Nun ist es Aufgabe der Klasse Spiel, die beiden Aspekte Kniffel und Würfelbecher zu integrieren.
Die Klasse Spiel enthält nur Code zur Integration der beiden anderen Klassen. Kniffel und Würfelbecher dagegen enthalten nur Domänenlogik und keinen Code zur Integration. Wenn wir nun noch berücksichtigen, dass die Klasse Interactors bislang lediglich die Klasse Kniffel integriert, wird klar, dass wir den Codeanteil der Klasse Kniffel, der für die Integration des Würfelbechers zuständig ist, in den Interactor auslagern können.
Zwischenfazit
An dieser Stelle wurde die Übungsaufgabe für mich wirklich ungemütlich. Ich musste bemerken, dass durch den anfänglichen Bottom-up-Ansatz einiges schiefgegangen war. Insbesondere hatte ich es versäumt, die Entwürfe der obersten Ebene zu verfeinern, bevor ich mit der Implementation begann.Entwurf verfeinert
In Bild 5 ist zu sehen, wie ich den Entwurf der Interaktion Würfeln verfeinert habe. Nun sind die Zuständigkeiten klar getrennt. Vor allem kommt es in der Klasse Kniffel nicht mehr zur Vermischung von Aspekten. Sie ist nur noch für das Kniffelspiel zuständig.
Das Würfeln ist nach wie vor in der Klasse Würfelbecher realisiert. Allerdings ist die Integration dieser beiden Aspekte nun in den Interactor gewandert. Folgendes Listing zeigt die relevante Änderung im Interactor:
public void Würfeln(out int[] becher, out int[] tisch) {
int[] be = null;
int[] ti = null;
kniffel.Wurf_Voraussetzungen_prüfen(
fertig: (b, t) => { be = b; ti = t; },
weiter: (anzahl) => {
var wurf = würfelbecher.Würfeln(anzahl);
kniffel.Würfel_übernehmen(wurf, out be, out ti);
}
);
becher = be;
tisch = ti;
}
Etwas unschön ist die Tatsache, dass die out-Parameter nicht direkt in den Lambda Expressions zugewiesen werden können. Dies lässt mich noch mal überdenken, ob out-Parameter eine gute Lösung sind, um mehrere Werte zurückzuliefern. Die Alternative wäre die Verwendung von Tuple<> oder einer eigenen Datenstruktur. Vielleicht hilft auch die bevorstehende C#-7.0-Syntax, die es gestattet, Tuple mittels return (x, y) zurückzugeben.