Dojos für Entwickler - Stefan Lieser - E-Book

Dojos für Entwickler E-Book

Stefan Lieser

0,0

Beschreibung

Das Sonderheft dotnetpro.dojo des Entwicklermagazins dotnetpro enthält 15 Aufgaben und die Lösungen dazu aus allen Bereichen der Softwareentwicklung. Aus dem Inhalt: Vier gewinnt implementieren, Data Binding, Testdatengenerator bauen, Mogeln mit EVA bei Minesweeper, Boxplot-Control bauen, RavenDB explorieren, Stack und Queue implementieren, einen Windows-Dienst bauen, Event-Based Components einsetzen, Baumstruktur implementieren, LINQ nachbauen, Twitter-Client, API für Graphen entwerfen und eine ToDo-Liste mit Synchronisation bauen. Zusätzlich gibt es noch zwei Grundlagenartikel: - Model-View-ViewModel und Event-Based Components: Wie passt das zusammen - Klassische Katas: Die bekanntesten Aufgabe der Coding Dojos. Stefan Lieser zeigt in jeder Lösung das genaue Vorgehen: Über welche Schritte kommt er zum geforderten Produkt. Dabei muss auch er die ein oder andere Hürde überklettern. Klettern Sie mit ihm und Sie lernen mit Ihm, worauf es bei der Softwareentwicklung ankommt.

Sie lesen das E-Book in den Legimi-Apps auf:

Android
iOS
von Legimi
zertifizierten E-Readern
Kindle™-E-Readern
(für ausgewählte Pakete)

Seitenzahl: 295

Veröffentlichungsjahr: 2012

Das E-Book (TTS) können Sie hören im Abo „Legimi Premium” in Legimi-Apps auf:

Android
iOS
Bewertungen
0,0
0
0
0
0
0
Mehr Informationen
Mehr Informationen
Legimi prüft nicht, ob Rezensionen von Nutzern stammen, die den betreffenden Titel tatsächlich gekauft oder gelesen/gehört haben. Wir entfernen aber gefälschte Rezensionen.



Einleitung

Der Spruch „Übung macht den Meister“ ist abgedroschen, weil oft bemüht, weil einfach richtig. Deshalb finden Sie in diesem Sonderheft 15 dotnetpro.dojos, also Übungsaufgaben inklusive einer Musterlösung und Grundlagen.

Ein Profimusiker übt täglich mehrere Stunden. Er übt Fingerfertigkeit, Phrasierung, Ansatz beziehungsweise Haltung, Intonation und Vom-Blatt-Spielen. Als Hilfsmittel verwendet er Tonleitern, Etüden, Ausschnitte von Stücken und Unbekanntes. Ohne Üben könnte er die Qualität seines Spiels nicht halten, geschweige denn verbessern. Üben gehört für ihn dazu.

Wie sieht das bei Ihnen und der Programmiererei aus? Sie sind doch auch Profi. Nicht in der Musik, aber doch beim Codieren an der Computertastatur. Üben Sie auch? Gemeint ist nicht die Aufführung, sprich das Program-mieren, mit dem Sie sich Ihr Einkommen verdienen. Gemeint sind die Etüden, das Üben von Fingerfertigkeit, Intonation, Ansatz und Vom-Blatt-Spielen.

Wie sehen diese Aufgaben denn bei einem Programmierer aus? Freilich ließe sich die Analogie bis zum Abwinken auslegen. Hier mag ein kleiner Ausschnitt genügen: Sie könnten als Etüde zum Beispiel trainieren, dass Sie immer erst den Test schreiben und dann die Implementation der Methode, die den Test erfüllt. Damit verwenden Sie künftig nicht immer wieder den falschen Fingersatz, sondern immer gleich die richtige Reihenfolge: Test - Implementation.

Klar, Üben ist zeitraubend und manchmal nervtötend - vor allem für die, die zuhören.

Aber Üben kann auch Spaß machen. Kniffeln, eine Aufgabe lösen und dann die eigene Lösung mit einer anderen Lösung vergleichen. Das ist der Grundgedanke beim dotnetpro.dojo. In jeder Ausgabe stellt dotnetpro eine Aufgabe, die in maximal drei Stunden zu lösen sein sollte. Sie investieren einmal pro Monat wenige Stunden und ge -winnen dabei jede Menge Wissen und Erfahrung.

Den Begriff Dojo hat die dotnetpro nicht erfunden. Dojo nennen die Anhänger fernöstlicher Kampfsportarten ihren Übungsraum. Aber auch in der Programmierung hat sich der Begriff eines Code Dojo für eine Übung eingebürgert.

Das können Sie gewinnen

Der Gewinnlässt sich in einWort fassen: Lernen. Das ist Sinn und Zweck eines Dojo. Sie können/dürfen/sollen lernen. Einen materiellen Preis loben wir nicht aus.

