Dojos für Entwickler. Stefan Lieser

Чтение книги онлайн.

Читать онлайн книгу Dojos für Entwickler - Stefan Lieser страница 3

Dojos für Entwickler - Stefan Lieser

Скачать книгу

Es zerfällt in weitere Funktionseinheiten, die eine Ebene tiefer angesiedelt sind. Diese können wiederum zerlegt werden. Bei der Zerlegung können zwei unterschiedliche Fälle betrachtet werden:

       vertikale Zerlegung,

       horizontale Zerlegung.

      Der Wurzelknoten des Baums ist das gesamte Spiel. Diese Funktionseinheit ist jedoch zu komplex, um sie „in einem Rutsch" zu implementieren. Also wird sie zerlegt. Durch die Zerlegung entsteht eine weitere Ebene im Baum. Dieses Vorgehen bezeichne ich daher als vertikale Zerlegung.

      Kümmert sich eine Funktionseinheit um mehr als eine Sache, wird sie horizontal zerlegt. Wäre es beispielsweise möglich, einen Spielzustand in eine Datei zu speichern, könnte das Speichern im ersten Schritt in der Funktionseinheit Spiellogik angesiedelt sein. Dann stellt man jedoch fest, dass diese Funktionseinheit für mehr als eine Verantwortlichkeit zuständig wäre, und zieht das Speichern heraus in eine eigene Funktionseinheit. Dies bezeichne ich als horizontale Zerlegung.

      Erst wenn die Funktionseinheiten hinreichend klein sind, kann ich mir Gedanken darum machen, wie ich sie implementiere. Im Falle des Vier-gewinnt-Spiels zerfällt das Problem in die eigentliche Spiellogik und die Benutzerschnittstelle. Die Benutzerschnittstelle muss in diesem Fall nicht weiter zerlegt werden. Das mag in komplexen Anwendungen auch mal anders sein. Diese erste Zerlegung der Gesamtaufgabe zeigt Abbildung 1.

      [Abb. 1] Die Aufgabe in Teile zerlegen: erster Schritt ...

      Die Spiellogik ist mir als Problem noch zu groß, daher zerlege ich diese Funktionseinheit weiter. Dies ist eine vertikale Zerlegung, es entsteht eine weitere Ebene im Baum. Die Spiellogik zerfällt in die Spielregeln und den aktuellen Zustand des Spiels. Die Zerlegung ist in Abbildung 2 dargestellt. Die Spielregeln sagen zum Beispiel aus, wer das Spiel beginnt, wer den nächsten Zug machen darf et cetera.

      [Abb. 2] ... und zweiter Schritt.

      Der Zustand des Spiels wird beim echten Spiel durch das Spielfeld abgebildet. Darin liegen die schon gespielten Steine. Aus dem Spielfeld geht jedoch nicht hervor, wer als Nächster am Zug ist. Für die Einhaltung der Spielregeln sind beim echten Spiel die beiden Spieler verantwortlich, in meiner Implementierung ist es die Funktionseinheit Spielregeln.

      Ein weiterer Aspekt des Spielzustands ist die Frage, ob bereits vier Steine den Regeln entsprechend zusammen liegen, sodass ein Spieler gewonnen hat. Ferner birgt der Spielzustand das Problem, wohin der nächste gelegte Stein fällt. Dabei bestimmt der Spieler die Spalte und der Zustand des Spielbretts die Zeile: Liegen bereits Steine in der Spalte, wird der neue Spielstein zuoberst auf die schon vorhandenen gelegt.

      Damit unterteilt sich die Problematik des Spielzustands in die drei Teilaspekte

       Steine legen,

       nachhalten, wo bereits Steine liegen,

       erkennen, ob vier Steine zusammen liegen.

      Vom Problem zur Lösung

      Nun wollen Sie sicher so langsam auch mal Code sehen. Doch vorher muss noch geklärt werden, was aus den einzelnen Funktionseinheiten werden soll. Werden sie jeweils eine Klasse? Eher nicht, denn dann wären Spiellogik und Benutzerschnittstelle nicht ausreichend getrennt. Somit werden Benutzerschnittstelle und Spiellogik mindestens eigenständige Komponenten. Die Funktionseinheiten innerhalb der Spiellogik hängen sehr eng zusammen. Alle leisten einen Beitrag zur Logik. Ferner scheint mir die Spiellogik auch nicht komplex genug, um sie weiter aufzuteilen. Es bleibt also bei den beiden Komponenten Benutzerschnittstelle und Spiellogik.

      Um beide zu einem lauffähigen Programm zusammenzusetzen, brauchen wir noch ein weiteres Projekt. Seine Aufgabe ist es, eine EXE-Datei zu erstellen, in der die beiden Komponenten zusammengeführt werden. So entstehen am Ende drei Komponenten.

      Abbildung 3 zeigt die Solution für die Spiellogik. Sie enthält zwei Projekte: eines für die Tests, ein weiteres für die Implementierung.

      [Abb. 3] Aufbau der Solution.

      Die Funktionseinheit Spielzustand zerfällt in drei Teile. Beginnen wir mit dem Legen von Steinen. Beim Legen eines Steins in das Spielfeld wird die Spalte angegeben, in die der Stein gelegt werden soll. Dabei sind drei Fälle zu unterscheiden: Die Spalte ist leer, enthält schon Steine oder ist bereits voll.

      Es ist naheliegend, das Spielfeld als zweidimensionales Array zu modellieren. Jede Zelle des Arrays gibt an, ob dort ein gelber, ein roter oder gar kein Stein liegt. Der erste Index des Arrays bezeichnet dabei die Spalte, der zweite die Zeile. Beim Platzieren eines Steins muss also der höchste Zeilenindex innerhalb der Spalte ermittelt werden. Ist dabei das Maximum noch nicht erreicht, kann der Stein platziert werden.

      Bleibt noch eine Frage: Wie ist damit umzugehen, wenn ein Spieler versucht, einen Stein in eine bereits gefüllte Spalte zu legen? Eine Möglichkeit wäre: Sie stellen eine Methode bereit, die vor dem Platzieren eines Steins aufgerufen werden kann, um zu ermitteln, ob dies in der betreffenden Spalte möglich ist. Der Code sähe dann ungefähr so aus:

      if(spiel.KannPlatzieren(3)) {

       spiel.LegeSteinInSpalte(3);

       }

      Dabei gibt der Parameter den Index der Spalte an, in die der Stein platziert werden soll. Das Problem mit diesem Code ist, dass er gegen das Prinzip „Tell don’t ask" verstößt. Als Verwender der Funktionseinheit, die das Spielbrett realisiert, bin ich gezwungen, das API korrekt zu bedienen. Bevor ein Spielstein mit LegeSteinlnSpalte() in das Spielbrett gelegt wird, müsste mit KannPlatzieren() geprüft werden, ob dies überhaupt möglich ist. Nach dem „Tell don’t ask"-Prinzip sollte man Klassen so erstellen, dass man den Objekten der Klasse mitteilt, was zu tun ist - statt vorher nachfragen zu müssen, ob man eine bestimmte Methode aufrufen darf. Im Übrigen bleibt bei der Methode LegeSteinInSpalte() das Problem bestehen: Was soll passieren, wenn die Spalte bereits voll ist?

      Eine andere Variante könnte sein, die Methode LegeSteinlnSpalte() mit einem Rückgabewert auszustatten. War das Platzieren erfolgreich, wird true geliefert, ist die Spalte bereits voll, wird false geliefert. In dem Fall müsste sich der Verwender der Methode mit dem Rückgabewert befassen. Am Ende soll der Versuch, einen Stein in eine bereits gefüllte Spalte zu platzieren, dem Benutzer gemeldet werden. Also müsste der Rückgabewert bis in die Benutzerschnittstelle transportiert werden, um dort beispielsweise eine Messagebox anzuzeigen.

      Die Idee, die Methode mit einem Rückgabewert auszustatten, verstößt jedoch ebenfalls gegen ein Prinzip, nämlich die „Command/Query Separation". Dieses Prinzip besagt, dass eine Methode entweder ein Command oder eine Query sein sollte, aber nicht beides. Dabei ist ein Command eine Methode, die den Zustand des Objekts verändert. Für die Methode LegeSteinlnSpalteO trifft dies zu: Der Zustand des Spielbretts ändert sich dadurch. Eine Query ist dagegen eine Methode, die eine Abfrage über den Zustand des Objekts enthält und dabei den Zustand nicht verändert. Würde die Methode LegeSteinInSpalte() einen Rückgabewert haben, wäre sie dadurch gleichzeitig eine Query.

      Nach diesen Überlegungen bleibt

Скачать книгу