Tic Tac Toe in C# (Teil 3 von 3)
Im letzten Teil dieses WPF und C# Tutorials werden wir den Code noch einmal verbessern und weitere Änderungen an der Oberfläche vornehmen. Bisher haben wir ein funktionsfähiges TicTacToe-Spiel für zwei Spieler implementiert. Betrachten wir zuerst noch einmal den Code bis zu diesem Punkt: Er funktioniert, aber die Methoden sind sehr lang und noch recht unübersichtlich – würde jemand fremdes auf den Code schauen, würde er vermutlich nicht sofort alle Schritte der einzelnen Funktionen überblicken können. Und auch wir könnten in ein paar Monaten vielleicht nicht mehr genau erklären, was in den einzelnen Zeilen passiert. Daher wollen wir nun zuerst zwei wichtige Clean Code-Praktiken anwenden: Das Vermeiden von sogenannten Magic Numbers und das Aufsplitten von Funktionen in besser überschaubare Teilfunktionen. Dieses nachtägliche Verbessern des Codes wird auch als Refactoring bezeichnet. Anschließend werden wir letzte Änderungen an der Oberfläche mit Hilfe eines Data Bindings vornehmen und lernen, wo man die ausführbare Datei für unser Programm findet.
Schritt 3: Refactoring
Praktik 1: Keine Magic Numbers / Kostanten verwenden
Als Magic Number bezeichnet man Zahlen, die einfach im Code stehen, ohne dass vollständig klar ist, was sie aussagen oder wo sie herkommen. Das hat einige Nachteile: Zum einen verschlechtert es die Lesbarkeit, zum anderen sind Zahlen im Code niemals zentral. Es ist durchaus möglich, dass sich die Zahl später ändert und dann müssen auch alle anderen numerischen Vorkommen angepasst werden. Ähnliches gilt für andere Konstanten und Werte, die im Code immer wieder vorkommen. Für unser TicTacToe-Spiel sind das unter anderem die Feldgröße und die Symbole für Spieler 1 und Spieler 2. Wir lagern diese also als Konstanten aus.
Per Konvention werden Konstanten immer vollständig mit Großbuchstaben und Unterstrichen als Trennsymbol benannt. Nun müssen wir die Vorkommen im Code nur noch ersetzen, beispielsweise:
Für den Leser ist nun klar, woraus sich die Zahlen, die im Code vorher an den entsprechenden Stellen gestanden haben, ergeben. Ändern wir die Feldgröße, müssen wir diese Information nur noch in der Konstanten ändern.
Praktik 2: Kurze Funktionen
Ein weiterer wichtiger Aspekt von Clean Code sind kurze Funktionen, die möglichst nur genau eine Sache tun. Vielleicht haben Sie schon bemerkt, dass der Code bisher mit wenig Kommentaren versehen ist. Kommentare haben den Nachteil, dass sie potentiell veralten können und dann mehr verwirren als helfen. Anstatt also Kommentare wie sie in der CheckForWinner-Methode vorkommen zu verwenden, können wir stattdessen einfach eine kleine Funktion schreiben, deren Name in etwa dem Kommentar entspricht und die an der entsprechenden Stelle aufgerufen wird. In der CheckForWinner-Methode würden wir also jeweils eine Funktion zur Prüfung der Zeilen, Spalten und Diagonalen verwenden:
Der Code für jede Prüfung wird einfach in eine Methode kopiert. Gibt eine dieser Methoden true zurück, so wurde das Spiel gewonnen. Wir können die Ergebnisse also mit einem logischen Oder verknüpfen.
Ähnlich gehen wir auch bei der Button_Click-Methode vor. Um beispielsweise klar zu machen, dass die erste If-Anweisung überprüft, ob das Feld legal selektiert werden kann, können wir der Zeile eine eigene Methode geben:
Beachten Sie, dass sich
if (FieldIsLegal(column, row))
fast nach einem richtigen Satz anhört und auf den ersten Blick verständlich macht, was passiert. Zudem kann diese Prüfung nun potentiell an anderen Stellen verwendet werden – auch, wenn wir das aktuell nicht benötigen.
Weiterhin ist es sinnvoll, eine Methode auszulagern, die das aktuelle Symbol, das verwendet werden soll, zurückgibt.
Wenn Sie wollen, können Sie an dieser Stelle den sogenannten ternären Operator verwenden. Dieser hat die Form (Bedingung) ? Option1 : Option2 und lässt If-Anweisungen mit einer Zeile formulieren:
Andere Teilschritte, die sich gut in eine andere Funktion auslagern lassen, sind das Markieren des Feldes und das Wechseln des aktiven Spielers:
In der Methode MarkField verwenden wir die eben geschriebene Methode GetCurrentSymbol.
Nachdem wir die Funktionen geschrieben haben, können wir sie in der Button_Click-Methode verwenden:
Die Methode ist nun deutlich kürzer und übersichtlich – es ist klar erkennbar, welche Schritte in welcher Reihenfolge durchlaufen werden müssen. Würde man dieses Prinzip, Methoden immer weiter aufzubrechen, bis zu Ende durchgehen, könnte man für die Button_Click-Methode noch folgende Teilfunktionen schreiben:
Wird für jegliche Option, die nach der Auswahl eines Feldes eintreten kann, weitere Logik benötigt, ist die Stelle, an der diese eingefügt werden muss, nun schnell gefunden.
Schritt 4: Letzte Änderungen an der Oberfläche
Bisher gibt es recht wenig Feedback für die Spieler, dass das Spiel beendet wurde. Das wollen wir im letzten Schritt ändern, indem wir ein einfaches Data Binding verwenden – das heißt, wir binden eine Eigenschaft der Oberfläche an eine Dateneigenschaft im Code. Wenn das Spiel beendet wurde, dann sollen die Buttons alle eine hellgraue Farbe bekommen und sie sollen Disabled sein, also nicht mehr angeklickt werden können. Dieses Verhalten muss an das Feld gameActive gebunden werden. Dazu muss das MainWindow das Interface INotifyPropertyChanged implementieren. Mit diesem Interface können wir mitteilen, ob sich die Eigenschaften oder Datenfelder des Fensters ändern.
public partial class MainWindow : Window, INotifyPropertyChanged
Um die Schnittstelle zu implementieren, benötigen wir einen PropertyChangedEventHandler. Diese Klasse wird durch C# vorgegeben und ermöglicht die Behandlung von sich ändernden Eigenschaften und Feldern.
public event PropertyChangedEventHandler PropertyChanged;
Die Codezeile wird durch das Interface vorgegeben, das heißt, Visual Studio erstellt sie automatisch, wenn Sie über die Hilfe Interface implementieren – wie in der Abbildung zu sehen – auswählen.
Anschließend brauchen wir eine Methode OnPropertyChanged, die immer dann aufgerufen werden soll, wenn sich eine Eigenschaft oder ein Feld ändert. Diese Methode erhält den Namen der Property als Parameter und „invoked“ anschließend den EventHandler PropertyChanged, ruft diesen also auf.
Das Fragezeichen hinter PropertyChanged sagt dem Programm, dass dieser Wert nicht null sein kann, also auf jeden Fall definiert ist. Der PropertyChangedEventHandler benötigt entsprechende Parameter, nämlich zum einen den Sender (in diesem Fall das Fenster selbst, also this) und zum anderen die Argumente, die speziell auf dieses Event zugeschnitten sind. Die Klasse, die hierfür ebenfalls durch C# vorgegeben wird, heißt PropertyChangedEventArgs und wird lediglich mit dem Namen der Property initialisiert. Beachten Sie, dass die Signatur damit ähnlich den Events ist, die wir bereits implementiert haben:
private void Button_Click(object sender, EventArgs e)
Anschließend müssen wir die Methode OnPropertyChanged noch für jede Property, auf deren Änderung wir reagieren wollen, aufrufen. Uns interessiert lediglich, ob sich die Variable gameActive ändert. Also schreiben wir eine weitere Property, die Getter und Setter besitzt und dieses private Feld kapselt. Einfache Getter und Setter für Properties sehen so aus:
Wird nun im Code GameActive = true gesetzt, wird der Setter aufgerufen und setzt auch das private Feld gameActive auf true. Wir fügen dem Setter allerdings noch den Aufruf der Methode OnPropertyChanged hinzu.
Zudem müssen wir alle Vorkommen in Code, an denen gameActive gesetzt wird, auf GameActive umstellen.
Nun wird die Property nur noch im Style in der XAML verknüpft:
Zuletzt müssen wir den DataContext bereits im Konstruktor des Fensters auf das aktuelle Objekt – symbolisiert durch this – setzen:
Wird nun das Spiel gestartet, sieht das Feld aus, wie vorher:
Nachdem das Spiel allerdings beendet wurde – unabhängig davon, ob unentschieden oder weil ein Spieler gewonnen hat – werden die Felder grau.
Nach dem Neustart muss der Spieler, der nun wieder zuerst am Zug ist, in das Informationslabel eingetragen werden. Wir schreiben dazu eine eigene Funktion, die an jeder Stelle verwendet werden kann, an der das Informationslabel auf den aktuellen Spieler gesetzt wird.
Damit ist das Spiel nun fertig implementiert.
Abschluss: Das Projekt bauen
Zum Abschluss brauchen wir das Projekt nun natürlich noch als .exe, also eine ausführbare Datei, damit wir es auch außerhalb von Visual Studio starten können. Dazu gehört im Grunde nicht viel. Im Ordner des Projektes gibt es einen bin-Ordner mit einem oder zwei Unterordnen, in dem die Ergebnisse des kompilierten Codes liegen:
In Visual Studio selbst gibt es verschiedene Einstellungen, wie das Projekt gebaut werden soll.
Im ersten Auswahlfeld können Sie zwischen Release und Debug wählen. Im Release-Modus gibt es einige Optimierungen, sodass Schritt-für-Schritt-Debugging nicht möglich ist. Wenn das Programm aber nur ausgeführt werden soll, ist das vorteilhaft. Über das Auswahlfeld, in dem Any CPU eingetragen ist, kann zusätzlich die Zielarchitektur – also 64-bit (x64) oder 32-bit (x86) – gewählt werden. Any CPU bedeutet, dass das Programm sowohl auf 64- als auch 32-bit-Architekturen lauffähig ist und daher zur Ausführungszeit entschieden werden kann, wie das Programm geladen wird. Stellen Sie nun den Modus nach Release, die Einstellung Any CPU kann gleich bleiben. Erstellen Sie das Programm, beispielsweise über das Menü Erstellen > Projektmappe erstellen. Anschließend befindet sich im Release-Ordner die Datei TicTacToe.exe, die Sie an einen beliebigen Ort ziehen und ausführen können.
Und wie geht es nun weiter?
Wenn Sie am TicTacToe-Spiel noch weiter arbeiten wollen, können Sie beispielsweise die Funktion zum Ermitteln des Gewinners verallgemeinern, sodass diese auch für 4×4 oder 5×5-Felder funktioniert. Sie können außerdem die Spieler das Symbol, mit dem sie Feldern markieren wollen, auswählen lassen oder einen Zähler einführen, wer bisher wie viele Spiele gewonnen hat.