Ein dot-netpro.dojo ist kein Contest. Dafür gilt aber:

Falsche Lösungen gibt es nicht. Es gibt möglicherweise elegantere, kürzere oder schnellere, aber keine falschen.

Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das können Sie, indem Sie Ihre Lösung mit der vergleichen, die Sie eine Ausgabe später in der dotnetpro finden.

Wer stellt die Aufgabe? Wer liefert die Lösung?

Die kurze Antwort lautet: Stefan Lieser. Die lange Antwort lautet: Stefan Lieser, seines Zeichens Mitinitiator der Clean Code Deve-loper Initiative. Stefan ist freiberuflicher Trainer und Berater und Fan von intelligenten Entwicklungsmethoden, die für Qualität der resultierenden Software sorgen. Er denkt sich die Aufgaben aus und gibt dann auch seine Lösung zum Besten. Er wird auch mitteilen, wie lange er gebraucht und wie viele Tests er geschrieben hat. Das dient - wie oben schon gesagt - nur als Anhaltspunkt. Falsche Lösungen gibt es nicht.

Inhalt

15 Aufgaben und Lösungen

Aufgabe 1: Vier gewinnt

Ein Spielfeld, zwei Spieler und jede Menge Spaß beim Programmieren: Das kleine Brettspiel ist genau das Richtige zum Warmwerden.

Aufgabe 2: Data Binding

Knüpfe Kontrollelement an Eigenschaft, und schon wirkt der Zauber: Veränderungen der Eigenschaft spiegeln sich im Control wider und auch andersherum.

Aufgabe 3: Testdatengenerator 

Meier, Müller, Schulze – ganze 250000 Mal: Für einen Testdatengenerator ist das eine Sache von Sekunden. Aber wie baut man einen solchen?

Aufgabe 4: Mogeln mit EVA

Statt Rein-Raus-Kaninchentechnik die Eingabe, Verarbeitung, Ausgabe: modernste Technik im Dienst des Mogelns beim Minesweeper-Spiel. Na super.

Aufgabe 5: Boxplot

Packen Sie den Sandsack wieder weg: nicht Box, platt, sondern Boxplot: Diese spezielle Grafikform zeigt kleinsten und größten Wert, Mittelwert und die Quartile.

Aufgabe 6: RavenDB

Computer aus, Daten weg? Von wegen: Eine Persistenzschicht sorgt für deren Überleben. Mit RavenDB braucht man dafür auch keinen SQL-Server.

Aufgabe 7: Stack und Queue

Wie bitte? Stack und Queue bietet doch das .NET Framework. Stimmt. Aber die Selbstimplementierung bringt viel Selbsterkenntnis. Sie werden es sehen.

Aufgabe 8: Windows-Dienst

Er arbeitet im Verborgenen, im Untergrund. Ist aber so wichtig, dass auf ihn nicht verzichtet werden kann. Bauen Sie doch mal einen.

Aufgabe 9: Event-Based Components

Was, bitte schön, hat Silbentrennung mit EBC zu tun? Erst einmal gar nichts. Es sei denn, die Aufgabe lautet: Baue Silbentrennservice mit EBCs.

Aufgabe 10: ITree<T>

Ich bau ’nen Baum für dich. Aus Wurzel, Zweig und Blatt und den Interfaces ITree<T> und INode<T>. Und Sie dürfen ihn erklettern.

Aufgabe 11: LINQ

Frage: Wie heißt die bekannteste Abfragesprache? Richtig: SQL. Aber in dieser Aufgabe geht es um eine andere: Language Integrated Query.

Aufgabe 12: Twitter

Es treten auf: mehrere Threads, eine Synchronisation, ein Timer, ein Control – wahlweise in WPF-,Windows-Forms- oder Silverlight-Qualität – und ein API. Fertig ist das Twitter-Band.

Aufgabe 13: Graphen

Entwerfen Sie ein API für den Umgang mit gerichteten Graphen, implementieren Sie die Datenstruktur und einen beliebigen Algorithmus dazu, wie etwa topologische Sortierung. Und los.

Aufgabe 14: ToDo, MVVM und Datenfluss

Am Ende haben Sie eine nützliche ToDo-Listen-Anwendung. Am Anfang haben Sie ein Problem:Wie modellieren Sie die Softwarearchitektur? Aber nur Mut: Auch das klappt.

Aufgabe 15: ToDo und die Cloud

Die ToDo-Listen-Anwendung soll jetzt noch richtig cool werden: durch eine Synchronisation über die Cloud. Ein bisschen Hirnschmalz ...

Grundlagen

MVVM und EBC

Model View ViewModel und Event-Based Components: Das sind zwei aktuelle Technologien, die sich aber gut miteinander kombinieren lassen. Stefan Lieser zeigt, wie das geht.

Klassische Katas

Sie heißen Kata Potter, Kata BankOCR oder Kata FizzBuzz: An klassischen Programmieraufgaben gibt es inzwischen schon ganze Kataloge. Tilman Börner stellt die wichtigsten vor.

Impressum

Impressum

AUFGABE

„Stefan, vielleicht sollten wir erst einmal mit etwas Einfacherem anfangen. Vielleicht wäre ein kleines Spiel zum Warmwerden genau das Richtige. Fällt dir dazu eine Aufgabe ein?“

Wer übt, gewinnt

In jeder dotnetpro finden Sie eine Übungsaufgabe von Stefan Lieser, die in maximal drei Stunden zu lösen sein sollte.Wer die Zeit investiert, gewinnt in jedem Fall – wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen.

Es gilt :

Falsche Lösungen gibt es nicht. Es gibt möglicherweise elegantere, kürzere oder schnellere Lösungen, aber keine falschen.

Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das können Sie, indem Sie Ihre Lösung mit der vergleichen, die Sie eine Ausgabe später in der dotnetpro finden.

Übung macht den Meister. Also − los geht’s. Aber Sie wollten doch nicht etwa sofort Visual Studio starten...

Klar, können wir machen. Wie wäre es beispielsweise mit dem Spiel 4 gewinnt? Bei dieser Aufgabe geht es vor allem um eine geeignete Architektur und die Implementierung der Logik und nicht so sehr um eine schicke Benutzeroberfläche.

4 gewinnt wird mit einem aufrecht stehenden Spielfeld von sieben Spalten gespielt. In jede Spalte können von oben maximal sechs Spielsteine geworfen werden. Ein Spielstein fällt nach unten, bis er entweder auf den Boden trifft, wenn es der erste Stein in der Spalte ist, oder auf den schon in der Spalte liegenden Steinen zu liegen kommt. Die beiden Spieler legen ihre gelben beziehungsweise roten Spielsteine abwechselnd in das Spielfeld. Gewonnen hat der Spieler, der zuerst vier Steine direkt übereinander, nebeneinander oder diagonal im Spielfeld platzierenkonnte.

Implementieren Sie ein Spiel...

Ein Spiel, das zwei Spieler gegeneinander spielen. Die Implementierung soll die Spielregeln überwachen. So soll angezeigt werden, welcher Spieler am Zug ist (Rot oder Gelb). Ferner soll angezeigt werden, ob ein Spieler gewonnen hat. Diese Auswertung erfolgt nach jedem Zug, sodass nach jedem Zug angezeigt wird, entweder welcher Spieler an der Reihe ist oder wer gewonnen hat. Hat ein Spieler gewonnen, ist das Spiel zu Ende und kann neu gestartet werden.

Damit es unter den Spielern keinen Streit gibt, werden die Steine, die zum Gewinn führten, ermittelt. Bei einer grafischen Benutzeroberfläche könnten die vier Steine dazu farblich markiert oder eingerahmt werden. Bei einer Konsolenoberfläche können die Koordinaten der Steine ausgegeben werden.

Die Bedienung der Anwendung erfolgt so, dass der Spieler, der am Zug ist, die Spalte angibt, in die er einen Stein werfen will. Dazu sind die Spalten von eins bis sieben nummeriert. Bei einer grafischen Benutzeroberfläche können die Spalten je durch einen Button gewählt werden. Wird das Spiel als Konsolenanwendung implementiert, genügt die Eingabe der jeweiligen Spaltennummer per Tastatur.

Die Abbildungen 1 und 2 zeigen, wie eine Oberfläche aussehen könnte. Ist die Spalte, in die der Spieler seinen Stein legen möchte, bereits ganz mit Steinen gefüllt, erfolgt eine Fehlermeldung, und der Spieler muss erneut einen Spielstein platzieren.

[Abb. 1 und 2] Eine mögliche Oberfläche (links) und die Anzeige der siegreichen vier Steine (rechts). Aber auf die Oberfläche kommt es bei dieser Übung nicht an.

Programmieraufgabe

Die Programmieraufgabe lautet, ein Spiel 4 gewinnt zu implementieren. Dabei liegt der Schwerpunkt auf dem Entwurf einer angemessenen Architektur, der Implementierung der Spiellogik und zugehörigen automatisierten Tests.

Die Benutzerschnittstelle des Spiels steht eher im Hintergrund. Ob animierte WPF-Oberfläche, WinForms, ASP.NET oder Konsolenanwendung, das ist nicht wichtig. ImVor-dergrund soll eine Lösung stehen, die leicht in eine beliebige Oberflächentechnologie integriert werden kann. Evolvierbarkeit und Korrektheit sollen hier also stärker bewertet werden als eine superschicke Oberfläche.

Im nächsten Heft zeigen wir eine exemplarische Musterlösung. „Die" Lösungkann es in einem solchen Fall bekanntlich eh nicht geben. Damit möchte ich Sie, lieber Leser, noch mal ermutigen, sich der Aufgabe anzunehmen. Investieren Sie etwas Zeit, und erarbeiten Sie eine eigene Lösung. Die können Sie dann später mit der hier vorgestellten vergleichen. Viel Spaß!

LÖSUNG

Eine Übung, bei der Sie nur gewinnen konnten

Vier gewinnt. Eine Lösung

Die Aufgabe war, das Spiel „Vier gewinnt" zu implementieren. Auf den ersten Blick ist das eine eher leichte Übung. Erst bei genauerem Hinsehen erkennt man die Schwierigkeiten. Wie zerlegt man beispielsweise die Aufgabenstellung, um überschaubare Codeeinheiten zu erhalten?

Leser, die sich der Aufgabe angenommen haben, ein Vier-gewinnt-Spiel zu implementieren [1], werden es gemerkt haben: Der Teufel steckt im Detail. Der Umgang mit dem Spielfeld, das Erkennen von Vierergruppen, wo soll man nur anfangen? Wer zu früh gezuckt hat und sofort mit der Codeeingabe begonnen hat, wird es vielleicht gemerkt haben: Die Aufgabe läuft aus dem Ruder, wächst einem über den Kopf.

Das ging mir nicht anders. Früher. Heute setze ich mich erst mit einem Blatt Papier hin, bevor ich beginne, Code zu schreiben. Denn die erste Herausforderung besteht nicht darin, das Problem zu lösen, sondern es zu verstehen.

Beim Vier-gewinnt-Spiel war eine Anforderung bewusst ausgeklammert: die Benutzerschnittstelle. In der Aufgabe geht es um die Logik des Spiels. Am Ende soll demnach eine Assembly entstehen, in der die Spiellogik enthalten ist. Diese kann dann in einer beliebigen Benutzerschnittstelle verwendet werden.

Beim Spiel selbst hilft es, sich die Regeln vor Augen zu führen. Zwei Spieler legen abwechselnd gelbe und rote Spielsteine in ein 7 x 6 Felder großes Spielfeld. Derjenige, der als Erster vier Steine seiner Farbe nebeneinander liegen hat, hat das Spiel gewonnen. Hier hilft es, sich mögliche Vierergruppen aufzumalen, um zu erkennen, welche Konstellationen im Spielfeld auftreten können.

Nachdem ich das Problem durchdrungen habe, zeichnet sich eine algorithmische Lösung ab. Erst jetzt beginne ich, die gesamte Aufgabenstellung in Funktionseinheiten zu zerlegen. Ich lasse zu diesem Zeitpunkt ganz bewusst offen, ob eine Funktionseinheit am Ende eine Methode, Klasse oder Komponente ist. Wichtig ist erst einmal, dass jede Funktionseinheit eine klar definierte Aufgabe hat.

Hat sie mehr als eine Aufgabe, zerlege ich sie in mehrere Funktionseinheiten. Stellt man sich die Funktionseinheiten als Baum vor, in dem die Abhängigkeiten die verschiedenen Einheiten verbinden, dann steht auf oberster Ebene das gesamte Spiel. 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 nur eine Variante übrig: Die Methode LegeSteinInSpalte() sollte eine Ausnahme auslösen, wenn das Platzieren nicht möglich ist. Die Ausnahme kann in der Benutzerschnittstelle abgefangen und dort in einer entsprechenden Meldung angezeigt werden. Damit entfällt die Notwendigkeit, einen Rückgabewert aus der Spiellogik bis in die Benutzerschnittstelle zu transportieren. Ferner sind die Prinzipien „Tell don’t ask" und „Com-mand/Query Separation" eingehalten.

Vier Steine finden

Nun sind mit dem zweidimensionalen Array und der Methode LegeSteinInSpalte() bereits zwei Teilprobleme des Spielzustands gelöst: Im zweidimensionalen Array ist der Zustand des Spielbretts hinterlegt, und die Methode LegeSteinlnSpalteO realisiert die Platzierungslogik. Das dritte Problem ist die Erkennung von Vierergruppen, also eines Gewinners.

Vier zusammenhängende Steine können beim Vier-gewinnt-Spiel in vier Varianten auftreten: horizontal, vertikal, diagonal nach oben, diagonal nach unten.

Diese vier Varianten gilt es zu implementieren. Dabei ist wichtig zu beachten, dass die vier Steine unmittelbar zusammen liegen müssen, es darf sich also kein gegnerischer Stein dazwischen befinden.

Ich habe zuerst versucht, diese Vierergruppenerkennung direkt auf dem zweidimensionalen Array zu lösen. Dabei habe ich festgestellt, dass das Problem in zwei Teilprobleme zerlegt werden kann:

Ermitteln der Indizes benachbarter Felder.

Prüfung, ob vier benachbarte Felder mit Steinen gleicher Farbe besetzt sind.

Für das Ermitteln der Indizes habe ich daher jeweils eigene Klassen implementiert, welche die Logik der benachbarten Indizes enthalten. Eine solche Vierergruppe wird mit einem Startindex instanziert und liefert dann die Indizes der vier benachbarten Felder. Diese Vierergruppen werden anschließend verwendet, um im Spielfeld zu ermitteln, ob die betreffenden Felder alle Steine derselben Farbe enthalten. Die betreffenden Klassen heißen HorizontalerVierer, VertikalerVierer, DiagonalHochVierer und DiagonalRunterVierer. Listing 1 zeigt exemplarisch die Klasse HorizontalerVierer.

Zunächst fällt auf, dass die Klasse internal ist. Sie wird im Rahmen der Spiellogik nur intern benötigt, daher soll sie nicht außerhalb der Komponente sichtbar sein. Damit Unit-Tests für die Klasse möglich sind, habe ich auf der Assembly das Attribut InternalsVisibleTo gesetzt. Dadurch kann die Assembly, welche die Tests enthält, auf die internen Details zugreifen.

Aufgabe der Klasse HorizontalerVierer ist es, vier Koordinaten zu horizontal nebeneinander liegenden Spielfeldern zu liefern. Dies erfolgt in den Properties Eins, Zwei, Drei und Vier. Dort werden jeweils die Indizes ermittelt.

Das Ermitteln eines Gewinners geschieht anschließend in einem Flow aus zwei Schritten. Im ersten Schritt wird aus einem Spielfeld die Liste der möglichen Vierergruppen bestimmt. Im zweiten Schritt wird aus dem Spielfeld und den möglichen Vierergruppen ermittelt, ob eine der Vierergruppen Steine derselben Farbe enthält.

Die beiden Schritte des Flows sind als Extension Methods realisiert. Dadurch sind sie leicht isoliert zu testen. Anschließend können sie hintereinander ausgeführt, also als Flow zusammengeschaltet werden:

Der Flow wird an zwei Stellen verwendet: zum einen beim Ermitteln des Gewinners, zum anderen, um zu bestimmen, welche Steine zum Sieg geführt haben. Da die Methode AlleVierer() ein IEnumerable liefert und SelbeFarbe() dies als ersten Parameter erwartet, können die beiden Extension Methods hintereinander geschrieben werden. Da das Spielfeld in beiden Methoden benötigt wird, verfügt SelbeFarbe() über zwei Parameter.

Das Ermitteln von vier jeweils nebeneinander liegenden Feldern übernimmt die Methode AlleVierer(). Ein kurzer Ausschnitt zeigt die Arbeitsweise:

Auch diese Methode ist internal, da sie außerhalb der Komponente nicht benötigt wird. In zwei geschachtelten Schleifen werden die Anfangsindizes von horizontalen Vierergruppen ermittelt. Für jeden Anfangsindex wird mit yield return eine Instanz eines HorizontalerVierers geliefert. Dieser übernimmt das Ermitteln der drei anderen Indizes.

Eine Alternative zur gezeigten Methode wäre, die möglichen Vierer als Konstanten zu hinterlegen. Es würde dann die Berechnung in AlleVierer() entfallen, ferner die Klassen HorizontalerVierer et cetera. Ob die Felder einer Vierergruppe alle mit Steinen der gleichen Farbe besetzt sind, analysiert die Methode SelbeFarbe(). Durch die Verwendung der Klassen HorizontalerVierer et cetera ist dies einfach: Jeder Vierer liefert seine vier Koordinaten. Damit muss nur noch im Spielfeld nachgesehen werden, ob sich an allen vier Koordinaten Steine gleicher Farbe befinden, siehe Listing 2.

Am Ende müssen die einzelnen Funktionseinheiten nur noch gemeinsam verwendet werden. Die dafür verantwortliche Klasse heißt VierGewinntSpiel. Sie ist public und repräsentiert nach außen die Komponente. Die Klasse ist für die Spielregeln zuständig. Da das abwechselnde Ziehen so einfach ist, habe ich mich entschlossen, diese Logik nicht auszulagern. In der Methode LegeSteinInSpalte(int spalte) wird der Zustand des Spiels aktualisiert. Dies geht ansatzweise wie folgt:

Die Ermittlung eines Gewinners erfolgt also im Spielbrett, während hier nur der Zustand des Spiels verwaltet wird.

Fazit: Die richtigen Vorüberlegungen sind der Schlüssel zu einer erfolgreichen Implementierung. [ml]

[1] Stefan Lieser, Wer übt, gewinnt, dotnetpro 3/2010, Seite 118 f., www.dotnetpro.de/A1003dojo

AUFGABE

INotifyPropertyChanged-Logik automatisiert testen

Zauberwort

DataBinding ist eine tolle Sache: Objekt an Formular binden und wie von Zauberhand stellen die Controls die Eigenschaftswerte des Objekts dar. DataBinding ist aber auch knifflig. Stefan, kannst du dazu eine Aufgabe stellen?

Wer übt, gewinnt

In jeder dotnetpro finden Sie eine Übungsaufgabe von Stefan Lieser, die in maximal drei Stunden zu lösen sein sollte.Wer die Zeit investiert, gewinnt in jedem Fall – wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen.

Es gilt :

Falsche Lösungen gibt es nicht. Es gibt möglicherweise elegantere, kürzere oder schnellere Lösungen, aber keine falschen.

Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das können Sie, indem Sie Ihre Lösung mit der vergleichen, die Sie eine Ausgabe später in der dotnetpro finden.

Übung macht den Meister. Also − los geht’s. Aber Sie wollten doch nicht etwa sofort Visual Studio starten...

DataBinding ist beliebt. Lästig daran ist: Man muss die INotifyPropertyChanged-Schnittstelle implementieren. Sie fordert, dass bei Änderungen an den Eigenschaften eines Objekts das Ereignis PropertyChanged ausgelöst wird. Dabei muss dem Ereignis der Name der geänderten Eigenschaft als Parameter in Form einer Zeichenkette übergeben werden. Die Frage, die uns diesmal beim dotnetpro.dojo interessiert, ist: Wie kann man die Implementierung der INotifyPropertyChanged-Schnittstelle automatisiert testen?

Die Funktionsweise des Events für eine einzelne Eigenschaft zu prüfen ist nicht schwer. Man bindet einen Delegate an den Property-Changed-Event und prüft, ob erbeiÄnderung der Eigenschaft aufgerufen wird. Außerdem ist zu prüfen, ob der übergebene Name der Eigenschaft korrekt ist, siehe Listing 3.

Um zu prüfen, ob der Delegate aufgerufen wurde, erhöhen Sie im Delegate beispielsweise eine Variable, die außerhalb definiert ist. Durch diesen Seiteneffekt können Sie überprüfen, ob der Event beim Ändern der Eigenschaft ausgelöst und dadurch der Delegate aufgerufen wurde. Den Namen der Eigenschaft prüfen Sie innerhalb des Delegates mit einem Assert.

Solche Tests für jede Eigenschaft und jede Klasse, die INotifyPropertyChanged implementiert, zu schreiben, wäre keine Lösung, weil Sie dabei Code wiederholen würden. Da die Eigenschaften einer Klasse per Reflection ermittelt werden können, ist es nicht schwer, den Testcode so zu verallgemeinern, dass damit alle Eigenschaften einer Klasse getestet werden können. Also lautet in diesem Monat die Aufgabe: Implementieren Sie eine Klasse zum automatisierten Testen der INotifyPropertyChanged-Logik. Die zu implementierende Funktionalität ist einWerkzeug zum Testen von ViewModels. Dieses Werkzeug soll wie folgt bedient werden:

NotificationTester.Verify<MyViewModel>();

Die Klasse, die auf INotifyPropertyChanged-Semantik geprüft werden soll, wird als generischer Typparameter an die Methode übergeben. Die Prüfung soll so erfolgen, dass per Reflection alle Eigenschaften der Klasse gesuchtwerden, die über einen Setter und Getter verfügen. Für diese Eigenschaften soll geprüft werden, ob sie bei einer Zuweisung an die Eigenschaft den PropertyChanged-Event auslösen und dabei den Namen der Eigenschaft korrekt übergeben. Wird der Event nicht korrekt ausgelöst, muss eine Ausnahme ausgelöst werden. Diese führt bei der Ausführung des Tests durch das Unit-Test-Frame-work zum Scheitern des Tests.

Damit man weiß, für welche Eigenschaft die Logik nicht korrekt implementiert ist, sollte die Ausnahme mit den notwendigen Informationen ausgestattet werden, also dem Namen der Klasse und der Eigenschaft, für die der Test fehlschlug.

In einer weiteren Ausbaustufe könnte das Werkzeug dann auch auf Klassen angewandt werden, die ebenfalls per Reflection ermittelt wurden. Fasst man beispielsweise sämtliche ViewModels in einem bestimmten Namespace zusammen, kann eine Assembly nach ViewModels durchsucht werden. Damit die so gefundenen Klassen überprüft werden können, muss es möglich sein, das Testwerkzeug auch mit einem Typ als Parameter aufzurufen :

NotificationTester.Verify (typeof(MyViewModel));

Im nächsten Heft finden Sie eine Lösung des Problems. Aber versuchen Sie sich zunächst selbst an der Aufgabe. [ml]

LÖSUNG

INotifyPropertyChanged-Logik automatisiert testen

Kettenreaktion

Das automatisierte Testen der INotifyPropertyChanged-Logik ist nicht schwer. Man nehme einen Test, verallgemeinere ihn, streue eine Prise Reflection darüber, fertig. Doch wie zerlegt man die Aufgabenstellung so in Funktionseinheiten, dass diese jeweils genau eine definierte Verantwortlichkeit haben? Die Antwort: Suche den Flow!

Wie man die INotifyPropertyChanged-Logik automatisiert testen kann, habe ich in der Aufgabenstellung zu dieser Übung bereits gezeigt [1]. Doch wie verallgemeinert man nun diesen Test so, dass er für alle Eigenschaften einer Klasse automatisiert ausgeführt wird?

Im Kern basiert die Lösung auf folgender Idee: Suche per Reflection alle Properties einer Klasse und führe den Test für die gefundenen Properties aus. Klingt einfach, ist es auch. Aber halt: Bitte greifen Sie nicht sofort zur Konsole! Auch bei vermeintlich unkomplizierten Aufgabenstellungen lohnt es sich, das Problem so zu zerlegen, dass kleine, überschaubare Funktionseinheiten mit einer klar abgegrenzten Verantwortlichkeit entstehen.

Suche den Flow!

Ich möchte versuchen, die Aufgabenstellung mit einem Flow zu lösen. Doch dazu sollte ich ein klein wenig ausholen und zunächst erläutern, was ein Flow ist und wo seine Vorteile liegen.

Vereinfacht gesagt ist ein Flow eine Aneinanderreihung von Funktionen. Ein Argument geht in die erste Funktion hinein, diese berechnet damit etwas und liefert ein Ergebnis zurück. Dieses Ergebnis geht in die nächste Funktion, auch diese berechnet damit wieder etwas und liefert ihr Ergebnis an die nächste Funktion. Auf diesem Weg wird ein Eingangswert nach und nach zu einem Ergebnis transformiert, siehe Listing 1.

Die einzelnen Funktionen innerhalb eines Flows, die sogenannten Flowstages, sind zustandslos, das heißt, sie erledigen ihre Aufgabe ausschließlich mit den Daten aus ihren Argumenten. Das hat den Vorteil, dass mehrere Flows asynchron ausgeführt werden können, ohne dass dabei die Zugriffe auf den Zustand synchronisiert werden müssten. Ferner lassen sich zustandslose Funktionen sehr schön automatisiert testen, weil das Ergebnis eben nur von den Eingangsparametern abhängt.

Einer nach dem anderen

Ein Detail ist bei der Realisierung von Flows ganz wichtig: Weitergereicht werden sollten nach Möglichkeit jeweils Daten vom Typ IEnumerable<T>. Dadurch besteht nämlich die Möglichkeit, auf diesen Daten mit LINQ zu operieren. Ferner können die einzelnen Flowstages dann beliebig große Datenmengen verarbeiten, da bei Verwendung von IEnumerable<T> nicht alle Daten vollständig im Speicher existieren müssen, sondern Element für Element bereitgestellt werden können. Im Idealfall fließt also zwischen den einzelnen Flowstages immer nur ein einzelnes Element. Es wird nicht etwa das gesamte Ergebnis der ersten Stage berechnet und dann vollständig weitergeleitet.

Im Beispiel von Listing 2 führt die Verwendung von yield return dazu, dass der Compiler einen Enumerator erzeugt. Dieser Enumerator liefert nicht sofort die gesamte Aufzählung, sondern stellt auf Anfrage Wert für Wert bereit. Bei Ausführung der Methode Flow() werden also zunächst nur die einzelnen Aufzählungen und Funktionen miteinander verbunden. Erst wenn das erste Element aus dem Ergebnis entnommen werden soll, beginnen die Enumerato-ren, Werte zu liefern. Der Flow kommt also erst dann in Gang, wenn jemand hinten das erste Element „herauszieht“.

Als erste ist die Funktion C an der Reihe. Sie entnimmt aus der ihr übergebenen Aufzählung x2 das erste Element. Dadurch kommt B ins Spiel und entnimmt ihrerseits der Aufzählung x1 den ersten Wert. Dies setzt sich fort, bis die Methode Input den ersten Wert liefern muss. Im Flow werden die einzelnen Werte sozusagen von hinten durch den Flow gezogen. Ein Flow bietet in Verbindung mit IEnumerable<T> und yield return die Möglichkeit, unendlich große Datenmengen zu verarbeiten, ohne dass eine einzelne Flowstage die Daten komplett im Speicher halten muss.

Lesbarkeit durch Extension Methods

Verwendet man bei der Implementierung der Flowstages Extension Methods, kann man die einzelnen Stages syntaktisch hintereinanderschreiben, sodass der Flow im Code deutlich in Erscheinung tritt. Dazu muss lediglich der erste Parameter der Funktion um das Schlüsselwort this ergänzt werden, siehe Listing 3. Natürlich müssen die Parameter und Return-Typen der Flowstages zueinander passen.

Lösungsansatz

Der erste Schritt des INotifyProperty-Changed-Testers besteht darin, die zu testenden Properties des Typs zu ermitteln. Anschließend muss er jedem dieser Properties einen Wert zuweisen, um zu prüfen, ob der Event korrekt ausgelöst wird. Zum Zuweisen eines Wertes benötigen Sie zur Laufzeit einen Wert vom Typ der Property. Wenn Sie auf eine string-Property stoßen, müssen Sie einen string-Wert instanzieren, das ist einfach.

Komplizierter wird die Sache, wenn der Typ der Property ein komplexer Typ ist. Denken Sie etwa an eine Liste von Points oder Ähnliches. Richtig knifflig wird es, wenn der Typ der Property ein Interfacetyp ist. Dann ist eine unmittelbare Instanzie-rung nicht möglich. Das Instanzieren der Werte scheint eine eigenständige Funktionseinheit zu sein, denn die Aufgabe ist recht umfangreich.

Wenn Sie die Properties und ihren jeweiligen Typ gefunden haben, müssen Sie für jede Property einen Test ausführen. Jeder dieser Tests ist eine Action<object>, die auf einer Instanz der Klasse ausgeführt wird, die zu testen ist. Wenn also die Klasse KundeViewModel überprüft werden soll, wird für jede Property eine Action<KundeView-Model> erzeugt. Sind die Actions erzeugt, müssen sie nur nacheinander ausgeführt werden. Dabei soll jede Action eine neue Instanz der zu testenden Klasse erhalten. Andernfalls könnte es zu Seiteneffekten beim Testen der Properties kommen.

Funktionseinheiten identifizieren

Die erste Aufgabe ist also das Ermitteln der zu testenden Properties. Eingangsparameter in diese Funktionseinheit ist der Typ, für den die INotifyPropertyChanged-Implementierung überprüft werden soll. Das Ergebnis der Flowstage ist eine Aufzählung der Property-Namen.

static IEnumerable<string> FindPropertyNames(this Type type)

An dieser Stelle fragen Sie sich möglicherweise, warum ich die Property-Namen als Strings zurückgebe und nicht etwa eine Liste von PropertyInfo-Objekten. Schließlich stecken in PropertyInfo mehr Informationen, insbesondere der Typ der Property, den ich später ebenfalls benötige. Ich habe mich dagegen entschieden, weil dies das Testen der nächsten Flowstage deutlich erschwert hätte. Denn diese hätte dann auf einer Liste von PropertyInfo-Objekten arbeiten müssen. Und da PropertyInfo-Instanzen nicht einfach mit new hergestellt werden können, wären die Tests recht mühsam geworden.

Nachdem die Property-Namen bekannt sind, kann die nächste Flowstage dazu den jeweiligen Typ ermitteln. Die Flowstage erhält also eine Liste von Property-Namen sowie den Typ und liefert eine Aufzählung von Typen.

static IEnumerable<Type> FindPropertyTypes( this IEnumerable<string> propertyNames, Type type)

Im Anschluss muss für jeden Typ ein Objekt instanziert werden. Diese Objekte werden später im Test den Properties zugewiesen. Die Flowstage erhält also eine Liste von Typen und liefert für jeden dieser Typen eine Instanz des entsprechenden Typs.

static IEnumerable<object> GenerateValues( this IEnumerable<Type> types)

Dann wird es spannend: Die Actions müssen erzeugt werden. Dabei lässt es sich leider nicht vermeiden, die Property-Namen aus der ersten Stage nochmals zu verwenden. Die Ergebnisse der ersten Stage fließen also nicht nur in die unmittelbar nächste Stage, sondern zusätzlich auch noch in die Stage, welche die Actions erzeugt. Die Namen der Properties werden benötigt, um mittels Reflection die jeweiligen Setter aufrufen zu können.

static IEnumerable<Action<object>> GenerateTestMethods(this IEnumerable<object> values, IEnumerable<string> propertyNames, Type type)

Der letzte Schritt besteht darin, die gelieferten Actions auszuführen. Dazu muss jeweils eine Instanz der zu testenden Klasse erzeugt und an die Action übergeben werden.