Aufgaben - Developer Week
Transcription
Aufgaben - Developer Week
#1 Stefan Lieser, Tilman Börner Dojos für Entwickler 15 Aufgaben und Lösungen in .NET EINLEITUNG Wer übt, gewinnt E in Profimusiker übt täglich mehrere Stunden. Er übt Fingerfertigkeit, Phrasierung, Ansatz beziehungsweise Haltung, Intonation und VomBlatt-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- www.dotnetpro.de dotnetpro.dojos.2011 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 Gewinn lässt sich in ein Wort 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. 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. 3 INHALT 15 Aufgaben und Lösungen 5 Aufgabe 1 : Vier gewinnt Ein Spielfeld, zwei Spieler und jede Menge Spaß beim Programmieren : Das kleine Brettspiel ist genau das Richtige zum Warmwerden. 9 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. 66 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. 71 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. 14 Aufgabe 3 : Testdatengenerator Meier, Müller, Schulze – ganze 250 000 Mal: Für einen Testdatengenerator ist das eine Sache von Sekunden. Aber wie baut man einen solchen? 22 Aufgabe 4 : Mogeln mit EVA Statt Rein-Raus-Kaninchentechnik die Eingabe, Verarbeitung, Ausgabe: modernste Technik im Dienst des Mogelns beim Minesweeper-Spiel. Na super. 26 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. 31 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. 38 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. 44 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. 50 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. 56 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. 61 Aufgabe 11 : LINQ Frage: Wie heißt die bekannteste Abfragesprache? Richtig: SQL. Aber in dieser Aufgabe geht es um eine andere: Language Integrated Query. 4 77 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. 87 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 82 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. 95 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 94 Impressum dotnetpro.dojos.2011 www.dotnetpro.de AUFGABE „Stefan, vielleicht sollten wir erst einmal mit etwas Einfacherem anfangen. Vielleicht wäre ein kleines Spiel zum Warmwerden genau K lar, 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 platzieren konnte. 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. www.dotnetpro.de dotnetpro.dojos.2011 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. 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 WPFOberfläche, WinForms, ASP.NET oder Konsolenanwendung, das ist nicht wichtig. Im Vordergrund 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ösung kann 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ß ! Wer übt, gewinnt das Richtige. Fällt dir dazu eine Aufgabe ein?“ 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 … [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. 5 LÖSUNG Eine Übung, bei der Sie nur gewinnen konnten Vier gewinnt. Eine Lösung. O 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? L eser, 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 ver- 6 schiedenen 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. 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. 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 dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG 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. 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 LegeSteinInSpalte() 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 LegeSteinInSpalte() mit einem Rückgabewert auszustatten. War das Platzieren erfolgreich, wird true geliefert, ist die Spalte bereits voll, wird false geliefert. In www.dotnetpro.de dotnetpro.dojos.2011 [Abb. 1] Die Aufgabe in Teile zerlegen: erster Schritt ... [Abb. 2] ... und zweiter Schritt. [Abb. 3] Aufbau der Solution. 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 LegeSteinInSpalte() 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 „Command/Query Separation“ eingehalten. Vier Steine finden Nun sind mit dem zweidimensionalen Array und der Methode LegeSteinInSpalte() bereits zwei Teilprobleme des Spielzu- stands gelöst: Im zweidimensionalen Array ist der Zustand des Spielbretts hinterlegt, und die Methode LegeSteinInSpalte() 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 7 LÖSUNG Listing 1 Vierergruppe ermitteln. internal struct HorizontalerVierer : IVierer { private readonly int x; private readonly int y; public HorizontalerVierer(int x, int y) { this.x = x; this.y = y; } public Koordinate Eins { get { return new Koordinate(x, y); } } public Koordinate Zwei { get { return new Koordinate(x + 1, y); } } public Koordinate Drei { get { return new Koordinate(x + 2, y); } } public Koordinate Vier { get { return new Koordinate(x + 3, y); } } public override string ToString() { return string.Format("Horizontal X: {0}, Y: {1}", x, y); } } 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 ausge- 8 führt, also als Flow zusammengeschaltet werden : var gewinnerVierer = spielfeld .AlleVierer() .SelbeFarbe(spielfeld); 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 : internal static IEnumerable<IVierer> AlleVierer(this int[,] feld) { for (var x = 0; x <= feld.GetLength(0) - 4; x++) { for (var y = 0; y < feld.GetLength(1); y++) { yield return new HorizontalerVierer(x, y); } } // Ebenso für Vertikal und Diagonal } 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 : if (Zustand == Zustaende.RotIstAmZug) { spielbrett.SpieleStein(Spieler.Rot, spalte); Zustand = Zustaende.GelbIstAmZug; } Es wird also ein entsprechender Spielstein gelegt und anschließend ermittelt, wer am Zug ist. Etwas später folgt dann die Auswertung eines möglichen Gewinners: if (spielbrett.Gewinner == Spieler.Rot){ Zustand = Zustaende.RotHatGewonnen; } 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 Listing 2 Prüfen auf gleiche Farben. internal static IEnumerable<IVierer> SelbeFarbe(this IEnumerable<IVierer> vierer, int[,] feld) { foreach (var vier in vierer) { if ((feld[vier.Eins.X, vier.Eins.Y] != 0) && (feld[vier.Eins.X, vier.Eins.Y] == feld[vier.Zwei.X, vier.Zwei.Y]) && (feld[vier.Eins.X, vier.Eins.Y] == feld[vier.Drei.X, vier.Drei.Y]) && (feld[vier.Eins.X, vier.Eins.Y] == feld[vier.Vier.X, vier.Vier.Y])) { yield return vier; } } } dotnetpro.dojos.2011 www.dotnetpro.de AUFGABE INotifyPropertyChanged-Logik automatisiert testen Zauberwort D ataBinding 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 PropertyChanged-Event und prüft, ob er bei Ä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 ein Werkzeug zum Testen von ViewModels. Dieses Werkzeug soll wie folgt bedient werden: NotificationTester.Verify<MyViewModel>(); Die Klasse, die auf INotifyPropertyChangedSemantik geprüft werden soll, wird als generischer Typparameter an die Methode über- www.dotnetpro.de dotnetpro.dojos.2011 geben. Die Prüfung soll so erfolgen, dass per Reflection alle Eigenschaften der Klasse gesucht werden, 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-Framework 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 : Wer übt, gewinnt 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? 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 dotnetpro finden. Übung macht den Meister. Also − los geht’s. Aber Sie wollten doch nicht etwa sofort Visual Studio starten … 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] Listing 3 Property changed? [Test] public void Name_Property_loest_PropertyChanged_Event_korrekt_aus() { var kunde = new Kunde(); var count = 0; kunde.PropertyChanged += (o, e) => { count++; Assert.That(e.PropertyName, Is.EqualTo("Name")); }; kunde.Name = "Stefan"; Assert.That(count,Is.EqualTo(1)); } 9 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! W ie 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, Listing 1 Ein einfacher Flow. var var var var 10 input = "input"; x1 = A(input); x2 = B(x1); result = C(x2); O 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 Enumeratoren, 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 INotifyPropertyChanged-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 Instanzierung 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 dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Listing 2 Rückgabedaten vom Typ IEnumerable nutzen. public void Flow() { var input = Input(); var x1 = A(input); var x2 = B(x1); var result = C(x2); foreach(var value in result) { ... } } public IEnumerable<string> Input() { yield return "Äpfel"; yield return "Birnen"; yield return "Pflaumen"; } public IEnumerable<string> A(IEnumerable<string> input) { foreach (var value in input) { yield return string.Format("({0})", value); } } public IEnumerable<string> B(IEnumerable<string> input) { foreach (var value in input) { yield return string.Format("[{0}]", value); } } public IEnumerable<string> C(IEnumerable<string> input) { foreach (var value in input) { yield return string.Format("-{0}-", value); } } Listing 3 Die Stages syntaktisch koppeln. public static IEnumerable<string> A(this IEnumerable<string> input) { foreach (var value in input) { yield return string.Format("({0})", value); } } ... var result = Input().A().B().C(); 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<KundeViewModel> 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. www.dotnetpro.de dotnetpro.dojos.2011 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. Abbildung 2 zeigt den gesamten Flow. Die einzelnen Flowstages sind als Extension Method implementiert. Der Flow selbst wird in der öffentlichen Methode NotificationTester.Verify zusammengesteckt. Testen 11 LÖSUNG [Abb. 2] Die zu testenden Listing 4 Properties ermitteln. Eine einfache Testklasse. public class ClassWithPublicGettersAndSetters { public string StringProperty { get; set;} public int IntProperty { get; set;} } möchte ich die einzelnen Stages aber isoliert. Denn nur so kann ich die Implementierung Schritt für Schritt vorantreiben und muss nicht gleich einen Integrationstest für den gesamten Flow schreiben. Einige Integrationstests sollten am Ende aber auch nicht fehlen. Diese Vorgehensweise hat einen weiteren Vorteil: Um den NotificationTester testen zu können, müssen Testdaten her. Da er auf Typen arbeitet, müssen also Testdaten in Form von Klassen erstellt werden. Das ist nicht nur aufwendig, sondern wird auch schnell unübersichtlich. Ganz kommt man zwar am Erstellen solcher Testklassen auch nicht vorbei, aber der Aufwand ist doch reduziert. Interna testbar machen Um die einzelnen Flowstages isoliert testen zu können, habe ich ihre Sichtbarkeit auf internal gesetzt. Damit sind die Methoden zunächst nur innerhalb der Assembly, in der sie implementiert sind, sichtbar. Um auch in der Test-Assembly darauf zugreifen zu können, muss diese zusätzliche Sichtbarkeit über das Attribut InternalsVisibleTo hergestellt werden : [assembly:InternalsVisibleTo( "INotifyTester.Tests")] Das Attribut kann prinzipiell in einer beliebigen Quellcodedatei in der Assembly untergebracht werden. Üblicherweise werden Attribute, die sich auf die Assembly be- 12 ziehen, in der Datei AssemblyInfo.cs untergebracht. Diese finden Sie im Visual Studio Solution Explorer innerhalb des Ordners Properties. Das Sichtbarmachen der internen Methoden nur zum Zwecke des Testens halte ich auf diese Weise für vertretbar. UnitTests sind Whitebox-Tests, das heißt, die Art und Weise der Implementierung ist bekannt. Im Gegensatz dazu stehen Blackbox-Tests, die ganz bewusst keine Annahmen über den inneren Aufbau der zu testenden Funktionseinheiten machen. Durch Verwendung von internal ist die Sichtbarkeit nur so weit erhöht, dass die Methoden in Tests angesprochen werden können. Eine vollständige Offenlegung mit public wäre mir zu viel des Guten. Übrigens halte ich es für keine gute Idee, auf die Interna einer zu testenden Klasse mittels Reflection zuzugreifen. Dabei entziehen sich nämlich die Interna, die über Reflection angesprochen werden, den Refaktorisierungswerkzeugen. Und wie man sieht, ist internal in Verbindung mit dem InternalsVisibleTo-Attribut völlig ausreichend. FindPropertyNames Die Namen der Properties werden durch die Flowstage FindPropertyNames geliefert. Dabei entscheidet diese Funktion bereits, welche Properties geprüft werden sollen. Es werden nur Properties berücksichtigt, die über öffentliche Getter und Setter verfügen. Um diese Funktion testen zu können, müssen Testklassen angelegt werden. Das lässt sich leider nicht vermeiden, da die Funktion auf einem Typ als Argument arbeitet. Bei der testgetriebenen Entwicklung steht der Test vor der Implementierung, also gilt es, Testdaten zu erstellen. Ich habe mich zunächst um das „Happy Day Szenario“ gekümmert, also einen Testfall, der später bei der Verwendung typisch ist, siehe Listing 4. Als Nächstes folgt eine Klasse, deren Properties private sind. Diese sollen in den Tests unberücksichtigt bleiben, ihr Name darf also nicht geliefert werden. Die Implementierung der Funktion ist mit LINQ ganz einfach, siehe Listing 5. Die beiden Where-Klauseln sorgen dafür, dass nur Properties berücksichtigt werden, die sowohl einen Getter als auch einen Setter haben. Durch die Binding Flags werden schon Properties ausgeschlossen, die nicht public sind. Durch die SelectKlausel wird festgelegt, wie die zu liefernden Ergebnisse aufgebaut sein sollen. FindPropertyTypes Die Funktion FindPropertyTypes erhält als Argumente die Liste der Property-Namen, die berücksichtigt werden sollen, sowie den Typ, zu dem die Properties gehören. Dazu liefert sie jeweils den Typ der Properties. Auch diese Tests benötigen wieder Testklassen. Ich habe einfach die schon vorhandenen Testklassen verwendet. Auch hier ist die Implementierung dank LINQ nicht schwierig. GenerateValues Um die Property-Setter später aufrufen zu können, muss jeweils ein Objekt vom Typ der Property erzeugt werden. Diese Aufgabe übernimmt die Funktion GenerateValues. Sie erhält als Argument die Liste der Typen und liefert dazu jeweils eine Instanz. Die Funktion ist derzeit recht einfach gehalten. Die Instanz wird einfach durch Verwendung von Activator.CreateInstance erzeugt. Ledig- dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Listing 5 Listing 7 Die zu prüfenden Properties finden. Flowstages zusammenstecken. internal static IEnumerable<string> FindPropertyNames(Type type) { return type.GetProperties(PropertyBindingFlags) .Where(propertyInfo => propertyInfo.CanRead) .Where(propertyInfo => propertyInfo.CanWrite) .Select(propertyInfo => propertyInfo.Name); } public static void Verify(Type type) { var propertyNames = type .FindPropertyNames(); propertyNames .FindPropertyTypes(type) .GenerateValues() .GenerateTestMethods( propertyNames, type) .ExecuteTestMethods(type); } Listing 6 Passende Objekte erzeugen. internal static IEnumerable<object> GenerateValues(this IEnumerable<Type> types) { return types.Select(type => CreateInstance(type)); } JetBrains ReSharper bezahlt. Der weist nämlich mit der Warnung „Access to modified closure“ auf das Problem hin. internal static object CreateInstance(Type type) { if (type == typeof(string)) { return ""; } return Activator.CreateInstance(type); } ExecuteTestMethods Der letzte Schritt im Flow ist die Ausführung der erzeugten Testmethoden. Diese Methode ist erst durch eine Refaktorisierung entstanden, daher teste ich sie nicht isoliert, sondern nur im Integrationstest. Und jetzt alle! lich Strings werden gesondert behandelt, da die Klasse über keinen parameterlosen Konstruktor verfügt, siehe Listing 6. Die Methode CreateInstance muss sicher im Laufe der Zeit angepasst werden. Sie ist in der gezeigten Implementierung nicht in der Lage, mit komplexen Typen zurechtzukommen. GenerateTestMethods Nun stehen alle Informationen zur Verfügung, um für jede Property eine Testmethode zu erzeugen. Die Funktion GenerateTestMethods erhält drei Argumente : ❚ die Liste der Werte für die Zuweisung, ❚ die Liste der Property-Namen, ❚ den Typ, auf den sich die Tests beziehen. Das Ergebnis ist eine Liste von Actions. static IEnumerable<Action<object>> GenerateTestMethods(this IEnumerable<object> values, IEnumerable<string> propertyNames, Type type) Das Testen dieser Funktion kommt leider auch wieder nicht ohne Testklassen aus, denn der Typ geht ja als Argument in die Funktion ein. Die erzeugten Testmethoden werden im Test aufgerufen, um so zu prüfen, dass sie jeweils einen bestimmten Aspekt der INotifyPropertyChangedSemantik überprüfen. Hier wird es schon www.dotnetpro.de dotnetpro.dojos.2011 schwierig, die Vorgehensweise zu beschreiben, da es sich um Tests handelt, die testen, dass generierte Testmethoden richtig testen, sozusagen Metatests. Die Implementierung der Funktion hat es ebenfalls in sich. Zunächst müssen zwei Aufzählungen „im Gleichschritt“ durchlaufen werden. Dazu wird der Enumerator einer der beiden Aufzählungen ermittelt. Anschließend wird der andere Enumerator in einer foreach-Schleife durchlaufen. Innerhalb der Schleife wird der erste Enumerator dann „per Hand“ mit MoveNext und Current bedient. Ich hätte dies gerne in eine Methode ausgelagert, das ist jedoch durch die Verwendung von yield return nicht möglich. Damit sind wir bei der zweiten Besonderheit der Funktion. Die einzelnen Testmethoden werden jeweils mit yield return zurückgeliefert. Da das Ergebnis der Funktion eine Aufzählung von Actions ist, liefert das yield return jeweils eine Action in Form einer Lambda Expression. Dabei müssen die Werte, die aus den Enumeratoren in der Schleife entnommen werden, in lokalen Variablen abgelegt werden, damit sie als Closure in die Lambda Expression eingehen können. Andernfalls würden am Ende alle Lambda Expressions auf demselben Wert arbeiten, nämlich dem aus dem letzten Schleifendurchlauf. Auch hier macht sich übrigens wieder mal der Einsatz von Nun müssen nur noch alle Flowstages zusammengesteckt werden. Das ist einfach, da die Stages als Extension Methods implementiert sind. Dadurch können sie hintereinandergereiht werden, wie Listing 7 zeigt. Der Flow wird lediglich dadurch etwas unterbrochen, dass die Namen der Properties in zwei Flowstages benötigt werden. Daher werden diese nach Ausführung der ersten Stage in einer Variablen zwischengespeichert, die dann weiter unten wieder in eine andere Stage einfließt. Fazit Die Realisierung dieses Testwerkzeugs ging mir recht leicht von der Hand. Dabei hat der Entwurf des Flows relativ viel Zeit in Anspruch genommen. Die anschließende Implementierung ging dafür rasch. Was mir an der Lösung gut gefällt, ist die Tatsache, dass Erweiterungen leicht vorzunehmen sind, weil es klar abgegrenzte Verantwortlichkeiten gibt. Bedarf für Erweiterungen erwarte ich vor allem beim Erzeugen der Testwerte, also in der Funktion CreateInstance. Diese ist bislang relativ einfach gehalten, kann aber leicht erweitert werden. [ml] [1] Stefan Lieser, Zauberwort, INotifyPropertyChanged-Logik automatisiert testen, dotnetpro 4/2010, S. 107, www.dotnetpro.de/A1004dojo 13 AUFGABE Testdaten automatisch generieren Meier, Müller, Schulze … Nach wie vor spielt die klassische „Forms over Data“-Anwendung eine große Rolle. Daten aus einer Datenbank sollen per Formular bearbeitet werden. Wenn diese Applikationen getestet werden, spielen Testdaten eine zentrale Rolle. Möglichst viele sollten es sein und möglichst realistisch geformt noch dazu. Stefan, fällt dir dazu eine Übung ein? Wer übt, gewinnt dnpCode: A1005dojo 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 dotnetpro finden. Übung macht den Meister. Also − los geht’s. Aber Sie wollten doch nicht etwa sofort Visual Studio starten … I mmer wieder begegnet man der Anforderung, Daten aus einer Datenbank in einem Formular zu visualisieren. Oft sind die Datenmengen dabei so groß, dass man nicht einfach alle Daten in einem Rutsch laden sollte. Stattdessen müssen die Daten seitenweise abgerufen und visualisiert werden. Suchen und Filtern kommen meistens hinzu, und schon stellt sich die Frage, ob der gewählte Ansatz auch noch funktioniert, wenn mehr als nur eine Handvoll Testdaten in der Datenbank liegen. Solche Tests auf Echtdaten Ihrer Kunden vorzunehmen wäre übrigens keine gute Idee. Diese unterliegen dem Datenschutz und sollten keinesfalls zu Testzwecken verwendet werden. Und für eine völlig neue Anwendung stehen natürlich noch gar keine Echtdaten zurVerfügung. Folglich bleibt nur die Möglichkeit, Testdaten zu generieren. Und genau darum geht es in dieser Übung: Erstellen Sie eine Bibliothek zum Erzeugen von Testdaten. Verschiedene Arten von Testdaten Die generierten Testdaten sollen eine Tabellenstruktur haben. Für jede Spalte wird definiert, von welchem Typ die Werte sind und wie sie erzeugt werden. Anschließend gibt man an, wie viele Zeilen generiert werden sollen, und die Testdaten werden generiert. Die Anforderungen an die Daten können sehr vielfältig sein. Um hier ausreichend flexibel zu sein, sollen die Daten nach verschiedenen Strategien erzeugt werden können. Reine Zufallsdaten sind ein erster Schritt, dürften aber in vielen Fällen nicht ausreichen. Zumindest eine Beschränkung innerhalb vorgegebener Minimum- und Maximumwerte erscheint sinnvoll. Eine weitere Strategie könnte darin bestehen, eine Liste von möglichen Werten vorzugeben, aus denen dann zufällig ausgewählt wird. So könnten beispielsweise Straßennamen generiert werden, die in [Abb. 1] So könnte das GUI für einen Testdatengenerator den Formularen dann auch aussehen. wie Straßennamen aussehen 14 statt wie zufällig zusammengewürfelte Zeichenfolgen. Es müssen lediglich einige Straßennamen vorgegeben werden. Das Gleiche bietet sich für die Namen von Personen an. Auch hier kann gut mit einer Liste von Namen gearbeitet werden, aus der dann zufällig Werte ausgewählt werden. Die Strategie für die Testdatenerzeugung soll möglichst flexibel sein. Ein Entwickler sollte mit wenig Aufwand einen eigenen Generator ergänzen können. Endergebnis der Datenerzeugung soll eine Aufzählung von Zeilen sein : IEnumerable<object[]> Die generierten Zeilen können dann beliebig verwendet werden. Sie können direkt in Tests einfließen oder auch zuerst als Datei gespeichert werden. Hier bietet sich beispielsweise die Speicherung als CSV-Datei an. Auch das Speichern in einer Datenbank ist natürlich ein typisches Szenario. Das konkrete Speichern der Daten sollte unabhängig sein vom Erzeugen. Es lohnt sich also wieder, sich vor der Implementierung ein paar Gedanken zur Architektur zu machen. Auch bei dieser Übung geht es wieder primär um eine Bibliothek und weniger um eine Benutzerschnittstelle. Wer mag, kann sich aber auch um eine Benutzerschnittstelle kümmern, denn die dürfte hier etwas anspruchsvoller sein. Schließlich benötigen die verschiedenen Generatoren unterschiedliche Eingabedaten. Genügen bei einem Zufallsgenerator vielleicht Minimum und Maximum, müssen bei einem anderen Generator Wertelisten eingegeben werden. Hinzu kommt, dass die Eingabedaten von unterschiedlichem Typ sein können, wofür unterschiedliche Eingabevalidierungen nötig sind. Abbildung 1 zeigt eine erste Skizze einer Benutzerschnittstelle. Und denken Sie stets an die Musiker: Die verbringen die meiste Zeit mit Üben, nicht mit Auftritten! Wir Softwareentwickler sollten auch regelmäßig üben, statt immer nur zu performen. Schließlich sollte man beim Auftritt keine Fehler machen, nur beim Üben ist das zulässig und sogar erwünscht: ohne Fehler keine Weiterentwicklung. Also üben Sie und machen Sie Fehler! [ml] dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Testdaten automatisch generieren Tückisches GUI Bei dieser Übung ging der Kern der Anwendung relativ leicht von der Hand. Die eigentliche Herausforderung lag in der dynamischen Benutzerschnittstelle. Jeder Datentyp verlangt andere Oberflächenelemente. Und der Anwender will seine Daten individuell strukturieren können. A uch bei dieser Aufgabe zeigte sich wieder, wie wichtig es ist, sich vor der Implementierung ein paar Gedanken zur Architektur zu machen. Der erste Gedanke, das Erzeugen der Daten vom Speichern zu trennen, liegt auf der Hand und wurde in der Aufgabenstellung schon erwähnt. Doch wie geht man generell vor, wenn für eine Aufgabenstellung eine Architektur entworfen werden soll? Ganz einfach: Man malt den „kleinen König“. Den gibt es immer, denn er ist schließlich derjenige, der die Anforderungen formuliert hat. Er ist der Grund dafür, dass das System überhaupt gebaut wird. Das zu implementierende System als Ganzes kann man auch sofort hinmalen. Damit liegt man nie verkehrt. Es ergibt sich damit das in Abbildung 1 gezeigte Bild. Das Diagramm nennt sich System-Umwelt-Diagramm, da es das System in seiner Umwelt zeigt. In der Umwelt des Systems gibt es immer mindestens einen Client, den kleinen König, der das System bedient. Bei manchen Systemen mag es mehrere unterschiedliche Clients geben, das spielt für den Testdatengenerator jedoch keine Rolle. Die zweite Kategorie von Elementen in der Umwelt stellen Ressourcen dar. Diese liegen außerhalb des zu erstellenden Systems und sollten daher in das System-Umwelt-Diagramm aufgenommen werden, denn unser System ist von diesen Ressourcen abhän- gig. Im Fall des Testdatengenerators sind als Ressourcen in der Umwelt CSV-Dateien und Datenbanken denkbar. Irgendwo müssen die generierten Testdaten schließlich hin. Folglich ergänze ich das System-Umwelt-Diagramm um diese Ressourcen. Das Ergebnis ist in Abbildung 2 zu sehen. Wer nun glaubt, ein solches Diagramm sei ein Taschenspielertrick, um Zeit zu schinden, ohne Nutzen für den Architekturentwurf, der irrt. Denn aus diesem Bild wird bereits deutlich, welche Komponenten mindestens entstehen müssen. Den Begriff Komponente verwende ich hier mit einer festen Bedeutung, siehe dazu die Erläuterungen im Kasten. Der Kern des Systems sollte gegenüber der Umwelt abgeschirmt werden, weil das System die Umwelt nicht kontrollieren kann. Die Umwelt kann sich verändern. Es können etwa neue Clients hinzukommen oder auch zusätzliche Ressourcen. Folglich müssen auf der Umrandung des Systems Komponenten entstehen, die den Kern des Systems über definierte Schnittstellen gegenüber der Umwelt isolieren. Andernfalls würde der Kern des Systems immer wieder von Änderungen in der Umwelt betroffen sein und wäre damit sehr anfällig. Und darin liegt die Bedeutung des System-UmweltDiagramms: Es zeigt, welche Komponenten das System von der Umwelt abschirmen. Für Clients, die das System verwenden, bezeichnen wir die Komponente, über wel- Komponente Eine Komponente ist eine binäre Funktionseinheit mit separatem Kontrakt : Binär bedeutet hier, dass die Komponente an den Verwendungsstellen binär referenziert wird. Es wird also bei der Verwendung keine Referenz auf das entsprechende Visual-Studio-Projekt gesetzt, sondern eine Referenz auf die erzeugte Assembly. Separater Kontrakt bedeutet, dass das Interface für die Komponente in einer eigenen Assembly abgelegt ist und nicht in der Assembly liegt, in welcher die Komponente implementiert ist. Daraus folgt, dass eine Komponente immer aus mindestens zwei Assemblies besteht, nämlich einer für den Kontrakt und einer für die Implementierung. Und natürlich gehören Tests dazu – also besteht jede Komponente aus mindestens drei Projekten. www.dotnetpro.de dotnetpro.dojos.2011 [Abb. 1] SystemUmweltDiagramm, Version 1. [Abb. 2] System-Umwelt-Diagramm, Version 2. che der Client mit dem System interagiert, als Portal. In Abhängigkeitsdiagrammen werden Portale immer als Quadrate dargestellt. Die Interaktion des Systems mit Ressourcen erfolgt über Adapter. Diese werden durch Dreiecke symbolisiert. Im konkreten Fall des Testdatengenerators können wir aufgrund des System-Umwelt-Diagramms also schon vier Komponenten identifizieren, siehe Abbildung 3: ❚ Portal, ❚ CSV-Adapter, ❚ Datenbank-Adapter, ❚ Testdatengenerator. Die Komponenten sollten normalerweise allerdings nicht im System-Umwelt-Diagramm eingezeichnet werden, weil dort sonst zwei Belange vermischt werden. Es soll hier nur gezeigt werden, dass sich Portal und Adapter immer sofort aus dem System-Umwelt-Diagramm ergeben. Aus dem in Abbildung 3 gezeigten Diagramm lässt sich das in Abbildung 4 gezeigte Abhängigkeitsdiagramm ableiten. Den Kern zerlegen Nachdem ich diese Komponenten identifiziert hatte, habe ich die Aufgabenstellung 15 LÖSUNG [Abb. 3] System-UmweltKomponenten. [Abb. 4] Abhängigkeitsdiagramm. [Abb. 6] Abhängigkeitsdiagramm der Komponenten. [Abb. 5] Generatoren, nach Typ geordnet, in Unterverzeichnissen. Erzeugen der Daten zerlegt. Aufgabe des Testdatengenerators ist es, Datenzeilen zu erzeugen. Dabei soll jede Datenzeile aus mehreren Spalten bestehen. Diese Aufgabe kann in folgende Funktionseinheiten zerlegt werden: ❚ Erzeugen eines einzelnen Wertes, ❚ Erzeugen einer Zeile, ❚ Erzeugen mehrerer Zeilen. Dabei scheint die Trennung in das Erzeugen einer Zeile und das Erzeugen mehrerer Zeilen auf den ersten Blick möglicherweise etwas merkwürdig. Wenn eine Zeile erzeugt werden kann, genügt doch eine simple Schleife, und schon können mehrere Zeilen erzeugt werden. Dennoch halte ich es für wichtig, diese beiden Funktionseinheiten zu identifizieren. Denn für die testgetriebene Entwicklung ist es nützlich, im Vorfeld zu wissen, welche Funktionseinheiten auf einen zukommen. So fällt es nämlich viel leichter, ausreichende Testfälle zu finden, sprich: die Anforderungen zu klären. Und bei den Anforderungen liegt die Herausfor- 16 derung eher darin, klar zu definieren, was die Anforderungen an das Erzeugen einer einzelnen Zeile sind. Dies dann zu übertragen auf die Erzeugung mehrerer Zeilen ist in der Tat trivial. Aber ohne die Trennung würde möglicherweise nur eine Funktionseinheit entstehen, die mehrere Datenzeilen erzeugt. Das würde die testgetriebene Entwicklung unnötig erschweren. Nachdem ich für das Erzeugen der Daten die Funktionseinheiten identifiziert hatte, habe ich überlegt, welche davon Komponenten werden sollen. Erst Komponenten erlauben eine parallele Entwicklung von Funktionseinheiten durch mehrere Entwickler oder Teams gleichzeitig. Dies ist zwar hier nicht das Ziel, doch resultiert aus der Trennung von Kontrakt und Implementierung, dass die Komponenten austauschbar sind. Dies betrachte ich beim Testdatengenerator an einer Stelle für besonders wichtig: bei den Generatoren. Die werden später sicher immer wieder ergänzt werden. Da ist es hilfreich, wenn dann nicht jeweils die gesamte Anwendung neu übersetzt werden muss, sondern neue Generatoren mit geringem Aufwand ergänzt werden können. In einer weiteren Ausbaustufe wäre es sogar denkbar, die Generatoren zur Laufzeit zu laden. Dann könnten später beliebige zusätzliche Generatoren verwendet werden, ohne dass am Testdatengenerator selbst etwas geändert werden muss. Damit sind die Generatoren zunächst einmal eine Komponente. Eine andere Aufteilung wäre ebenfalls denkbar, man könnte Generatoren zum Beispiel nach Typ in Komponenten zusammenfassen. Eine Komponente mit Stringgeneratoren, eine für intGeneratoren et cetera. Zurzeit sind es nur wenige Generatoren, daher habe ich mich dafür entschieden, sie alle in einer Komponente unterzubringen. Innerhalb der Komponente habe ich die Generatoren nach Typ in Unterverzeichnisse geordnet. Dies ist in Abbildung 5 zu sehen. Eine weitere Komponente bildet die Funktionseinheit, die dafür zuständig ist, Zeilen aus Einzelwerten zu bilden. Diese Komponente habe ich DataPump genannt. Eine dritte Komponente bildet das Speichern der Daten. Implementiert habe ich einen CsvDataAdapter. Ein DbDataAdapter zum Speichern der Testdaten in einer dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Listing 1 Listing 2 Einen generischen Typparameter verwenden. Spalten definieren. public interface IGenerator<T> { T GenerateValue(); } Datenbank liegt auf der Hand, auf diesen habe ich aus Zeitgründen jedoch verzichtet. Übergangsweise kann man sich damit behelfen, die CSV-Dateien mit einem ETLProzess (Extract, Transform, Load) in die Datenbank zu schaufeln. Die Komponenten Generators, DataPump, DbDataAdapter und CsvDataAdapter haben nur geringe Abhängigkeiten, wie Abbildung 6 zeigt. Der CsvDataAdapter ist nicht von den anderen Komponenten abhängig, weil er lediglich auf dem gemeinsamen Datenmodell aufsetzt. Einzelne Werte Für das Erzeugen eines einzelnen Wertes habe ich mich für die Verwendung eines Generators entschieden. Dieser hat die Aufgabe, zu einem gegebenen Typ einen Wert zu liefern. Die dabei verwendete Strategie bestimmt der Generator. So ist ein Generator denkbar, der zufällige Werte erzeugt. Genauso kann aber auch ein Generator erstellt werden, der eine Liste von Daten erhält und daraus zufällig auswählt. Die Beschreibung der zu erzeugenden Datenzeilen besteht also darin, pro Spalte einen Generator zu definieren. Ferner wird pro Spalte der Name der Spalte benötigt. Im Kontrakt der Generatoren habe ich einen generischen Typparameter verwendet, siehe Listing 1. Dadurch wird bereits zur Übersetzungszeit geprüft, ob der Rückgabewert der Methode GenerateValue zum Generatortyp passt. Die Generatoren werden in den Spaltendefinitionen verwendet. Da sie einen generischen Typparameter haben, muss dieser bei Verwendung des Generators entweder durch einen konkreten Typ oder an der Verwendungsstelle durch einen generischen Typparameter belegt werden. Für die Klasse ColumnDefinition würde das bedeuten, dass diese ebenfalls einen generischen Typparameter erhält, siehe Listing 2. So weit, so gut. Doch eine Zeile besteht aus mehreren Spalten. Daher müssen meh- www.dotnetpro.de dotnetpro.dojos.2011 public class ColumnDefinition<T> { public ColumnDefinition(string columnName, IGenerator<T> generator) { ColumnName = columnName; Generator = generator; } public string ColumnName { get; private set; } public IGenerator<T> Generator { get; private set; } } Listing 3 Werte erzeugen. public static IEnumerable<object> GenerateValues(this IEnumerable<ColumnDefinition> columnDefinitions) { return columnDefinitions .Select(x => x.Generator) .Select(x => x.GenerateValue()); } Listing 4 Eine Datenzeile generieren. public static Line GenerateLine(this IEnumerable<object> values) { return new Line(values); } Listing 5 Mehrere Zeilen generieren. public IEnumerable<Line> GenerateTestData(IEnumerable<ColumnDefinition> columnDefinitions, int rowCount) { for (var i = 0; i < rowCount; i++) { yield return columnDefinitions .GenerateValues() .GenerateLine(); } } rere ColumnDefinition<T>-Objekte in einer Liste zusammengefasst werden. Da natürlich jede Spalte einen anderen Typ haben kann, muss es möglich sein, beispielsweise eine ColumnDefinition<string> sowie eine ColumnDefinition<int> in diese Liste auf- zunehmen. Dies ist jedoch mit C# 3.0 aufgrund der fehlenden Ko-/Kontravarianz noch nicht möglich. Würde man die Liste als List<object> definieren, müsste die Liste Kovarianz unterstützen. Das tut sie jedoch nicht, Ko- und Kontravarianz stehen erst 17 LÖSUNG mit C# 4.0 zur Verfügung. Ich habe daher den Generator in der ColumnDefinition als IGenerator<object> definiert, statt ColumnDefinition generisch zu machen. Dies kann man dann mit Erscheinen von Visual Studio 2010 ändern. [Abb. 7] Flow zum Erzeugen einer Datenzeile. Eine Zeile Durch die Generatoren können die einzelnen Werte der Spalten erzeugt werden. Um eine ganze Datenzeile zu erzeugen, muss jeder Generator einmal aufgerufen werden, um seinen jeweils nächsten Wert zu liefern. Dies ist bei Verwendung von LINQ ganz einfach, siehe Listing 3. In der Methode wird über die Aufzählung der Spaltendefinitionen iteriert und durch das erste Select jeweils der Generator aus der Spaltendefinition entnommen. Durch das zweite Select wird aus jedem Generator ein Wert abgerufen. Das Ergebnis ist eine Aufzählung der von den Generatoren gelieferten Werte. Diese Aufzählung wird später an den Konstruktor einer Zeile übergeben, siehe Listing 4. eines Generators für int-Werte gezeigt werden, der zufällige Werte innerhalb vorgegebener Minimum- und Maximumwerte erzeugt. Die Implementierung des Generators ist ganz einfach. Ich verwende einen Zufallszahlengenerator System.Random aus dem .NET Framework und weise ihn an, einen Wert innerhalb der definierten Grenzen zu liefern, siehe Listing 6. Die spannende Frage ist nun: Wie kann man einen solchen Generator testen, der zufällige Werte liefern soll? Sie werden bemerkt haben, dass oben im Listing der Zufallszahlengenerator random nirgendwo instanziert und zugewiesen wird. Dies liegt in der Notwendig- Listing 6 Ein Generator für int-Werte. Mehrere Zeilen Die Erzeugung mehrerer Zeilen erfolgt in einer for-Schleife. Dabei wird die Schleife so oft durchlaufen, dass die Anzahl der gewünschten Datensätze erzeugt wird. Dabei kommt wieder einmal ein yield return zum Einsatz, siehe Listing 5. public class RandomIntGenerator : IGenerator<object> { private readonly int minimum; private readonly int maximum; private readonly Random random; ... public object GenerateValue() { return random.Next(minimum, maximum + 1); } Flow Und schon wieder konnte ich einen Flow identifizieren. Die Aufzählung der ColumnDefinitions fließt in die Methode GenerateValues. Heraus kommt eine Aufzählung mit Werten. Diese wird weitergeleitet in die Methode GenerateLine, die aus den Werten eine Zeile erstellt : columnDefinitions .GenerateValues() .GenerateLine(); Um den Flow so formulieren zu können, sind die beiden Methoden als Extension Methods realisiert. Dadurch wird das Aneinanderreihen der Methoden besonders einfach. Abbildung 7 zeigt den Flow. Damit ist die Komponente DataPump bereits beschrieben. Weiter geht es bei den Generatoren. } Listing 7 Den Zufallsgenerator testen. [TestFixture] public class RandomIntGeneratorTests { private RandomIntGenerator sut; [SetUp] public void Setup() { sut = new RandomIntGenerator(1, 5, new Random(0)); } [Test] public void Zufaellige_Werte_zwischen_Minimum_und_Maximum_werden_geliefert() { Assert.That(sut.GenerateValue(), Is.EqualTo(4)); Assert.That(sut.GenerateValue(), Is.EqualTo(5)); Generatoren Ein Generator ist für das Erzeugen von Werten eines bestimmten Typs zuständig. Welche Strategie dabei verfolgt wird, ist Sache des Generators. Dies soll am Beispiel 18 ... } } dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG keit begründet, den Generator automatisiert testen zu können. Würde der Generator den Zufallszahlengenerator selbst instanzieren, würde er immer zufällige Werte liefern. Dies soll er natürlich tun, aber im Test benötigen wir die Kontrolle darüber, welche Werte „zufällig“ geliefert werden, siehe Listing 7. Die Testmethode ist hier verkürzt dargestellt. Ich rufe im Test so lange Werte ab, bis alle möglichen Werte innerhalb von Minimum und Maximum mindestens einmal geliefert wurden. Der Trick, dass sich der Random-Generator immer gleich verhält, liegt darin, dass ich ihn im Test immer mit demselben Startwert (Seed) 0 instanziere. Um das zu ermöglichen, habe ich einen internal-Konstruktor ergänzt, der nur im Test verwendet wird, um den Random-Generator in die Klasse zu injizieren. Der öffentliche Konstruktor der Klasse instanziert den Random-Generator ohne Seed, sodass dieser zufällige Werte liefert, siehe Listing 8. Bei Konstruktoren sollte man übrigens generell das Highlander-Prinzip beachten: Es kann nur einen geben (eine Anspielung auf den Film Highlander – Es kann nur einen geben). Der interne Konstruktor ist derjenige, der die eigentliche Arbeit verrichtet. Der öffentliche Konstruktor verfügt nur über die beiden Parameter für Minimum und Maximum. Er bezieht sich auf den internen Konstruktor und übergibt diesem, neben den beiden Grenzwerten, auch einen mit new Random() erzeugten Random-Generator. Das Highlander-Prinzip sollte beachtet werden, damit es in den Konstruktoren nicht zur Verletzung des Prinzips Don’t Repeat Yourself (DRY) kommt. Der öffentliche Konstruktor könnte ja die Grenzwerte selbst an die Felder zuweisen, dann würden diese Zuweisungen jedoch an zwei Stellen auftreten. Eine weitere interessante Implementierung bietet der RollingIntGenerator. Er liefert, ausgehend von einem Minimumwert, [Abb. 8] Das fertige Portal. www.dotnetpro.de dotnetpro.dojos.2011 Listing 8 Zufallsgenerator für int-Werte. public RandomIntGenerator(int minimum, int maximum) : this(minimum, maximum, new Random()) { } internal RandomIntGenerator(int minimum, int maximum, Random random) { this.minimum = minimum; this.maximum = maximum; this.random = random; } Listing 9 Zufällig einen Stringwert auswählen. internal RandomSelectedStringsGenerator(Random random, params string[] values) { this.random = random; this.values = values; } immer den nächsten Wert, bis er beim Maximumwert angekommen ist. Dann wird wieder von vorn begonnen. Bei diesem Generator lag die Herausforderung darin, korrekt mit dem größtmöglichen int-Wert (int.MaxValue) umzugehen. Ohne UnitTests wäre das ein elendiges Rumprobieren geworden. So war es ganz leicht. Für Stringwerte habe ich einen Generator implementiert, der eine Liste von Strings erhält und daraus zufällig einen auswählt. Die zur Verfügung stehenden Strings habe ich im Konstruktor als Parameter-Array definiert, siehe Listing 9. Das ist für die Unit-Tests ganz angenehm, weil man einfach eine beliebige Liste von Stringwerten übergeben kann : sut = new RandomSelectedStringsGenerator( new Random(0), "Apfel", "Birne", "Pflaume"); Bei der Verwendung des Generators aus Sicht einer Benutzerschnittstelle ist es wünschenswert, einen String zu übergeben, der eine Liste von Werten enthält, die mit Semikolon getrennt sind: "Apfel; Birne; Pflaume" Um das zu ermöglichen, habe ich eine separate Extension Method ToValues() implementiert, die einen String entsprechend zerlegt. Diese Methode kann bei Bedarf in den Konstruktoraufruf eingesetzt werden: "Apfel; Birne; Pflaume".ToValues().ToArray() Natürlich hätte ich das Zerlegen des Strings in die Einzelwerte auch im entsprechenden Generator implementieren können. Dann hätte der sich aber um mehr als eine Verantwortlichkeit gekümmert. Ferner war die Implementierung so etwas einfacher, da ich mich jeweils auf eine einzelne Aufgabenstellung konzentrieren konnte. Portal Das Portal hatte es in sich. Obwohl ich mit WPF schon einiges gemacht habe, fühlte ich mich etwas unsicher, diese sehr dynamische Aufgabenstellung mit WPF anzugehen, und entschied mich daher, das Problem mit Windows Forms zu lösen, weil mir das schneller von der Hand geht. Doch der Reihe nach. Wie die Benutzerschnittstelle des Testdatengenerators ungefähr aussehen könnte, habe ich in der Aufgabenstellung bereits durch ein Mockup angedeutet. Abbildung 8 zeigt, wie mein Ergebnis aussieht. 19 LÖSUNG Listing 10 Eine Spalte definieren. public class SpaltenDefinition { public string Bezeichnung { get; set; } public Type ControlType { get; set; } public Func<string, object, ColumnDefinition> Columndefinition { get; set; } } [Abb. 9] Document Outline. Listing 11 Spalten definieren. new SpaltenDefinition { Bezeichnung = "Random DateTime", ControlType = typeof(MinimumMaximum), Columndefinition = (columnName, control) => new ColumnDefinition(columnName, new RandomDateTimeGenerator( DateTime.Parse(((MinimumMaximum)control).Minimum), DateTime.Parse(((MinimumMaximum)control).Maximum))) } Ich sehe beim Portal zwei Herausforderungen: Die Anzahl der Spalten in den zu generierenden Daten ist variabel. Daraus ergibt sich, dass die Anzahl der Controls für Spaltendefinitionen variabel sein muss. Im Mockup habe ich daher Schaltflächen vorgesehen, mit denen eine Spaltendefinition entfernt bzw. hinzugefügt werden kann. Die zweite Herausforderung sehe ich im Aufbau der Spaltendefinitionen. Je nach ausgewähltem Generatortyp sind unterschiedliche Eingaben notwendig. Mal sind zwei Textfelder für Minimum und Maximum erforderlich, mal nur eine für die Elemente einer Liste. Das heißt, dass sich der Aufbau der Benutzeroberfläche mit der Wahl des Generatortyps ändert. Um diese beiden Herausforderungen möglichst isoliert angehen zu können, habe ich für die variablen Anteile einer Spaltendefinition mit UserControls gearbeitet. So habe ich für einen Generator, der Minimum- und Maximumwerte benötigt, ein UserControl erstellt, in dem zwei Textboxen mit zugehörigen Labels zusammengefasst sind. Wird aus der Dropdownliste ein Generator ausgewählt, muss das zum Generator passende Control angezeigt werden. Ferner muss zum ausgewählten Generator später die zugehörige ColumnDefinition erzeugt werden, um damit dann die Daten zu generieren. Diese Informationen habe ich im Portal in einer Datenklasse Spalten- 20 Definition zusammengefasst. Objekte dieser Klasse werden direkt in der Dropdownliste verwendet. Daher enthält die SpaltenDefinition auch eine Beschreibung. Diese wird als DisplayMember in der Dropdownliste angezeigt, siehe Listing 10. Die Eigenschaft ControlType enthält den Typ des zu verwendenden Controls. Genügt ein Textfeld, kann hier typeof(TextBox) gesetzt werden. In komplizierteren Fällen wird der Typ eines dafür implementierten UserControls gesetzt. Um für den ausgewählten Generatortyp eine ColumnDefinition erzeugen zu können, habe ich eine Eigenschaft ergänzt, die eine Funktion erhält, die genau dies bewerkstelligt: Sie erzeugt eine ColumnDefinition. Dazu erhält sie als Eingangsparameter zum einen den Namen der Spalte, zum anderen das Control mit allen weiteren Angaben. Da der Typ des Controls variabel ist, wird es vom Typ object übergeben. Die Funktion muss dieses Objekt dann auf den erwarteten Typ casten. Bei der Initialisierung des Portals wird für die verfügbaren Generatoren jeweils eine Spaltendefinition erzeugt und in die Item-Liste des Dropdown-Controls gestellt, siehe Listing 11. Interessant hierbei ist die Lambda Expression. Diese erhält die beiden Parameter columnName und control und erzeugt daraus eine ColumnDefinition mit dem ausgewählten Generator. Da diese Lambda Expression im Kontext einer SpaltenDefinition steht, kann das übergebene Control gefahrlos auf den Typ gecastet werden, der auch in der Eigenschaft ControlType verwendet wird. Auch hier sähe eine Lösung mit Generics sicher eleganter aus, ist aber ohne Ko-/Kontravarianz nicht möglich. Wird nun in der Combobox ein anderer Generatortyp ausgewählt, muss das in der Spaltendefinition angegebene Control angezeigt werden. Um dynamisch die zugehörigen Controls zu finden, füge ich alle Controls, die zu einer Spalte gehören (Plusund Minus-Button, Textfeld für den Spaltennamen, Combobox, Platzhalter für UserControl) in ein Panel ein. Um in diesem Panel später dynamisch das UserControl austauschen zu können, füge ich dieses zusätzlich in ein weiteres Panel. Dieses dient jeweils als Platzhalter für das auszutauschende Control. In der Form sind nur wenige statische Elemente vorhanden. Den Aufbau der Form zeigt die Document Outline in Abbildung 9. Darin ist dargestellt, wie die einzelnen Controls ineinandergeschachtelt sind. Abbildung 10 zeigt, wie die Controls für eine Spaltendefinition dynamisch zur Laufzeit zusammengesetzt werden. Dabei zeigen die Pfeile an, auf welches Control gegebenenfalls die Tag-Eigenschaft verweist. Nun zur zweiten Herausforderung, dem dynamischen Ergänzen und Löschen von Spaltendefinitionen. Jede Spaltendefinition verfügt über die beiden Schaltflächen zum Hinzufügen und Löschen von Spaltendefinitionen. Zurzeit füge ich eine neue Spaltendefinition jeweils ans Ende an, künftig könnte diese aber auch an der betreffenden Position eingefügt werden. Daher habe ich bereits an jeder Spaltendefinition einen Plus-Button vorgesehen. Für die Aufnahme aller Spaltenbeschreibungen ist im statischen Teil der Form ein Panel zuständig. Wird mit der Minus-Schaltfläche versucht, eine Spaltenbeschreibung zu entfernen, müssen die zugehörigen Controls aus die- dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG [Abb. 10] Controls im Panel. sem Panel entfernt werden. Um dies zu vereinfachen, ist das zugehörige Panel an der Tag-Eigenschaft des Buttons gesetzt. So „weiß“ der Button, zu welchem Panel er gehört und kann dieses aus dem umschließenden Panel entfernen. Wird im Portal die Schaltfläche Generieren angeklickt, muss für jede Spaltenbeschreibung eine ColumnDefinition erzeugt werden, um dann die Testdaten zu generieren. Dazu wird die Liste der Spaltenbeschreibungen im statischen Panel durchlaufen. Darin befindet sich jeweils ein Textfeld, das den Namen der Spalte enthält. Ferner befindet sich im Platzhalterpanel ein Control, in dem die Parameter für den Generator enthalten sind. In der Dropdownliste enthält das SelectedItem eine SpaltenDefinition, aus der sich die ColumnDefinition erstellen lässt. Dazu wird aus der SpaltenDefinition die Funktion zum Erzeugen der ColumnDefinition aufgerufen. Insgesamt hat das Erstellen des Portals knapp zwei Stunden in Anspruch genommen. Automatisierte Tests habe ich dazu fast keine erstellt. Diese würde ich allerdings in einem „echten“ Projekt im Nachhinein ergänzen, da die Logik für den dynamischen Aufbau des Portals doch recht umfangreich geworden ist. Um hier bei späteren Erweiterungen Fehler auszuschließen, würde ich die typischen Bedienungsschritte eines Anwenders automatisiert testen. Host Am Ende benötigen wir für die gesamte Anwendung noch eine EXE-Datei, mit der die Anwendung gestartet werden kann. Aufgabe dieses Hosts ist es, die benötigten Komponenten zu beschaffen und sie den Abhängigkeiten gemäß zu verbinden. Die Abhängigkeiten sind hier in Form von Konstruktorparametern modelliert. Folglich muss der Host die Komponenten in der richtigen Reihenfolge instanzieren, im Abhängigkeitsbaum von unten nach oben, von den Blattknoten zur Wurzel. Anschließend übergibt er die Kontrolle an das Portal. Für die vorliegende Anwendung, bestehend aus einer Handvoll Komponenten, ist diese Aufgabe trivial. Bei größeren Anwendungen kostet diese Handarbeit Zeit und sollte automatisiert werden. Die Grund- www.dotnetpro.de dotnetpro.dojos.2011 idee dabei ist: Man überlässt das Instanzieren der Komponenten einem DI-Container wie StructureMap [2] oder Castle Windsor [3]. Über ein eigenes Interface identifiziert man den Startpunkt der Anwendung, und los geht’s. Ein solcher Host kann dann sogar generisch sein und in allen Anwendungen verwendet werden. Denkbare Erweiterungen Für wiederkehrende Aufgaben wäre es sinnvoll, das Schema der Datengenerierung speichern und laden zu können. Dies kann beispielsweise mit dem Lounge Repository [4] erfolgen. In der Architektur würde dafür ein weiterer Adapter ergänzt, mit dem ein Schema gespeichert und geladen werden kann. Natürlich müssten im Portal entsprechende Anpassungen vorgenommen werden, um entsprechende Menüfunktionen zu ergänzen. Des Weiteren wäre es denkbar, die Generatoren zur Laufzeit dynamisch zu laden. Damit könnten Entwickler ihre eigenen Generatoren implementieren und verwenden, ohne dazu die gesamte Anwendung übersetzen zu müssen. Mithilfe eines DIContainers wie StructureMap oder des Managed Extensibility Framework MEF [5] sollte auch diese Erweiterung keine große Hürde darstellen. Wir liefern passgenaue Strategien und Lösungen für Ihre Inhalte auf iPhone/iPad Android BlackBerry Windows Phone 7 dem mobilen Browser Fazit Bei dieser Aufgabe stellte sich heraus, dass die Benutzerschnittstelle einer Anwendung durchaus einige Zeit in Anspruch nehmen kann. Die eigentliche Funktionalität war dagegen schnell entworfen und implementiert. Das lag maßgeblich daran, dass ich mir im Vorfeld einige Gedanken über die Architektur gemacht hatte. Danach ging die testgetriebene Entwicklung flüssig von der Hand. [ml] [1] Stefan Lieser, Meier, Müller, Schulze …, Testdaten automatisch generieren, dotnetpro 5/2010, Seite 108 ff., www.dotnetpro.de/A1005dojo [2] http://structuremap.sourceforge.net/ [3] http://www.castleproject.org/container/ [4] http://loungerepo.codeplex.com/ und Ralf Westphal, Verflixte Sucht, dotnetpro 11/2009, Seite 52 f. www.dotnetpro.de/A0911Sandbox [5] http://mef.codeplex.com/ Besuchen Sie uns unter www.digitalmobil.com AUFGABE Daten umformen Mogeln mit EVA Eingabe, Verarbeitung, Ausgabe: Das EVA-Prinzip durchdringt die gesamte Softwareentwicklung. Eine Analyse der Datenstrukturen und die Verwendung der passenden Algorithmen spielen dabei eine herausragende Rolle. Stefan, fällt dir dazu eine Übung ein? Wer übt, gewinnt dnpCode: A1006dojo 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 dotnetpro finden. Übung macht den Meister. Also − los geht’s. Aber Sie wollten doch nicht etwa sofort Visual Studio starten … W er kennt nicht Minesweeper, das beliebte Spiel, welches zum Lieferumfang von Windows gehört? Doch keine Sorge, es geht dieses Mal nicht wieder um eine aufwendige Benutzerschnittstelle, sondern um eine kleine Kommandozeilenanwendung. Die soll aus einer Eingabedatei eine Ausgabedatei erzeugen. Die Eingabedatei enthält die Beschreibung eines Minesweeper-Spielfeldes. Das Programm erzeugt als Ausgabedatei einen dazu passenden „Mogelzettel“. Der Aufruf erfolgt folgendermaßen: mogelzettel spiel1.txt mogelzettel1.txt Der Aufbau der Eingabedatei ist wie folgt: Die erste Zeile enthält die Anzahl der Zeilen und Spalten des Spielfeldes. Beide Zahlen sind durch ein Leerzeichen getrennt. Die nachfolgenden Zeilen enthalten dann jeweils die Konfiguration einer Zeile des Spielfeldes. Dabei sind freie Felder durch einen Punkt dargestellt, Felder mit einer Mine durch einen Stern. Hier ein Beispiel: 5 6 ....*. ...*.. ...... *....* .*.... Für diese Eingabedatei soll eine Ausgabedatei erzeugt werden. Die Ausgabedatei gibt für jedes Feld die Anzahl der Minen in der unmittelbaren Nachbarschaft an. Jedes Feld hat maximal acht Nachbarn, folglich können maximal acht Minen in der Nachbarschaft eines Feldes vorkommen, am Rand des Spielfeldes sind es natürlich weniger. Felder, die selbst eine Mine enthalten, sollen mit der Ziffer 0 belegt sein, es sei denn, in der Nachbarschaft befinden sich Minen. Dann sollen diese ebenfalls gezählt werden. Der Mogelzettel gibt also keine direkte Auskunft darüber, wo die Minen liegen, sondern nur über die Anzahl der Minen in den jeweils benachbarten Feldern. Für das obige Beispiel soll folgende Ausgabedatei erzeugt werden : 22 001211 001121 111121 121010 211011 Sie dürfen davon ausgehen, dass die Eingabedatei im korrekten Format vorliegt. Geht in der Folge etwas schief, ist gegebenenfalls das Ergebnis inkorrekt, oder das Programm bricht sogar ab. Dies soll hier keine Rolle spielen. Die Vorgehensweise bei der Lösung dieser Übung dürfte Lesern der vorhergehenden Übungen inzwischen geläufig sein. Zunächst sollten die Anforderungen geklärt werden. Dazu ist es sinnvoll, Beispiele aufzuschreiben. Eines habe ich Ihnen gegeben, vielleicht definieren Sie weitere, um beispielsweise Randfälle zu definieren. Da ich Ihnen als „Kunde“ nicht so ohne Weiteres für Rückfragen zur Verfügung stehe, treffen Sie gegebenenfalls selber sinnvolle Annahmen. Nach der Klärung der Anforderungen beginnt die Planung. Dieser Phase sollten Sie viel Aufmerksamkeit und Zeit schenken. Je intensiver Sie sich mit dem Problem und seiner Lösung auseinandersetzen, desto besser sind Sie vorbereitet für die Implementierung. Zerlegen Sie das Gesamtproblem in kleinere Teilprobleme, suchen Sie mögliche Flows. Wenn Sie sich bei der Herangehensweise nicht sicher sind, ob eine Idee tatsächlich zur Lösung des Problems führen wird, lohnt es sich, einen Spike zu erstellen. Ein Spike dient dazu, eine Idee zu überprüfen oder eine Technik oder Technologie zu explorieren. Code, der bei einem Spike entsteht, wird nicht produktiv verwendet, daher sind keine Tests erforderlich, und auch gegen Prinzipien darf gern verstoßen werden. Es geht um den Erkenntnisgewinn. Es könnte sich dabei schließlich auch herausstellen, dass eine Idee nicht zum Ziel führt. Im Anschluss an den Spike beginnen Sie dann testgetrieben mit der Entwicklung des Produktionscodes. Dabei beachten Sie selbstverständlich alle Prinzipien und Praktiken. Und nun frisch ans Werk! Mogeln Sie aber bitte nur beim Minesweeper-Spielen, nicht bei der Softwareentwicklung. [ml] dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Daten verarbeiten und auswerten So mogeln Sie mit EVA! Den Mogelzettel für ein Minesweeper-Minenfeld erstellen Sie nach dem EVA-Prinzip: Eingabe, Verarbeitung, Ausgabe. Die Lösung gestaltet sich nach gründlicher Planung recht einfach. Da im Detail aber Variationen möglich sind, können Sie diese Übung mit Gewinn auch mehrmals lösen. D ie Aufgabenstellung lautete vorigen Monat: Entwickle ein Programm, das einen Mogelzettel für Minesweeper erstellt [1]. Es soll so aufgerufen werden: mogelzettel spiel1.txt mogelzettel1.txt Hier ein Beispiel für den Aufbau der Eingabedatei : 5 6 ....*. ...*.. ...... *....* .*.... Für das obige Beispiel soll folgende Ausgabedatei erzeugt werden : 001211 001121 111121 121010 211011 In der Aufgabenstellung zum Minesweeper-Mogelzettel habe ich erwähnt, dass vor der Implementierung die Planung stehen sollte. Und so habe ich dieses Mal wieder mit einem Blatt Papier begonnen. Obwohl ich die Aufgabenstellung schon mehr als einmal selbst gelöst habe und in zahlreichen Seminaren beobachten durfte, wie andere Entwickler an die Lösung herange- hen, habe ich dennoch nicht sofort begonnen, Code zu schreiben. Die Planung auf Papier hilft mir, mich zu fokussieren. Und sie hilft, auch mal auf andere Lösungen zu kommen. Schließlich soll Üben dem Gewinnen von Erkenntnissen dienen. www.dotnetpro.de dotnetpro.dojos.2011 weder Mine für Mine... Erste Zerlegung Die Aufgabenstellung lässt sich auf der obersten Ebene in drei Funktionseinheiten zerlegen : ❚ Lesen des Minenfeldes, ❚ Ermitteln des Mogelzettels, ❚ Schreiben des Mogelzettels. Diese drei Funktionseinheiten bilden einen Flow, wie Abbildung 1 zeigt. In die Methode LeseMinenfeld geht der Dateiname als Parameter ein, die Methode liefert eine Aufzählung von Zeichenketten. Dabei wird die erste Zeile der Datei einfach ignoriert. Sie enthält die Anzahl der Zeilen und Spalten. Diese Information wird jedoch nicht benötigt, da sie aus dem Inhalt der Datei ebenso hervorgeht. Das Ergebnis von LeseMinenfeld wird an die Methode BerechneMogelzettel weitergeleitet. Ergebnis ist wieder eine Aufzählung von Zeichenketten, diesmal ist es aber nicht das Minenfeld, sondern der dazu generierte Mogelzettel. Die einzelnen Zeilen des Mogelzettels werden zuletzt, gemeinsam mit dem Dateinamen, an die Methode SchreibeMogelzettel übergeben und von dieser als Datei geschrieben. Mogelzettel berechnen [Abb. 1] Der Flow der Funktionen. [Abb. 2] Ent- Natürlich muss die Methode BerechneMogelzettel weiter zerlegt werden. Dies war in der Planung mein nächster Schritt. Die beiden Methoden zum Laden und Speichern von Minenfeld und Mogelzettel erscheinen mir recht einfach, daher habe ich auf eine weitere Zerlegung verzichtet. Prinzipiell sehe ich zwei Möglichkeiten, den Mogelzettel zu erstellen. Man kann entweder die Felder suchen, auf denen eine Mine liegt und dann die darum herum lie- [Abb. 3] ... oder Feld für Feld vorgehen. genden Zähler jeweils um eins erhöhen. Abbildung 2 zeigt dieses Verfahren, das aus zwei Schritten besteht: ❚ Minenpositionen ermitteln, ❚ Nachbarfelder inkrementieren. Oder man geht Feld für Feld vor und sucht in der Umgebung nach Minen. Man erhöht den Zähler für das in Bearbeitung befindliche Feld für jede gefundene Mine. Diese Variante zeigt Abbildung 3. Ich habe nach Variante 1 implementiert, suche also alle Minen und erhöhe dann bei den Zellen, die um die Mine herum liegen, den Zähler jeweils um eins. Die algorithmische Vorgehensweise lässt sich wieder sehr schön in einem Flow implementieren, er ist in Abbildung 4 zu sehen. Aus der StringRepräsentation werden zuerst die Koordinaten der Minen und die Dimensionen des Minenfeldes extrahiert. Beides wird dann verwendet, um daraus den Mogelzettel in Form eines zweidimensionalen Arrays zu berechnen. Das Array wird danach wieder in eine Aufzählung von Strings übersetzt. Das Ermitteln der Minenkoordinaten habe ich zunächst für eine einzelne Zeile implementiert. Listing 1 zeigt einen Test dazu. 23 LÖSUNG Listing 1 Methoden auch erst im Nachhinein durch Refaktorisieren. Diese belasse ich bei einer privaten Sichtbarkeit und teste sie nur in der Integration mit der verwendenden Methode. Das Einlesen einer Zeile testen. [Test] public void Eine_Zeile_mit_einigen_Minen() { Assert.That(Mogelzettel.MinenErmitteln("..*.*..*"), Is.EqualTo(new[] {2, 4, 7})); } Listing 2 Das Einlesen mehrerer Zeilen testen. [Test] public void Mehrere_Zeilen_voller_Minen() { Assert.That(Mogelzettel.MinenErmitteln(new[] {"***", "***", "***"}), Is.EqualTo(new[] { new Point(0, 0), new Point(1, 0), new Point(2, 0), new Point(0, 1), new Point(1, 1), new Point(2, 1), new Point(0, 2), new Point(1, 2), new Point(2, 2) })); } Listing 3 Minen ermitteln. internal static IEnumerable<int> MinenErmitteln(string zeile) { var x = 0; foreach (var zeichen in zeile) { if (zeichen == '*') { yield return x; } x++; } } internal static IEnumerable<Point> MinenErmitteln(IEnumerable<string> zeilen) { var y = 0; foreach (var zeile in zeilen) { foreach (var x in MinenErmitteln(zeile)) { yield return new Point(x, y); } y++; } } Die Methode liefert eine Aufzählung der Indizes, an denen sich in der Zeile eine Mine befindet. Wenn man eine Schleife um die Methode macht, lassen sich die Koordinaten ganzer Minenfelder ermitteln. Listing 2 zeigt einen Test. Die Implementierung der beiden Methoden ist einfach. Interessant ist die Verwendung von yield return und internal. Der Rückgabewert beider Methoden ist eine Aufzählung, technisch gesprochen vom Typ IEnumerable<T>. Wenn der Rückgabewert einer Methode von diesem Typ ist, steht innerhalb der Methode das Schlüsselwort yield zur Verfügung. Wie in [2] erläutert, erstellt der C#-Compiler einen Automaten für 24 die Methode. So entfällt die Notwendigkeit, innerhalb der Methode eine Liste für das Sammeln der Ergebniswerte zu erstellen. Der Code wird kompakter und besser lesbar. Ein zweiter Aspekt ist die Verwendung von Methoden, die mit internal sozusagen halb versteckt werden, siehe Listing 3. Auch darauf wurde an anderer Stelle bereits hingewiesen, etwa bei der Lösung zum INotifyPropertyChanged-Tester in [3]. Ich verwende dieses Muster immer dann, wenn sich im Architekturentwurf Methoden abzeichnen, die weiter zerlegt werden können. Erfolgt diese Zerlegung bereits im Rahmen der Planung, teste ich diese Methoden isoliert, so wie hier gezeigt. Manchmal entstehen Fummelei beim Index Eine Besonderheit gibt es bei den Indizes zu beachten: Berücksichtigen Sie an den Rändern, dass es nicht in allen Richtungen benachbarte Felder gibt. Das bedeutet, dass Sie bei jedem Zugriff auf die Nachbarfelder prüfen müssen, ob die Zelle an einem der Ränder liegt. Diese Indexprüfungen machen den Code recht unübersichtlich. if ((point.Y - 1 >= 0) && (point.X - 1 >= 0)) { result[point.Y - 1, point.X - 1]++; } Um die Indizes nicht prüfen zu müssen, können Sie einen Trick anwenden: Wenn Sie das Array mit einem zusätzlichen Rand anlegen und dann nur Indizes von 1 bis Length – 2 statt von 0 bis Length – 1 verwenden, können Sie sich die Indexprüfungen sparen. Da der zusätzliche Randbereich mit Nullen initialisiert ist, macht es nichts, dort auch die Minen aufzuaddieren. Nach der Berechnung wird der Rand einfach wieder entfernt. Ob diese Lösung besser lesbar ist als bei der Variante, bei der die Indizes vor jedem Zugriff geprüft werden, sei dahingestellt. Letztlich gewinnt man durch den „RandTrick“ etwas Lesbarkeit beim Zugriff auf das Array, muss aber zusätzlich das Umkopieren zum Entfernen des Randes implementieren, siehe Listing 4. Da ich mit dieser Lösung nicht zufrieden war, entschied ich mich zu einer weiteren Variante. Ich wollte versuchen, das Bestimmen der Indizes der acht benachbarten Felder zu trennen vom Test auf Gültigkeit der Indizes. Die Grundidee ist hier also: Erst mal alle acht möglichen Indizes bestimmen, dann prüfen, welche davon gültig sind. Listing 5 zeigt das Ergebnis. Am Ende waren die beiden Lösungen vom Umfang des Codes her miteinander vergleichbar. Die Lesbarkeit und Verständlichkeit der Lösung ohne „Rand-Trick“ scheint mir etwas besser, da man den Trick mit dem Rand eben nicht benötigt. Struktur Auch dieses Mal verwende ich bei der Lösung die Projekt- und Verzeichnisstruktur, die ich durchgängig immer in allen Projekten verwende. Das hat den Vorteil, dass mir diese Schritte so zur Gewohnheit werden, dass ich nicht mehr darüber nachdenken dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG und die einzelnen Komponenten würden je eine eigene Solution erhalten. [Abb. 4] Der Flow für die Vorgehensweise nach Mine. Fazit Der Minesweeper-Mogelzettel ist eine zeitlich überschaubare Übung. Der Umgang mit den Indizes bietet ein reichhaltiges Betätigungsfeld. Falls Sie es also bislang noch nicht selbst versucht haben: Mogeln Sie mal wieder beim Minesweeper, aber nicht beim Übungspensum ! [ml] [Abb. 5] Die bewährte Projektstruktur. muss. Ich erstelle die Verzeichnisse, lege Projekte an, setze Referenzen, ändere Ausgabepfade, alles ist immer gleich. Wenn Sie jetzt denken, dass das eine Wiederholung ist, welche gegen das Prinzip Don't Repeat Yourself (DRY) verstößt, dann haben Sie mich erwischt. Eigentlich sollte ich diese immer wiederkehrenden Handgriffe automatisieren. Ein Vorteil der immer gleichen Struktur ist, dass ich mich in allen Projekten sofort zurechtfinde. Alles liegt immer am gleichen Platz. Um eine solche Konvention zu etablieren, muss sie sich allerdings zunächst in der Praxis bewähren. Denn wenn das nicht der Fall ist, nützt die schönste Konvention nichts.Wenn ich regelmäßig Projektstrukturen anlege und verwende, zeigt sich, ob im Detail noch Verbesserungen möglich sind. Die in Abbildung 5 gezeigte Struktur hat sich in vielen Projekten, Übungen und Seminaren bewährt. Die Pfeile zeigen, welche Projekte referenziert werden. Beim Mogelzettel habe ich mich für zwei Implementierungsprojekte sowie ein Testprojekt entschieden. Bei der Implementierung unterscheide ich zwischen der Logik und dem Host. Im Host wird lediglich die Konsolenschnittstelle zur Verfügung gestellt. Die beiden Parameter der Kommandozeile werden als Dateinamen interpretiert. Durch die Abtrennung des Hosts lässt sich die Logik des Mogelzettels auch einmal in einem GUI-Host verwenden. Diesmal habe ich keine komponentenorientierte Architektur gewählt, da ich mich www.dotnetpro.de dotnetpro.dojos.2011 auf das Problem der Indizes konzentrieren wollte. Dennoch sind in der Solution mehrere Projekte mit klar definierten Aufgaben vorhanden. Für eine komponentenorientierte Lösung kämen die Kontrakte hinzu, [1] Stefan Lieser: Mogeln mit EVA, Daten umformen, dotnetpro 6/2010, Seite 116 ff. www.dotnetpro.de/A1006dojo [2] Golo Roden: yield return, yield break, yield ... Golos scharfes C, dotnetpro 5/2010, S. 122 f. www.dotnetpro.de/A1005ScharfesC [3] Stefan Lieser: Kettenreaktion, INotifyPropertyChanged-Logik automatisiert testen, dotnetpro 5/2010, S. 108 ff. www.dotnetpro.de/A1005dojo Listing 4 Den Rand beim Rand-Trick entfernen. internal static int[,] RandEntfernen(int[,] array) { var result = new int[array.GetLength(0) - 2,array.GetLength(1) - 2]; for (var i = 1; i < array.GetLength(0) - 1; i++) { for (var j = 1; j < array.GetLength(1) - 1; j++) { result[i - 1, j - 1] = array[i, j]; } } return result; } Listing 5 Erst Indizes bestimmen, dann prüfen. internal static int[,] MogelzettelBerechnen(Size groesse, IEnumerable<Point> minenKoordinaten) { var result = new int[groesse.Height,groesse.Width]; foreach (var mine in minenKoordinaten) { var indizes = new[] { new Point(mine.X - 1, mine.Y - 1), new Point(mine.X - 1, mine.Y), new Point(mine.X - 1, mine.Y + 1), new Point(mine.X, mine.Y - 1), new Point(mine.X, mine.Y + 1), new Point(mine.X + 1, mine.Y - 1), new Point(mine.X + 1, mine.Y), new Point(mine.X + 1, mine.Y +1), }; foreach (var index in indizes) { if ((index.X >= 0) && (index.X < groesse.Width) && (index.Y >= 0) && (index.Y < groesse.Height)) { result[index.Y, index.X]++; } } } return result; } 25 AUFGABE Zahlenreihen visualisieren mit Boxplots Papa, was ist ein Boxplot? Wie lange dauert und was kostet dies und jenes im Durchschnitt, höchstens, mindestens und am wahrscheinlichsten? Statistik ist das halbe Leben, in Form von Zahlen und in Form von Grafiken. Stefan, kannst du dazu eine Aufgabe stellen? Wer übt, gewinnt dnpCode: A1007DojoAufgabe 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 dotnetpro finden. Übung macht den Meister. Also − los geht’s. Aber Sie wollten doch nicht etwa sofort Visual Studio starten… H aben Sie sich auch schon mal gefragt, wie Sie eine Zahlenreihe anschaulich darstellen können? Eine übersichtliche Form bieten die sogenannten Boxplots [1]. Darauf hat mich meine Tochter gebracht. Sie musste Boxplots als Hausaufgabe in Mathe zeichnen. 7. Klasse! Da werden Sie sich als gestandener Softwareentwickler doch nicht wegducken wollen, oder? Boxplots dienen dazu, die Verteilung der Werte zu visualisieren. Dazu werden neben dem kleinsten und dem größten Wert auch die sogenannten Quartile visualisiert. Die Zahlenreihe wird in vier Bereiche unterteilt. Sind diese Bereiche gleich groß, bedeutet das, dass die Werte in der Zahlenreihe gleichmäßig verteilt sind. Dazu ein Beispiel. Wenn Sie sich fragen, ob der Pizzadienst um die Ecke immer gleich lange braucht, um die Pizza anzuliefern, oder ob es auch schon mal große Ausreißer gibt, können Sie die Werte mit einem Boxplot schön visualisieren. Ich habe nicht gemessen, aber die Werte könnten so aussehen : 18, 24, 19, 19, 20, 25, 24, 18, 24, 17 Aus den reinen Zahlen wird man nicht sofort etwas erkennen können. Helfen würde schon mal der Mittelwert. Wie der berechnet wird, ist jedem sofort klar. Aber wie sieht es mit dem Median aus? Erinnern Sie sich noch? Zur Berechnung des Medians müssen die Werte zunächst sortiert werden. Dann nimmt man einfach den mittleren Wert. Wenn die Anzahl der Werte gerade ist, nimmt man die beiden mittleren Werte und bildet daraus den Mittelwert. Im obigen Beispiel sind es zehn Werte. Nach dem Sortieren sieht die Zahlenreihe wie folgt aus: 17, 18, 18, 19, 19, 20, 24, 24, 24, 25 Die beiden mittleren Werte sind 19 und 20. Der Mittelwert aus diesen ist (19 + 20) / 2 = 19,5. Beim unteren und oberen Quartil geht es sinngemäß, wie Abbildung 1 zeigt. Aus den Werten wird dann ein Boxplot erstellt, in dem Minimum und Maximum die untere bzw. obere Begrenzung bilden. Dazwischen werden die beiden Quartile sowie der Median eingezeichnet, fertig 26 [Abb. 1] Die Ausgangs-Zahlenreihe. [Abb. 2] Grafische Darstellung mit einem Boxplot. ist der Boxplot. Abbildung 2 zeigt das Ergebnis. Aufgabe des dojos ist es, ein Control zu entwickeln, das einen Boxplot darstellt. Ob Sie das mit Windows Forms, WPF oder Silverlight lösen, ist egal. Selbst eine Ausgabe auf der Konsole wäre reizvoll. Die Logik von der Umsetzung trennen Bedenken Sie, dass eine saubere Trennung der Belange wichtig ist. Die Logik des Boxplots sollte unabhängig sein von den technischen Details des Controls. Das hilft in jedem Fall beim automatisierten Testen. Ferner schaffen Sie durch diese Trennung die Grundlage dafür, dass Sie das Control später auch einmal in einer anderen Technologie realisieren können. Denken Sie auch darüber nach, wie die Schnittstellen der beteiligten Funktionseinheiten am besten aussehen. Sollte das Control die komplette Zahlenreihe erhalten? Oder direkt die Quartile und die anderen benötigten Werte? Viele Fragen, nächsten Monat gibt es hier wieder Antworten. Bis dahin, viel Spaß mit Boxplots! [ml] [1] http://de.wikipedia.org/wiki/Boxplot dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Mit Boxplots Zahlenreihen visualisieren So boxen Sie mit Silverlight! Statistik hat immer mit Zahlen zu tun. Und Zahlen kann man immer irgendwie grafisch darstellen, eine Zahlenreihe zum Beispiel in einem Boxplot. Aber wer versucht, ein entsprechendes Silverlight-Control testgetrieben zu entwickeln, muss feststellen, dass auch Silverlight 4 die testgetriebene Entwicklung nur mangelhaft unterstützt. W er sich an der Aufgabenstellung versucht hat und auf die Schnelle keine Definition für Quartile im Kopf hatte, wird die Suchmaschine seines Vertrauens bemüht haben. Das Ergebnis dürfte überraschen: Man findet unterschiedliche Vorschläge, wie das untere und obere Quartil zu bestimmen seien. Unter [1] ist eine Erklärung zu finden, aus der auch hervorgeht, wie Excel die Quartile berechnet. Unter [2] finden sich Beispielaufgaben. Dabei wird, soweit erkennbar, das Verfahren verwendet, das [3] beschreibt. Ich habe ebenfalls nach dem dort beschriebenen Verfahren implementiert. Doch bevor ich meine Implementierung beschreibe, möchte ich auf ein Problem hinweisen, welches mich „während der Fahrt“ erwischt hat. Ich habe die Aufgabe als Silverlight-Anwendung begonnen. Ein Grund dafür war, dass ich sehen wollte, ob Visual Studio 2010 im Bereich Testen von Silverlight-Anwendungen endlich etwas zu bieten hat. Doch leider – Fehlanzeige. Es gibt von Microsoft nach wie vor nur das Silverlight Unit Test Framework aus dem Toolkit [4]. Dies ist jedoch für die testgetriebene Entwicklung nicht geeignet, da man nicht die Möglichkeit hat, aus Visual Studio heraus einen einzelnen Test zu starten. Mir wurde es nach kurzer Zeit zu lästig, immer mit [Ctrl] + [F5] den Test Runner im Browser zu starten. Also habe ich nach einer Alternative gesucht. Doch selbst beim Versionsstand 4 von Silverlight sieht es im Bereich automatisiertes Testen nach wie vor dürftig aus. Zwar gibt es einige Werkzeuge, mit denen Silverlight-Controls und Anderes getestet werden können. Und natürlich müssen diese Tests im Browser laufen, um eine vollständige Silverlight-Umgebung abzubilden. Was fehlt, ist Unterstützung für das Testen von Nicht-UI-Klassen. Ich bin schließlich doch noch fündig geworden. Roy Osherove hat eine Ergänzung zu TypeMock Isolator [5] entwickelt, mit der Silverlight-Tests innerhalb von Visual www.dotnetpro.de dotnetpro.dojos.2011 Studio laufen können. Das SilverUnit genannte Open-Source-Projekt ist unter [6] zu finden. Es setzt allerdings eine kostenpflichtige Lizenz von TypeMock Isolator voraus. Doch zurück zum Boxplot. Die Aufgabenstellung lässt sich grob in zwei Bereiche unterteilen : ❚ Benutzerschnittstelle (UI, Control), ❚ Berechnung. Ausgangspunkt eines Boxplots ist eine Aufzählung von Werten. Für diese Werte müssen Sie für die Visualisierung folgende Größen ermitteln : Minimum, Unteres Quartil, Median, Oberes Quartil, Maximum. Zur Ermittlung dieser Größen ist es erforderlich, die Werte zu sortieren. Es ist naheliegend, die Implementierung so vorzunehmen, dass die Ausgangswerte nur einmal sortiert werden. Aber Vorsicht vor Optimierungen! Die Größen sind unabhängig voneinander und können daher auch unabhängig implementiert werden. Widerstehen Sie dem Reflex, von Anfang an eine Implementierung vorzusehen, in der die Sortierung herausgezogen wird. Sollte sich später herausstellen, dass das mehrfache Sortieren zu Problemen bei der Geschwindigkeit führt, können Sie immer noch nach Abhilfe suchen. Fest steht: Bei der Berechnung benötigen Sie einige Methoden, die aus den Grunddaten die zur Visualisierung benötigten Größen ermitteln. Diese Methoden lassen sich testgetrieben recht gut entwickeln. Control-API Die Schnittstelle des Controls sollte für den Verwender möglichst komfortabel sein: Ich möchte das Control auf ein Formular ziehen, die Größe einstellen, fertig. Insbesondere mit der Skalierung sollte der Verwender nichts zu tun haben. Auf der anderen Seite sollte das Control allerdings auch nicht zu viel tun. Insbesondere das Berechnen der darzustellenden Größen aus den Werten liegt nicht im Verantwortungsbereich des Controls. Die Ermittlung des Medians hat nichts mit der Funktionalität eines Controls zu tun. Damit sich das Control um die Skalierung kümmern kann, müssen die Größen wie Minimum, Maximum, Median und so weiter nichtskaliert, das heißt als Originalwert, angegeben werden. Aufgabe des Controls ist es, den Bereich zwischen Minimum und Maximum in der zur Verfügung stehenden Breite darzustellen. Folglich müssen sämtliche Größen auf die zur Verfügung stehende Breite skaliert werden. Ich Listing 1 Eine Dependency-Property beschreiben. public static readonly DependencyProperty LowScaledProperty = DependencyProperty.Register("LowScaled", typeof(double), typeof(BoxPlot), new PropertyMetadata(Changed)); Listing 2 Eine Eigenschaft für die Dependency-Property. public double LowScaled { get { return (double)GetValue(LowScaledProperty); } } 27 LÖSUNG habe mich daher entschlossen, für die darzustellenden Werte jeweils zwei Eigenschaften im Control bereitzustellen: eine Eigenschaft für den nichtskalierten Originalwert sowie eine für den skalierten Wert. Das Control übernimmt das Skalieren. Immer wenn einer der nichtskalierten Werte verändert wird, muss das Control den skalierten Wert anpassen. Realisiert man die Eigenschaften als sogenannte Dependency-Properties, können sie per Data-Binding im Control verwendet werden. Mit dieser Idee zur grundsätzlichen Vorgehensweise habe ich mich allerdings etwas unsicher gefühlt. Mir war nämlich nicht ganz klar, ob dies tatsächlich funktioniert. Dazu fehlt mir die Praxis mit Silverlight. Da dies ein übliches Problem in Projekten ist, will ich es hier kurz thematisieren. Entwickler stehen immer wieder vor Herausforderungen, zu denen sie zwar eine grobe Vorstellung über mögliche Lösungswege entwickeln können. Am Ende bleiben jedoch manchmal Unsicherheiten über den konkreten Lösungsweg. Diese können beispielsweise in konkreten Details der zu verwendenden Technologie oder auch in Algorithmen liegen. Um diese Unsicherheit in den Griff zu bekommen, kann man einen sogenannten Spike implementieren. Ein Spike ist eine Art „Forschungsprojekt“. Der Spike soll dazu dienen, Unsicherheiten zu beseitigen. Ziel eines Spikes ist also der Erkenntnisgewinn, nicht etwa produktionsfertige Software. Daher werden an den Spike andere Anforderungen gestellt. Er muss nicht testgetrieben entwickelt werden. Es gibt allerdings auch ein großes „Aber“: Beim Spike ist zwar alles erlaubt, aber das Ergebnis wird nicht in der Produktion verwendet. Nachdem der Spike zum Erkenntnisgewinn geführt hat, muss die betreffende Funktionalität anschließend nach allen Regeln der Kunst testgetrieben implementiert werden. Nachdem ich also per Spike geklärt hatte, dass einer Implementierung mittels Dependency-Properties nichts im Wege steht, begann ich mir über die Architektur Gedanken zu machen. Diese erwies sich als trivial: Auf der einen Seite gibt es ein Control mit Eigen- Listing 3 set veranlasst die Skalierung. public double Low { get { return (double)GetValue(LowProperty); } set { SetValue(LowProperty, value); SetValue(LowScaledProperty, Scaled(value)); } } Listing 4 Die Skalierung berechnen. private double Scaled(double value) { return (value - Min + 1) * ((ActualWidth - StrokeThickness) / (Max - Min)); } Listing 5 Testbeispiele entwickeln. [TestMethod] public void Drei_sortierte_Werte() { Assert.AreEqual(2.0, Zahlenreihe.Median(new[] {1.0, 2.0, 3.0})); } [TestMethod] public void Vier_sortierte_Werte() { Assert.AreEqual(2.5, Zahlenreihe.Median(new[] {1.0, 2.0, 3.0, 4.0})); } 28 schaften für Minimum, Quartile, Median und Maximum. Auf der anderen Seite gibt es einige statische Methoden, um diese Werte zu berechnen. Damit sind Control und Berechnungslogik unabhängig voneinander. Beim Control habe ich eine einfache Lösung gewählt: Minimum und Maximum liegen jeweils am Rand des Controls. Damit füllt der Boxplot den für das Control zur Verfügung stehenden Platz vollständig aus. Nun habe ich mir eine Skizze gemacht, die visualisiert, aus welchen Primitiven der Boxplot aufgebaut ist, siehe Abbildung 1. [Abb. 1] Den Boxplot aus einzelnen Linien aufbauen. Ich habe dazu als Primitive nur Linien verwendet. Bei einem waagerecht liegenden Boxplot sind die y-Koordinaten nicht von den darzustellenden Größen abhängig, sondern nur vom zur Verfügung stehenden Platz. Folglich sind lediglich die x-Koordinaten per Data-Binding an die Dependency-Properties gebunden. Die Implementierung des Controls besteht somit aus drei Teilen : ❚ XAML-Datei zur Definition der Linien und der Data-Bindings, ❚ Dependency-Properties für die darzustellenden Größen, ❚ Skalierungslogik. Die XAML-Datei enthält einen Canvas als Container. Darin liegen die neun LineElemente, bei denen die in Abbildung 1 markierten x-Koordinaten über Data-Binding von den Dependency-Properties abhängen. Damit das Data-Binding sich auf eigene Eigenschaften des Controls bezieht, müssen Sie im UserControl den DataContext wie folgt setzen : <UserControl ... DataContext="{Binding RelativeSource={RelativeSource Self}}"> Dadurch können Sie in den Line-Elementen beim Data-Binding Eigenschaften des Controls verwenden : <Line X1="1" Y1="50" X2="{Binding Path=LowScaled}" Y2="50" Canvas.Left="1" Canvas.Top="1" StrokeThickness="2" Stroke="Black" /> dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Im Beispiel ist LowScaled die Dependency-Property, welche den skalierten Wert für das untere Quartil enthält. Für die Dependency-Properties wird jeweils ein statisches Feld definiert, welches die Dependency-Property beschreibt, siehe Listing 1. Zusätzlich sind normale C#-Eigenschaften definiert, um in der üblichen Art und Weise auf die Eigenschaften zugreifen zu können, siehe Listing 2. Der Umweg über die Dependency-Properties ist erforderlich, damit die Visualisierung jeweils aktualisiert wird, wenn sich an den zugrunde liegenden Werten etwas ändert. Die Skalierung der Größen erfolgt in den Settern der jeweiligen zugehörigen nichtskalierten Größe. Dort wird sowohl der zu setzende Originalwert als auch der berechnete skalierte Wert in die jeweiligen Dependency-Properties übertragen, wie in Listing 3 zu sehen. Auf diese Weise wird beim Setzen der Low-Eigenschaft auch die LowScaled-Eigenschaft gesetzt. Das Skalieren übernimmt die in Listing 4 gezeigte Methode. Um die korrekte Visualisierung des Controls prüfen zu können, bleibt nichts anderes übrig, Listing 6 Gerade und ungerade Anzahl von Werten testen. [TestMethod] public void Drei_unsortierte_Werte() { Assert.AreEqual(2.0, Zahlenreihe.Median(new[] {2.0, 3.0, 1.0})); } [TestMethod] public void Vier_unsortierte_Werte() { Assert.AreEqual(2.5, Zahlenreihe.Median(new[] {3.0, 1.0, 4.0, 2.0})); } Listing 7 Das Minimum über LINQ ermitteln. [TestMethod] public void Minimum() { Assert.AreEqual(1, new[]{5.0, 1.0, 2.0}.Min()); } als einen kleinen Testrahmen zu erstellen, in dem das Control angezeigt wird. Das bedeutet allerdings nicht, dass in solchen Tests nichts zu automatisieren wäre. Der Testrahmen kann immerhin dazu verwendet werden, Beispieldaten automatisiert zum Control zu übertragen. So entfällt die manuelle Interaktion mit dem Control zum codekicker.de Die deutschsprachige Q&A-Plattform für Software-Entwickler codekicker.de – Antworten für Entwickler LÖSUNG Testzeitpunkt. Zudem sind dadurch die verwendeten Testdaten dokumentiert. Berechnungen Nachdem das Control fertiggestellt ist, geht es an die Berechnung der benötigten Größen. Dabei kann man dank SilverUnit und TypeMock Isolator wieder testgetrieben vorgehen. Ich habe zunächst einige Beispiele zusammengestellt und diese dann nach und nach in automatisierte Tests überführt, siehe Listing 5. Da mir der Algorithmus zur Berechnung des Medians vor der Implementierung vertraut war, habe ich die Tests so gewählt, dass ich den Algorithmus schrittweise implementieren konnte. Zunächst habe ich daher den Median aus einer bereits sortierten Aufzählung ermittelt. Dabei sind zwei Fälle zu unterscheiden: Die Anzahl der Werte kann ungerade oder gerade sein. Bei einer geraden Anzahl von Werten werden die beiden mittleren Werte herangezogen und aus diesen der Mittelwert berechnet. Die Ergänzung um das Sortieren war keine große Sache, wie Listing 6 zeigt. Nach dem Median kamen Minimum und Maximum an die Reihe – auch kein großes Problem. Nach dem Sortieren den ersten beziehungsweise letzten Wert zu verwenden ist einfach. Aber halt: Gibt es diese Funktionalität nicht in LINQ? Listing 7 zeigt es. Siehe da, ganz einfach. Bleiben noch die beiden Quartile. Hier war, wie eingangs schon erwähnt, eher die Frage, welcher Algorithmus verwendet werden sollte. Die Tests sind wieder keine große Sache. Das Beispiel von Listing 8 ist auf SilverUnit und NUnit ausgelegt, daher sehen die Attribute an der Testmethode etwas anders aus als in den vorigen Beispielen. Listing 9 zeigt die zugehörige Implementierung. Die Methode verwendet zum Sortieren die in Listing 10 gezeigte Extension Method. Dadurch wird die Anwendung des Sortierens besser lesbar. Zudem ist die Implemen- Listing 8 Die Berechnung von Quartilen testen. [Test] [SilverlightUnitTest] public void Quartil_25_bei_7_Werten() { Assert.AreEqual(2.0, Zahlenreihe.UnteresQuartil(new[] {1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0})); } Listing 9 Quartile berechnen. public static double UnteresQuartil(IEnumerable<double> zahlenreihe) { var werte = zahlenreihe.Sort(); if (werte.Count % 4 == 0) { var x1 = werte[werte.Count / 4 - 1]; var x2 = werte[werte.Count / 4]; return (x1 + x2) / 2; } return werte[(int)Math.Ceiling(werte.Count / 4.0) - 1]; } Listing 10 Die Werte sortieren. public static class ArrayExtensions { public static IList<T> Sort<T>(this IEnumerable<T> enumerable) { var values = enumerable.ToArray(); Array.Sort(values); return values; } } 30 [Abb. 2] Geschafft: Ein Boxplot im Browser. tierung des Sortierens damit in einer Methode zusammengefasst. Sollte sich später zeigen, dass das Sortieren über Arrays zu Performance- oder Speicherproblemen führt, kann dies an einer einzigen Stelle behoben werden. Abbildung 2 zeigt das fertige Control im Browser. Fazit Die Herausforderung lag diesmal im Tooling. Automatisiertes Testen von SilverlightAnwendungen ist immer noch ein schwieriges Unterfangen. Bei dem kostenlos verfügbaren Tool aus dem Silverlight Toolkit stört mich persönlich vor allem, dass es auf MSTest basiert. Als NUnit-Anwender fällt es mir schwer, Assert.AreEqual zu schreiben statt Assert.That. Ferner ist die ausschließliche Ausführung im Browser nicht zu tolerieren. Hier sollte Microsoft schnell nachbessern und einen in Visual Studio integrierten Unit Test Runner liefern. Dass dies möglich ist, zeigt Testdriven.NET [7]. Leider kann damit aber immer nur ein einziger Test ausgeführt werden. Die Alternative lautet zurzeit SilverUnit. Dazu ist zwar eine kostenpflichtige Lizenz von TypeMock Isolator erforderlich, das dürfte aber für ernsthafte kommerzielle Entwicklungen im Silverlight-Umfeld kein Problem [ml] darstellen. [1] Nach welchem Verfahren berechnet Excel eigentlich Quartile? Hinter die Kulissen von Excel geschaut, www.dotnetpro.de/SL1008dojo1 [2] Übungen zu Boxplots, www.dotnetpro.de/SL1008dojo2 [3] Zeichnen von Boxplots mithilfe von Excel, Anleitung, www.dotnetpro.de/SL1008dojo3 [4] http://code.msdn.microsoft.com/silverlightut/ [5] http://typemock.com [6] http://cthru.codeplex.com/ [7] http://testdriven.net dotnetpro.dojos.2011 www.dotnetpro.de AUFGABE Experimentieren mit Raven DB Was kann der Rabe? Kaum eine Software kommt ohne Persistenz aus. Auf diesem Gebiet stehen die relationalen Datenbanken in fest gefügter Phalanx. Aber geht Persistenz nicht auch anders? Da gibt es doch diese NoSQL-Dokumentendatenbanken. Stefan, fällt dir dazu eine Übung ein? P www.dotnetpro.de dotnetpro.dojos.2011 dnpCode: A1008DojoAufgabe 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. [Abb. 1] Ungefähr so könnte die Seite für Produktbewertungen aussehen. sehen. Überlegen Sie sich also ein kleines Datenmodell, bestehend aus Produkten, Kategorien, in die ein Produkt fällt, sowie Bewertungen und Kommentaren zu einem Produkt. Da Raven DB eben gerade nicht relational ist, besteht die Herausforderung möglicherweise darin, sich von der in uns schlummernden relationalen Denkweise ganz bewusst zu lösen. Um die Fähigkeiten von Raven DB zu erkunden, sollten Sie in der Anwendung ein Feature vorsehen, das Daten aggregiert. Sie können beispielsweise aus allen abgegebenen Bewertungen zu einem Produkt den Mittelwert bilden. Oder die Bewertungen aller Produkte einer Kategorie aggregieren. Oder das Produkt mit der besten Bewertung innerhalb einer Kategorie ermitteln. Lassen Sie Ihrer Fantasie freien Lauf. Einige Ideen liefert das in Abbildung 1 gezeigte Mockup. Somit sind Sie diesen Monat eigentlich in zweifacher Weise herausgefordert: Die erste Herausforderung ist einfach die Beschäftigung mit Raven DB. Die zweite besteht darin, beispielhafte Anforderungen mit Raven DB umzusetzen. Viel Spaß bei der Arbeit als Forscher auf unbekanntem Terrain. [ml] 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 dotnetpro finden. Übung macht den Meister. Also − los geht’s. Aber Sie wollten doch nicht etwa sofort Visual Studio starten… [1] Raven DB, http://ravendb.net/ 31 Wer übt, gewinnt ersistenz ist ein wichtiger Aspekt in vielen Anwendungen. Seit Jahrzehnten bewährt sich die Technologie der relationalen Datenbanken. Sie ist allerdings nicht in allen Fällen gut geeignet, die Anforderungen umzusetzen. Wenn das Schema der Daten flexibel sein muss, bieten sich Alternativen an. Mit dieser Problematik befasst sich unter dem Stichwort „NoSQL“ inzwischen eine ganze Reihe von Projekten. Sie setzen ganz bewusst nicht auf SQL. Zugleich wollen diese Projekte die relationalen Datenbanken nicht ersetzen, sondern verstehen sich als Alternative, die in bestimmten Kontexten sinnvoll ist. Daher wird NoSQL oft auch mit „not only SQL“ übersetzt. Da liegt es doch nahe, sich im Rahmen des dotnetpro.dojo einmal mit einem NoSQL-Projekt zu befassen. Schließlich bedeutet regelmäßiges Üben für Softwareentwickler auch, sich ab und zu mal mit völlig neuen Dingen zu beschäftigen. Da Ayende Rahien gerade sein neuestes Projekt Raven DB [1] veröffentlicht hat, bietet sich die Chance, zu den Early Adoptern zu gehören. Daher lautet die Aufgabe des Monats: Schreibe eine kleine Raven-DB-Anwendung. Beim Einsatz einer neuen Technologie, mit der man noch nicht vertraut ist, bietet es sich an, dies in Form eines sogenannten Spikes zu bewerkstelligen. Ziel eines Spikes ist nicht, Code zu schreiben, der Produktionsqualität erreicht, sondern Ziel ist Erkenntnisgewinn. Doch wenn sich automatisierte Tests für ein flüssiges Entwickeln von Produktionscode eignen, mögen sie auch in einem Spike nützlich sein, um schnell voranzukommen. Denn nach dem Speichern und Laden eines Objektes mit Raven DB wird schnell der Wunsch entstehen, auch die anderen Fähigkeiten des APIs auszuloten. Da kommt man mit einer Reihe von Tests, die im Unit Test Runner einzeln gestartet werden können, zügig voran. Nach den ersten Schritten, die vor allem dazu dienen, sich mit dem API vertraut zu machen, soll eine kleine Aufgabenstellung bearbeitet werden. Implementieren Sie daher eine kleine Anwendung zur Bewertung von Produkten. Die Anwender sollen damit in die Lage versetzt werden, Produktbewertungen abzugeben und sie einzu- LÖSUNG Die NoSQL-Dokumentendatenbank Raven DB ausprobieren So sammeln Raben Daten Zum Entwickleralltag gehört es, sich in neue Technologien einzuarbeiten, beispielsweise in eine NoSQL-Datenbank. Der Code, der dabei entsteht, muss nicht die Qualität von Produktionscode haben. Ein testgetriebener Ansatz ist dafür aber dennoch nützlich, denn die Tests dokumentieren die gewonnenen Erkenntnisse in leicht nachvollziehbarer Form. I m vergangenen Monat war das dotnetpro.dojo etwas anders gelagert als sonst. Es ging nicht darum, eine konkrete Aufgabenstellung zu implementieren, sondern darum, sich mit einem bislang unbekannten Framework ausein-anderzusetzen. Auch das ist eine Form der Übung: das schnelle Sich-Einarbeiten in eine neue Technologie über einen Spike. Ein Spike dient vor allem dem Erkenntnisgewinn. Dieser steht im Vordergrund und mag im Zweifel auch schon mal Prinzipien und Praktiken zurückdrängen, die man bei Produktionscode in jedem Fall anwenden würde. Das bedeutet jedoch nicht, dass Spikes ein Freifahrtschein für schlechte Angewohnheiten wären. Bei der Überlegung, welche Prinzipien und Praktiken ich anwende, lasse ich mich auch beim Spike vom Wertesystem der Clean-Code-Developer-Initiative leiten [1]. Einer dieser Werte ist die Produktionseffizienz. Daraus ergibt sich für mich beispielsweise, dass ich auch Spikes in der gewohnten Verzeichnis- und Projektstruktur anlege. Das hat zum einen den Vorteil, dass der Spike eine weitere Gelegenheit bietet, diese Struktur anzuwenden und zu hinterfragen. Zum anderen ergibt sich daraus ein Effizienzvorteil, weil ich es eben immer gleich tue. Ich gestehe, es fehlt ein Stück Automatisierung, viele der Schritte erledige ich in Handarbeit. Aber da ich sie so oft anwende, gehen sie flüssig von der Hand. So landen bei mir auch in Spikes die Tests in einem eigenen Projekt. Und auch die benötigten Frameworks wie NUnit und in diesem Fall RavenDB werden nicht aus dem GAC oder sonst woher referenziert, sondern „nach den Regeln der Kunst“ aus einem Verzeichnis innerhalb der Projektstruktur. Würde ich nicht so verfahren, hät- 32 ten Sie als Leser später das Nachsehen. Denn dann würden sich die Beispiele, die Sie zu diesem Artikel auf der Heft-DVD finden, nicht sofort übersetzen lassen. Diese Situation ist keinesfalls speziell, nur weil ich den Code zur Veröffentlichung in einem Artikel schreibe. Auch in Ihren täglichen Projekten werden andere Entwickler den Code aus der Quellcodeverwaltung entnehmen und übersetzen wollen. Wenn Projekte dann nicht „self-contained“, also in sich abgeschlossen, sind, fängt der Ärger an: Referenzierte Assemblies werden nicht gefunden. Oder noch „gemeiner“: Sie liegen in einer anderen Version vor und verursachen dadurch Probleme. Und gehen Sie nicht davon aus, dass vermeintliche Selbstverständlichkeiten wie NUnit zu der Umgebung gehören würden, die Sie bei jedem Entwickler voraussetzen können. Je weniger Abhängigkeiten das Projekt zu seiner Umgebung hat, desto besser. Sie werden möglicherweise fragen, was denn automatisierte Tests in einem Spike zu suchen haben. Automatisierte Tests haben bei der Erkundung neuer Techniken zwei Vorteile: Zum einen verwende ich sie, um die verschiedenen Szenarien damit starten zu können. Anstatt eine Konsolenanwendung zu erstellen, welche mit Console.WriteLine versucht darzustellen, was gerade passiert, verwende ich automatisierte Tests. Das bietet den Vorteil, dass ich in einem Projekt mehrere Szenarien unterbringen kann, die sich alle einzeln starten lassen. Ferner sind diese Tests ebenfalls „self-contained“, also in sich abgeschlossen. Ein Blick auf den Test genügt, um zu verstehen, was da passiert. Es ist nicht notwendig, eine Anwendung laufen zu lassen, um zusätzlich noch die Konsolenausgabe zu sehen. Der zweite Vorteil von Tests ist, dass ich sie zur Dokumentation der Funktionalität verwenden kann. Wenn ich mir nicht sicher bin, ob eine bestimmte Funktionalität sich nun so oder anders verhält, erstelle ich einen Test, der das Verhalten dokumentiert. Im weiteren Verlauf des Artikels wird ein solcher Test beispielsweise zeigen, zu welchem Zeitpunkt RavenDB Schlüsselwerte erzeugt. Woher nehmen, den Raben? Nachdem ich eine Solution mit zwei Projekten angelegt hatte und die Referenz auf NUnit gesetzt war, stand ich vor der Frage: Woher RavenDB nehmen? Klar, dass sich die Antwort hinter der URL [2] verbirgt, ich fragte mich aber, ob ich eine fertig übersetzte, binäre Version verwenden oder auf den Quellcode setzen sollte. Ich entschied mich für eine binäre Version, die jeweils aktualisiert unter [3] zum Download erhältlich ist. Wenn schon täglich aktualisierte Binärversionen zur Verfügung stehen, muss ich mir nicht die Mühe machen, lokal jeweils eine aktuelle Version zu übersetzen. Leider ist bei Weitem nicht für alle OpenSource-Projekte ein täglich aktualisierter Listing 1 Ein Objekt speichern. var store = new DocumentStore {Url = "http://localhost:8080"}; store.Initialize(); var produkt = new Produkt(); using (var session = store.OpenSession()) { session.Store(produkt); session.SaveChanges(); } dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Build verfügbar, daher bietet sich in anderen Fällen die Arbeit mit den Quellen an. Nach dem Download stand mir RavenDB nun zur Verfügung. Ich habe es komplett in das lib-Verzeichnis innerhalb der Projektstruktur abgelegt. Damit unterliegt es der Versionierung, und das Projekt ist in sich abgeschlossen. Client und Server RavenDB kann auf verschiedenen Wegen verwendet werden: eingebettet in die Anwendung oder getrennt in Client und Server. Ich entschied mich dafür, RavenDB als Server zu starten. Im Implementierungsprojekt muss dann nur die Assembly Raven.Client.Lightweight.dll aus dem ClientVerzeichnis referenziert werden. Der Server befindet sich im Verzeichnis Server. Klingt logisch, oder? Dennoch sind solche klaren Strukturen nicht selbstverständlich. Oft befinden sich alle binären Artefakte eines Frameworks gemeinsam in einem bin-Verzeichnis, aus dem man sich selbst heraussuchen muss, was man benötigt. Da gefällt mir diese Aufteilung bei RavenDB doch sehr gut. Sie vereinfacht die ersten Schritte. Doch zurück zum Server. Der kann innerhalb eines IIS gehostet werden oder auch als Windows-Dienst laufen. Ich habe nur den Windows-Dienst ausprobiert. Dazu muss man zwei Befehle ausführen : ❚ RavenDb.exe /install ❚ RavenDb.exe /start Der erste Befehl installiert RavenDB als Windows-Dienst, der zweite startet den Dienst. Weil das Installieren und Starten von Diensten unterWindows nicht jedem Nutzer erlaubt sind, kümmert sich RavenDB bei Bedarf um die Elevation, also das Beschaffen der nötigen Rechte. Das ist vorbildlich! Statt eine kryptische Fehlermeldung auszugeben, eventuell mit dem Hinweis, man möge das Programm als Administrator starten, wird’s mir hier sehr einfach gemacht. CRUDe Methoden Nun möchte ich als Erstes ein Objekt in der RavenDB-Datenbank abspeichern. Dazu habe ich die Klasse Produkt angelegt. Die Klasse berücksichtigt keine Infrastruktur, Instanzen sind sogenannte POCOs: Plain Old CLR Objects. Damit wird im Allgemeinen die sogenannte Infrastrukturignoranz bezeichnet. RavenDB stellt (fast) keine Anforderungen an eine zu persistierende Klasse. Es muss nicht von einer Basisklasse abgeleitet werden, es muss kein spezielles Interface implementiert werden, und es sind keine Attribute erforderlich. Nur eine www.dotnetpro.de dotnetpro.dojos.2011 Konvention ist einzuhalten: RavenDB benötigt eine Eigenschaft namens Id vom Typ string. In dieser Eigenschaft wird der eindeutige Schlüssel des Objekts von RavenDB erwartet. Natürlich kann diese Konvention geändert werden, wenn sie nicht passt. public class Produkt { public string Id { get; set; } public string Name { get; set; } public string Kategorie { get; set; } Um eine Instanz der Klasse mit RavenDB zu persistieren, benötigt man einen sogenannten DocumentStore. Dieser sollte pro Anwendung nur einmal erzeugt werden. Mithilfe des DocumentStore wird eine Do- cumentSession erzeugt. Innerhalb einer Session werden Änderungen vorgenommen und am Ende persistiert. Listing 1 zeigt das Speichern eines Objektes. Der DocumentStore enthält keinen Zustand, dafür ist die DocumentSession zuständig. Während der Lebenszeit der Session sorgt diese für die Objektidentität: Wird ein und dasselbe Dokument mehrfach aus der Datenbank geladen, liefert die Session jeweils ein und dasselbe Objekt. So ist sichergestellt, dass innerhalb einer Session nur genau eine Instanz eines Dokumentes der Datenbank existiert. Ohne diese Objektidentität wäre die Gefahr sehr groß, dass an unterschiedlichen Objekten Änderungen vorgenommen werden, die Listing 2 RavenDB sichert die Objektidentität. [Test] public void Session_stellt_Objektidentität_sicher() { var store = new DocumentStore {Url = "http://localhost:8080"}; store.Initialize(); var produkt = new Produkt(); using (var session = store.OpenSession()) { session.Store(produkt); session.SaveChanges(); var produkt2 = session.Load<Produkt>(produkt.Id); Assert.That(produkt2, Is.SameAs(produkt)); var produkt3 = session.Load<Produkt>(produkt.Id); Assert.That(produkt3, Is.SameAs(produkt)); } using (var session = store.OpenSession()) { var produkt2 = session.Load<Produkt>(produkt.Id); Assert.That(produkt2, Is.Not.SameAs(produkt)); } } Listing 3 Jedes Objekt erhält eine eigene Id. [Test] public void Id_wird_durch_Save_erzeugt_aber_Entity_noch_nicht_gespeichert() { var store = new DocumentStore {Url = "http://localhost:8080"}; store.Initialize(); var produkt = new Produkt(); Assert.That(produkt.Id, Is.Null); using (var session = store.OpenSession()) { session.Store(produkt); } Assert.That(produkt.Id, Is.Not.Null); Produkt result; using (var session = store.OpenSession()) { result = session.Load<Produkt>(produkt.Id); } Assert.That(result, Is.Null); } 33 LÖSUNG sich tatsächlich aber auf dasselbe Dokument beziehen. Um zu dokumentieren, wie sich RavenDB in diesem Punkt verhält, dient der in Listing 2 gezeigte Test. Der Test zeigt, was passiert, wenn man innerhalb einer Session ein Dokument mehrfach liest. Die Session liefert jeweils identische Objekte. Im zweiten Teil des Tests ist zu sehen, dass dies nur innerhalb einer Session gilt. Objekte, die in der zweiten Session geladen werden, beziehen sich zwar auf dasselbe Dokument, es wird jedoch nicht dasselbe Objekt geliefert. Dieser Test wirft gleich eine weitere Frage auf: Offensichtlich sorgt RavenDB dafür, dass das Dokument eine Id erhält, über die es später wieder aus der Datenbank geholt werden kann. Doch zu welchem Zeitpunkt passiert das? Wird die Id bereits bei session.Store gebildet oder erst bei session.SaveChanges? Die Frage ist insofern wichtig, als davon abhängt, wie viele Zugriffe auf den Server erforderlich sind. Ferner hängt davon ab, zu welchem Zeitpunkt man die Id verwenden kann, um Referenzen zwischen Dokumenten herzustellen (auch wenn man dies vermeiden sollte, es ist schließlich keine relationale Datenbank). Der in Listing 3 gezeigte Test dokumentiert das Verhalten von RavenDB: Die Id wird bereits bei session.Store gebildet. Erst mit session.SaveChanges werden die Änderungen zur Datenbank übertragen. Die Id wird bei session.Store lokal gebildet, dazu ist keine Kommunikation mit dem Server erforderlich. Natürlich kann die Id auch vorgegeben werden. Ist dies der Fall, erzeugt RavenDB keine Id, sondern übernimmt die vorhandene. Natürlich muss diese eindeutig sein, andernfalls wird das bereits vorhandene Dokument von RavenDB einfach überschrieben. Das Laden eines Dokumentes ist in den obigen Tests bereits zu sehen: Mittels session.Load laden Sie ein Dokument, dessen Id bekannt ist. Dabei ist der Typ des Dokuments in Form eines generischen Methodenparameters anzugeben, damit RavenDB weiß, welcher Typ instanziert werden soll. Damit hätten wir Create und Retrieve aus CRUD gelöst. Wie sieht es mit Update aus? Ganz einfach: Wenn Sie session.Store mit einem Objekt aufrufen, das bereits eine Id hat, dann sorgt Ra- venDB dafür, dass das Dokument entweder neu angelegt oder aktualisiert wird. Dies soll in einem größeren Kontext gezeigt werden. In einer Anwendung wird man RavenDB nicht unmittelbar verwenden, da es sich bei der Datenbank um eine externe Ressource handelt. Der Kern der Anwendung sollte generell über einen Adapter von Ressourcenzugriffen isoliert werden. Dies wurde in zurückliegenden Artikeln der Dojo-Serie bereits thematisiert. Für Datenbanken wird hier der Begriff „Repository“ verwendet. Um zu sehen, wie so ein Repository, realisiert mit RavenDB, aussehen kann, habe ich ein solches bei meinen weiteren Spike-Schritten implementiert. Der Vorteil: Jetzt ist das Repository die Stelle, an der die Konfiguration des DocumentStore erfolgt. Innerhalb eines Repositorys kann über den darin vorhandenen DocumentStore jeweils bei Bedarf eine DocumentSession eröffnet werden. Listing 4 zeigt den Test für Updates, und Listing 5 zeigt die Implementation des Repositorys. Als letzte CRUD-Operation steht das Löschen an. Dazu verfügt DocumentSession über die Methode Delete, der das zu lö- LÖSUNG Listing 4 Ein Update überprüfen. [TestFixture] public class ProductRepositoryTests { private ProduktRepository sut; [SetUp] public void Setup() { sut = new ProduktRepository(); } [Test] public void Ein_Produkt_ändern() { var produkt = new Produkt { Id = "#1", Name = "iPad", Kategorie = "Zeugs" }; sut.Save(produkt); produkt.Kategorie = "Gadget"; sut.Save(produkt); var result = sut.Load("#1"); Assert.That(result.Kategorie, Is.EqualTo("Gadget")); } } schende Objekt übergeben wird. Dabei stellte sich mir die Frage, wie man ein Dokument aus der Datenbank löscht, dessen Id man kennt, das aber nicht als Objekt geladen wurde. Natürlich kann man das Objekt zuerst über seine Id laden, um es dann an Delete zu übergeben. Dabei fallen aber zwei Zugriffe auf den Server an, das sollte doch auch mit einem Zugriff zu machen sein. Das Löschen über die Id ist im ClientAPI nicht vorgesehen. Das bedeutet aber nicht, dass es unmöglich ist. Das Client-API ist nur ein Wrapper, der über das HTTPProtokoll gelegt ist, und in diesem API ist Löschen per Id nicht vorgesehen. Wie man das API erweitert, habe ich mir allerdings in diesem Spike nicht weiter angesehen. Konkurrierende Zugriffe Beim Erforschen des APIs ist mir an dieser Stelle die Frage gekommen, wie RavenDB sich bei konkurrierenden Zugriffen verhält. Damit meine ich Zugriffe, die in zwei unterschiedlichen Sessions stattfinden. Stellen Sie sich dazu eine Anwendung vor, von der mehrere Instanzen laufen. Was passiert, wenn ein Dokument aus der Datenbank von beiden Anwendern geladen und verändert wird. Merkt RavenDB das beim Update? Um die Frage zu klären, habe ich einen Test geschrieben. Um dabei mit zwei Sessions ar- www.dotnetpro.de dotnetpro.dojos.2011 beiten zu können, habe ich die Sessions ineinandergeschachtelt. Das heißt, während die erste Session noch aktiv ist, wird innerhalb des using-Blocks eine zweite Session geöffnet, um dort ein Update vorzunehmen. Dann wird aus der ersten Session ebenfalls ein Update abgesetzt. Das Verhalten von RavenDB hängt an dieser Stelle davon ab, ob die Session Optimistic Concurrency unterstützen soll. Dies kann man per Session einstellen, standardmäßig ist es abgestellt. Das bedeutet, wenn man keine weitere Vorkehrung trifft, werden Konflikte bei konkurrierenden Zugrif- Listing 5 Das Repository implementieren. public class ProduktRepository { private readonly DocumentStore store; public ProduktRepository() { store = new DocumentStore {Url = "http://localhost:8080"}; store.Initialize(); } public void Save(Produkt produkt) { using (var session = store.OpenSession()) { session.Store(produkt); session.SaveChanges(); } } public Produkt Load(string id) { using (var session = store.OpenSession()) { var result = session.Load<Produkt>(id); return result; } } } Listing 6 Konkurrierende Zugriffe erkennen. [Test] public void Optimistic_Concurrency_bei_Updates_in_mehreren_Sessions() { var store = new DocumentStore {Url = "http://localhost:8080"}; store.Initialize(); // Initialzustand der Datenbank herstellen var produkt = new Produkt {Name = "a"}; using (var session = store.OpenSession()) { session.Store(produkt); session.SaveChanges(); } // Erste Session lädt das Dokument als 'referenz1' using (var session1 = store.OpenSession()) { session1.UseOptimisticConcurrency = true; var referenz1 = session1.Load<Produkt>(produkt.Id); // Zweite Session lädt das Dokument als 'referenz2' // und modifiziert es using (var session2 = store.OpenSession()) { var referenz2 = session2.Load<Produkt>(produkt.Id); Assert.That(referenz2, Is.Not.SameAs(referenz1)); referenz2.Name = "b"; session2.SaveChanges(); } // 'referenz1' hat die Änderungen aus der zweiten // Session noch nicht gesehen, daher Bumm! referenz1.Name = "c"; Assert.Throws<ConcurrencyException>(session1.SaveChanges); } } 35 LÖSUNG fen nicht erkannt und Updates einfach der Reihe nach ausgeführt. Um diese Erkennung zu aktivieren, müssen Sie in der Session die Option UseOptimisticConcurrency auf true setzen. Listing 6 zeigt den Test für die Erkennung konkurrierender Zugriffe. Um die ConcurrencyException zu vermeiden, können Sie das Objekt vor der Änderung mit session.Refresh(referenz1) auf den aktuellen Stand bringen. Allerdings gehen damit natürlich alle Änderungen verloren, die am Objekt zuvor bereits vorgenommen wurden. Eine Strategie könnte dann sein, das Refresh nur dann auszuführen, wenn die ConcurrencyException tatsächlich aufgetreten ist. Dann könnten die Änderungen, die in den beiden konkurrierenden Sessions vorgenommen wurden, zusammengeführt werden. Wie das im Einzelnen geschieht, hängt von der Business Domain ab. Query Der nächste Schritt in meinem Spike sollte die Frage klären, wie man Dokumente aus der Datenbank lesen kann, die bestimmte Anforderungen erfüllen. Ich wollte zum Beispiel alle Produkte einer Kategorie ermitteln. In einer relationalen Datenbank muss man dazu lediglich das passende SELECTKommando absetzen. Bei RavenDB ist es zunächst erforderlich, einen Index anzulegen. Das liegt daran, dass die RavenDB-Datenbank Dokumente als JSON-Strings [4] speichert. Damit entziehen sie sich einem effizienten suchenden Zugriff, denn das würde bedeuten, dass bei jeder Suche die JSON-Strings aller Dokumente interpretiert werden müssten. Bei einer relationalen Datenbank ist die Suche nur möglich, weil es dort von vornherein ein Schema gibt, welches dafür sorgt, dass die Daten in Spalten abgelegt werden. Dieses Schema fehlt in RavenDB ganz bewusst. Um einen Index anzulegen, muss man eine LINQ-Query definieren, die angibt, welche Eigenschaften des Objekts in den Index aufgenom- Listing 7 Einen Index erstellen. store.DatabaseCommands.PutIndex( "ProdukteNachKategorie", new IndexDefinition<Produkt> { Map = produkte => from produkt in produkte select new {produkt.Kategorie} }); 36 Listing 8 Alle Produkte einer Kategorie suchen. using (var session = store.OpenSession()) { var result = session.LuceneQuery<Produkt>("ProdukteNachKategorie") .Where(string.Format("Kategorie:{0}", kategorie)) .ToArray(); return result; } Listing 9 Die Produktsuche testen. [Test] public void Alle_Produkte_einer_Kategorie_ermitteln() { sut.Save(new Produkt {Id = "#1", Name = "iPad", Kategorie = "Elektronik"}); sut.Save(new Produkt {Id = "#2", Name = "iPod", Kategorie = "Elektronik"}); sut.Save(new Produkt {Id = "#3", Name = "Apfel", Kategorie = "Obst"}); sut.Save(new Produkt {Id = "#4", Name = "Kartoffel", Kategorie = "Gemüse"}); sut.Save(new Produkt {Id = "#5", Name = "Banane", Kategorie = "Obst"}); var result = sut.ProdukteDerKategorie("Elektronik"); Assert.That(result.Select(x => x.Id).ToArray(), Is.EquivalentTo(new[] {"#1", "#2"})); Assert.That(result.Select(x => x.Name).ToArray(), Is.EquivalentTo(new[] {"iPad", "iPod"})); Assert.That(result.Select(x => x.Kategorie).ToArray(), Is.EquivalentTo(new[] {"Elektronik", "Elektronik"})); } men werden sollen. Ferner muss der Index einen Namen erhalten, damit man ihn bei der Suche benennen kann. Listing 7 zeigt, wie Sie einen Index für den Zugriff auf alle Produkte einer Kategorie erstellen. Das Erstellen dieses Index muss einmalig erfolgen. RavenDB sorgt dafür, dass der Index jeweils aktualisiert wird, wenn sich die zugehörigen Daten ändern. Soll die Definition des Index geändert werden, muss man ihn zunächst löschen. Das geht ganz einfach: store.DatabaseCommands.DeleteIndex( "ProdukteNachKategorie"); Wenn man, wie in den bisherigen Beispielen gezeigt, anonym auf den RavenDBServer zugreift, trifft man an dieser Stelle auf ein Problem: Der Server verweigert das Anlegen des Index. Da das Erstellen von Dokumenten gestattet ist, habe ich mich zunächst gewundert. Die Lösung ist auf zwei Wegen möglich: Entweder man übergibt beim Öffnen einer Session Anmeldedaten, sogenannte Credentials, oder man erlaubt auch anonymen Nutzern den vollständigen Zugriff. In einer lokalen Testumgebung ist es einfacher, den Server zu öffnen. Im Produktivbetrieb sollte man das natürlich keinesfalls tun. Um anonyme Zugriffe für sämtliche Operationen zu berechtigen, muss man im Server-Verzeichnis die Datei RavenDb.exe.config bearbeiten. Darin muss man in der folgenden Zeile das Get durch ein All ersetzen : <add key="Raven/AnonymousAccess" value="Get"/> Anschließend müssen Sie den RavenDBServer-Dienst neu starten. Doch zurück zum Index. Der wichtigste Teil beim Erstellen des Index ist die LINQQuery, die für den Map-Vorgang zuständig ist. Diese Query wird von RavenDB für jedes Dokument ausgeführt. Das Ergebnis der Query, in diesem Fall ein Objekt mit der Eigenschaft Kategorie, wird in den Index übernommen. Der durch die Map-Funktion ermittelte Wert dient im Index als Schlüssel. Im Ergebnis sind zu einer gegebenen Kategorie im Index Referenzen auf die betreffenden Produkte abgelegt. Damit wird eine Suche nach sämtlichen Produkten einer Kategorie möglich, siehe dazu Listing 8. Wichtig ist hier der Aufruf von ToArray(). Da die Query innerhalb einer Session aufgerufen wird, die mit Verlassen des using- dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Blocks geschlossen wird, muss das tatsächliche Lesen der Daten innerhalb der Session passieren. Lässt man das ToArray() weg, findet das Lesen erst beim Iterieren durch das Ergebnis statt, dann allerdings zu einem Zeitpunkt, da die Session bereits geschlossen ist. RavenDB verwendet für die Indizierung übrigens Lucene, was ebenfalls ein Open-Source-Projekt ist [5]. Listing 9 zeigt, wie Sie die so erstellte Methode zum Ermitteln aller Produkte einer Kategorie gegen eine Datenbank testen können. Auch hier gilt es, noch einen weiteren Stolperstein zu beachten. RavenDB führt das Aktualisieren der Indizes im Hintergrund aus. Es kann daher sein, dass der Index zum Zeitpunkt des Lesevorgangs noch nicht aktualisiert ist. Zu Testzwecken kann man es bei Anwendung der Query mit der Methode WaitForNonStaleResults() erzwingen, dass auf die Aktualisierung des Index gewartet wird, bevor Ergebnisse geliefert werden. In einer Produktivumgebung sollten Sie diese Option allerdings nicht verwenden, da sie mit Performance- einbußen verbunden ist. An dieser Stelle zeigt es sich, dass Dokumentdatenbanken einen anderen Schwerpunkt setzen als relationale Datenbanken. Bei einer relationalen Datenbank geht die Konsistenz der Daten immer vor. Bei Dokumentdatenbanken wie RavenDB dagegen steht die Konsistenz hinter Skalierbarkeit und Ausfallsicherheit zurück. Map/Reduce Als Nächstes habe ich mir ein Feature angeschaut, das bei Dokumentdatenbanken sehr häufig zum Einsatz kommt und großen Einfluss auf die Skalierbarkeit hat: Map/Reduce. Eine Map-Funktion wurde bereits für den Index benötigt, mit dem alle Produkte einer Kategorie ermittelt werden können. Fügt man einem solchen Index noch eine Reduce-Methode hinzu, können Daten aus den Dokumenten aggregiert werden. Damit ist es möglich, beispielsweise einen Index zu erstellen, der aus den Produkten alle Kategorien ermittelt. Zusätzlich ist es durch die Aggregation möglich zu zählen, wie viele Produkte in Listing 10 Map und Reduce verwenden. store.DatabaseCommands.PutIndex( "ProduktKategorien", new IndexDefinition<Produkt, KategorieMitAnzahl> { Map = produkte => from produkt in produkte select new { produkt.Kategorie, Anzahl = 1 }, Reduce = results => from result in results group result by result.Kategorie into g select new { Kategorie = g.Key, Anzahl = g.Sum(x => x.Anzahl) } }); Listing 11 Den Index testen. [Test] public void Alle_Kategorien_ermitteln() { sut.Save(new Produkt {Id = "#1", Name = "iPad", Kategorie = "Elektronik"}); sut.Save(new Produkt {Id = "#2", Name = "iPod", Kategorie = "Elektronik"}); sut.Save(new Produkt {Id = "#3", Name = "Apfel", Kategorie = "Obst"}); sut.Save(new Produkt {Id = "#4", Name = "Kartoffel", Kategorie = "Gemüse"}); sut.Save(new Produkt {Id = "#5", Name = "Banane", Kategorie = "Obst"}); var result = sut.Kategorien(); Assert.That(result.ToArray(), Is.EquivalentTo(new[] { new KategorieMitAnzahl { Kategorie = "Elektronik", Anzahl = 2}, new KategorieMitAnzahl { Kategorie = "Obst", Anzahl = 2}, new KategorieMitAnzahl { Kategorie = "Gemüse", Anzahl = 1}, })); } www.dotnetpro.de dotnetpro.dojos.2011 der jeweiligen Kategorie enthalten sind. Das Erstellen dieses Index gleicht dem vorhergehenden Beispiel. Der wesentliche Unterschied besteht in der zusätzlichen Reduce-LINQ-Query. Dabei liegt das Hauptproblem darin, dass man darauf achten muss, dass Map- und Reduce-Query auf gleich aufgebauten Objekten arbeiten. Da hier anonyme Typen verwendet werden, ist das Unterfangen fehleranfällig, siehe Listing 10. Die Map-Query liefert für jedes Produkt ein Objekt zurück. Dieses Objekt hat zwei Eigenschaften: Kategorie und Anzahl. Die Anzahl ist immer 1, dies ist der Startwert für die spätere Aufsummierung. In der Reduce-Query wird nun auf Objekten vom Typ KategorieMitAnzahl gearbeitet. Der Typ muss als zweiter generischer Typparameter im Konstruktor von IndexDefinition angegeben werden. Die Herausforderung liegt darin, dass man in den beiden LINQ-Queries anonyme Typen verwenden muss. Wenn diese Queries Ergebnisse vom Typ KategorieMitAnzahl liefern, erhält man eine Fehlermeldung vom RavenDB-Server, die besagt, man müsse anonyme Typen verwenden. Dennoch müssen diese anonymen Typen den gleichen Aufbau haben wie der definierte Typ. Wenn man diese Hürde genommen hat, steht einem Test des Index nichts mehr im Weg, wie Listing 11 zeigt. Das Schöne an diesem Index ist, dass RavenDB ihn jeweils auf dem aktuellen Stand hält. Jede Änderung an Dokumenten, die im Index verwendet werden, führt zu einem entsprechenden Update des Index. Fazit Ich habe mir in diesem Spike die Funktionalität von RavenDB nur ausschnittweise angesehen. RavenDB hat darüber hinaus noch mehr zu bieten wie etwa die Verteilung einer Datenbank auf mehrere Server. Die ersten Schritte gingen zügig voran. Aber beim Map/Reduce hat es doch etwas länger gedauert, bis ich die zu berücksichtigenden Konventionen alle zusammenhatte. Hier wäre ein Beispiel in der Dokumentation sicherlich hilfreich. Gelernt habe ich wieder einiges, und das war schließ[ml] lich Zweck der Übung. [1] http://clean-code-developer.de [2] http://ravendb.net/ [3] http://builds.hibernatingrhinos.com/builds/ ravendb [4] http://de.wikipedia.org/wiki/JSON [5] http://lucene.apache.org/lucene.net/ 37 AUFGABE Algorithmen und Datenstrukturen Was ist im Stapel? In den Zeiten der großen Programmier-Frameworks geht leicht das Wissen um die grundlegenden Algorithmen und Datenstrukturen verloren. Stefan, kannst du mal eine Aufgabe stellen, die zu den Wurzeln der Programmierung zurückführt? Wer übt, gewinnt dnpCode: A1009DojoAufgabe 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 dotnetpro finden. Übung macht den Meister. Also − los geht’s. Aber Sie wollten doch nicht etwa sofort Visual Studio starten… atürlich ist mir bekannt, dass es im .NET Framework eine Klasse Stack<T> gibt. Aus diesem Grund muss man eine solche elementare Datenstruktur nicht mehr selbst implementieren. Aber gerade weil die Funktionalität so gut bekannt ist, bietet sich ein Stack als Übung an. Hier können Sie sich voll auf den Entwurf einer Lösung konzentrieren und anschließend testgetrieben implementieren. Die Aufgabe soll gelöst werden, ohne dass vorhandene Datenstrukturen aus dem .NET Framework verwendet werden. Aus einer Liste einen Stack zu machen scheidet also aus. Und natürlich soll der Stack generisch sein. Das bedeutet, dass man den Typ der Elemente als generischen Typparameter angeben kann. Ein Stack für IntegerElemente wird also folgendermaßen instanziert: N Stack zur Laufzeit abzubilden. Die Verwendung von Collections aus dem .NET Framework scheidet aus. Auch ein Array scheidet aus, da der Stack keine Größenbeschränkung haben soll. Die in einem Stack angewandte Strategie beim Entnehmen eines Elementes lautet: Last In/First Out, abgekürzt LIFO. Das Element, welches als letztes in den Speicher gegeben wurde, wird als erstes entnommen. Eine andere Strategie ist die FIFO-Strategie: First In/First Out. Hier wird das Element, welches als erstes gespeichert wurde, auch wieder als erstes entnommen. Kommt Ihnen bekannt vor? Ja, so funktioniert die Schlange an der Kasse im Supermarkt. Und damit sind wir beim zweiten Teil der Übung: Implementieren Sie eine Warteschlange, engl. Queue. Das Interface der zu implementierenden Methoden sieht folgendermaßen aus: var stack = new Stack<int>(); public interface IQueue<TELement> { void Enqueue(TELement element); TELement Dequeue(); } Dabei stellt der Typ int in spitzen Klammern den generischen Typparameter dar. Alle Elemente des Stacks sind somit vom Typ int. Die zwei Operationen auf dem Stack sind schnell erklärt: ❚ Mit Push(element) kann ein Element oben auf den Stack gelegt werden. Jede weitere PushOperation legt ein weiteres Element obendrauf. ❚ Das oberste Element des Stacks kann mit der Pop()-Operation wieder vom Stack entfernt werden. Die Pop()-Operation macht also genau genommen zwei Dinge: Sie liefert das oberste Element an den Aufrufer und entfernt es vom Stack. Hier die Signaturen der beiden Methoden in Form eines Interfaces: public interface IStack<TElement> { void Push(TElement element); TElement Pop(); } In diesem Interface ist TElement der generische Typ. Er wird jeweils durch den konkreten Typ ersetzt. Im obigen Beispiel ist TElement mit dem Typ int belegt. Ein Tipp zur Implementierung : Überlegen Sie sich, welche Datenstruktur geeignet ist, einen 38 Auch hier gilt: Überlegen Sie sich eine Datenstruktur, mit welcher die Aufgabenstellung gelöst werden kann. Wie immer hilft es, sich dazu ein Blatt Papier zu nehmen. Oder lösen Sie die Übung mit Kollegen gemeinsam im Team, und planen Sie am Whiteboard. Die testgetriebene Entwicklung wird häufig so verstanden, dass man einfach mit einem ersten Test loslegt und sich von da an alles schon irgendwie ergeben wird. Ich halte das für falsch. Ein bisschen Planung vor dem Codieren schadet nicht, ganz im Gegenteil. Wer sich mit der Übung unterfordert fühlt, kann übrigens noch eine weitere Methode auf Queue<T> ergänzen: void Reverse(); Diese Methode soll die Reihenfolge der Elemente in der Warteschlange umkehren. Dazu sollen die Verweise zwischen den Elementen so verändert werden, dass die Queue „in place“ verändert wird. Wie immer gilt: test first! Viel Spaß. [ml] dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Stack und Queue implementieren Der Nächste bitte! Immer hübsch der Reihe nach: Das gilt nicht nur im Wartezimmer, sondern auch im Stack und in der Queue der Informatiker. Und wer sich das Entwicklerleben vereinfachen will, sollte auch bei ihrer Implementierung die richtige Reihenfolge einhalten: Erst planen, dann Tests entwickeln, dann implementieren. A ls Entwickler nehmen wir Stack und Queue, wie viele andere Datenstrukturen, als selbstverständlich hin, sind sie doch im .NET Framework enthalten. Weil ihre Funktionsweise so einfach ist, besteht sicherlich die Versuchung, direkt mit der Implementierung zu beginnen. Doch schon kommen die ersten Fragen um die Ecke: Wie soll der erste Test aussehen? Wie testet man einen Stack überhaupt, das heißt, kann man Push isoliert testen? Oder kann man Push nur testen, indem man Pop ebenfalls testet? Bevor Sie versuchen, diese Fragen zu beantworten, sollten Sie den Konsolendeckel besser erst mal schließen und zu einem Stück Papier greifen. Denn die zentrale Frage vor dem ersten Test lautet, wie denn die interne Repräsentation des Stacks überhaupt aussieht. Dies mit Papier und Bleistift zu planen ist einfacher, als es einfach so im Kopf zu tun. Dabei übersieht man schnell mal ein Detail. Ein Stack muss in der Lage sein, jeweils das oberste Element zu liefern. Das ist die Aufgabe der Pop-Methode. Nachdem das oberste Element geliefert wurde, muss der Stack beim nächsten Mal das nächste Element liefern, das also unmittelbar auf das oberste folgt. Daraus ergibt sich die Notwendigkeit, innerhalb des Stacks jeweils zu wissen, welches das oberste Element ist. Ferner muss zu jedem Element bekannt sein, welches das nächste Element ist. So ergibt sich eine ganz einfache interne Datenstruktur für den Stack, siehe Abbildung 1. Hat man diese Datenstruktur erst einmal aufgemalt, ist es ein Leichtes, sie in Code zu übersetzen. Aber Achtung, den Test nicht vergessen! Bei einem Stack bietet es sich an, zwei unterschiedliche Zustände zu betrachten: ❚ einen leeren Stack, ❚ einen nicht leeren Stack. In solchen Fällen erstelle ich für die unterschiedlichen Szenarien gerne getrennte www.dotnetpro.de dotnetpro.dojos.2011 [Abb. 1] top- und Listing 1 next-Zeiger im Stack. Testklassen. Dann kann nämlich das Setup des Tests dafür sorgen, dass das Szenario bereitgestellt wird. Die Testklasse, welche sich mit einem leeren Stack befasst, instanziert einfach einen neuen Stack. In der Testklasse zum Szenario „nicht leerer Stack“ wird der Stack im Setup gleich mit ein paar Werten befüllt. So können sich die Tests jeweils auf das konkrete Szenario beziehen. Doch wie sieht nun der erste Test aus? Ich habe mich entschieden, den ersten Test zu einem leeren Stack zu erstellen. Es erscheint mir nicht sinnvoll, bei dem Szenario eines nicht leeren Stacks zu beginnen, weil dann vermutlich für den ersten Test bereits sehr viel Funktionalität implementiert werden muss. Ich möchte lieber in kleinen Schritten vorgehen, um sicher zu sein, dass ich wirklich nur gerade so viel Code schreibe, dass ein weiterer Test erfolgreich verläuft. Für den ersten Test zu einem leeren Stack überlege ich mir, wie die interne Repräsentation eines leeren Stacks aussehen soll, und komme zu dem Schluss, dass der Zeiger, der auf das erste Element verweist, null sein soll. Listing 1 zeigt den ersten Test. In der SetupMethode der Testklasse wird ein leerer Stack für int-Elemente instanziert. Der erste Test prüft, ob dann der top-Zeiger gleich null ist. Nun wird vielleicht dem einen oder anderen der Einwand im Kopf herumkreisen, dass damit ja erstens die Sichtbarkeit der internen Repräsentation nach außen getragen wird und zweitens Interna getestet werden. Zur Sichtbarkeit sei gesagt, dass Einen leeren Stack testen. [TestFixture] public class Ein_leerer_Stack { private Stack<int> sut; [SetUp] public void Setup() { sut = new Stack<int>(); } [Test] public void Hat_kein_top_Element() { Assert.That(sut.top, Is.Null); } } ich das Feld top auf internal setze. Damit ist es zunächst nur innerhalb der Assembly sichtbar, in der die Stack-Implementierung liegt. Durch ein Attribut in der Datei AssemblyInfo.cs wird die Sichtbarkeit dann dosiert erweitert auf die Testassembly. Das Attribut sieht wie folgt aus : [assembly: InternalsVisibleTo("stack.tests")] Damit kann nun auch aus der Testassembly auf die Interna zugegriffen werden. Und schon sind wir beim zweiten Einwand, dass nun diese Interna im Test verwendet werden. Das halte ich für vernachlässigbar. Sicher ist es erstrebenswert, Tests so zu schreiben, dass sie möglichst wenige Abhängigkeiten zu den Interna der Klasse haben. Denn wenn nur über die öffentliche Schnittstelle getestet wird, sind die Tests Listing 2 Datenstruktur für die StackElemente. internal class Element<TData> { public TData Data { get; set; } public Element<TData> Next { get; set; } } 39 LÖSUNG Listing 3 Erste Stack-Implementierung. public class Stack<TElement> : IStack<TElement> { internal Element<TElement> top; public void Push(TElement element) { throw new NotImplementedException(); } public TElement Pop() { throw new NotImplementedException(); } } weniger zerbrechlich. Allerdings sind sie dann häufig weniger fokussiert. Im Fall eines Stacks stellt sich nämlich die Frage, wie man Push ausschließlich über die öffentliche Schnittstelle testen kann, ohne dabei andere Methoden des Stacks zu verwenden. Natürlich wird man auch einen Test schreiben, der Push und Pop in Beziehung setzt, und beide Methoden in einem Test verwenden. Aber gerade bei den ersten Schritten der Implementierung ist es vorteilhaft, wenn man eine einzelne Methode isoliert betrachten kann. Greift man dabei auf Interna zu, dann ist dies möglich. Um mit der Implementierung weiterzukommen, müssen Sie überlegen, von welchem Typ der top-Zeiger sein soll. Das ist dank der Skizze ganz einfach. Denn aus der Skizze ergibt sich, dass jedes Element im Stack neben den Daten einen Zeiger auf das nächste Element hat. Folglich ist der topZeiger einfach das erste Element im Stack. Listing 2 zeigt die Datenstruktur für die Elemente. Da diese Datenstruktur außerhalb des Stacks nicht in Erscheinung tritt, wird sie durch die Sichtbarkeit internal verborgen. Wie weiter oben erwähnt, kann dennoch in Tests darauf zugegriffen werden. Listing 3 zeigt die Implementierung des Stacks für den ersten Test. Die Methoden Push und Pop werden eigentlich noch nicht benötigt, sind aber syntaktisch erforderlich aufgrund des Interfaces IStack<T>. Nun stand ich vor der Wahl, ob ich als Nächstes mit Push oder Pop weitermachen wollte. Ich halte Push für naheliegender, denn bei einem leeren Stack wird Pop ohnehin nur zu einer Ausnahme führen. Gerade zu Beginn der Implementierung Listing 4 Erster Test für Push. [Test] public void Macht_das_mit_Push_übergebene_Element_zum_top_Element() { sut.Push(5); Assert.That(sut.top.Data, Is.EqualTo(5)); } Listing 5 Der Next-Zeiger ist null. [Test] public void Enthält_nach_einem_Push_nur_dieses_eine_Element() { sut.Push(5); Assert.That(sut.top.Next, Is.Null); } Listing 6 Push und Pop testen. [Test] public void Kann_ein_Element_aufnehmen_und_wieder_abliefern() { sut.Push(5); Assert.That(sut.Pop(), Is.EqualTo(5)); } 40 möchte ich weiterkommen und mich nicht mit Rand- und Fehlerfällen befassen. Aber das ist sicherlich Geschmackssache. Listing 4 zeigt den ersten Test für Push. Ferner kann man beim ersten Push feststellen, dass das übergebene Element das einzige auf dem Stack ist, der Next-Zeiger also null ist, siehe Listing 5. Ob man dies tatsächlich in einem eigenständigen Test überprüft oder ein zweites Assert im vorhergehenden Test zulässt, sei dahingestellt. Ich habe mich für zwei getrennte Tests entschieden, weil ich keine treffende Bezeichnung für einen Test finden konnte, der beides prüft. Die Regel, nur ein Assert pro Test zuzulassen, halte ich jedenfalls für dogmatisch. Ein Test sollte sich mit einer Sache befassen. Wenn diese eine Sache mit mehr als einem Assert überprüft werden muss, finde ich das völlig in Ordnung. An dieser Stelle kann man mit einem leeren Stack nicht viel mehr anstellen, außer nun doch Push und Pop in Beziehung zu setzen. Also sieht mein nächster Test so aus wie in Listing 6. Bei diesem Test liegt die Versuchung nahe, auch noch zu prüfen, ob der Stack nach dem Pop auch wieder leer ist. Doch diesen Test habe ich in den Szenarien angesiedelt, die sich mit einem nicht leeren Stack befassen. Bis hierher besteht die Implementierung nur darin, bei Push ein neues top-Element zu erzeugen und dieses bei Pop als Ergebnis zu liefern. Der wichtigste Teil der Implementierung folgt nun bei den Szenarien mit nicht leerem Stack. Das Szenario wird in der Setup-Methode dadurch hergestellt, dass der Stack direkt mit einem Element gefüllt wird. Somit kann Listing 7 Pop testen. [TestFixture] public class Ein_Stack_mit_einem_Element { private Stack<string> sut; [SetUp] public void Setup() { sut = new Stack<string>(); sut.Push("a"); } [Test] public void Kann_das_top_Element_liefern() { Assert.That(sut.Pop(), Is.EqualTo("a")); } } dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Listing 8 Listing 10 Auf einen leeren Stack testen. Die fertige Implementierung des Stacks. [Test] public void Enthaelt_nach_der_Entnahme_des_top_Elementes_keine_weiteren_Elemente() { sut.Pop(); Assert.That(sut.top, Is.Null); } Listing 9 Mehrere Push-Aufrufe. [Test] public void Macht_das_naechste_uebergebene_Element_zum_top_Element() { sut.Push("b"); Assert.That(sut.top.Data, Is.EqualTo("b")); } [Test] public void Legt_das_vorhandene_Element_bei_Uebergabe_eines_weiteren_unter_dieses() { sut.Push("b"); Assert.That(sut.top.Next.Data, Is.EqualTo("a")); } in einem ersten Test geprüft werden, ob dieses Element bei Aufruf der Pop-Methode zurückgegeben wird, siehe Listing 7. Nun kann überprüft werden, ob der Stack denn nach dem Pop auch wieder leer ist, siehe Listing 8. Und jetzt hilft alles nichts, wir müssen uns mit mehr als einem Push befassen. Dabei kommt es darauf an, das neue Element vor das bisherige topElement einzuordnen. Dazu muss der topZeiger geändert werden sowie der NextZeiger des top-Elements. Listing 9 zeigt die entsprechenden Tests. Daraus ergibt sich dann die fertige Implementierung des Stacks wie in Listing 10. Reflexion Die testgetriebene Vorgehensweise hat mir keine großen Probleme bereitet. Das lag zum Großteil daran, dass ich meine Skizze zur Hand hatte. So konnte ich bei Fragen sofort nachsehen, wie die top- und NextZeiger jeweils aussehen müssen. Und dadurch, dass die Tests auf die Interna zugreifen können, musste ich nicht schon für den ersten Test gleich zwei Methoden implementieren. Das ist ein großer Vorteil, der bei einem so kleinen Beispiel wie einem Stack möglicherweise nicht so deutlich wird. Ich habe diesen Effekt jedoch schon in einigen Fällen als vorteilhaft empfunden, bei denen es um den internen Zustand von Klassen ging. Immer da, wo www.dotnetpro.de dotnetpro.dojos.2011 mehrere Methoden auf einem internen Zustand arbeiten, kann es sich lohnen, den Zustand für die Tests sichtbar zu machen, um bei der Implementierung Methode für Methode vorgehen zu können. Queue<T> Bei der Warteschlange bin ich so verfahren wie schon beim Stack: Ich habe mir überlegt, wie man eine Warteschlange in einer Datenstruktur darstellen kann. Meine Überlegung hier: Bei einer Warteschlange sollte es offensichtlich zwei Zeiger geben, die jeweils auf ein Element verweisen. Zum einen auf das zuletzt eingefügte Element, um dort bei der Enqueue-Methode ein weiteres Element ergänzen zu können, sowie einen Zeiger auf das nächste zu entnehmende Element für die Dequeue-Methode. In meiner ersten Skizze malte ich also das erste und letzte Element einer Warteschlange und verwies darauf jeweils mit den Zeigern enqueue und dequeue, wie es Abbildung 2 zeigt. [Abb. 2] enqueueund dequeueZeiger. public class Stack<TElement> : IStack<TElement> { internal Element<TElement> top; public TElement Pop() { if (top == null) { throw new InvalidOperationException(); } var result = top.Data; top = top.Next; return result; } public void Push(TElement element) { var newTop = new Element<TElement> { Data = element, Next = top }; top = newTop; } } Im Anschluss habe ich überlegt, wie die Elemente untereinander sinnvoll zu verbinden sind. Dabei gibt es mehrere Möglichkeiten : ❚ Jedes Element zeigt mit Next auf das ihm folgende. ❚ Jedes Element zeigt mit Prev auf das hinter ihm liegende. ❚ Jedes Element enthält sowohl Next- als auch Prev-Zeiger. Die doppelte Verkettung habe ich nicht weiter berücksichtigt, da ich vermutete, dass es auch ohne gehen muss. Dabei stand nicht der Reflex im Vordergrund, dass eine doppelte Verkettung mehr Speicher braucht als eine einfache. Ich dachte eher daran, dass ich bei doppelter Verkettung beim Einfügen und Entfernen von Elementen zwei Zeiger korrigieren muss. Das erschien mir in jedem Fall mühsamer, als nur einen Zeiger korrigieren zu müssen. Es siegte sozusagen die pure Faulheit. Es blieb noch die Frage zu klären, ob Next- oder Prev-Zeiger sinnvoller sind. Das habe ich mir wieder anhand meiner Skizze überlegt. Wenn ein neues Element in die Warteschlange eingefügt wird, muss enqueue anschließend auf das neue Element zeigen. Bei Verwendung von Next-Zeigern muss dann nichts korrigiert werden, bei Prev-Zeigern muss das bisher erste Element auf das neue erste zurückverweisen. Beides ist kein Problem, es ergibt sich also hier noch keine Präferenz für eines der bei- 41 LÖSUNG den Verfahren. Beim Entfernen eines Elements aus der Warteschlange ist ebenfalls klar, welches Element geliefert werden muss, denn darauf verweist ja der dequeueZeiger. Dieser muss anschließend auf das vorhergehende Element verändert werden. Wenn die Elemente mit Next jeweils auf das nächste verweisen, wäre diese Korrektur nur möglich, indem die gesamte Warteschlange durchlaufen wird, bis das vorletzte Element erreicht ist. Bei Verwendung von Prev-Zeigern enthält das letzte Element den benötigten Verweis auf seinen Vorgänger. Damit war klar: Prev-Zeiger sind hier eindeutig einfacher, siehe Abbildung 3. Das bedeutete aber auch, dass es für die Queue<T> eine eigene Klasse Element<T> geben muss, da beim Stack auf Next gezeigt wird. Hier bemerkte ich einen weiteren Reflex, nämlich den Versuch, die Klasse Element<T> wiederzuverwenden. Das lag irgendwie nahe, hätte jedoch dazu geführt, dass Stack<T> und Queue<T> durch eine gemeinsam verwendete Klasse nicht völlig entkoppelt wären. „Glücklicherweise“ kam es aber durch die unterschiedlichen Anforderungen erst gar nicht zurWiederverwendung. Doch nun zum ersten Test. Auch bei der Warteschlange ging es mit einer leeren Queue los. Diese zeichnet sich dadurch aus, dass enqueue- und dequeue-Zeiger beide null sind, siehe Listing 11. Der nächste Test sollte ausdrücken, was beim Hinzufügen des ersten Elements in die Warteschlange passiert. Beide Zeiger verweisen dann nämlich auf das neue Element, siehe Listing 12. Als Nächstes kam wieder ein Test der öffentlichen Schnittstelle, der prüft, was bei der Entnahme eines Elements aus der Warteschlange passiert. Zum einen wird das einzige Element der Warteschlange zurückgegeben, zum anderen ist die Warte- Listing 11 Listing 12 Ein erstes Element hinzufügen. [Test] public void Setzt_bei_Enqueue_enqueue_und_dequeue_auf_das_neue_Element() { sut.Enqueue(42); Assert.That(sut.enqueue.Data, Is.EqualTo(42)); Assert.That(sut.dequeue.Data, Is.EqualTo(42)); } Listing 13 Ein Element entnehmen. [Test] public void Liefert_bei_Dequeue_das_zuvor_mit_Enqueue_übergebene_Element() { sut.Enqueue(42); Assert.That(sut.Dequeue(), Is.EqualTo(42)); } [Test] public void Ist_nach_Entnahme_eines_zuvor_übergebenen_Elements_wieder_leer() { sut.Enqueue(42); sut.Dequeue(); Assert.That(sut.enqueue, Is.Null); Assert.That(sut.dequeue, Is.Null); } Listing 14 Ein weiteres Element hinzufügen. [Test] public void Nach_Aufnahme_eines_weiteren_Elements_zeigt_dequeue_immer_noch_auf_das_erste_Element() { sut.Enqueue("b"); Assert.That(sut.dequeue.Data, Is.EqualTo("a")); } [Test] public void Nach_Aufnahme_eines_weiteren_Elements_zeigt_enqueue_auf_das_neue_Element() { sut.Enqueue("b"); Assert.That(sut.enqueue.Data, Is.EqualTo("b")); } [Test] public void Nach_Aufnahme_eines_weiteren_Elements_zeigt_dequeue_prev_auf_das_neue_Element() { sut.Enqueue("b"); Assert.That(sut.dequeue.Prev.Data, Is.EqualTo("b")); } Eine leere Queue testen. [TestFixture] public class Eine_leere_Queue { private Queue<int> sut; [SetUp] public void Setup() { sut = new Queue<int>(); } [Test] public void Ist_leer() { Assert.That(sut.enqueue, Is.Null); Assert.That(sut.dequeue, Is.Null); } } 42 schlange dann wieder leer, siehe Listing 13. Die Implementierung war bis hierher trivial. Als Nächstes ging es wieder um eine Warteschlange, die bereits ein Element enthält. Wenn nämlich ein weiteres Element in die Warteschlange gegeben wird, unterscheiden sich enqueue- und dequeue-Zeiger. Ferner muss der Prev-Zeiger des schon enthaltenen Elements gesetzt werden, siehe Listing 14. Im Anschluss habe ich Tests ergänzt, welche nur die öffentliche Schnittstelle verwen- den und demonstrieren, wie sich eineWarteschlange verhält, siehe Listing 15. Listing 16 zeigt zu guter Letzt die Implementierung. Auch hier war, ähnlich wie beim Stack, die Implementierung keine große Sache. Die Skizze sowie die Vorüberlegungen zu den Prev-/Next-Zeigern haben sich gelohnt, da die Implementierung dadurch leicht von der Hand ging. Im Anschluss habe ich noch das Umkehren der Elementreihenfolge implementiert. Dabei zeigte sich wieder die große Stärke dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Listing 15 Listing 16 Listing 17 Die öffentliche Schnittstelle verwenden. Die Queue implementieren. Die Umkehr der Reihenfolge testen. [Test] public void FIFO_verschachtelt() { sut.Enqueue(1); Assert.That(sut.Dequeue(), Is.EqualTo(1)); sut.Enqueue(2); Assert.That(sut.Dequeue(), Is.EqualTo(2)); sut.Enqueue(3); sut.Enqueue(4); Assert.That(sut.Dequeue(), Is.EqualTo(3)); Assert.That(sut.Dequeue(), Is.EqualTo(4)); } von automatisierten Tests. Die Skizze einer Warteschlange im Vorher-Nachher-Vergleich war schnell erstellt, siehe Abbildung 4. Doch bis das Umdrehen der Zeiger korrekt lief, mussten die Tests einige Male durchlaufen. Ich habe wirklich keine Ahnung, wie man so etwas ohne automatisierte Tests hinkriegen will. Na ja, ich habe irgendwann auch mal ohne automatisierte Tests entwickelt. Aber das ist glücklicherweise schon lange her. Listing 17 zeigt den Test für die Umkehr der Reihenfolge. Bei der Implementierung habe ich mit zwei Zeigern gearbeitet, die jeweils auf das aktuelle in Arbeit befindliche Element (current) sowie das nächste Element (next) verweisen. Da die Elemente jeweils mit Prev auf ihren Vorgänger verweisen, wird die Warteschlange von hinten nach vorne abgearbeitet. Daher ist jeweils das Element, welches im zurückliegenden Schleifendurch- public class Queue<TElement> { internal Element<TElement> enqueue; internal Element<TElement> dequeue; public void Enqueue(TElement element) { var newElement = new Element<TElement> {Data = element}; if (enqueue != null) { enqueue.Prev = newElement; } enqueue = newElement; if (dequeue == null) { dequeue = newElement; } } public TElement Dequeue() { if (dequeue == null) { throw new InvalidOperationException(); } var result = dequeue.Data; dequeue = dequeue.Prev; if (dequeue == null) { enqueue = null; } return result; } } [TestFixture] public class ReverseTests { private Queue<int> sut; [SetUp] public void Setup() { sut = new Queue<int>(); } [Test] public void Eine_Queue_mit_drei_Elementen_kann _umgekehrt_werden() { sut.Enqueue(1); sut.Enqueue(2); sut.Enqueue(3); sut.Reverse(); Assert.That(sut.Dequeue(), Is.EqualTo(3)); Assert.That(sut.Dequeue(), Is.EqualTo(2)); Assert.That(sut.Dequeue(), Is.EqualTo(1)); } } Listing 18 Die Reihenfolge umkehren. lauf bearbeitet wurde, das nächste Element im Sinne der normalen Vorwärts-Reihenfolge. Am Ende sind noch enqueue und dequeue zu vertauschen, siehe Listing 18. Die Methode habe ich über die öffentliche Schnittstelle getestet. Man hätte sicherlich auch hier die interne Repräsentation heranziehen können, ich glaube aber, dass die Tests dadurch schlecht lesbar geworden wären. Daher wird die Beispielwarteschlange mit enqueue aufgebaut, anschließend public void Reverse() { var current = dequeue; Element<TElement> next = null; while(current != null) { var prev = current.Prev; current.Prev = next; next = current; current = prev; } var dummy = enqueue; enqueue = dequeue; dequeue = dummy; } [Abb. 3] Prev-Zeiger bei der Queue. umgedreht und dann mit dequeue überprüft, ob die Elemente in der richtigen, umgekehrten Reihenfolge geliefert werden. Fazit [Abb. 4] Die Reihenfolge der Elemente umkehren. www.dotnetpro.de dotnetpro.dojos.2011 Bei der testgetriebenen Entwicklung hat sich für mich bestätigt, wie nützlich die Planung auf Papier ist. Mir ist deutlich geworden, dass die Auswahl der Reihenfolge der Tests bei TDD wichtig ist. Daher sollte man in schwierigeren Szenarien immer erst Testfälle sammeln und diese dann in eine sinnvolle Reihenfolge bringen, bevor [ml] man mit dem ersten Test beginnt. 43 AUFGABE Infrastruktur Wie zähmt man den Dämon? In der Unix-Welt heißen sie Dämonen: die Dienste, die im Hintergrund ihre Arbeit verrichten. Stefan, stell doch mal eine Aufgabe, die in die Unterwelt der Windows-Dienste führt. Wer übt, gewinnt dnpCode: A1010DojoAufgabe 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 dotnetpro finden. Übung macht den Meister. Also − los geht’s. Aber Sie wollten doch nicht etwa sofort Visual Studio starten… 44 E in Windows-Dienst ist aus .NET-Sicht eine Konsolenanwendung. Da Dienste im System im Hintergrund laufen, auch wenn kein Benutzer am System angemeldet ist, gelten einige Besonderheiten. So kann ein Dienst logischerweise nicht über eine grafische Benutzerschnittstelle verfügen. Kein Benutzer, keine Interaktion, so einfach ist das. Damit der Dienst vom Betriebssystem ohne Zutun eines Benutzers gestartet werden kann, müssen in der Registry einige Angaben zum Dienst hinterlegt werden. Die wichtigste Information ist, unter welchem Benutzer der Dienst laufen soll, weil sich daraus die Rechte ableiten, die dem Dienst zur Verfügung stehen. Weitere Einstellungen betreffen das Startverhalten: Soll der Dienst beim Systemstart automatisch mit gestartet werden? Ist der Dienst von anderen Diensten abhängig, die dann zuerst gestartet werden müssen? Bevor ein Windows-Dienst gestartet werden kann, muss er installiert werden. Dazu gibt es im .NET Framework eine entsprechende Infrastruktur. Die Details sind nicht kompliziert, dennoch ist es lästig, für jeden Dienst erneut den Installationsvorgang entwickeln zu müssen. Daher geht es in diesem Monat darum, eine wiederverwendbare Infrastruktur zu entwickeln, mit der Windows-Dienste erstellt werden können. Mit „wiederverwendbar“ und „Infrastruktur“ stecken in dieser Übung gleich zwei Fallen, die man im Blick behalten sollte. Das Ziel der Wiederverwendbarkeit zieht sich zwar durch die Literatur zur objektorientierten Programmierung. Es birgt jedoch die Gefahr, zu viel tun zu wollen. Denn es droht das Risiko, maximal flexibel zu sein, dadurch aber auch maximalen Aufwand zu betreiben. Der Infrastrukturaspekt birgt das Risiko, „von unten“ zu beginnen. Statt also Anforderungen „von oben“ aus Sicht des Anwenders zu definieren, wird beim Fokus auf Infrastruktur oft der Fehler gemacht, Dinge vorzusehen, die am Ende niemand braucht. Ohne klare Anforderungen bleibt nur der Blick in die Glaskugel. Die Herausforderung lautet daher, Wiederverwendbarkeit und Infrastruktur besonders kritisch im Blick zu behalten, um nicht in diese Fallen zu laufen. Die Anforderungen für die Übung lauten: Erstellen Sie eine Komponente, mit der ein Windows-Dienst realisiert werden kann. Als Anwender möchte ich den Dienst an der Konsole folgendermaßen bedienen können : ❚ Mit myService.exe /install und myService.exe /uninstall kann der Dienst installiert beziehungsweise aus dem System entfernt werden. ❚ myService.exe /run führt den Dienst als normale Konsolenanwendung aus, ohne dass er zuvor als Windows-Dienst registriert werden muss. Dies ist für Test und Fehlersuche sehr hilfreich. Das Starten und Stoppen des Dienstes kann zunächst mit den Windows-Bordmitteln bewerkstelligt werden : ❚ net start myService ❚ net start myService Eine denkbare Erweiterung wäre, den Dienst auch mit myService.exe /start starten zu können. Aber dies ist ein Feature, welches erst umgesetzt werden soll, wenn die anderen Anforderungen implementiert sind. Nicht zu viel auf einmal tun, lautet die Devise. Als Entwickler möchte ich möglichst wenig mit der Windows-Infrastruktur konfrontiert werden. Ich möchte den Namen des Dienstes angeben sowie zwei Methoden, die beim Starten und Stoppen des Dienstes aufgerufen werden. Dabei sollte sich die Infrastruktur nicht in meine Klassen drängeln. Die Klasse, welche die Logik des Dienstes implementiert, sollte nicht von einer vorgegebenen Basisklasse ableiten müssen. Die Herausforderung der Übung liegt in zwei Bereichen : Zum einen geht es um die Technologie von Windows-Diensten. Wer sich damit noch nicht befasst hat, kann sich mit einem Spike mit der Technologie vertraut machen. Der andere Bereich ist der Entwurf der Lösung. Hier geht es darum, die Balance zu finden zwischen zu viel und zu wenig. Zu viel wäre beispielsweise, wenn im ersten Entwurf schon überlegt wird, wie man den Dienst zur Laufzeit kontrollieren kann. Zu wenig wäre, wenn die Anforderungen an die Bedienung der Kommandozeile nicht umgesetzt [ml] wären oder alles in einer Klasse landet. dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Windows-Dienste implementieren So beherrschen Sie den Dienst Ein Windows-Dienst ist eng in die Infrastruktur des Betriebssystems integriert. Das erschwert automatisierte Tests. Wenn Sie den eigentlichen Kern des Dienstes unabhängig von der Infrastruktur halten, ist er dennoch für automatisierte Tests zugänglich. I n der Aufgabenstellung zu dieser Übung habe ich auf zwei Risiken hingewiesen, die bei Infrastrukturprojekten oftmals auftreten. Zum einen bergen sie das Risiko, den Aspekt der Wiederverwendbarkeit zu stark zu berücksichtigen. Zum anderen ist die Versuchung groß, „von unten nach oben“ zu entwickeln. Beides führt in der Tendenz dazu, dass man zu viel tut. Nun mögen Sie sich vielleicht fragen, was denn so schlimm daran ist, mehr zu tun als gefordert. Sicher, es wäre schlimmer, weniger zu tun als gefordert. Jedoch wird beim „mehr tun“ Aufwand getrieben, den am Ende niemand bezahlen möchte. Möglicherweise wird sogar der geplante Termin nicht gehalten, weil unterwegs hier und da noch zusätzliche „Schmankerl“ eingebaut wurden. Aus diesem Grund sollten Anforderungen möglichst exakt umgesetzt werden. Und damit sind wir bei einem weiteren Knackpunkt: Was sind denn eigentlich die Anforderungen? Solange die nicht klar sind, kann ein Entwickler bei der Implementierung eigentlich nur falschliegen. Folglich sollte er so oft wie nötig nachfragen, um unklare Anforderungen zu präzisieren. Nun stand ich Ihnen während der Übung nicht als Kunde unmittelbar zur Verfügung, aber in der Aufgabenstellung war ein Feature explizit als „denkbare Erweiterung“ aufgeführt, nämlich das Starten und Stoppen des Windows-Dienstes. Folglich sollte die Implementierung so voranschreiten, dass dieses Feature nicht sofort von Anfang an umgesetzt wird. Andererseits darf es auch nicht aufwendiger sein, das Feature nachträglich zu ergänzen, statt es von vornherein vorzusehen. Hilfreich ist es deshalb, die möglichen Features zunächst zu sammeln. Dann können Kunde und Entwickler die Features priorisieren und in der „richtigen“ Reihenfolge abarbeiten. Beim Sammeln von Features muss eine Kundensicht eingenommen werden. Alle Features sollen dem Kunden Nutzen bringen. Am Ende muss schließlich der Kunde das Feature als „fertig“ akzeptieren und ab- www.dotnetpro.de dotnetpro.dojos.2011 nehmen. Das ist nur möglich, wenn das Feature für den Kunden tatsächlich relevant ist. Ein Feature wie etwa „Das Programm von 32 Bit auf 64 Bit umstellen“ spiegelt nicht den unmittelbaren Kundennutzen wider. Lautet das Feature jedoch „Das Programm kann mit sehr großen Datenmengen umgehen“, liegt der Fokus auf dem Kundennutzen statt auf einem technischen Detail. Featureliste Für die Aufgabenstellung„Windows-Dienst“ könnten die Features wie folgt aussehen: z F1: Ein Windows-Dienst kann installiert und deinstalliert werden. z F2: Der Windows-Dienst kann auch als Konsolenanwendung gestartet werden, ohne dass man ihn vorher als Dienst installieren muss. z F3: Der Windows-Dienst kann gestartet und gestoppt werden. Abnahmekriterien Um die Anforderungen zu präzisieren, sollten Abnahmekriterien definiert werden. Dadurch weiß der Entwickler, wann er mit der Arbeit fertig ist − eben dann, wenn alle Abnahmekriterien erfüllt sind. Die Abnahmekriterien für Feature F1 lauten: z Wenn der Dienst mit myservice.exe /install installiert wird, ist er unter Systemsteuerung/Services sichtbar. z Der Dienst kann nach der Installation über Systemsteuerung/Services oder net start myservice gestartet werden (und natürlich auch gestoppt werden). z Wenn der Dienst mit myservice.exe/uninstall deinstalliert wird, taucht er unter Systemsteuerung/Services nicht mehr auf. Spätestens an dieser Stelle beschleicht mich der Verdacht, dass hier mit automatisierten Tests nicht viel auszurichten ist. Am Ende hilft es nichts, der Dienst muss mit dem Betriebssystem korrekt zusammenarbeiten. Das lässt sich nur durch einen Integrationstest wirklich sicherstellen. Das bedeutet nun nicht, dass wir gar keine auto- matisierten Tests sehen werden, aber es werden wenige sein. Entwurf Für Feature F1 kann nun eine Architektur entworfen werden. Dabei ist einerseits zu berücksichtigen, dass möglicherweise nicht alle Features sofort implementiert werden. Ein Feature wird nach dem anderen implementiert, niedrig priorisierte möglicherweise gar nicht. Wird ein Feature nach dem anderen implementiert, bietet das für den Kunden sehr viel Flexibilität: Er kann die Priorisierung von Features jederzeit ändern. Und er kann Features streichen oder auch neue hinzunehmen. Logischerweise gilt dies nicht für Features, die bereits in Arbeit sind. Aber alle noch nicht begonnenen Features stehen zur Disposition. Folglich sollte ein Architekturentwurf nur die Features im Detail berücksichtigen, die konkret zur Implementierung anstehen. Da weitere mögliche Features schon bekannt sind, kann und sollte man diese beim Architekturentwurf im Blick behalten. Diese Features sollten jedoch keinen nennenswerten Einfluss auf die Architektur nehmen, denn es droht jederzeit das Risiko, dass sie doch nicht benötigt werden. Gefragt ist also ein Blick über den Tellerrand der anstehenden Features, ohne dass man dabei gleich zu viel tut. Orientierung liefert die Fokussierung auf den Kundennutzen. Für den Kunden ist es weitaus angenehmer, das wichtigste Feature einsatzbereit geliefert zu bekommen, als viele unvollendete Features, die nicht einsatzbereit sind. Um einen möglichst flexiblen Architekturentwurf zu erreichen, müssen die beteiligten Funktionseinheiten lose gekoppelt sein. So ist die Wahrscheinlichkeit groß, dass zusätzliche Features leicht integriert werden können. Es liegt also auf der Hand, hier Event-Based Components (EBC) zu verwenden. Doch bevor es so weit ist, müssen die Anforderungen weiter präzisiert werden, denn noch ist nicht klar, wie die geforderte Dienstinfrastruktur eingesetzt werden soll. 45 LÖSUNG Listing 1 Ein Interface für den Dienst. public interface IService { string Name { get; } string DisplayName { get; } string Description { get; } void OnStart(); void OnStop(); } Als Entwickler, der einen WindowsDienst implementieren soll, möchte ich es so einfach wie möglich haben. Das bedeutet für mich unter anderem: geringe Abhängigkeiten. Denkbar wäre die Realisierung über ein Interface, das wie in Listing 1 zu implementieren ist. Durch dieses Interface würden alle Informationen bereitgestellt, die relevant sind, damit man einen Windows-Dienst installieren und starten kann. Das könnte so wie in Listing 2 aussehen. Das Anlegen einer Klasse, die IService implementiert, geht zwar schnell von der Hand. Noch schneller bin ich aber, wenn ich nicht für jeden Dienst eine Klasse implementieren muss. Stattdessen könnte ich eine generische Klasse verwenden, von der Instanzen angelegt werden, siehe Listing 3. Ich habe die Klasse EasyService genannt, weil es damit so schön einfach ist, die erforderlichen Angaben für einen WindowsDienst zusammenzustellen. Es bleibt allerdings die Frage, wozu eine Instanz von EasyService angelegt werden muss. Das Objekt würde nur dazu dienen, die Dienstbeschreibung zu transportieren. Eigentlich wird aber kein Zustand benötigt, also genügt auch eine statische Methode Run innerhalb der Klasse, siehe Listing 4. Nachdem klar ist, wie der Kunde seine Software bedienen will, können Sie einen Architekturentwurf für Feature F1 angehen. Von ferne betrachtet, ist das Feature eine Funktionseinheit, die als Parameter die Kommandozeilenargumente sowie eine Dienstbeschreibung erhält. Die Dienstbeschreibung besteht vor allem aus dem Namen des Dienstes. Ferner sind zwei Lambda-Ausdrücke nötig, die beim Starten und Stoppen des Dienstes ausgeführt werden sollen. Abbildung 1 zeigt den Entwurf. Listing 2 Grundlagen eines Dienstes. public class MyService : IService { public string Name { get { return "myService"; } } public string DisplayName { get { return "Mein Service"; } } public string Description { get { return "Ein Service, der nichts tut."; } } public void OnStart() { Trace.WriteLine("OnStart aufgerufen"); } public void OnStop() { Trace.WriteLine("OnStop aufgerufen"); } } Listing 3 Eine generische Klasse verwenden. var myService = new EasyService { Name = "myService", DisplayName = "Mein Service", Description = "Ein Service, der nichts tut.", OnStart = () => Trace.WriteLine("OnStart aufgerufen"), OnStop = () => Trace.WriteLine("OnStop aufgerufen") }; myService.Run(args); 46 [Abb. 1] Erster Entwurf. Der nächste Schritt besteht darin, diesen Entwurf zu verfeinern. Dazu wird die Funktionseinheit zerlegt. Ohne mir schon zu viele Gedanken um die Details der Dienstinstallation machen zu müssen, fällt es mir leicht, vier Funktionseinheiten zu identifizieren: z Argumente auswerten, z Dienst installieren, z Dienst deinstallieren, z Dienst ausführen. Diese Einheiten bilden die Verfeinerung meines Entwurfs, siehe Abbildung 2. Damit bin ich bereits auf einem Abstraktionsniveau angekommen, mit dem ich zufrieden bin. In die technischen Details der Dienstinstallation will ich bei diesem Entwurf nicht weiter hineinzoomen. Sicherlich werden da noch ein paar technische Details stecken, doch in der Rolle des Architekten gehe ich davon aus, dass der Entwickler diese Details in der Funktionseinheit „Dienst installieren“ sinnvoll unterbringen kann. Sollte sich während der Implementierung herausstellen, dass dem nicht so ist, muss der Entwurf möglicherweise weiter verfeinert werden. Aus dem Entwurf ergeben sich folgende Kontrakte : z Ein Datenmodell für die Dienstbeschreibung. z Ein Kontrakt für das Auswerten der Kommandozeilenargumente. z Je ein Kontrakt für die Installation, die Deinstallation und die Ausführung des Dienstes. Das Datenmodell wird anstelle von einfachen Typen wie string oder Ähnlichem verwendet, weil dadurch ein höheres Abstraktionsniveau erreicht wird. Im Entwurf werden bei den Input- und Outputpins ebenfalls Bezeichner verwendet, die aus der sogenannten allgegenwärtigen Sprache (Ubiquitous Language) stammen. Da es sich bei Begriffen wie „Dienstbeschreibung“ um einen Begriff aus der Problemdomäne handelt, ist es gut, diese auch im Code wiederzufinden. Das erleichtert das Verständnis, da keine gedankliche Übersetzung erforderlich ist. Würde eine Me- dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG thode mehrere Parameter von einfachen Typen erwarten, müsste jemand, der den Code liest, daraus gedanklich die Dienstbeschreibung erst wieder zusammensetzen. Das Datenmodell für die Dienstbeschreibung sieht aus wie in Listing 5. Die Kontrakte für die Funktionseinheiten sind in EBC-Manier erstellt. Das bedeutet, dass sie über sogenannte Inputund Outputpins verfügen. Inputpins werden in Form von Methoden modelliert, Outputpins sind Events. Zu weiteren Details über EBCs lesen Sie am besten die Artikelserie von Ralf Westphal, zu finden unter [1] [2] [3]. Der Kontrakt für das Auswerten der Argumente sieht aus wie in Listing 6. Auf dem Inputpin In_Process der Funktionseinheit werden die Kommando- Listing 4 Eine statische Methode „Run” verwenden. EasyService.Run( args, "myService", "Mein Service", "Ein Service, der nichts tut.", () => Trace.WriteLine ("OnStart aufgerufen"), () => Trace.WriteLine ("OnStop aufgerufen") ); Listing 5 Datenmodell für die Dienstbeschreibung. public class ServiceBeschreibung { public string Name { get; set; } public string DisplayName { get; set; } public string Description { get; set; } } [Abb. 2] Verfeinerung des Entwurfs. zeilenparameter in Form eines string-Arrays übergeben. Je nachdem, welcher Parameter übergeben wurde, wird daraufhin der korrespondierende Outputpin ausgelöst. Die beiden Kontrakte für die Installation und Deinstallation sind noch einfacher, da sie nicht über Outputpins verfügen, siehe Listing 7. Und zu guter Letzt ist da noch der Kontrakt für die Ausführung des Service, siehe Listing 8. Damit haben Sie nun alle Kontrakte zusammen und können mit der Implementierung beginnen. Wie schon angedeutet, sind automatisierte Tests an den Stellen schwierig, an denen die Windows-Infrastruktur relevant ist. Dies betrifft das Installieren und Deinstallieren des Dienstes. Ferner sollte der Kontrakt IServiceAusführen mit der abstrakten Klasse ServiceBase aus dem .NET Framework kombiniert werden. Diese stellt die erforderliche Infrastruktur für die Dienstausführung zur Verfügung. Damit die Dienste nicht von ServiceBase ableiten müssen und tatsächlich keine Abhängigkeiten zur Windows-Infrastruktur haben, verwende ich einen Dienstproxy. Diese Klasse leitet von ServiceBase ab und bietet zwei Events, die beim Starten bzw. Stoppen des Dienstes ausgeführt werden. Dadurch kann ein Lambda-Ausdruck verwendet werden, und der Dienst ist infra- strukturunabhängig. Der Dienstproxy sieht aus, wie in Listing 9 gezeigt wird. Diese Klasse ist so simpel, dass ich auf Tests verzichtet habe. Sie zu ergänzen würde im Übrigen auch erfordern, die protected Methoden OnStart und OnStop im Test aufzurufen. Aufgrund der Vererbung kann die Sichtbarkeit nicht zu internal geändert werden. Aufwand und Nutzen stünden daher in einem sehr ungünstigen Verhältnis. Da das gesamte Projekt ohnehin einen Integrationstest erfordert, wird der ServiceProxy dort mitgetestet. Sehr gut automatisiert zu testen ist die Implementierung der Argumentauswertung. Ich habe die Klasse ArgumenteAuswerten testgetrieben entwickelt. Listing 10 zeigt zwei der Tests als Beispiele. Der erste Test prüft, ob beim Aufruf ohne Parameter der Outputpin Out_RunAsService ausgelöst wird. Dies ist wichtig, da Windows den Dienst so startet. Der zweite Test prüft, ob bei Aufruf mit /install der Event Out_Install ausgelöst wird. Die zugehörige Implementierung ist einfach gehalten. Zunächst wird geprüft, ob kein Argument übergeben wurde. In dem Fall wird der Event Out_RunAsService ausgeführt. Andernfalls wird über ein switchStatement in die jeweiligen Events verzweigt. Diese Form der Argumentauswertung ist zwar zu einfach, um damit etwa zusätzliche Parameter zu den Optionen parsen Listing 7 Listing 6 Argumente auswerten. public interface IArgumenteAuswerten { void In_Process(params string[] args); event Action Out_Install; event Action Out_Uninstall; event Action Out_RunAsService; } www.dotnetpro.de dotnetpro.dojos.2011 Installation und Deinstallation. public interface IServiceInstallieren { void In_Installieren(ServiceBeschreibung beschreibung); } public interface IServiceDeinstallieren { void In_Deinstallieren(ServiceBeschreibung beschreibung); } 47 LÖSUNG Listing 8 Den Dienst ausführen und stoppen. public interface IServiceAusführen { void In_Start(); void In_Stop(); } zu können. Kommandos mit Parametern wie etwa /install myService2 erfordern etwas mehr Aufwand beim Parsen. Für Feature F1 genügt ein solch einfacher Parser aber völlig, warum also mehr tun. Tatsächlich ist sogar die Groß-/Kleinschreibung signifikant, das heißt /INSTALL würde zu einem Fehler führen. Aber auch diese Einschränkung ist in Ordnung, es sei denn, der Kunde würde explizit fordern, dass Groß-/ Kleinschreibung zu ignorieren sei. Als Nächstes kommt das Installieren des Dienstes an die Reihe. Obwohl ich die Dienstinstallation schon mal implementiert habe, musste ich die Suchmaschine meiner Wahl bedienen, um verschiedene Details zusammenzusammeln. Auch automatisierte Tests schieden aus. So erinnerte mich die Implementierung dieser Funktionseinheit eher an einen Spike, und leichtes Unbehagen ließ sich nicht vermeiden. Um überhaupt etwas testen zu können, musste ich einen kleinen Minidienst implementieren. Der kann nun gleichzeitig als Beispiel dienen, wie man Dienste mithilfe der geschaffenen Infrastruktur imple- mentiert. Dennoch war mir nicht wohl bei der Sache, da ich diese Arbeitsweise dank testgetriebener Entwicklung gar nicht mehr gewohnt bin. Aber es hilft nichts − die korrekte Installation eines Dienstes innerhalb des Betriebssystems lässt sich nun mal nicht anders überprüfen. Neben den Details der Dienstinstallation ging es auch um andere Details. Denn natürlich darf nicht jeder Benutzer einen Dienst im Betriebssystem registrieren, es sei denn, man arbeitet immer noch als Administrator und schaltet die User Account Control (UAC) aus. Folglich musste ich mich damit befassen, wie man dem Betriebssystem mitteilen kann, dass ein Programm Administratorberechtigungen benötigt. Das geht ganz einfach: Man fügt dem Projekt, mit dem man das Programm erstellt, also dem EXE-Projekt, eine Manifestdatei hinzu. In dieser XML-Datei kann die Administratorberechtigung angefordert werden, sodass Windows die sogenannte Elevation beim Benutzer anfordern kann. Der relevante Ausschnitt aus der Manifestdatei app.manifest sieht aus, wie in Listing 11 gezeigt wird. Für die Installation eines Dienstes stehen im .NET Framework die Klassen TransactedInstaller, ServiceProcessInstaller und ServiceInstaller zur Verfügung. Der TransactedInstaller ist dafür zuständig, eine Installation transaktional auszuführen. Das bedeutet, die angeforderte Installation wird entweder vollständig ausgeführt oder bei einem Fehler komplett wieder rückgängig gemacht. ServiceProcessInstaller und ServiceInstaller werden konfiguriert und zum TransactedInstaller hinzugefügt; die- Listing 9 Ein Proxy für den Dienst. public class ServiceProxy : ServiceBase, IServiceAusführen { public event Action Out_Start = delegate { }; public event Action Out_Stop = delegate { }; public void In_Start() { Out_Start(); } public void In_Stop() { Out_Stop(); } protected override void OnStart(string[] args) { Out_Start(); } protected override void OnStop() { Out_Stop(); } } 48 Listing 10 Argumente auswerten. [TestFixture] public class ArgumenteAuswertenTests { private IArgumenteAuswerten sut; [SetUp] public void Setup() { sut = new ArgumenteAuswerten(); } [Test] public void Ohne_Argumente() { var count = 0; sut.Out_RunAsService += () => count++; sut.In_Process(); Assert.That(count, Is.EqualTo(1)); } [Test] public void Install_als_Argument() { var count = 0; sut.Out_Install += () => count++; sut.In_Process("/install"); Assert.That(count, Is.EqualTo(1)); } } ser wird anschließend angewiesen, die Installation durchzuführen. Da die Schritte für die Deinstallation alle gleich sind, liegt es auf der Hand, die beiden Kontrakte IServiceInstallieren und IServiceDeinstallieren in einer Klasse zusammenzufassen, wie Listing 12 zeigt. Nachdem die einzelnen Prozessschritte implementiert sind, müssen sie nur noch in EBC-Manier miteinander verbunden werden. Diese Arbeit übernimmt die statische Run-Methode in der Klasse EasyService. Ich habe mich auch hier dagegen entschieden, die „Verdrahtung“ der Bausteine automatisiert zu testen. Technisch wäre das natürlich möglich. Dazu müssten die einzelnen Bausteine durch ein Mock-Framework instanziert und in die Klasse EasyService injiziert werden. Anschließend könnte automatisiert geprüft werden, ob die Zuordnung von Methoden zu Events korrekt ist. Im Ergebnis ist der Nutzen abermals recht gering, da die Verdrahtung aufgrund der Event- und Methodensignaturen kaum falsch gemacht werden kann. Um nun tatsächlich feststellen zu können, ob Feature F1 fertiggestellt ist, müssen die Abnahmekriterien überprüft werden. Dazu muss ein exemplarischer Dienst implementiert werden, um so zu prüfen, ob dieser tatsächlich in der Systemsteuerung sichtbar ist und gestartet werden kann. dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Listing 11 Die Administratorberechtigung anfordern. [Abb. 3] DebugView zeigt die Trace-Ausgaben. Hier stellte sich mir die Frage, wie man am einfachsten überprüft, ob der Dienst tatsächlich gestartet und gestoppt werden kann. Eine Ausgabe auf der Konsole scheidet aus, denn schließlich handelt es sich um einen Dienst ohne Benutzerinteraktion. Ich entschied mich für eine Ausgabe mittels System.Diagnostics.Trace aus dem .NET Framework. Diese kann nämlich mit dem Programm DebugView aus der SysInternals-Sammlung [4] angezeigt werden. Damit Trace-Ausgaben von Diensten angezeigt werden, muss im DebugView die Einstellung „Capture Global Win32“ aktiviert werden. Voraussetzung dafür ist wiederum, dass das Programm mit Administratorrechten gestartet wird. Abbildung 3 zeigt die Ausgabe von DebugView. Die Features F2 und F3 zu ergänzen ist dank der EBCs ganz leicht. Dazu musste ich lediglich die Auswertung der Argumente so ergänzen, dass weitere Kommandozeilenparameter erkannt werden. Das Ausführen des Dienstes von der Konsole aus, also ohne Installation im Betriebssystem, war ganz leicht und benötigte keine weitere Funktionseinheit. Für das Starten und Stoppen des Dienstes habe ich die Klasse ServiceStarter ergänzt. Anschließend konnte ich in der Klasse EasyService die Verdrahtung ergänzen, und das neue Feature war fertig. <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2"> <security> <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3"> <requestedExecutionLevel level="requireAdministrator" uiAccess="false" /> </requestedPrivileges> </security> </trustInfo> Listing 12 Den Service installieren und entfernen. public class ServiceInstallation : IServiceInstallieren, IServiceDeinstallieren { public void In_Installieren(ServiceBeschreibung name) { var transactedInstaller = CreateTransactedInstaller(name, "Install.log"); transactedInstaller.Install(new Hashtable()); } public void In_Deinstallieren(ServiceBeschreibung name) { var transactedInstaller = CreateTransactedInstaller (name, "UnInstall.log"); transactedInstaller.Uninstall(null); } private static TransactedInstaller CreateTransactedInstaller (ServiceBeschreibung name, string logFilePath) { var serviceProcessInstaller = new ServiceProcessInstaller { Account = ServiceAccount.LocalSystem }; var transactedInstaller = new TransactedInstaller(); transactedInstaller.Installers.Add(serviceProcessInstaller); var path = string.Format("/assemblypath={0}", Assembly.GetEntryAssembly().Location); var installContext = new InstallContext(logFilePath, new[] {path}); transactedInstaller.Context = installContext; var serviceInstaller = new ServiceInstaller { ServiceName = name.Name, DisplayName = name.DisplayName, Description = name.Description }; transactedInstaller.Installers.Add(serviceInstaller); return transactedInstaller; Fazit Bei dieser Übung ist die Testabdeckung recht gering ausgefallen. Dies hat mich natürlich nicht kaltgelassen. Allerdings gibt es zwei Dinge zu berücksichtigen: Zum einen wären automatisierte Tests aufgrund des sehr hohen Infrastrukturanteils sehr aufwendig, darauf habe ich weiter oben an den entsprechenden Stellen bereits hingewiesen. Zum anderen sorgt die hier vorgestellte Dienstinfrastruktur aber dafür, dass der eigentliche Kern des zu implementierenden Dienstes völlig befreit ist von Infrastrukturabhängigkeiten. Dadurch ist der Kern des Dienstes besser zu testen. Insofern kann ich akzeptieren, dass der Infrastrukturanteil mittels manueller Integra- www.dotnetpro.de dotnetpro.dojos.2011 } } tionstests überprüft wird. Die Vorgehensweise, ausgehend von einer Featureliste über die EBC-Architektur hin zur Implementierung, hat sich bewährt. Wie sah das bei Ihnen aus? Schreiben Sie doch einmal einen Leserbrief zu Ihren Erfahrungen beim dotnetpro dojo! [ml] [1] Ralf Westphal, Zusammenstecken – funktioniert, Event-Based Components, dotnetpro 6/2010, S. 132ff., www.dotnetpro.de/ A1006ArchitekturKolumne [2] Ralf Westphal, Stecker mit System, dotnetpro 7/2010, S. 126 ff., www.dotnetpro.de/A1007ArchitekturKolumne [3] Ralf Westphal, Nicht nur außen schön, dotnetpro 8/2010, S. 126ff., www.dotnetpro.de/A1008ArchitekturKolumne [4] DebugView for Windows v4.76, www.dotnetpro.de/SL1011dojoLoesung1 49 AUFGABE Event-Based Components Wie baue ich einen Legostein? Softwarekomponenten so einfach wie Legosteine zusammenstecken zu können – mit diesem Versprechen tritt das Konzept der Event-Based Components an. Stefan, kannst du dazu eine Übung stellen? Wer übt, gewinnt dnpCode: A1011DojoAufgabe 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 : z Falsche Lösungen gibt es nicht. Es gibt möglicherweise elegantere, kürzere oder schnellere Lösungen, aber keine falschen. z 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 dotnetpro finden. Übung macht den Meister. Also − los geht’s. Aber Sie wollten doch nicht etwa sofort Visual Studio starten… 50 I n Ergänzung zur Artikelserie von Ralf Westphal [1] [2] [3] [4] über Event-Based Components (EBC) lautet die Aufgabe in diesem Monat: Entwickeln Sie eine Textumbruchkomponente. Für die Silbentrennung können Sie die Komponente NHunspell [5] verwenden. Abbildung 1 zeigt, wie eine kleine Testanwendung aussehen könnte, die den Textumbruch als Komponente verwendet. Die Komponente soll über folgenden Kontrakt verfügen : public interface ITextumbruch { string Umbrechen(string text, int breiteInZeichen); } Der Text sowie die gewünschte Breite werden in die Methode gegeben, diese liefert den umbrochenen Text zurück. Die Breite des Textes wird der Einfachheit halber als Anzahl der Zeichen angegeben. Eine Angabe in Millimetern würde es erfordern, dass man die Laufweiten der jeweiligen Zeichen berücksichtigt. Das wäre für die Übung dann doch zu viel des Guten. So weit zum gewünschten API der Komponente. Diese ist damit eine Komponente im klassischen Sinne, also eine binäre Funktionseinheit mit separatem Kontrakt. Intern soll sie jedoch durch EBCs realisiert werden. Überlegen Sie sich dazu, welche Bearbeitungsschritte nötig sind, um den Text zu umbrechen. Entwerfen Sie dabei nicht gleich in Verantwortlichkeiten, sondern in Prozessschritten oder Aktionen. Die Verantwortlichkeiten ergeben sich daraus ganz von allein. Für die Silbentrennung mag es auf der Hand liegen, dass NHunspell die Verantwortlichkeit für diesen Prozessschritt übernimmt. Für alle anderen Schritte dürfte es nicht so offenkundig sein. Die Anforderungen an die Komponente sollten Sie sich vorher notieren. Auch mögliche Testfälle sollten Sie sammeln. In einem realen Projekt würden Sie solche Testfälle mit dem Kunden diskutieren. Bei dieser Übung treffen Sie selber sinnvolle Annahmen. So könnten Sie beispielsweise entscheiden, dass ein Wort, welches selbst nach der Silbentrennung zu lang ist, einfach übersteht. „Spielen“ Sie ein wenig mit verschiedenen Texten, es werden Ihnen sicher zahlreiche [Abb. 1] Testanwendung für den Textumbruch. interessante Szenarien auffallen. Dieses Mal sollen automatisierte Tests eine größere Rolle spielen als bei der vorherigen Übung zum WindowsDienst. Die einzelnen Funktionseinheiten sollen möglichst isoliert getestet werden. Und natürlich dürfen ein paar Integrationstests nicht fehlen. Für das explorative Testen ist die Testanwendung gedacht. Damit können Sie ausprobieren, wie sich die Komponente bei bestimmten Konstellationen verhält. Ich wünsche viel Spaß und großen Erkenntnisgewinn! [ml] [1] Ralf Westphal, Zusammenstecken – funktioniert, Event-Based Components, dotnetpro 6/2010, S. 132ff., www.dotnetpro.de/A1006ArchitekturKolumne [2] Ralf Westphal, Stecker mit System, Event-Based Components, dotnetpro 7/2010, S. 126 ff., www.dotnetpro.de/A1007ArchitekturKolumne [3] Ralf Westphal, Nicht nur außen schön, Event-Based Components, dotnetpro 8/2010, S. 126 ff., www.dotnetpro.de/A1008ArchitekturKolumne [4] Ralf Westphal, Staffel-Ende mit Happy End, Event-Based Components, dotnetpro 9/2010, S. 132ff., www.dotnetpro.de/A1009ArchitekturKolumne [5] http://nhunspell.sourceforge.net/ dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Event-Based Components So trennt man Feu-er-wehr Das Konzept der Event-Based Components einzuüben – das war das Ziel dieses dojos. Die konkrete Aufgabe bestand darin, eine Komponente für den Textumbruch mit Silbentrennung zu entwickeln. Zum Glück hat Stefan Lieser ein eigenes Test-GUI entwickelt, denn damit konnte er viele Fehler entdecken und beseitigen. E inen Textumbruch mit Silbentrennung zu implementieren, hört sich schwierig an. Und das ist es auch, wenn man an eine Textverarbeitung wie Word denkt. Allerdings gab die Aufgabenstellung mit dem Hinweis auf NHunspell [1] einen Tipp für das Problem der Silbentrennung. Als Erstes sind die Anforderungen zu klären. Im Falle des Textumbruchs können die Anforderungen sehr gut anhand von Beispielen dargestellt werden. Der folgende Satz soll etwa auf eine Breite von zehn Zeichen umbrochen werden: Bauer Klaus erntet Kartoffeln. Dann soll das Ergebnis folgendermaßen aussehen: Bauer Klaus erntet Kartoffeln. Das Beispiel enthält keine besonderen Schwierigkeiten oder Spezialfälle. Aber genau die gilt es natürlich ebenfalls in den Blick zu nehmen. So stellt sich beispielsweise die Frage, wie mit Zeilenumbrüchen verfahren werden soll, die im Eingangstext schon vorhanden sein können: Bauer Klaus erntet Kartoffeln. Das Ergebnis soll das gleiche sein wie oben. Bereits vorhandene Zeilenumbrüche werden also ignoriert. Das soll auch für mehrere hintereinander stehende Zeilenumbrüche gelten. Auch Absätze werden damit ignoriert. Diese Vereinfachung ist der Tatsache geschuldet, dass es hier nur um eine Übung geht. Als Nächstes ist Leerraum zu betrachten. Wenn im Satz zusätzliche Leerzeichen stehen, sollen diese erhalten bleiben, es soll also keine Normalisierung stattfinden. Allerdings sollen Leerzeichen am Anfang einer Zeile entfernt werden, weil das Ergebnis sonst doch sehr fragwürdig aussieht. www.dotnetpro.de dotnetpro.dojos.2011 Dazu ein Beispiel, in dem die Leerzeichen durch einen Punkt ersetzt sind, damit sie besser zu erkennen sind : Leerzeichen am Zeilenanfang werden also entfernt, innerhalb des Satzes oder auch am Ende bleiben sie erhalten. Um die Anforderungen für die Übung möglichst einfach zu halten, lassen wir es dabei zunächst bewenden. Denn zwischen den Wörtern steht Leerraum, der erhalten bleiben muss. Als Oberbegriff für Wort und Leerraum habe ich Zeichenfolge gewählt. Der Text wird also zunächst in Zeichenfolgen zerlegt. Das lässt auch Spielraum für mögliche Erweiterungen. Schließlich können im Text auch Zahlen als Zeichenfolgen auftreten, die möglicherweise besonders behandelt werden müssen. Ein weiteres Beispiel sind Interpunktionszeichen, auch diese kann man unter den Überbegriff Zeichenfolgen stellen. Nachdem Zeichenfolgen in Silben zerlegt sind, müssen die Silben so zu Zeilen zusammengefasst werden, dass die einzelnen Zeilen höchstens die maximale Länge haben. Dabei stehen die einzelnen Silben natürlich nicht für sich. Denn Silben können nur innerhalb der Zeile einfach so aneinandergereiht werden. Am Zeilenende muss ein Trennstrich ergänzt werden, wenn die letzte Silbe der Zeile zum selben Wort gehört wie die erste Silbe der Folgezeile. Daher muss der Zusammenhang zwischen Silben und Wörtern erhalten bleiben. Offensichtlich genügt es daher nicht, das Zusammenfassen zu Zeilen auf Basis eines Stroms von Silben zu implementieren. Algorithmus Entwurf Nachdem die Anforderungen präzisiert sind, müssen Sie eine Idee für einen Algorithmus entwickeln. Dabei steht die Frage im Vordergrund, wie das Problem algorithmisch gelöst werden kann. Es geht noch nicht darum, wie der Algorithmus konkret zu implementieren ist und welche Funktionseinheiten dabei eine Rolle spielen. Die erste Idee für den Textumbruch sah bei mir folgendermaßen aus : ❚ Zerlege den Text in Wörter. ❚ Zerlege die Wörter in Silben. ❚ Fasse die Silben neu zu Zeilen zusammen. Aus diesen Vorüberlegungen entstand mein Entwurf für eine EBC-Architektur (EventBased Components). Abbildung 1 zeigt die folgenden vier Aktionen: ❚ Zerlegen in Zeichenfolgen, ❚ Zeichenfolgen in Silben trennen, ❚ Zusammenfassen zu Zeilen, ❚ Zusammenfassen zu Text. Bauer•••••••Klaus••erntet•Kartoffeln. Wenn dieser Satz auf eine Breite von zehn Zeichen unter Beibehaltung der Leerzeichen umbrochen wird, ergibt sich zunächst folgendes Ergebnis: Bauer••••• ••Klaus•• erntet• Kartoffeln. Dabei sind die Leerzeichen in der zweiten Zeile vor dem Wort „Klaus“ jedoch störend. Folglich sollen sie entfernt werden, mit folgendem Ergebnis: Bauer••••• Klaus••erntet•Kartoffeln. Dabei bemerkte ich schnell, dass die Zerlegung des Textes in „Wörter“ nicht wirklich präzise beschreibt, was zu tun ist. Diese vier Aktionen sind zu einer EBCAktivität zusammengefasst, welche die Aktionen umschließt. Aufgabe der Aktivität ist es, die Input- und Outputpins der beteiligten Aktionen zu verbinden. Die Aktivität selbst verfügt über je einen Input- und Outputpin und verbirgt die internen Details der Realisierung. Sollten später Aktionen hin- 51 LÖSUNG zukommen, können diese Änderungen lokal innerhalb der Aktivität gehalten werden. In der Abbildung verwende ich an den Pfeilen, welche einen Datenstrom von einem Output- zu einem Inputpin darstellen, die Sternnotation. Die Bezeichnung Zeile* bedeutet daher „mehrere Zeilen“. Ob dies am Ende durch ein Array, eine Liste oder ein IEnumerable realisiert wird, ist auf der Ebene des Architekturentwurfs nicht entscheidend. Wichtig ist, dass sich die nachfolgende Implementation an die Kardinalität hält. Wenn also beispielsweise die Aktion Zusammenfassen zu Zeilen im Entwurf mehrere Zeilen liefert, darf nicht in der Implementation ein einzelner string zurückkommen. Dies ergibt sich aus dem Prinzip „Implementation spiegelt Entwurf“ [2], welches dafür sorgt, dass die Implementation besser verständlich ist. Würde man in der Implementation vom Entwurf abweichen, wäre für einen Entwickler, der später in den Code einsteigt, ein Übersetzungsaufwand erforderlich. Ein Blick in den Entwurf würde ihm dann nicht viel helfen, wenn die Implementation immer wieder davon abweicht. Um das in den Anforderungen beschriebene API bereitzustellen, kommt noch eine Klasse hinzu, in der die Aktivität verwendet wird. So ist die Realisierung als EBC außen nicht mehr sichtbar. Wo beginnen? Bei vier Aktionen, einer Aktivität und der API-Klasse stellt sich die Frage, wo man anfangen soll. Vereinfacht gesagt stehen Topdown oder Bottom-up-Vorgehensweisen zur Auswahl. Bei einer Top-down-Vorgehensweise beginnt man bei der Benutzerschnittstelle oder in diesem Fall beim API. Von dort arbeitet man sich „nach unten“ durch. Andersherum beim Bottom-upVorgehen: Hier beginnt man mit den Aktionen und arbeitet sich langsam nach oben [Abb. 1] Der Entwurf in EBC-Architektur. zur Integration vor. Mein Favorit ist die Top-down-Vorgehensweise. Diese bietet den Vorteil, dass die Implementation jeweils aus der Sicht eines Verwenders erfolgt. Zu jeder Komponente, Klasse oder Methode, die so entsteht, gibt es dann bereits einen Verwender. Dieser stellt ganz konkrete Anforderungen. So wird die Gefahr minimiert, sich mögliche Anforderungen aus den Fingern zu saugen. Bei einer Bottomup-Vorgehensweise ist diese Gefahr nicht zu unterschätzen. Sie führt häufig dazu, dass Funktionalität implementiert wird, von der niemand weiß, ob sie benötigt wird. Um zu überprüfen, ob der Architekturentwurf der Problemstellung angemessen ist und funktioniert, ist es ganz wichtig, als Erstes einen Durchstich zu realisieren, bei dem alle entworfenen Funktionseinheiten beteiligt sind. Dieses sogenannte Tracer Bullet Feature soll keine echte Funktionalität erzeugen, sondern nur zeigen, dass die Integration der Funktionseinheiten funktioniert. Hier kann daher auch auf automatisierte Tests verzichtet werden. Im Falle eines APIs ist in Ermangelung einer anderen Benutzerschnittstelle lediglich ein Test erforderlich, der das API bedient. Das Tracer Bullet Feature sorgt dafür, dass die Entwurfsskizzen quasi in Code gegossen werden. Dadurch werden vor allem die Schnittstellen zwischen den Funktions- Listing 1 Aktionen verdrahten. var var var var zerlegenInZeichenfolgen = new ZerlegenInZeichenfolgen(); zeichenfolgenInSilbenTrennen = new ZeichenfolgenInSilbenTrennen(); zusammenfassenZuZeilen = new ZusammenfassenZuZeilen(); zusammenfassenZuText = new ZusammenfassenZuText(); zerlegenInZeichenfolgen.Out_Result += zeichenfolgenInSilbenTrennen.In_Process; zeichenfolgenInSilbenTrennen.Out_Result += zusammenfassenZuZeilen.In_Process; zusammenfassenZuZeilen.Out_Result += zusammenfassenZuText.In_Process; zusammenfassenZuText.Out_Result += text => Out_Result(text); 52 einheiten viel rigoroser unter die Lupe genommen, als dies am Whiteboard möglich ist. So werden Ungereimtheiten frühzeitig aufgedeckt. Im konkreten Fall des Textumbruchs habe ich also zunächst die API-Klasse Textumbruch sowie die Aktivität TextumbruchAktivität erstellt. Anschließend habe ich die einzelnen Aktionen mit ihren Input- und Outputpins erstellt und innerhalb der Aktivität verdrahtet. Um die Aktionen nicht einzeln ausimplementieren zu müssen, habe ich lediglich die Daten der Inputpins auf die Outputpins übertragen. Dabei müssen natürlich nach Bedarf entsprechende Datenobjekte erzeugt werden, um den Signaturen von Input- und Outputpins gerecht zu werden. Nach der Verdrahtung der Aktionen in der Aktivität konnte ich dann sehen, dass ein Text, der über den Inputpin in die Aktivität hineingegeben wird, tatsächlich am Outputpin wieder herauskommt. Wunderbar! Commit nicht vergessen! Zusätzlich kann man bei der Implementation des Tracer Bullet Features auch Trace-Ausgaben ergänzen. Dadurch lässt sich mit Tools wie DebugView [3] verfolgen, ob der Ablauf der einzelnen Aktionen korrekt erfolgt. Listing 1 zeigt die Verdrahtung der Aktionen in der Aktivität. Zunächst werden von den benötigten Aktionen Instanzen erstellt. Anschließend werden Input- und Outputpins gemäß dem Entwurf verbunden. Die letzte Zeile zeigt die Verbindung zum Outputpin der Aktivität. Doch wie erfolgt dieVerbindung zum Inputpin der Aktivität? Dazu wird im Konstruktor der Aktivität eine Action<string, int> erstellt, die zur Signatur des Inputpins passt: process = (text, breiteInZeichen) => { zusammenfassenZuZeilen. In_SetzeBreite(breiteInZeichen); zerlegenInZeichenfolgen.In_Process(text); }; Diese Action ist als Feld der Klasse deklariert und kann daher im Inputpin aufgerufen werden : public void In_Process(string text, int breiteInZeichen) { process(text, breiteInZeichen); } Dank dieses Kniffs müssen in der Klasse keine anderen Felder definiert werden, um auf die Aktionen zugreifen zu können. Und Action! Nach der Aktivität ging es an die Implementation der einzelnen Aktionen. Auch dabei dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG bin ich immer in Durchstichen vorgegangen. Statt also die Zerlegung in Zeichenfolgen komplett fertigzustellen und erst dann mit der Silbentrennung zu beginnen, habe ich die Zerlegung erst nur ganz simpel realisiert und dann mit der Silbentrennung begonnen. Hier gilt es im realen Projekt abzuwägen, welche Teilfunktionalität dem Kunden den jeweils größten Nutzen bringt. Das kann bedeuten, eine Funktion komplett zu realisieren und andere nur rudimentär. Genauso gut kann es aber nützlich sein, an allen Stellen einen Teil der geforderten Leistung zu erbringen. Im Zweifel gilt: Reden hilft! Eine Rückfrage beim Kunden oder Product Owner sollte den Sachverhalt klären. Doch zurück zu den Aktionen. Das Aufteilen des Textes in Zeichenfolgen basiert im Wesentlichen auf der Anwendung von string.Split. Dadurch bleiben zwar nicht alle Leerzeichen erhalten, aber dies habe ich für die erste Version in Kauf genommen. Der erste Test befasst sich mit einem Text, der nur aus einem Wort besteht. Dieser Test diente mir dazu, den Testrahmen zu erstellen. Man beachte, dass es hier um den Test einer EBC-Aktion geht, bei der das Ergebnis über einen Event geliefert wird. Den Rahmen für den Test zeigt Listing 2. Die Rückgabe des Ergebnisses erfolgt bei EBCs über einen Outputpin. Outputpins werden über Events realisiert. Daher muss im Test geprüft werden, ob der Event das richtige Ergebnis liefert. Dazu erstelle ich im Setup der Testklasse eine Instanz des Prüflings und binde einen kleinen Lambda-Ausdruck an den Event. Der LambdaAusdruck kopiert das Argument des Events in das Feld result der Testklasse. So kann in den Testmethoden auf das Ergebnis des Events zugegriffen werden. Die Klasse Zeichenfolge ist trivial. Sie enthält lediglich eine Eigenschaft für den string, siehe Listing 3. Ferner ist eine Equals-Methode implementiert, damit Zeichenfolgen im Test verglichen werden können. Das Erzeugen von Equals und GetHashCode übernimmt für mich das ReSharper-Add-in. Im Prinzip wäre eine Implementation ohne die Datenklasse Zeichenfolge denkbar. Schließlich kapselt diese lediglich eine string-Eigenschaft. Durch die Einführung dieser Datenklasse ist die Typisierung der Input- und Outputpins jedoch strenger, da so nur Input- und Outputpins verbunden werden können, die eine Zeichenfolge als Argument erwarten. Das fördert nicht nur die Verständlichkeit, sondern vereinfacht auch ein automatisiertesVerdrahten der Ak- www.dotnetpro.de dotnetpro.dojos.2011 tionen, auch wenn das hier nicht verwendet wird. Ferner verwendet die Implementation so auch die Ubiquitous Language, die allgegenwärtige Sprache des Projektes, was ebenfalls zur Verständlichkeit beiträgt. Die Implementation der Textzerlegung sieht am Ende so aus wie in Listing 4. Was mich daran stört, ist der Umgang mit den Zeilenumbrüchen. Diese werden nämlich hier ebenfalls behandelt, obwohl die Aufgabe der Klasse das Zerlegen des Textes in Zeichenfolgen ist. Damit kümmert sich die Klasse um zwei Dinge und verstößt so gegen das Single Responsibility Principle [4]. In der nächsten Iteration würde ich das ändern und die Behandlung der Zeilenumbrüche herausziehen. Als Nächstes kam die Silbentrennung an die Reihe. Durch den Einsatz von NHunspell steht die Funktionalität bereits zur Verfügung. Es geht also lediglich darum, das NHunspell-API an unsere Bedürfnisse anzupassen. Die Implementation ist einfach, bedarf aufgrund der Verwendung von LINQ aber einer kurzen Erklärung, siehe Listing 5. Der Inputpin erhält eine Aufzählung von Zeichenfolgen. Für jede Zeichenfolge muss die Silbentrennung aufgerufen werden. Anschließend muss jeweils eine Instanz vom Typ GetrennteZeichenfolge erstellt werden. Das riecht nach einer Schleife, ist aber mit LINQ viel eleganter realisierbar. Doch zuvor habe ich mich auf die eigentliche Kernfunktionalität konzentriert, das Trennen einer einzelnen Zeichenfolge. Daher habe ich eine internal-Methode erstellt, welche eine einzelne Zeichenfolge in Silben trennt. Diese Methode habe ich isoliert getestet. Damit die Tests möglichst wenig Rauschen enthalten und dadurch gut verständlich sind, arbeitet die Methode mit strings statt mit Zeichenfolge und GetrennteZeichenfolge. Listing 6 zeigt einen der Tests. Diese Vorgehensweise bietet den großen Vorteil, dass die beiden Concerns „Silbentrennung“ und „Iterieren“ sauber getrennt sind. Das vereinfacht die Tests und schafft Übersichtlichkeit in der Implementation. Das Iterieren erledigt dann LINQ. Durch Einsatz von Select wird über die Aufzählung der Zeichenfolgen iteriert und jeweils eine Instanz von GetrennteZeichenfolge erzeugt. Zusammensetzen Die Silben müssen nun wieder zu Zeilen zusammengefasst werden. Dabei muss zum einen die gewünschte Breite der Zeilen berücksichtigt werden. Zum anderen müssen gegebenenfalls Trennstriche am Zeilenende Listing 2 Ein erster Test. private ZerlegenInZeichenfolgen sut; private IEnumerable<Zeichenfolge> result; [SetUp] public void Setup() { sut = new ZerlegenInZeichenfolgen(); sut.Out_Result += wörter => result = wörter; } [Test] public void Einzelnes_Wort() { sut.In_Process("A"); Assert.That(result, Is.EqualTo(new[] { new Zeichenfolge("A") })); } Listing 3 Klasse für die Zeichenfolge. public class Zeichenfolge { public Zeichenfolge(string text) { Text = text; } public string Text { get; private set; } } ergänzt werden. Dazu muss der Zusammenhang von Silben und Wörtern bekannt sein. Am Ende stellte sich heraus, dass das Zusammenfassen der Silben zu Zeilen die schwierigste Funktionseinheit darstellt. Hier hatte ich mal wieder das Gefühl, dass ich ohne automatisierte Tests völlig aufgeschmissen wäre. Während der Implementation enthielt die Schleife sogar zeitweise ein goto. Doch die Community konnte mich über Twitter [5] überzeugen, dass dies keine gute Idee ist. Und am Ende ging es tatsächlich auch ganz leicht ohne dieses Konstrukt. Um aber zu dem hier in Listing 7 gezeigten Ergebnis zu kommen, waren einige Refaktorisierungen notwendig. Ich glaube, dass diese Methode auch ohne den Abdruck aller verwendeten privaten Methoden verständlich ist. Die Details können Sie sich wie gewohnt im Quellcode auf der Heft-DVD anschauen. Die hier gezeigte Methode realisiert das Zusammenfassen der Silben zu Zeilen auf relativ hohem Abstraktionsniveau. Die Bedingungen für die zahlreichen if-Statements sind konsequent als Methoden herausgezogen. Das bietet den Vorteil, einen sprechenden Namen verwen- 53 LÖSUNG Listing 4 Texte zerlegen. public class ZerlegenInZeichenfolgen { public void In_Process(string text) { Out_Result(TrenneInZeichenfolgen(text)); } private static IEnumerable<Zeichenfolge> TrenneInZeichenfolgen(string text) { var textOhneZeilenumbruch = text.Replace(Environment.NewLine, " "); var zeichenfolgen = textOhneZeilenumbruch.Split(' '); for (var i = 0; i < zeichenfolgen.Length; i++) { var zeichenfolge = zeichenfolgen[i]; yield return new Zeichenfolge(zeichenfolge); if (IstNichtDieLetzteZeichenfolge(i, zeichenfolgen.Length)) { yield return new Zeichenfolge(" "); } } } public event Action<IEnumerable<Zeichenfolge>> Out_Result; private static bool IstNichtDieLetzteZeichenfolge(int i, int anzahlZeichenfolgen) { return i + 1 < anzahlZeichenfolgen; } } Listing 5 Die Silbentrennung implementieren. public class ZeichenfolgenInSilbenTrennen { private readonly Hyphen hyphen; public ZeichenfolgenInSilbenTrennen() { hyphen = new Hyphen("hyph_de_DE.dic"); } public void In_Process(IEnumerable<Zeichenfolge> zeichenfolgen) { Out_Result(zeichenfolgen.Select( x => new GetrennteZeichenfolge(x.Text) { Silben = Silben(x.Text) })); } public event Action<IEnumerable<GetrennteZeichenfolge>> Out_Result; internal IEnumerable<string> Silben(string zeichenfolge) { var result = hyphen.Hyphenate(zeichenfolge); return (result == null) ? new[]{""} : result.HyphenatedWord.Split('='); } } den zu können. So muss man beim Lesen nicht interpretieren, was die Bedingung eigentlich testet, sondern kann die Bedeutung aus dem Namen ableiten. Bei dieser Implementation wäre es interessant auszuprobieren, wie schwierig es ist, die Zeilenbreite nicht in Zeichen, sondern in Millimetern zu definieren. Dazu müsste die Schriftart des Textes herangezogen werden, um die tatsächliche Breite ermitteln zu können. Gerade bei proportionalen Schriftarten ist dies wichtig, da hier jedes Zeichen seine eigene Breite hat. Ferner kann man nicht einfach die Zeichenbreiten addieren, da bestimmte Zeichenkombinationen enger aneinandergestellt werden als andere. 54 Die Entscheidung, ob eine Silbe noch in die Zeile passt oder die nächste Zeile begonnen wird, steht an genau einer Stelle in der Methode SilbePasstNochInDieZeile. Hier müsste man also mit einer entsprechenden Erweiterung ansetzen. Richtig spannend wird es bei solchen Erweiterungen ja immer dann, wenn nicht der ursprüngliche Autor des Codes diese Erweiterung vornimmt, sondern jemand, der den Code bislang noch nicht kennt. Ein Test-GUI Bei konsequentem Einsatz von automatisierten Unit-Tests verliert man schon mal die Integration aus den Augen. Aber auch diese muss getestet werden. Also habe ich Tests ergänzt, welche „ganz oben“ auf dem öffentlichen API aufsetzen. So ist sichergestellt, dass die Integration von Aktivität und Aktionen korrekt funktioniert. Bei dieser einfachen Aufgabenstellung hat mir das Tracer Bullet Feature schon die Sicherheit gegeben, dass die Integration der einzelnen Funktionseinheiten korrekt ist. In komplexeren Szenarien sind dazu häufig mehrere automatisierte Integrationstests erforderlich. Aber selbst Unit-Tests plus Integrationstests genügen nicht. Man kommt nicht umhin, auch den Bereich der explorativen Tests abzudecken. Je leichter es fällt, die Funktionalität „mal eben auf die Schnelle“ auszuprobieren, desto größer ist die Wahrscheinlichkeit, Fehler zu finden. Aus gutem Grund sind Softwaretester nicht plötzlich überflüssig geworden, nur weil die Entwickler ihren Code endlich selber automatisiert testen. Ich habe also ein Test-GUI erstellt, ganz in Anlehnung an den Entwurf, der in der Aufgabenstellung abgedruckt war. Und schon der erste Versuch mit dem Test-GUI förderte einen Fehler zutage: Ich habe einfach mal auf den Umbrechen-Schalter geklickt, um einen leeren Text zu umbrechen. Dabei zeigte sich, dass die Silbentrennung NHunspell bei einem leeren Eingabetext null als Ergebnis liefert. Sehr unschön, aber so ist es nun mal. Was tun? Klar ist: Man muss null abfangen und eine leere Silbenliste zurückliefern. Aber vorher sollte das Problem durch einen automatisierten Test reproduziert werden. Dieser sollte so nah wie möglich an der für das Problem verantwortlichen Funktionseinheit ansetzen. Also bei der Aktion ZeichenfolgenInSilbenTrennen. So ist sichergestellt, dass der Test fokussiert und überschaubar bleibt. Hier zeigt sich übrigens, wie wichtig es ist, Funktionalität zu kapseln. Im Entwurf ist eine Schnittstelle für die Silbentrennung entstanden. Dabei habe ich keine Rücksicht auf NHunspell genommen (mir war dieses fragwürdige Verhalten vorher gar nicht bekannt). Hätte ich NHunspell direkt verwendet, ohne eine eigene Klasse drumherum zu legen, wäre es möglicherweise schwieriger, dieses unschöne Verhalten an einer Stelle zu beseitigen. Dies gilt übrigens auch für den Rückgabewert. NHunspell liefert als Ergebnis nicht etwa eine Liste der Silben, sondern einen string, in dem die Silben durch ein Gleichheitszeichen abgetrennt sind. Für Feuerwehrauto wird Feu=er=wehr=au=to geliefert. Dies muss dem Entwurf gemäß umgesetzt werden, in eine Liste von Silben. dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Die zweite Erkenntnis aus den Versuchen mit dem Test-GUI: Zeilenumbrüche wurden nicht berücksichtigt. In den Anforderungen habe ich diese zwar aufgeführt, aber nicht sofort implementiert. Das war schnell nachgeholt, allerdings mit den bereits weiter oben erwähnten Einschränkungen. Dritte Erkenntnis: Nicht trennbare Wörter können länger als die maximale Zeilenlänge sein. Ferner können in trennbaren Wörtern Silben auftreten, die länger als die maximale Zeilenlänge sind. Dadurch drehte sich eine Schleife beim Zusammenfassen von Silben zu Zeilen im Kreis. Auch hier habe ich erst zwei Tests ergänzt, um den Fehler automatisiert reproduzieren zu können. Erst danach habe ich das Problem behoben. Die Vorgehensweise ist hierbei pragmatisch: Zu lange Wörter oder Silben werden nicht umbrochen. Das nächste Problem, das ich durch Ausprobieren mit dem Test-GUI identifiziert habe, hängt damit zusammen, wie NHunspell auf Interpunktionszeichen reagiert. Beim Zerlegen des Textes in Zeichenfolgen werden Interpunktionszeichen nicht gesondert betrachtet, sondern einfach an die Wörter mit angehängt. Dadurch entstanden aber merkwürdige Trennungen. So wurde „Welt“ in „Wel-t“ getrennt. Auch hier konnten automatisierte Tests das Verhalten von NHunspell reproduzieren. Hängt nämlich am Wort „Welt“ noch ein Punkt, also „Welt.“, trennt NHunspell es zu „Wel-t.“. Ich habe dieses Verhalten lediglich durch entsprechende Tests dokumentiert, an der Implementierung jedoch nichts geändert. Die Berücksichtigung von Interpunktionszeichen würde eine ganze Reihe von Änderungen nach sich ziehen, die den Umfang dieser Übung sprengen würden. Listing 6 Die Silbentrennung testen. [Test] public void Feuerwehrauto_wird_getrennt() { Assert.That(sut.Silben("Feuerwehrauto"), Is.EqualTo(new[] {"Feu", "er", "wehr", "au", "to"})); } Listing 7 Silben zu Zeilen zusammensetzen. private IEnumerable<string> ErzeugeZeilen(IEnumerable<GetrennteZeichenfolge> zeichenfolgen) { var zeile = ""; foreach (var zeichenfolge in zeichenfolgen) { foreach (var silbe in zeichenfolge.Silben) { if (SilbeIstLeerraumAmZeilenanfang(zeile, silbe)) { continue; } if (SilbePasstNochInDieZeile(zeile, zeichenfolge, silbe) || IstLeerraum(zeile)) { zeile += silbe; continue; } if (TrennstrichErforderlich(silbe, zeichenfolge)) { zeile += "-"; } yield return zeile; zeile = IstLeerraum(silbe) ? "" : silbe; } } if (ZeileIstNichtLeer(zeile)) { yield return zeile; } } fallen Absätze natürlich ebenfalls unter den Tisch. Hier müssten also zwei hintereinander stehende Zeilenumbrüche anders behandelt werden. Auch das wäre mit einem endlichen Automaten realisierbar. Fazit und Nachtrag Erweiterungen Einige Erweiterungsmöglichkeiten sind mir durch „Spielen“ mit dem Test-GUI aufgefallen. Der Umgang mit mehreren aufeinanderfolgenden Leerzeichen ist in meiner Implementation stark vereinfacht. Da zum Trennen des Textes in Zeichenfolgen die Methode string.Split verwendet wird, bleiben die Leerzeichen nicht ordnungsgemäß erhalten. Die Implementation müsste also erweitert werden, da string.Split doch zu simpel für die Aufgabe ist. Vermutlich wäre hier ein endlicher Automat besser geeignet. Eine weitere Vereinfachung betrifft Absätze. Zeilenumbrüche werden derzeit komplett entfernt, bevor mit der Trennung in Zeichenfolgen begonnen wird. Dadurch www.dotnetpro.de dotnetpro.dojos.2011 Einerseits bin ich überrascht, dass sich mit vergleichsweise wenig Aufwand doch eine recht leistungsfähige Textumbruchkomponente realisieren lässt. Andererseits zeigt sich, dass man für ein reales Projekt weitaus mehr Aufwand in die Analyse der Anforderungen und den Entwurf stecken müsste. Das wird beispielsweise beim Umgang mit Leerraum und Zeilenumbrüchen deutlich. Ich bin, nachdem dieser Artikel fertig war, noch der Frage nachgegangen, ob die Lösung tatsächlich so evolvierbar ist, dass eine Umstellung von Breite in Zeichen auf Breite in Millimetern leicht zu bewerkstelligen ist. Um ein aussagekräftigeres Ergebnis zu erreichen, habe ich Ralf Westphal gebe- ten, diese Änderung ebenfalls vorzunehmen. Dazu habe ich ihm lediglich den Code und eine Entwurfskizze zur Verfügung gestellt. Mein Ergebnis: Nach 50 Minuten war die Änderung fertig. Und erfreulicherweise hat auch Ralf es in dieser Zeit geschafft. Das lag zum einen daran, dass der Entwurf 1:1 in der Implementation zu finden ist. So konnte er anhand des Entwurfs verorten, wo Änderungen vorzunehmen sind. Zum anderen hat die konsequente Einhaltung der EBC-Konventionen geholfen. Ein schönes Ergebnis. [ml] [1] NHunspell, http://nhunspell.sourceforge.net [2] Blauer 5. Grad der Clean Code Developer, http://clean-code-developer.de/wiki/ CcdBlauerGrad [3] DebugView for Windows v4.76, www.dotnetpro.de/SL1012dojoLoesung1 [4] Oranger 2. Grad der Clean Code Developer, http://clean-code-developer.de/wiki/ CcdOrangerGrad [5] http://twitter.com/stefanlieser 55 AUFGABE Algorithmen und Datenstrukturen Wie viele Blätter hat der Baum? Baumstrukturen sind in der Informatik allgegenwärtig. Wer selbst Bäume implementiert, lernt dabei viel über ihre Arbeitsweise. Stefan, kannst du dazu eine Übung stellen? Wer übt, gewinnt dnpCode: A1012DojoAufgabe 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 dotnetpro finden. Übung macht den Meister. Also − los geht’s. Aber Sie wollten doch nicht etwa sofort Visual Studio starten… W enn man den sprichwörtlichen Wald vor lauter Bäumen nicht mehr sehen kann, mag es helfen, einmal über die Implementierung von Bäumen nachzudenken. Im .NET Framework gibt es dazu zwar keine generische Implementation, dennoch werden Bäume auch dort verwendet, nämlich an so prominenter Stelle wie LINQ. Genauer gesagt übersetzt der Compiler Lambda-Ausdrücke in sogenannte Expression Trees. Auf diese Weise ist es möglich, aus LINQ-Ausdrücken SQL-Code zu erzeugen. Mit den WCF-RIA-Services können LINQ-Ausdrücke sogar übers Netz übertragen werden. Doch wie implementiert man eine solche Datenstruktur? Dazu soll hier nur das API vorgegeben werden. Die Realisierung ist Ihre Aufgabe für diesen Monat. Ein Baum hat immer genau einen Wurzelknoten. Dieser und alle anderen Knoten können beliebig viele untergeordnete Knoten, sogenannte Kinder, haben. Das API für Bäume besteht aus zwei Interfaces, einem für den Baum, genannt ITree<T>, sowie einem für die Knoten, genannt INode<T>, siehe Listing 1. Das Interface für den Baum ist simpel, es enthält lediglich den Wurzelknoten des Baums. Beim Knoten sieht das schon anders aus. Jeder Listing 1 Interfaces für die Baumstruktur. public interface ITree<T> { INode<T> Root { get; } } public interface INode<T> { T Value { get; } Node<T> Add(T nodeValue); IEnumerable<Node<T>> Children { get; } IEnumerable<T> ChildValues { get; } IEnumerable<T> PreOrderValues(); IEnumerable<T> PostOrderValues(); } 56 [Abb. 1] Eine einfache Baumstruktur. Knoten hat einen Wert. Dieser ist vom generischen Typ T. Der Wert eines Knotens kann über die Eigenschaft Value gelesen werden. Um einem Knoten einen Kindknoten hinzuzufügen, rufen Sie die Add-Methode auf und übergeben den Wert des neuen Knotens. Für den Wert müssen Sie intern einen Knoten anlegen und in die Children-Liste aufnehmen. Mit der Eigenschaft ChildValues können Sie die Werte aller Kindknoten eines Knotens abrufen. Nun geht es an das Traversieren des Baums. Im Gegensatz zum Traversieren von Listen ist das Traversieren von Bäumen auf unterschiedliche Weise möglich. Bei Listen wird ein Element nach dem anderen geliefert. Bei Bäumen ist aber die Frage, ob zuerst der Knoten und dann seine Kinder geliefert werden sollen (Pre-Order) oder umgekehrt (Post-Order). Am einfachsten wird das an einem Beispiel deutlich. Abbildung 1 zeigt einen Baum. Bei der sogenannten Pre-Order-Traversierung wird der Knoten vor seinen Kindern geliefert. Für den Baum aus Abbildung 1 ergibt das folgende Reihenfolge: 1, 2, 5, 6, 3, 7, 8, 4, 9, 10 Bei der Post-Order-Traversierung werden zuerst die Kinder geliefert, dann der Knoten selbst. Für den Beispielbaum ergibt sich daher folgendes Ergebnis : 5, 6, 2, 7, 8, 3, 9, 10, 4, 1 Damit sind die Anforderungen klar. Implementieren Sie die Datenstruktur und die zugehörigen Traversierungsalgorithmen, natürlich inklusive automatisierter Unit-Tests. Happy Learning! [ml] dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Algorithmen und Datenstrukturen So bauen Sie Bäume Im .NET Framework gibt es keine vordefinierte Datenstruktur für Bäume. Wer seine Daten in einer Baumstruktur ablegen will, muss sich diese Struktur selbst implementieren. Eine ideale Aufgabe für das dotnetpro.dojo! A lgorithmen und Datenstrukturen stellen auch heute noch eine wichtige Grundlage der Softwareentwicklung dar. Zwar sind viele der Datenstrukturen im Laufe der Zeit in die Frameworks gewandert, sodass man als Entwickler heute nur noch selten eine der klassischen Strukturen wie Listen oder Stacks selbst implementieren muss. Andererseits ist es für einen Entwickler wichtig, eine Vorstellung davon zu erlangen, was hinter den Kulissen geschieht. Nicht zuletzt eignen sich Datenstrukturen sehr gut dafür, die testgetriebene Entwicklung einzuüben. Und da die Datenstruktur „Baum“ im .NET Framework nicht zur Verfügung steht, lohnt es sich tatsächlich, eine solche Implementation vorzunehmen. Die Aufgabenstellung hat das API für Bäume vorgegeben, siehe Listing 1. Sie besteht aus zwei Interfaces: einem für den Baum, genannt ITree<T>, und einem für die Knoten, genannt INode<T>. Ein Baum besteht aus einem Wurzelknoten, Root genannt. Dieser hat selbst einen Wert (Value) sowie Kindknoten (Children). Dabei bezieht sich der Baum auf das Interface INode. Der Baum selbst fügt eigentlich keine Funktionalität hinzu, sondern bezieht diese aus den Knoten. Bei der Implementation habe ich mit einem einzelnen Knoten Node<T> begonnen. Der erste Test betrifft die Value-Eigen- Listing 1 Das API für die Bäume. public interface ITree<T> { INode<T> Root { get; } } public interface INode<T> { T Value { get; } Node<T> Add(T nodeValue); IEnumerable<Node<T>> Children {get;} IEnumerable<T> ChildValues { get; } IEnumerable<T> PreOrderValues(); IEnumerable<T> PostOrderValues(); } www.dotnetpro.de dotnetpro.dojos.2011 Listing 2 Ein erster Test. [Test] public void Node_liefert_den_Konstruktorwert_als_Value() { var node = new Node<int>(42); Assert.That(node.Value, Is.EqualTo(42)); } Listing 3 Die Children-Eigenschaft testen. [Test] public void Node_hat_nach_dem_Instanzieren_keine_Nachkommen() { var node = new Node<string>(""); Assert.That(node.Children, Is.Empty); } schaft für den Wert des Knotens. Da das Interface lediglich einen Getter erwartet, müssen Sie sich überlegen, wie ein Knoten zu seinem Wert kommt. Eine Möglichkeit wäre, die Eigenschaft zusätzlich mit einem Setter auszustatten. Allerdings wären die Knoten damit veränderbar. Solange Sie dies nicht benötigen, genügt ein privater Setter, um den Wert einmalig setzen zu können. Der Wert muss dann im Konstruktor zugewiesen werden, da private Setter nur innerhalb der Klasse verwendet werden können. Solche nicht änderbaren Objekte, sogenannte Immutable Objects, bieten den Vorteil, dass man damit den Problemen des parallelen Zugriffs bei Multithreading aus dem Weg geht. Mein erster Test prüft also, ob die ValueEigenschaft den Wert liefert, der dem Knoten im Konstruktor übergeben wurde, siehe Listing 2. Dies ist nur ein Minischritt, und es ist fraglich, ob dieser Test von hohem Wert ist. Andererseits muss für diesen ersten Test die Klasse Node<T> erzeugt werden. Es entsteht also das grobe Codegerüst der Klasse, welches erforderlich ist, um das Interface zu implementieren. Gerade Anfängern der testgetriebenen Entwick- lung sei empfohlen, auch solche Minischritte zu gehen. Das flexible Anpassen der „Schrittweite“ beim testgetriebenen Entwickeln hat viel mit Erfahrung zu tun und muss daher geübt werden. Der nächste Test betrifft die ChildrenEigenschaft. Nach dem Instanzieren eines neuen Knotens soll diese Liste leer sein. Ganz wichtig an dieser Stelle: Vergessen Sie null in dem Zusammenhang. Es ist keine gute Idee, hier null zu liefern anstelle einer leeren Liste, weil dann der Verwender der Klasse vor jedem Zugriff eine null-Prüfung vornehmen müsste. Die Tatsache, dass der Knoten keine Nachfolger hat, wird perfekt repräsentiert durch eine leere Liste. Diese kann beispielsweise auch ohne vorherige Prüfung mit foreach durchlaufen werden. Wenn die Liste leer ist, wird die Schleife halt nicht ausgeführt. Eine zusätzliche null-Prüfung würde den Code nur unnötig aufblähen. Listing 3 zeigt den Test für die Children-Eigenschaft. Die Implementation ist einfach, der Test treibt also auch hier noch nicht viel voran. Sie definieren damit jedoch den Initialzustand eines Knotens. Sich darüber klarzuwerden ist nicht ganz unwichtig. 57 LÖSUNG Listing 4 Listing 6 ChildValues testen. Einen neuen Knoten hinzufügen. [Test] public void Node_hat_nach_dem_Instanzieren_keine_Nachkommenwerte() { var node = new Node<int>(1); Assert.That(node.ChildValues, Is.Empty); } public Node<T> Add(T nodeValue) { var node = new Node<T>(nodeValue); children.Add(node); return node; } Listing 5 Einen neuen Knoten testen. [Test] public void Für_einen_hinzugefügten_Wert_wird_ein_Knoten_angelegt() { var node = new Node<int>(1); var child = node.Add(2); Assert.That(node.Children, Is.EquivalentTo(new[] {child})); } Weiter geht es mit der Eigenschaft ChildValues, siehe Listing 4. Auch hier ist die Implementation für einen Knoten ohne Nachkommen trivial. Immerhin haben Sie jetzt die Initialwerte aller Eigenschaften definiert und können sich einer neuen Aufgabe zuwenden. Die einzige ändernde Operation, die ein Knoten anbietet, ist das Hinzufügen eines weiteren Knotens. Dabei habe ich mich in der API-Definition dafür entschieden, dem Knoten einen weiteren Wert hinzuzufügen. Nehmen wir an, die Knoten hätten Zeichenketten als Werte. Dann gäbe es für das API die beiden folgenden Möglichkeiten : ❚ Hinzufügen eines Knotens vom Typ Node<string>. ❚ Hinzufügen einesWertes vom Typ string. Ferner muss der Wert des Knotens in der Liste der Nachkommenswerte (ChildValues) auftauchen. Dass der neu erzeugte Knoten von Add als Ergebnis zurückgeliefert wird, ist übrigens nicht der Testbarkeit geschuldet. Beim Aufbauen eines Baumes ist es handlich, auf die jeweils erzeugten Knoten zugreifen zu können, weil auf diesen dann weitere Methoden aufgerufen werden können. Damit wird das API für den Anwender komfortabel in der Benutzung. Übrigens habe ich diesen Test in einer weiteren Testklasse angelegt. Die ersten drei Tests befassen sich mit dem Initialzustand von Knoten, daher habe ich die Testklasse Node_Instanzieren_Tests genannt. Das Hinzufügen von Knoten mit der AddMethode teste ich in der Klasse Node_ Add_ Tests. Die Implementation der Add-Methode verwendet eine interne Liste aller Nachkommen des Knotens. Dieser Liste wird bei Add ein neues Element hinzugefügt, siehe Listing 6. Der nächste Test prüft, ob der hinzugefügte Wert in der Eigenschaft ChildValues vertreten ist, siehe Listing 7. Mithilfe von LINQ ist die Implementation der ChildValues-Eigenschaft simpel, siehe Listing 8. Die Select-Methode aus dem Namespace System.Linq ist eine Extension Method auf IEnumerable<T>. Dadurch steht sie auf allen Aufzählungen zur Verfügung. Ergebnis der Select-Methode ist wieder eine Aufzählung. Der übergebene Lambda-Ausdruck gibt an, wie die Werte für die Ergebnisaufzählung gebildet werden sollen. Im vorliegenden Fall sollen aus der Aufzählung von Knoten alle Werte extrahiert werden. Folglich gibt der Lambda-Ausdruck an, dass node.Value geliefert werden soll. Wenn die Knoten implementiert sind, können Sie sich dem Baum zuwenden. Da Listing 7 Das Hinzufügen eines Knotens sähe in der Anwendung wie folgt aus : Das erfolgreiche Hinzufügen eines Knotens testen. node.Add(new Node("a"); [Test] public void Der_hinzugefügte_Wert_wird_in_die_ChildValues_aufgenommen() { var node = new Node<char>('x'); node.Add('y'); Assert.That(node.ChildValues, Is.EquivalentTo(new[]{'y'})); } Der erforderliche Aufruf des Konstruktors kann entfallen, wenn das API die Möglichkeit bietet, direkt einen Wert hinzuzufügen : node.Add("a"); Die Add-Methode muss folglich einen neuen Knoten anlegen und mit dem übergebenen Wert initialisieren, siehe Listing 5. Als Ergebnis liefert die Add-Methode den neu eingefügten Knoten als Rückgabewert zurück. So können Sie nach dem Hinzufügen beobachten, dass der neue Knoten in der Liste der Nachkommen vorhanden ist. 58 Listing 8 LINQ nutzen. public IEnumerable<T> ChildValues { get { return Children.Select(node => node.Value); } } dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Listing 9 Einen neu angelegten Baum prüfen. [Test] public void Tree_mit_einem_Wurzelknoten_initialisieren() { var tree = new Tree<string>("Wurzel"); Assert.That(tree.Root.Value, Is.EqualTo("Wurzel")); } dieser nur die Eigenschaft des Wurzelknotens Root hat, sollte die Implementation leicht von der Hand gehen. Ähnlich wie beim Wert eines Knotens hat auch die Root-Eigenschaft nur einen Getter. Auch hier habe ich mich entschieden, den Konstruktor des Baumes für die Initialisierung zu verwenden. Da beim Knoten im Konstruktor der Wert des Knotens übergeben wird, ist es konsequent, beim Baum ebenso zu verfahren. Der Konstruktor des Baumes legt also den Wurzelknoten an und initialisiert diesen mit dem übergebenen Wert. Listing 9 zeigt den zugehörigen Test. Auch hier bietet es sich an, den Test in eine eigene Testklasse zu schreiben. Schließlich geht es nicht um Knoten, sondern um Bäume. Ich habe die Testklasse Tree_Tests genannt. Traversieren Nun geht es an das Traversieren der Bäume. Dazu sind im API die Methoden PreOr- Listing 10 Pre-Order-Traversierung testen. [Test] public void Ein_Knoten_mit_Nachkommen() { var node = new Node<int>(1); node.Add(2); node.Add(3); Assert.That(node.PreOrderValues(), Is.EqualTo(new[]{1, 2, 3})); } Listing 11 Pre-Order-Traversierung durchführen. public IEnumerable<T> PreOrderValues(){ yield return Value; foreach (var child in Children) { yield return child.Value; } } www.dotnetpro.de dotnetpro.dojos.2011 derValues und PostOrderValues zu implementieren. Die beiden Methoden müssen jeweils den Baum durchlaufen und alle Knotenwerte in Form einer Aufzählung IEnumerable<T> liefern. Auch hier stand die Entscheidung an, ob die Knoten oder deren jeweilige Werte geliefert werden sollen. Alternativ wäre das Ergebnis also vom Typ IEnumerable<INode<T>>. Sollte sich später zeigen, dass eine solche Aufzählung benötigt wird, können die zugehörigen Methoden leicht ergänzt werden. Zunächst steht wieder die Frage an, wie der erste Test aussehen soll. Einen „leeren“ Baum zu traversieren ist nicht wirklich spannend. Vor allem würde durch diesen Test die Implementation nicht wirklich vorangetrieben. Schließlich besteht ein neu initialisierter Baum lediglich aus einem Knoten, der keine Nachkommen hat. Am anderen Ende stehen Tests, für die gleich die komplette Traversierung implementiert werden muss. Ein Test „dazwischen“ wäre gut: mehr als nur ein einzelner Knoten, aber weniger als gleich ein ganzer Baum. Ein einzelner Knoten mit Nachkommen dürfte das Kriterium erfüllen. Damit ergeben sich folgende Testszenarien : ❚ Traversieren eines Knotens, der keine Nachkommen hat; ❚ Traversieren eines Knotens, der Nachkommen hat; ❚ Traversieren eines Knotens, dessen Nachkommen ebenfalls Nachkommen haben. Ob man nun mit dem ersten oder dem zweiten Szenario beginnt, ist Geschmacksache. Das Traversieren eines einzelnen Knotens ohne Nachkommen ist eigentlich so trivial, dass man es nicht gesondert testen muss. Spürt man aber Unsicherheit bei der Implementation, hilft es möglicherweise, in solch kleinen Minischritten voranzuschreiten. Wichtig ist jedoch festzuhalten, dass man sich vor dem ersten Test Gedanken über die Testdaten machen muss. Dabei hilft es, Beispiele zu sammeln und diese anschließend in Äquivalenzklassen zu- sammenzufassen. So liegt beispielsweise das Traversieren eines Knotens mit drei Nachkommen in der gleichen Äquivalenzklasse wie das Traversieren von fünf Nachkommen. Haben die Nachkommen jedoch selbst wieder Nachkommen, so ergibt sich eine andere Äquivalenzklasse, denn hier muss plötzlich rekursiv vorgegangen werden. Würde man gleich mit einem ersten Test beginnen, ohne vorher über die Testdaten und eine sinnvolle Reihenfolge der Implementation nachzudenken, wäre der anstehende Implementationsaufwand für den nächsten Schritt möglicherweise zu groß. Nachdenken hilft! Die Tests zur Traversierung habe ich wieder in eine eigene Testklasse abgelegt. Sie heißt Node_Pre_Order_Traversieren_Tests. Für die Implementation dieses Tests von Listing 10 ist es lediglich notwendig, erst den eigenen Knotenwert und anschließend die Werte der Nachkommen zu liefern. Unter tatkräftiger Mithilfe von yield return sieht die Implementation dazu wie in Listing 11 aus. Ergänzt man nun einen Test für das erste Szenario, bei dem ein Knoten keine Nachkommen hat, wird man feststellen, dass dieser sofort erfolgreich verläuft, ohne dass an der Implementation etwas geändert werden muss. Doch nun geht es ans Eingemachte. Der letzte Test befasst sich mit dem Szenario eines Knotens, dessen Nachkommen ebenfalls Nachkommen haben, siehe Listing 12. Das riecht doch sehr nach Rekursion! Die Implementation könnte folgendermaßen aussehen: Gib den eigenen Knotenwert aus, und rufe anschließend die Traversierung für jeden Nachkommen auf. Dazu müssen Sie allerdings die bisherige Implementation zunächst in eine Methode auslagern, die einen Knoten als Parameter erhält. Die Methode ist dann dafür verantwortlich, Listing 12 Einen Baum testen. [Test] public void Ein_Knoten_deren_Nachkommen_auch_Nachkommen_haben() { var node1 = new Node<int>(1); var node2 = node1.Add(2); var node3 = node2.Add(3); var node4 = node2.Add(4); Assert.That(node1.PreOrderValues(), Is.EqualTo(new[] { 1, 2, 3, 4 })); } 59 LÖSUNG Listing 13 Listing 14 PreOrderValues refaktorisieren. Rekursion einführen. public IEnumerable<T> PreOrderValues() { return TraversePreOrder(this); } private static IEnumerable<T> TraversePreOrder(INode<T> node) { yield return node.Value; foreach (var child in node.Children) { yield return child.Value; } } private static IEnumerable<T> TraversePreOrder(INode<T> rootNode) { yield return rootNode.Value; foreach (var child in rootNode.Children) { foreach (var childValue in TraversePreOrder(child)) { yield return childValue; } } } Listing 15 Post-Order-Traversierung implementieren. einen einzelnen Knoten zu traversieren und kann sich dabei rekursiv aufrufen. Der erste Schritt ist also eine Refaktorisierung, bei der eine Methode mit einem Knoten als Parameter eingeführt wird, siehe Listing 13. Nach dieser Refaktorisierung können Sie die Rekursion in der Methode TraversePreOrder einführen, siehe Listing 14. Durch Verwendung von yield return kann hier das Ergebnis des rekursiven Aufrufs nicht direkt als Resultat zurückgegeben werden. Schließlich ist das Ergebnis vom Typ IEnumerable<T>. yield return erwartet aber einzelne Elemente vom Typ T. Daher muss das Ergebnis in einer Schleife durchlaufen werden und Element für Element an yield return übergeben werden. Und jetzt andersrum Um die Post-Order-Traversierung zu implementieren, ist nur eine kleine Änderung notwendig. Sie müssen lediglich die Reihenfolge der Bearbeitung umstellen. Statt zuerst den Knotenwert auszugeben und dann die Nachkommen zu bearbeiten, werden erst die Nachkommen bearbeitet, und danach wird der Knotenwert ausgegeben, siehe Listing 15. Vergessen Sie bei diesem Copy-PasteVorgang nicht, den rekursiven Aufruf anzupassen. Hier muss TraversePostOrder rekursiv aufgerufen werden. Bei der Kontrolle kann mal wieder JetBrains ReSharper behilflich sein. In Abbildung 1 sehen Sie einen Ausschnitt aus der Methode. ReSharper erkennt den rekursiven Aufruf und markiert diesen am linken Rand durch den kreisför- private static IEnumerable<T> TraversePostOrder(INode<T> rootNode) { foreach (var child in rootNode.Children) { foreach (var childValue in TraversePostOrder(child)) { yield return childValue; } } yield return rootNode.Value; } migen Pfeil. Die Tests für die Post-OrderTraversierung befinden sich in der Klasse Node_Post_Order_Traversieren_Tests. Zuletzt habe ich die Beispiele aus der Aufgabenstellung noch in einer Testklasse mit dem Namen Integrationstests überprüft. Alternativ hätte ich sie auch Akzeptanztests nennen können, da es sich um die Beispiele handelt, die quasi zwischen Kunde und Auftragnehmer besprochen wurden. Wenn man als Entwickler mit dem Kunden Beispielfälle durchgeht mit dem Ziel, die Anforderungen zu verstehen, dann sollte man diese Beispiele als Akzeptanztests festhalten. So ist sichergestellt, dass man nach der Implementation das Gespräch mit dem Kunden leicht wieder aufnehmen kann. Zeigt man ihm dabei anhand der automatisierten Tests, dass die besprochenen Beispiele erfolgreich implementiert sind, so schafft dies eine gute Vertrauensbasis. Fazit Datenstrukturen machen Spaß! Das Schöne an der Implementation von Datenstrukturen ist, dass sich diese meist gut automa- [Abb. 1] Rekursion mit ReSharper visualisieren. 60 tisiert testen lassen. Dadurch kann man sich bei solchen Übungen darauf konzentrieren, eine geeignete Reihenfolge für die Tests zu finden. Voraussetzung ist, dass man vor dem ersten Test Testfälle sammelt. Beim Zusammenstellen der Testfälle sollte man möglichst darauf achten, ob diese in dieselbe Äquivalenzklasse fallen. Schließlich genügt es, jeweils einen Repräsentanten der Äquivalenzklasse für einen Test herauszugreifen. Bezüglich der Addition sind etwa die beiden zu addierenden Zahlen 2 und 3 sowie 4 und 5 in derselben Äquivalenzklasse. Es genügt daher, für eines der beiden Zahlenpaare die Addition zu testen. Ein zusätzlicher Test mit einem weiteren Repräsentanten derselben Äquivalenzklasse würde keinen weiteren Erkenntnisgewinn bringen. Beim Traversieren genügt es, einen Knoten zu testen, dessen Nachkommen ebenfalls einen Nachkommen haben. Es ist nicht nötig, einen Baum mit vielen Ebenen zu testen. Durch die rekursive Implementation ist dies auch ganz offensichtlich. In anderen Fällen mag es nicht so offensichtlich sein, wie die Äquivalenzklassen der Testdaten aussehen. Dann muss man möglicherweise länger darüber nachdenken und mehr Testdaten sammeln. Einfach drauflos mit den Tests zu beginnen ist aber so oder so wenig hilfreich, da man dann Gefahr läuft, Testfälle zu übersehen. [ml] dotnetpro.dojos.2011 www.dotnetpro.de AUFGABE .NET Framework Grundlagen Wie funktioniert LINQ? Manche Grundlagen versteht man besser, wenn man sie einmal selbst implementiert hat. Stefan, kannst du dazu eine Übung stellen? L INQ ist nun schon seit einiger Zeit Bestandteil des .NET Frameworks sowie der C#- und VB.NET-Compiler [1]–[4]. Dennoch geht dazu bei Entwicklern noch oft genug einiges durcheinander. Angefangen bei der Frage, wie LINQ ausgesprochen wird, bis zur Verwechslung von LINQ mit SQL oder einem Object Relational Mapper. Da lohnt es sich doch mal aufzuräumen. Beginnen wir mit der Aussprache: LINQ wird gesprochen wie „Link“. So einfach ist das. Und obwohl LINQ in Verbindung mit Object Relational Mappern verwendet wird, ist es nicht selbst ein Mapper. Mit LINQ to Objects arbeitet LINQ auf allem, was aufzählbar ist, sprich IEnumerable<T> implementiert. LINQ besteht aus zwei Teilen: ❚ der sogenannten Query Comprehension Syntax, die so sehr an SQL erinnert; ❚ einer Reihe von Extension Methods im .NET Framework aus dem Namespace System.Linq. Die Query Comprehension Syntax sorgt dafür, dass Sie im Quellcode Abfragen schreiben können wie beispielsweise diese: var query = from kunde in kunden where kunde.Ort == "Köln" select kunde; Kurz und knackig und für die meisten Entwickler gut zu lesen. Aber schon nachdem der Compiler sein Werk verrichtet hat, ist von der Query Comprehension nichts mehr übrig: Der Compiler übersetzt diese Query nämlich in äquivalente Aufrufe der Extension Methods aus dem Namespace System.Linq. Das sieht dann etwa so aus: var query = kunden .Where(kunde => kunde.Ort == "Köln") .Select(kunde => kunde); Diese Compilermagie ist dem C#- sowie dem VB.NET-Compiler spendiert worden. Die CLR hat also von LINQ keine Ahnung und musste dazu nicht verändert werden. Die eigentliche Funktionalität von LINQ steckt folglich in den Extension Methods. Diese haben nämlich die Aufgabe, den jeweiligen Teil der Query auszuführen. So wird die Where-Klausel einer Query in einen Auf- www.dotnetpro.de dotnetpro.dojos.2011 ruf der Where-Methode übersetzt. Gleiches geschieht für Select, Order, Group by etc. Um dabei möglichst flexibel zu bleiben, ist LINQ auf dem Interface IEnumerable<T> definiert. Die Signatur der Where-Methode sieht wie folgt aus: static IEnumerable<T> Where<T>(this IEnumerable<T> values, Predicate<T> predicate); Where arbeitet also auf einer Aufzählung und liefert eine solche zurück. Damit stellt die WhereMethode eine Selektion oder Filterung dar. Für jedes Element der Aufzählung wird nämlich mithilfe des Prädikats geprüft, ob die Bedingung für das Element zutrifft. Und ausschließlich Elemente, für die das Prädikat true zurückgibt, landen im Ergebnis. Um sich mit der Funktionsweise von LINQ auseinanderzusetzen, lautet die Aufgabe dieses Mal daher: Implementieren Sie LINQ Extension Methods. Beginnen Sie mit Where und Select, beide sind recht einfach. Etwas kniffliger wird es beim Gruppieren. Schauen Sie sich dazu die Signatur der GroupBy-Methode im Framework an, und überlegen Sie, wie Sie eine Gruppierung implementieren können. Anschließend gehen Sie testgetrieben vor. Weitere interessante Herausforderungen finden Sie in den Methoden Distinct, Union, Intersect und Except. Oder versuchen Sie sich an Count, Min, Max und Average. Langeweile dürfte so schnell nicht aufkommen. Viel Spaß! [ml] 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 dotnetpro finden. Übung macht den Meister. Also − los geht’s. Aber Sie wollten doch nicht etwa sofort Visual Studio starten… [1] Mirko Matytschak, Was steckt hinter LINQ? Language Integrated Queries: Neue Sprachmerkmale für C# und VB. dotnetpro 2/2006, Seite 97ff. www.dotnetpro.de/A0602Linq [2] Ralf Westphal, dotnetpro.tv: LINQ – Language Integrated Query, dotnetpro 11/2006, Seite 46, www.dotnetpro.de/A0611dotnetpro.tv [3] Patrick A. Lorenz, Kochen mit Patrick, zum Thema LINQ, dotnetpro 8/2008, Seite 116 ff., www.dotnetpro.de/A0808Kochstudio [4] Christian Liensberger, LINQ to Foo, Ein LINQ-Provider für den eigenen Datenspeicher, dotnetpro 8/2008, Seite 72ff., www.dotnetpro.de/A0808LINQ2X 61 Wer übt, gewinnt dnpCode: A1101DojoAufgabe LÖSUNG LINQ im Eigenbau So geLINQt es Grundlagen muss man gut verstanden haben. Wer sie besonders gut verstehen will, sollte sie nachbauen. Bei dem Versuch, LINQ selbst zu implementieren, hat auch Stefan wieder etwas dazugelernt. O b man eine Technologie wirklich versteht und beherrscht, merkt man, sobald man versucht, sie selbst zu implementieren. Im Fall von LINQ liegt der Schwerpunkt auf der reinen Funktionalität. Unterschätzen Sie nicht, wie viel Sie lernen können, wenn Sie vorhandene Funktionalität nachbauen. Wissen Sie beispielsweise, wie generische Methoden definiert werden? Die Reise durch LINQ soll bei der WhereMethode starten. Der erste Schritt ist die Signatur. Where ist eine Extension Method auf dem Typ IEnumerable<T>. Die Anforderungen an eine Extension Method sind überschaubar : ❚ Die Methode muss in einer statischen Klasse deklariert sein. Dadurch ist sie selbst ebenfalls statisch. ❚ Der erste Parameter der Methode muss zusätzlich mit dem Schlüsselwort this gekennzeichnet werden. Für die Where-Methode kommt hinzu, dass sie generisch sein muss. Das bedeutet, dass der Typ der Aufzählung nicht fix ist, sondern als generischer Typparameter angegeben werden kann. Lässt man die Be- dingung der Where-Methode fürs Erste einmal weg, ergibt sich folgende Signatur : Damit haben Sie alle Zutaten für die Signatur der Where-Methode zusammen : IEnumerable<T> Where<T>(this IEnumerable<T> values); public static IEnumerable<T> Where<T>(this IEnumerable<T> values, Predicate<T> predicate); Die Methode arbeitet also auf einer Aufzählung vom Typ T und liefert eine ebensolche zurück. Der generische Typparameter T muss syntaktisch beim Methodennamen deklariert werden. Doch es fehlt noch das Prädikat. In der Logik liefert ein Prädikat für ein Element einen booleschen Wert. Den erforderlichen Typ gibt es natürlich im .NET Framework, aber das gilt ja auch für die Where-Methode. Daher hier die Definition von Predicate<T>: public delegate bool Predicate<T>(T t); Die delegate-Deklaration definiert den Typ einer Methode. Der Name dieses Methodentyps lautet Predicate. Methoden dieses Typs sind generisch, der Typparameter T ist daher beim Methodentyp definiert. Des Weiteren definiert diese delegateDeklaration, dass der Rückgabewert der Methode vom Typ bool ist und dass der Methode ein Parameter vom generischen Typ T übergeben werden muss. Listing 1 Seid ihr alle da? [Test] public void Prädikat_liefert_immer_true() { var values = new[] {1, 2, 3}.Where(x => true); Assert.That(values, Is.EqualTo(new[] {1, 2, 3})); } Listing 2 Daten selektieren. [Test] public void Prädikat_liefert_nur_für_gerade_Werte_true() { var values = new[] {1, 2, 3, 4}.Where(x => x % 2 == 0); Assert.That(values, Is.EqualTo(new[] {2, 4})); } 62 Mein erster Test für die Implementation prüft, ob alle Elemente geliefert werden, wenn das Prädikat immer true liefert, siehe Listing 1. Die zugehörige Implementation ist leicht: Die Eingabe wird einfach zurückgeliefert und das Prädikat ignoriert. Der nächste Test erfordert dann bereits das Iterieren und elementweise Auswerten des Prädikats. Der in Listing 2 gezeigte Test filtert die Aufzählung nach geraden Werten. Die Implementation ist dank des Operators yield return sehr überschaubar, siehe Listing 3. Listing 3 yield return nutzen. public static IEnumerable<T> Where<T>(this IEnumerable<T> values, Predicate<T> predicate) { foreach (var value in values) { if (predicate(value)) { yield return value; } } } Wer yield return bislang nicht kannte, wird die Werte, für die das Prädikat true liefert, vermutlich in einer List<T> gesammelt haben. Doch Vorsicht, neben dem eleganteren, weil kürzeren Äußeren unterscheiden sich die beiden Lösungen deutlich in der Semantik. Verwendet man eine Liste, in der das Ergebnis zusammengestellt wird, erfolgt das Zusammenstellen komplett innerhalb der Where-Methode. Das bedeutet vor allem, dass alle Elemente gleichzeitig im Speicher Platz finden müssen. Bei kleinen Datenmengen ist das sicher kein Problem – aber überlegen Sie, was passiert, wenn Sie auf diesem Weg eine etwas größe- dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Listing 4 Listing 5 Berechnen mit Select. Select anwenden. [Test] public void String_Länge_wird_ermittelt() { var values = new[] {"abc", "a", "ab"}.Select(s => s.Length); Assert.That(values, Is.EqualTo(new[]{3, 1, 2})); } public delegate TOutput Func<TInput, TOutput>(TInput input); re Datei einlesen und dann filtern. Bei Verwendung von yield return sorgt der Compiler für etwas Magie. Denn er erzeugt einen endlichen Automaten für das Zusammenstellen der Aufzählung. Dieser sorgt dafür, dass die Methode immer nur dann aufgerufen wird, wenn wieder ein Element benötigt wird. Es muss quasi erst jemand an der Aufzählung „ziehen“, damit ein Element durch das Prädikat überprüft wird. Die Auswertung des Prädikats erfolgt somit Element für Element statt für die gesamte Eingabe auf einmal. Somit können Sie mit yield return potenziell unendlich große Datenmengen bearbeiten. Select Danach steht die Select-Methode an. Sie dient dazu, den Elementtyp der Aufzählung zu transformieren. Enthält die ursprüngliche Aufzählung beispielsweise Adressen, können Sie mit Select eine einzelne Eigenschaft selektieren. Dabei können natürlich auch Berechnungen erfolgen, wie der Test in Listing 4 zeigt. Der Test liefert zu jedem Eingangsstring dessen Länge zurück. Dazu müssen Sie die Aufzählung durchlaufen und die SelectFunktion auf jedes Element anwenden. Mit yield return ist das ebenfalls keine Hexerei, siehe Listing 5. Für die Select-Methode benötigen Sie wiederum eine delegate-Deklaration. Diesmal definieren Sie einen Methodentyp mit einem Eingangsparameter und einem Rückgabewert. Beide sind von generischem Typ. Es handelt sich damit um eine Funktion, die ein Element vom Eingabetyp TInput in den Ausgabetyp TOutput transformiert. In der Select-Methode wird diese Funktion innerhalb einer Schleife auf jedes Element der Aufzählung angewandt. Das Ergebnis wird mit yield return an den Aufrufer geliefert. GroupBy Kommen wir nun zu den etwas kniffligeren Methoden. Die GroupBy-Methode grup- www.dotnetpro.de dotnetpro.dojos.2011 public static IEnumerable<TOutput> Select<TInput, TOutput>(this IEnumerable<TInput> values, Func<TInput, TOutput> selector) { foreach (var value in values) { yield return selector(value); } } Listing 6 IGrouping implementiert IEnumerable. public interface IGrouping<TKey, TElement> : IEnumerable<TElement> { TKey Key { get; } } Listing 7 Gruppierung ermöglichen. public class Grouping<TKey, TElement> : IGrouping<TKey, TElement> { private readonly TKey key; private readonly IEnumerable<TElement> values; public Grouping(TKey key, IEnumerable<TElement> values) { this.key = key; this.values = values; } public IEnumerator<TElement> GetEnumerator() { return values.GetEnumerator(); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public TKey Key { get { return key; } } } piert die Elemente einer Aufzählung nach einem Schlüsselwert und liefert eine neue Aufzählung zurück. Die Ergebnisaufzählung enthält für jeden Schlüsselwert der Eingabe ein Element. Ein Beispiel: Sie möchten die Zahlen von 1 bis 10 danach gruppieren, ob sie gerade oder ungerade sind. Das Ergebnis von GroupBy(x => x % 2 == 0) würde dann folgendermaßen aussehen : new[]{ new[] { 1, 3, 5, 7, 9 }, new[] { 2, 4, 6, 8, 10} } Das Ergebnis ist also eine Aufzählung, die wiederum zwei Aufzählungen enthält. Der Elementtyp dieser Aufzählung lautet IGrouping<bool, int>. Der Trick an der Stelle ist: IGrouping<TKey, TElement> implementiert IEnumerable<TElement>. Dadurch sind die einzelnen Elemente der Aufzählung ebenfalls aufzählbar. Aber IGrouping hat noch eine weitere Eigenschaft, wie das Interface in Listing 6 zeigt. Über die Eigenschaft Key kann der Schlüsselwert ermittelt werden, der zu diesem Element der Gruppierung gehört. Listing 7 zeigt das Interface. Im Konstruktor werden Schlüssel und zugehörige Werte übergeben und in Feldern abgelegt. Doch wie erfolgt nun die Gruppierung der Eingangsdaten? Schauen Sie sich dazu zunächst die Signatur der GroupBy-Methode an: 63 LÖSUNG Listing 8 Listing 10 Zeichenketten gruppieren. Wer ist der Erste? [Test] public void GroupBy_Länge_des_Wortes() { var values = new[] {"abc", "a", "ab", "a", "abc"}; var groups = values.GroupBy(x => x.Length); Assert.That(groups, Is.EqualTo(new[]{new[]{"abc", "abc"}, new []{"a", "a"}, new[]{"ab"}})); } [Test] public void Drei_Elemente() { Assert.That(new[] {1, 2, 3}.First(), Is.EqualTo(1)); } Listing 11 Listing 9 Mit MoveNext auf den 1.Platz. GroupBy, selbst gebaut. public static IEnumerable<IGrouping<TKey, TElement>> GroupBy<TKey, TElement> (this IEnumerable<TElement> values, Func<TElement, TKey> keyFunction) { var dictionary = new Dictionary<TKey, IList<TElement>>(); foreach (var value in values) { if (!dictionary.ContainsKey(keyFunction(value))) { dictionary[keyFunction(value)] = new List<TElement>(); } dictionary[keyFunction(value)].Add(value); } foreach (var d in dictionary) { yield return new Grouping<TKey, TElement>(d.Key, d.Value); } } public static IEnumerable<IGrouping<TKey, TElement>> GroupBy<TKey, TElement>(this IEnumerable<TElement> values, Func<TElement, TKey> keyFunction) Die Methode erhält neben den Eingangsdaten eine Funktion, die zu einem Element den zugehörigen Schlüsselwert liefert. Aufgabe der GroupBy-Methode ist es nun, die Eingangsdaten Element für Element zu durchlaufen und jeweils den Schlüssel des Elements zu ermitteln. Anschließend muss das Element in die zu seinem Schlüssel gehörige Aufzählung eingereiht werden. Gruppiert man beispielsweise Zeichenketten nach ihrer Länge, muss das Element „a“ in die Aufzählung zum Schlüsselwert 1 eingereiht werden. Listing 8 zeigt einen Test, der Zeichenketten nach ihrer Länge gruppiert. Wenn man nun überlegt, wie man diese Funktionalität implementieren kann, wird klar, dass die Eingangsdaten innerhalb der GroupBy-Methode vollständig behandelt werden müssen, ehe das Ergebnis geliefert werden kann. Das Ergebnis kann nicht Element für Element gebildet werden, weil die Elemente des Ergebnisses selbst wieder 64 Aufzählungen sind. Um die erste gruppierte Liste herausgeben zu können, müssen die Schlüssel aller Eingangselemente geprüft worden sein. Also ist es für diese Methode angemessen, eine Variable zu verwenden, in der das Ergebnis erst vollständig gebildet wird. Die GroupBy-Methode ist übrigens nicht die einzige, bei der das Ergebnis vollständig gebildet werden muss, ehe es als Rückgabewert herausgegeben werden kann. Das Sortieren der Elemente ist ein weiteres Beispiel. Für die Gruppierung bietet es sich an, mit einem Dictionary zu arbeiten. Darin können Sie die Schlüsselwerte der Elemente als Schlüssel im Dictionary verwenden. Der zugehörige Wert der Dictionary-Einträge ist dann jeweils eine Liste von Elementen. Das Dictionary ist daher von folgendem Typ : var dictionary = new Dictionary<TKey, IList<TElement>>(); Damit sieht die Implementation der GroupBy-Methode wie in Listing 9 aus. Die Methode besteht aus zwei Teilen. Im ersten Teil werden die Elemente in die zu ihrem Schlüssel gehörige Liste eingereiht. Dabei ist jeweils zu prüfen, ob die Liste bereits public static T First<T> (this IEnumerable<T> values) { var enumerator = values.GetEnumerator(); enumerator.MoveNext(); return enumerator.Current; } existiert. Immer wenn ein Schlüssel zum ersten Mal auftritt, wird das Element im Dictionary angelegt. Im zweiten Teil wird das fertige Dictionary durchlaufen und für jedes Element ein Grouping zurückgegeben. First Beim Ausprobieren der GroupBy-Methode aus dem .NET Framework habe ich in einem Test die First-Methode verwendet. Diese liefert das erste Element einer Aufzählung, siehe Listing 10. Für die Implementation ist es wichtig zu wissen, wie ein Enumerator funktioniert. Er muss nämlich vor dem ersten Zugriff auf das aktuelle Element durch einen Aufruf von MoveNext initialisiert werden. Mit dieser Kenntnis ist die Implementation einfach, siehe Listing 11. Ganz wichtig ist hier übrigens die lokale Variable für den Enumerator. Die Methode GetEnumerator liefert nämlich bei jedem Aufruf einen neuen Enumerator. Da MoveNext und Current jedoch auf demselben Enumerator aufgerufen werden müssen, ist das Zwischenspeichern in einer Variablen notwendig. Distinct Weiter geht’s mit der Methode Distinct. Sie liefert jedes Element einer Aufzählung nur genau einmal. Enthält die Aufzählung ein Element mehrfach, wird es nur einmal weitergeleitet. Dazu ist es erforderlich, dass sich die Methode merkt, welche Elemente dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Listing 12 Distinct testen. [Test] public void Zwei_mal_das_gleiche_Element() { Assert.That(new[] {1, 1}.Distinct(), Is.EqualTo(new[] {1})); } [Test] public void Mehrere_mehrfach_auftretende_Elemente() { Assert.That(new[] {1, 2, 1, 2, 3}.Distinct(), Is.EqualTo(new[] {1, 2, 3})); } sie bereits geliefert hat. Mithilfe dieses „Merkzettels“ ist es möglich, jedes Element einzeln zu behandeln. Im Gegensatz zu GroupBy oder Sort muss also nicht die gesamte Aufzählung auf einmal bearbeitet werden. Listing 12 zeigt die ersten Tests. Den „Merkzettel“ habe ich über eine List<T> realisiert. Der eine oder andere Leser wird vermutlich sofort zusammenzucken und sich über die Performance Gedanken machen. Doch Obacht! Keep it Simple Stupid (KISS) lautet die Devise. Sollte sich später wirklich ein PerformanceEngpass zeigen, kann immer noch eine effizientere Implementation gesucht werden. Listing 13 zeigt meine Implementation. Min Fortgesetzt habe ich meinen kleinen LINQAusflug mit der Min-Methode. Denn dabei stellt sich eine weitere Herausforderung: Wie kann man Elemente eines beliebigen generischen Typs miteinander vergleichen? Für die bislang gezeigten Methoden genügte es, dass zwei Elemente auf Gleichheit überprüft werden konnten. Da die Equals-Methode zum Bestandteil der Basisklasse object gehört, war dies bislang kein Problem. Doch um das kleinste Element einer Aufzählung zu ermitteln, müssen die Elemente verglichen werden können. Listing 14 zeigt dazu einen Test. Für den Vergleich zweier Elemente haben Sie mehrere Möglichkeiten : ❚ Sie können mehrere Überladungen der Min-Methode anbieten. Die dabei verwendeten Elementtypen müssen einen Vergleichsoperator definieren. ❚ Sie können den generischen Elementtyp T mit einem Constraint versehen, welches dafür sorgt, dass der Typ T das Interface IComparable implementieren muss. ❚ Sie können die Klasse Comparer aus dem .NET Framework verwenden. Die erste Variante wird im .NET Framework zwar verwendet, doch das hat sicherlich seinen Grund in besserer Performance. Ich habe daher zunächst nach Variante zwei implementiert und den Elementtyp mit einem Constraint versehen, siehe Listing 15. Das Constraint where T : IComparable sorgt dafür, dass der Compiler überprüft, ob der Typ T das Interface IComparable implementiert. Damit wird zur Kompilierzeit sichergestellt, dass CompareTo aufgerufen werden kann. Etwas unschön am generischen Typ ist, dass man ohne ein weiteres Constraint nicht davon ausgehen kann, dass es sich um einen Referenztyp handelt. Damit kann die Variable minimum, die das bislang kleinste gefundene Element hält, nicht mit null initialisiert werden, um anzuzeigen, dass es noch kein Minimum gibt. Den Typ T mit einem Constraint auf Referenztypen einzuschränken wäre auch nicht sinnvoll, denn dann könnte kein Minimum einer Integer-Aufzählung ermittelt werden. Daher habe ich eine boolescheVariable minimumGefunden eingeführt, in der festgehalten wird, ob bereits ein Minimum gefunden wurde. Nur dann darf das aktuelle Element gegen das bislang gefundene kleinste Ele- Listing 13 Distinct realisieren. public static IEnumerable<T> Distinct<T>(this IEnumerable<T> elements) { var distinctElements = new List<T>(); foreach (var element in elements) { if (!distinctElements.Contains(element)) { distinctElements.Add(element); yield return element; } } } www.dotnetpro.de dotnetpro.dojos.2011 Listing 14 Die kleinste Zahl finden. [Test] public void Mehrere_ints() { Assert.That(new[] {4, 2, 3, 1}.Min(), Is.EqualTo(1)); } Listing 15 Daten vergleichen. public static T Min<T>(this IEnumerable<T> elements) where T : IComparable { var minimumGefunden = false; var minimum = default(T); foreach (var element in elements) { if (!minimumGefunden) { minimumGefunden = true; minimum = element; } else if (element.CompareTo(minimum) < 0) { minimum = element; } } if (!minimumGefunden) { throw new InvalidOperationException(); } return minimum; } ment verglichen werden. Zu Variante drei sei verraten, dass ich darauf auch erst durch einen Blick in den .NET-FrameworkQuellcode gekommen bin. Bislang wusste ich nicht, dass es die statische Klasse Comparer gibt. Mit ihrer Hilfe kann man sich zu einem Typ einen Comparer liefern lassen: var comparer = Comparer<T>.Default; Dadurch kann das Typconstraint entfallen. Wieder was dazugelernt! Fazit Ich habe bei dieser Übung zwei Dinge gelernt: Obwohl ich GroupBy schon oft verwendet habe, waren mir die Details von IGrouping nicht klar. Und dass man sich beim .NET Framework einfach so einen Comparer abholen kann, war mir auch neu. Also hat sich die kleine Übung gelohnt! [ml] [1] Patrick A. Lorenz, Kochen mit Patrick zum Thema LINQ, dotnetpro 8/2008, Seite 116 ff., www.dotnetpro.de/A0808Kochstudio 65 AUFGABE Einen Twitterticker realisieren Was pfeifen die Spatzen? Gute Übungsaufgaben müssen cool sein. Sonst macht das Herumtüfteln keinen Spaß. Also, Stefan: Kannst du eine Aufgabe stellen, bei der ein cooles Programm entsteht, das zugleich technisch herausfordernd ist? Wer übt, gewinnt dnpCode: A1102DojoAufgabe 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 dotnetpro finden. Übung macht den Meister. Also − los geht’s. Aber Sie wollten doch nicht etwa sofort Visual Studio starten… T witterwalls erfreuen sich auf Veranstaltungen wachsender Beliebtheit. Eine Twitterwall zeigt regelmäßig aktualisiert Tweets, die ein vordefiniertes Hashtag enthalten. Die gefundenen Tweets werden an eine Wand projiziert. Auf diese Weise können Teilnehmer der Veranstaltung Tweets zur Veranstaltung absetzen und die Twitterwall wie ein Schwarzes Brett nutzen. Doch in diesem dotnetpro.dojo soll es nicht um eine Twitterwall gehen, sondern um ein Twitterband. Auch das Twitterband soll Tweets mit einem definierten Hashtag suchen und darstellen. Allerdings sollen die Tweets wie ein visuelles Laufband dargestellt werden, ähnlich den Börsentickern bei einschlägigen Nachrichtensendern. Der Suchbegriff soll dem Programm über die Kommandozeile übergeben werden. Anschließend soll das Programm die Tweets abrufen und darstellen. Dabei sollen die üblichen Angaben visualisiert werden: ❚ Text des Tweets, ❚ Benutzername, ❚ Profilfoto, ❚ Zeitstempel, ❚ Client, mit dem der Tweet abgesetzt wurde. Zu Anfang können Sie natürlich den Funktionsumfang reduzieren und zunächst nur den Text des Tweets anzeigen. In regelmäßigen Abständen muss das Programm die Tweets aktualisieren. Dazu muss erneut eine Anfrage an Twitter abgesetzt werden. Die Benutzerschnittstelle soll während der Abfrage nicht einfrieren. Hier kommt also Multithreading ins Spiel. Ein Timer, der in regelmäßigen Abständen ein Ereignis auslöst, kann hier zum Einsatz kommen. Doch Obacht! Die beiden Threads müssen dann synchronisiert werden, damit die Aktualisierung der Benutzerschnittstelle auf dem UI-Thread erfolgt. Den Zugriff auf das Twitter-API könnten Sie natürlich selbst entwickeln. Hier empfehle ich jedoch, eines der vorhandenen Open-SourceFrameworks einzusetzen. Andernfalls wird Sie diese Übung längere Zeit beschäftigen. Ich habe gute Erfahrungen gemacht mit Twitterizer [1]. Die Benutzerschnittstelle können Sie mit Windows Forms oder WPF angehen. Auch eine Silverlight-Anwendung wäre denkbar. Abbildung 1 zeigt einen groben Entwurf der Benutzerschnittstelle. Die einzelnen Tweets sollen nebeneinander angezeigt werden und von rechts nach links durchs Fenster laufen. Zusammengenommen besteht die Herausforderung dieser Übung darin, ein Modell für die Implementation zu entwickeln und dieses umzusetzen. Bei der Umsetzung geht es vor allem um Multithreading und die damit verbundene Synchronisation. Aber auch in der Benutzerschnittstelle stecken Herausforderungen. Zur Modellierung und Umsetzung empfehle ich, Event-Based Components einzusetzen. Ralf Westphal hat dazu in zurückliegenden Heften einiges veröffentlicht [2]–[4]. Probieren Sie es doch mal aus! Im nächsten Heft gibt’s meine Lösung. [ml] [1] http://www.twitterizer.net/ [2] Ralf Westphal, Zusammenstecken – funktioniert, Event-Based Components, dotnetpro 6/2010, S. 132ff., www.dotnetpro.de/A1006ArchitekturKolumne [3] Ralf Westphal, Stecker mit System, Event-Based Components, dotnetpro 7/2010, S. 126 ff., www.dotnetpro.de/A1007ArchitekturKolumne [4] Ralf Westphal, Nicht nur außen schön, Event-Based Components, dotnetpro 8/2010, S. 126 ff., www.dotnetpro.de/A1008ArchitekturKolumne [Abb. 1] Ein Twitterticker. 66 dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Einen Twitter-Ticker realisieren Der Zwitscherfinder Hat da jemand 'piep' gesagt? Der Zwitscherfinder weiß die Antwort. Alle paar Minuten checkt er die Twitter-Website nach dem gesuchten Schlüsselwort und präsentiert das Ergebnis. Übrigens lässt sich auch ein Zwitscherfinder vorteilhaft über Event-Based Components realisieren. A m Anfang dieser kleinen Anwendung stand für mich ein Spike. Ich hatte nämlich keine klare Vorstellung davon, wie das Open-Source-Framework Twitterizer [1] zu verwenden ist. Ferner wusste ich nicht so ganz genau, wie das Ergebnis der Twitter-Suche, eine Liste von Tweets, mit WPF visualisiert werden kann. Dass das irgendwie mit Databinding und einem Item-Template in einer ListView gehen würde, war mir klar. Aber wie genau? Also habe ich eine Spike-Solution erstellt. In der ist sozusagen alles erlaubt. Logik im UI, keine Tests, alles in Ordnung. Solange das Ergebnis am Ende lediglich dazu verwendet wird, Erkenntnisse zu gewinnen. Das bedeutet nicht zwangsläufig wegwerfen. Häufig dient eine Spike-Solution auch später noch mal dazu, wieder in ein Thema reinzukommen. Oder ein Kollege möchte sich mit der Technik vertraut machen. Legen Sie daher Ihre Spikes ruhig im Versionskontrollsystem ab, natürlich gut gekennzeichnet in einem separaten Verzeichnis. Nachdem ich durch den Spike herausgefunden hatte, wie das Twitter-API zu bedienen ist, habe ich begonnen, die Lösung des Problems zu modellieren. Dabei bin ich in zwei Iterationen vorgegangen: In einem ersten Modell habe ich das periodische Aktualisieren der Tweets weggelassen. Statt also nach einer gewissen Zeit erneut bei Twitter nach Tweets zu suchen, wird dort nur einmal gesucht und die so gefundenen Tweets werden angezeigt. Dadurch wurde das Modell einfacher und ich konnte mich auf den Kern der Anwendung konzentrieren: Ausgehend von einem Suchbegriff werden die gefundenen Tweets in einem Laufband visualisiert. Das vereinfachte Modell und dessen Umsetzung bietet schon einen großen Nutzen für den potenziellen Kunden: Die Anwendung kann in dieser abgespeckten Form sicherlich schneller entwickelt werden als in der Komplettversion. Damit steht das Feedback des Kunden auch schneller zur Verfügung. Ich komme auf diesen Aspekt später zurück. www.dotnetpro.de dotnetpro.dojos.2011 [Abb. 1] Erster Schritt mit zwei Funktionseinheiten. Die Modellierung habe ich in Form von Datenflüssen vorgenommen. Pfeile bedeuten hier den Fluss von Daten, Kreise stehen für Funktionseinheiten. Eine Funktionseinheit kann atomar oder zusammengesetzt sein. In Anlehnung an die Elektrotechnik und die Umsetzung des Datenflussmodells mittels Event-Based Components werden die atomaren Funktionseinheiten Bauteile (engl. Parts) genannt, die zusammengesetzten heißen Platinen (engl. Boards) [2]. Ein Bauteil enthält Logik, während eine Platine dafür zuständig ist, Funktionseinheiten zu verdrahten. Das können wieder Bauteile oder Platinen sein. Im Modell sieht man den Funktionseinheiten nicht an, ob sie Bauteil oder Platine sind. Das ist gut so, denn es ermöglicht die spätere Verfeinerung durch Hierarchisierung. Eine ursprünglich als Bauteil modellierte Funktionseinheit kann später zu einer Platine verfeinert werden. Dadurch ist am ursprünglichen Diagramm nichts zu ändern, sondern es entsteht ein weiteres Diagramm, welches das Innenleben der Platine zeigt. Es handelt sich dabei also um ein hierarchisches Modell. Die Schachtelung kann beliebig tief erfolgen. Im Modell der Twitterband-Anwendung habe ich zunächst mit zwei Funktionseinheiten begonnen, wie Abbildung 1 zeigt: ❚ Die Funktionseinheit UI steht für die Benutzerschnittstelle, also alle Elemente, über die der Benutzer mit der Anwendung interagiert. Ob dabei alles in einer Form realisiert wird oder zusätzlich User Controls eingesetzt werden, spielt bei der Modellierung noch keine Rolle, denn es ist ein Implementationsdetail. ❚ Die Funktionseinheit Tweets suchen enthält die Logik der Anwendung. Ausge- hend von einem Suchbegriff als Eingabe produziert diese Funktionseinheit mehrere Tweets als Ausgabe. Im Modell bedeutet der Datenfluss (Tweet*), dass mehrere Tweets geliefert werden. Dies wird durch den Stern angezeigt. Ob dies später als Array, List oder IEnumerable realisiert wird, spielt im Modell keine Rolle. Wichtig ist hier lediglich auszudrücken, dass es mehrere Tweets sind. Die Daten sind in Klammern notiert, um deutlich zu machen, dass dies die Daten der Nachricht sind. Eine Benennung der Nachricht fehlt, weil sich diese aus dem Namen der Funktionseinheit Tweets suchen ergibt. Würde die Funktionseinheit beispielsweise Twitter heißen, wäre es notwendig, den Datenfluss zu benennen. So könnte die Eingabe dann beispielsweise Tweets suchen (Suchbegriff) heißen, während die Ausgabe Ergebnis liefern (Tweet*) heißen könnte. Hier wird der Unterschied deutlich zwischen einer Modellierung mit Aktivitäten wie Tweets suchen und Akteuren wie Twitter. Bei der Modellierung mit Aktivitäten kann die Benennung der Nachricht meist entfallen, während sie bei Modellierung mit Akteuren für das Verständnis notwendig ist. In der Praxis hat sich gezeigt, dass die konsequente Modellierung in Aktivitäten natürlicher ist und nach einer kurzen Gewöhnungsphase leichter fällt. Die Gewöhnungsphase ist vor allem für Entwickler erforderlich, die sehr in der Objektorientierung verhaftet sind. Sie sind eher gewohnt, die Substantive zu suchen, um daraus Akteure zu machen. Für die Tweets, die von der Funktionseinheit Tweets suchen zum UI geliefert wer- 67 LÖSUNG [Abb. 2] Die Funktionseinheit „Tweets suchen“ weiter zerlegen. den, habe ich einen eigenen Datentyp Tweet vorgesehen. Alternativ hätte ich den Datentyp Tweet aus dem Twitterizer-Framework verwenden können. Dann wäre das UI jedoch von dieser Infrastruktur abhängig. Im UI wäre dann eine Referenz auf die Twitterizer-Assembly erforderlich gewesen. Das wollte ich in jedem Fall vermeiden, schließlich sind Benutzerschnittstelle und Ressourcenzugriffe völlig unterschiedliche Concerns und sollten daher getrennt werden. Ferner bietet ein eigener Datentyp die Möglichkeit, die Daten bereits so aufzubereiten, dass sie vom UI direkt verwendet werden können. Das führt dazu, dass das UI die Daten nicht deuten muss. Damit bleibt das UI extrem dünn und enthält keinerlei Logik. Ein automatisiertes Testen des UIs kann entfallen. Aus der Tatsache, dass Tweets suchen die gefundenen Tweets in einem Datenmodell ablegt, folgt, dass sich Tweets suchen um zwei Belange kümmert: Zum einen findet hier der Zugriff auf Twitter über das Twitterizer-API statt. Zum anderen ist die Funktionseinheit dafür zuständig, die Daten aus dem Twitterizer-Datentyp in meinen eigenen Datentyp zu mappen und dabei gegebenenfalls aufzubereiten. Da diese Erkenntnis bereits während des Modellierens zutage trat, habe ich die Funktionseinheit Tweets suchen weiter zerlegt. Um auf dem gleichen Abstraktionsniveau zu bleiben und das bisherige Modell nicht mit Details zu verwässern, die auf der Ebene nicht relevant sind, habe ich die Verfeinerung in einem weiteren Diagramm modelliert. Die Funktionseinheit Tweets suchen zerfällt dadurch intern in weitere Funktionseinheiten, ist demnach also eine Platine und kein Bauteil. Diese hierarchische Zerlegung ist durch die Modellierung in Datenflüssen und die Umsetzung mit Event-Based Components auf einfache Weise möglich. Vor allem kann die Schachtelung in beliebiger Tiefe erfolgen, ohne dass dadurch bei der späteren Implementation Probleme auftreten. Abbildung 2 zeigt die Zerlegung von Tweets suchen. Die äußere Schnittstelle ist 68 logischerweise gleich geblieben, andernfalls würde die Funktionseinheit nicht mehr in das sie umgebende Modell passen. Der Suchbegriff wird an die Funktionseinheit Twitter abfragen übergeben. Diese liefert daraufhin eine Liste der gefundenen Tweets im Datentyp des Twitterizer-Frameworks. Die Tweets werden von der Funktionseinheit Tweets mappen in das eigene Datenmodell übersetzt. Dabei werden unter anderem Eigenschaften aus dem Originaltweet zu Zeichenketten zusammengefasst, damit das UI diese Informationen nicht deuten muss, sondern sie direkt per Databinding anzeigen kann. Nun liegen in unserem Modell zwei verschiedene Arten von Funktionseinheiten vor : ❚ Bauteile, die nicht weiter verfeinert sind und Logik enthalten. ❚ Platinen, die weitere Funktionseinheiten enthalten und für deren Verbindungen zuständig sind. Die Funktionseinheiten UI, Twitter abfragen und Tweets mappen sind Bauteile. Dagegen ist die Funktionseinheit Tweets suchen durch die Verfeinerung jetzt eine Platine. Sie ist dafür zuständig, die beiden enthaltenen Bauteile zu verdrahten, und enthält selbst keine Logik. In dieser ersten Modellierung habe ich das regelmäßige Aktualisieren der Tweets bewusst weggelassen. Das hat mehrere Vorteile. Zum einen wird dadurch die Modellierung vereinfacht. Das gilt natürlich nur unter der Prämisse, dass spätere Ergänzungen einfach möglich sind. Durch die Modellierung mit Datenflüssen ist das gegeben. Wäre eine spätere Ergänzung des Modells mit hohem Änderungsaufwand verbunden, wäre die vorläufige Vereinfachung teuer eingekauft. Der zweite Vorteil ist darin zu sehen, dass nun dieses Modell bereits implementiert werden kann. Dabei wird zwar noch nicht die gesamte geforderte Funktionalität umgesetzt, aber auch hier gilt, dass man diese später ergänzen kann, ohne dabei die bereits implementierten Funktionseinheiten ändern zu müssen. Das liegt maßgeblich daran, dass ich zur Implementation des Modells Event-Based Components verwende. Somit bietet die Vorgehensweise den Vorteil der iterativen Entwicklung mit sehr kurzer Iterationsdauer. Dadurch kann der Kunde oder Product Owner sehr früh Feedback geben. Und so wie das gesamte Modell iterativ entwickelt werden kann, kann man auch bei der Implementation iterativ vorgehen: ❚ Die Übergabe des Suchbegriffs als Kommandozeilenparameter kann zunächst weggelassen werden. Stattdessen wird ein fester Suchbegriff verwendet, der in der Anwendung hart codiert ist. ❚ Statt direkt auf das Twitter-API zuzugreifen und die eingehenden Tweets zu mappen, kann zunächst eine hart codierte Liste von Tweets zurückgegeben werden. Dadurch lässt sich die Entwicklung des Controls zur Anzeige der Tweets vorantreiben. ❚ Umgekehrt kann auch zunächst auf das Control verzichtet werden. Stattdessen Listing 1 Die Bauteile verdrahten. public class TwitterSearch { private readonly Action<string> search; public TwitterSearch() { var twitter = new Twitter(); var mapper = new Mapper(); twitter.Out_Result += mapper.In_Map; mapper.Out_Result += tweets => Out_Update(tweets); search = query => twitter.In_Search(query); } public event Action<IEnumerable<Tweet>> Out_Update; public void In_Search(string query) { search(query); } } dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG werden die Tweets ganz simpel in einem Label als Text angezeigt. Listing 2 Die Anwendung testen. Es bieten sich also zahlreiche Möglichkeiten, das Feature Tweets suchen und anzeigen in kleinen Schritten umzusetzen. Ganz wichtig dabei: Es handelt sich trotzdem immer um Längsschnitte durch alle Funktionseinheiten. Dadurch kann man die Anwendung bereits sehr früh an den Kunden übergeben, um Feedback einzuholen. Für die Implementation von Tweets suchen müssen zwei Bauteile und eine Platine implementiert werden. Das eine Bauteil ermittelt mithilfe des Twitter-APIs die Liste der Tweets. Das andere bringt die gefundenen Tweets in eine Form, die vom UI unmittelbar verwendet werden kann. Um beide Bauteile herum liegt eine Platine, die für die Verdrahtung der Bauteile zuständig ist. Dazu muss der eingehende Methodenaufruf an das erste Bauteil der Platine weitergereicht werden. Das Ergebnis der Suche muss zum Mapper weitergeleitet werden. Und schließlich muss das Ergebnis des Mappers als Endergebnis der Platine zurückgegeben werden. Diese Verdrahtung zeigt Listing 1. Hier werden die Bauteile im Konstruktor unmittelbar instanziert, statt sie per Parameter zu injizieren. Da das Board lediglich für die Verdrahtung zuständig ist, verzichte ich auf einen automatisierten Test dieser Verdrahtung. Dieser wäre relativ aufwendig, weil dazu mittels Attrappen überprüft werden müsste, ob die Verdrahtung korrekt erfolgt ist. Ebenso verzichte ich auf einen automatisierten Test der Twitter-Suche, weil das Bauteil lediglich einen Aufruf des Twitter-APIs kapselt. Ich habe allerdings je einen Test ergänzt, der explizit gestartet werden muss, um damit zu überprüfen, ob das Twitter-API prinzipiell korrekt verwendet wird und irgendein Ergebnis zurückgeliefert wird. Einen vergleichbaren Test habe ich für die Platine ebenfalls erstellt. So kann überprüft werden, ob die Verdrahtung korrekt erfolgt ist, ohne dass dazu die komplette Anwendung gestartet werden muss. Allerdings handelt es sich hierbei nicht um Unit-, sondern um Integrationstests, die dazu noch vom realen Twitter-API abhängig sind. Da jedoch Platine und Twitter-Bauteil praktisch keine Logik enthalten, halte ich das Vorgehen für angemessen. Das Explicit-Attribut an den Testmethoden sorgt dafür, dass diese Tests nur ausgeführt werden, wenn dies explizit angefordert wird, siehe Listing 2. So kann man weiterhin alle Tests der Assembly aus- www.dotnetpro.de dotnetpro.dojos.2011 [TestFixture] public class TwitterTests { private Twitter sut; private TwitterSearchResultCollection result; [SetUp] public void Setup() { sut = new Twitter(); sut.Out_Result += x => result = x; } [Test, Explicit] public void Suche_nach_einem_Hashtag() { sut.In_Search("#ccd"); Assert.That(result.Count, Is.GreaterThan(0)); } } führen, ohne dabei jedesmal lange auf die Antwort von Twitter warten zu müssen. Für das Mappen der Daten vom Twitterizer-Datentyp in den eigenen Datentyp können natürlich Tests geschrieben werden, da diese Operation ja lediglich auf Daten arbeitet. Für das Laufband-Control habe ich die Suchmaschine meiner Wahl befragt. Dies führte in der Tat zu einem WPF-Treffer. Den gefundenen Quellcode habe ich dahingehend angepasst, dass die Breite des durchlaufenden Contents mit in die Laufzeit der Animation eingeht. Ohne diese Modifikation liefen Suchergebnisse mit vielen Treffern sehr schnell durch das Fenster, während Treffer mit nur einem Tweet sehr langsam angezeigt wurden. Die gewählte Lösung der Animation scheint mir relativ viel Prozessorzeit zu verbraten. Ich gestehe erneut, dass ich kein WPF-Spezialist bin. Wenn einem Leser eine bessere Lösung für das Marquee-Control einfällt, möge er sich bitte melden. Nachdem das Feature Tweets suchen und anzeigen umgesetzt ist, muss das nächste Feature modelliert werden: Periodisches Aktualisieren der Suche. Dabei will ich natürlich auf dem schon vorhandenen Modell aufsetzen und dieses erweitern. Durch die Modellierung mit Datenflüssen ist das einfach möglich, da zusätzliche Funktionseinheiten leicht in einen schon vorhandenen Datenfluss eingesetzt werden können. Um die Suche periodisch zu aktualisieren, habe ich zwischen UI und Tweets suchen eine weitere Funktionseinheit gesetzt, siehe Abbildung 3. Dieser Periodic Dispenser hat die Aufgabe, beim Eintreffen von Daten einen Timer zu starten und die erhaltenen Daten periodisch herauszugeben. So wird aus einem einmaligen Datenfluss ein sich periodisch wiederholender. Das Schöne dabei: An den vorhandenen Funktionseinheiten müssen keine Änderungen vorgenommen werden. Einzige Ausnahme stellt die Platine dar, die für die Verdrahtung der Funktionseinheiten zuständig ist. Diese erhält zusätzliche Funktionseinheiten, und die Verdrahtung muss abgeändert werden. Ein weiterer schöner Effekt: Der Timer hat nichts mit einer Twittersuche zu tun. Das Bauteil ist also völlig generisch und kann auch in einem anderen Kontext eingesetzt werden. Wenn man den Periodic Dispenser in die Anwendung integriert hat, stellt man fest, dass nun die aktualisierten Tweets auf einem anderen Thread beim UI eintreffen als bislang. Vor dem Einsatz des Timers gab es [Abb. 3] Einen Timer ergänzen. 69 LÖSUNG Listing 3 Periodische Aktualisierung ermöglichen. public class PeriodicDispenser<T> { private T t; private readonly int timerIntervalInSeconds; public PeriodicDispenser() : this(60) { } internal PeriodicDispenser(int timerIntervalInSeconds) { this.timerIntervalInSeconds = timerIntervalInSeconds; Timer_konfigurieren(); } public void In_Event(T t) { this.t = t; Out_Event(t); } public event Action<T> Out_Event; private void Timer_konfigurieren() { var timer = new Timer { Interval = timerIntervalInSeconds * 1000 }; timer.Elapsed += (sender, e) => Out_Event(t); timer.Start(); } } Listing 4 Die Synchronisierung durchführen. public class Synchronizer<T> { private readonly SynchronizationContext synchronizationContext; public Synchronizer() { synchronizationContext = SynchronizationContext.Current ?? new SynchronizationContext(); } public void In_Event(T t) { synchronizationContext.Send(state => Out_Event(t), null); } public event Action<T> Out_Event; } in der Anwendung nur einen einzigen Thread, nun sind es zwei. Damit das UI nicht meckert, müssen die Aktualisierungen der UI-Controls auf dem UI-Thread vorgenommen werden. Nun könnte man dazu Anpassungen im UI vornehmen und dort das Wechseln des Threads implementieren. Einfacher ist es allerdings, auch hier wieder eine zusätzliche Funktionseinheit in den Datenfluss einzusetzen, die den Threadwechsel vornimmt. Diese Synchronization-Funktionseinheit lässt sich mit einem SynchronizationContext aus dem .NET Framework einfach realisieren. Diese Vorgehensweise führt dazu, dass die unterschiedlichen Belange sowohl im 70 Modell als auch in der Implementation sauber getrennt bleiben. Ferner taucht der Aspekt der Synchronisation im Modell explizit auf und sorgt damit für bessere Verständlichkeit. Würde man die Synchronisation im UI implementieren, indem dort das übliche Muster von InvokeRequired-Abfrage und Invoke-Aufruf angewandt würde, wäre der Aspekt in der Implementation verborgen. Dadurch würde das Verständnis der Implementation erschwert. Hinzu kommt, dass nun die Funktionseinheiten Periodic Dispenser und Synchronization zu Standardbauteilen werden, die auch in anderen Anwendungen zum Einsatz kommen können. Damit ist das Thema Timer und Threadsynchronisation abgehakt. Und das in einer Form, die es erlaubt, den Aspekt im Modell zu visualisieren. Damit dient das Modell wirklich dem Verständnis der Implementation. Wäre die Synchronisation im UI auf die übliche Art und Weise realisiert worden, würde die Implementation den Entwurf nicht widerspiegeln. Einziger Ausweg: den Aspekt im Modell weglassen. Damit wäre aber niemandem gedient. Der PeriodicDispenser ist als generische Klasse implementiert, siehe Listing 3. Dabei gibt der generische Typ an, von welchem Typ der Datenfluss ist. Durch den parameterlosen Defaultkonstruktor wird der Timer fix auf 60 Sekunden eingestellt. Für einen automatisierten Test habe ich einen weiteren Konstruktor ergänzt, der allerdings nur internal sichtbar ist. Durch das Attribut InternalsVisibleTo ist der Konstruktor auch in der Testassembly sichtbar. Beim eingehenden Pin In_Event werden die Daten in einem Feld abgelegt. Dadurch stehen sie in der Timerroutine zur Verfügung, um periodisch mit dem Out_Event wieder ausgeliefert zu werden. Die Synchronisation erfolgt mithilfe des SynchronizationContext aus dem .NET Framework, siehe Listing 4. Dieser wird im Konstruktor angelegt. Daher muss das Bauteil auf dem Thread erzeugt werden, auf den später die eingehenden Ereignisse synchronisiert werden sollen. Auch dieses Bauteil ist generisch, um den Typ des ein- und ausgehenden Datenflusses angeben zu können. Fazit Durch die Zerlegung der Gesamtanwendung in Features und deren getrennte Modellierung konnte die Gesamtaufgabe in Iterationen aufgeteilt werden. Die weitere Zerlegung der Features in Featurescheiben bot weitere Möglichkeiten, iterativ vorzugehen. Das gibt beim Entwickeln ein gutes Gefühl, weil man den Fortschritt sieht und immer wieder etwas wirklich fertig wird. Durch den Einsatz von Funktionseinheiten für die periodische Wiederholung und die Synchronisation der Threads sind diese beiden wichtigen Aspekte im Modell sichtbar. Das fördert das Verständnis und erhöht den Nutzen der Modellierung als Dokumentation der Implementation. [ml] [1] www.twitterizer.net [2] Ralf Westphal, Zusammenstecken – funktioniert, Event-Based Components, dotnetpro 6/2010, S. 132 ff., www.dotnetpro.de/A1006ArchitekturKolumne dotnetpro.dojos.2011 www.dotnetpro.de AUFGABE Algorithmen und Datenstrukturen zu Graphen Wie hängt alles zusammen? Mit einem Graphen kann man darstellen, wie die Dinge miteinander zusammenhängen. Weil aber alles mit allem irgendwie zusammenhängt, kann man mit Graphen eigentlich alles darstellen. Das ist interessant, und deswegen gibt es hier dazu eine Übung. G raphen sind eine Datenstruktur, mit der sich viele Probleme auf einfache und elegante Art lösen lassen. Nehmen wir als Beispiel das Referenzieren von Projekten und Assemblies in Visual-Studio-Projekten. Dabei ergibt sich die Frage, in welcher Reihenfolge die Projekte einer Solution übersetzt werden müssen. Die Fragestellung ist leicht zu lösen, wenn man von einer Baumstruktur ausgeht. Solange also zirkuläre Referenzen unterbunden werden, genügt es, alle referenzierten Projekte in einer Baumstruktur abzulegen und anschließend den Baum zu traversieren. Allerdings muss die Projektstruktur nicht zwingend einen einzigen Baum ergeben, sondern es können mehrere Bäume sein. Ferner stellt sich die Frage, wie man erkennt, ob es bei den Referenzen zu Kreisen kommt. Hier kommt die Datenstruktur Graph ins Spiel. Ein Graph besteht ganz allgemein gesagt aus Knoten und Kanten. Eine Kante setzt zwei Knoten in eine Beziehung. Dabei ist zu unterscheiden, ob die Kanten gerichtet oder ungerichtet sind. Bei ungerichteten Kanten werden einfach zwei Knoten in Beziehung gesetzt, ohne dabei eine Traversierungsrichtung mit abzulegen. Bei gerichteten Graphen hat jede Kante eine Richtung, zeigt also von einem Quellknoten auf einen Zielknoten. Hierbei ist eine Traversierung in Kantenrichtung oder auch gegen die Kantenrichtung möglich. Enthält ein Graph nur ungerichtete Kanten, spricht man von einem ungerichteten Graphen. Enthält er gerichtete Kanten, spricht man von einem gerichteten Graphen. Das Beispiel der Projektreferenzen lässt sich mit einem gerichteten Graphen abbilden. Die Richtung der Kante definiert, wer wen referenziert. Eine Kante von A nach B bedeutet in dem Fall, dass Projekt A das Projekt B referenziert. Normalerweise geht man bei Graphen davon aus, dass es maximal eine Kante zwischen denselben Knoten geben kann. Sollen mehrere Kanten möglich sein, so spricht man von einem Multigraphen. www.dotnetpro.de dotnetpro.dojos.2011 Das Schöne an Graphen ist, dass in der Literatur zahlreiche Algorithmen zu finden sind, um beispielsweise herauszufinden, ob zwei Knoten miteinander verbunden sind. Im Falle der Projektreferenzen würde das eine Abhängigkeit der betroffenen Projekte bedeuten. Durch eine topologische Sortierung lässt sich die Reihenfolge der Übersetzung von abhängigen Projekten herausfinden. Und auch für die Frage nach Kreisen gibt es Algorithmen, die herausfinden, ob ein Graph kreisfrei ist. In vorangegangenen dotnetpro-dojos war beim Thema Datenstruktur jeweils ein API vorgegeben. Diesmal ist es ein Bestandteil der Übung: Entwerfen Sie ein API für den Umgang mit gerichteten Graphen. Anschließend implementieren Sie die Datenstruktur. Dazu gibt es in der Literatur genügend Vorschläge. Im Anschluss sollten Sie sich einen der Algorithmen vornehmen und implementieren. Dabei geht es nicht darum, einen Graphenalgorithmus selbst zu „erfinden“, sondern es geht lediglich um die Umsetzung. Beginnen Sie beispielweise mit der topologischen Sortierung. Da die Algorithmen meist von einer bestimmten Art und Weise der Implementation der Datenstruktur ausgehen, beispielsweise einer Adjazenzmatrix, sollten Sie sich mit dem Algorithmus befassen, bevor Sie die Datenstruktur umsetzen. Eine spannende Ergänzung zur Implementation der Datenstruktur ist natürlich die Visualisierung eines Graphen. Auch dabei muss man das Rad nicht neu erfinden, sondern kann Bibliotheken wie das freie GraphViz [1] oder das lizenzpflichtige MSAGL [2] verwenden. MSAGL ist inzwischen in der MSDN Subscription enthalten. Im Downloadbereich findet man es unter Automatic Graph Layout. Genügend Stoff, um Neues zu lernen. Oder wie sagte der Professor in derVorlesung über Graphentheorie? „Das ganze Leben ist ein Graph.“ [ml] 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 dotnetpro finden. Übung macht den Meister. Also − los geht’s. Aber Sie wollten doch nicht etwa sofort Visual Studio starten… [1] www.graphviz.org/ [2] http://research.microsoft.com/en-us/projects/msagl/ 71 Wer übt, gewinnt dnpCode: A1103DojoAufgabe LÖSUNG Algorithmen und Datenstrukturen zu Graphen Wie die Welt zusammenhält „Adjazenz“ bezeichnet keinen geistlichen Würdenträger und ist auch kein militärischer Dienstgrad, sondern steht für die Beziehung zwischen Knoten und Kanten. Über adjazente, also miteinander verbundene Knoten kann man Zusammenhänge modellieren und erforschen. dotnetpro macht einen Ausflug in die Graphentheorie, für die es viele praktische Anwendungen gibt. dnpCode: A1104DojoLoesung Stefan Lieser ist Softwareentwickler aus Leidenschaft. Nach seinem Informatikstudium mit Schwerpunkt auf Softwaretechnik hat er sich intensiv mit Patterns und Principles auseinandergesetzt. Er arbeitet als Berater und Trainer, hält zahlreiche Vorträge und hat gemeinsam mit Ralf Westphal die Clean Code Developer Initiative ins Leben gerufen. Sie erreichen ihn unter [email protected] oder lieser-online.de/blog. L iteratur zu Graphenalgorithmen zu finden ist nicht schwer. Ich habe Robert Sedgewicks „Algorithms in Java“ [1] herangezogen. Die Tatsache, dass die Beispiele in Java vorliegen, sollte Sie nicht abschrecken. Java ist nicht so weit weg von C#, dass man die Beispiele nicht problemlos übernehmen könnte. Bevor es an die Algorithmen geht, muss man sich natürlich Gedanken über die zugrunde liegende Datenstruktur machen. Auch hierbei hilft die Literatur. Sie enthält Vorschläge, wie Graphen implementiert werden können. Ich habe mit der Repräsentation in Form einer sogenannten Adjazenzmatrix begonnen. Der Begriff Adjazenz steht für die Beziehung zwischen Knoten oder Kanten. Zwei Knoten sind adjazent, wenn sie durch eine Kante verbunden sind. Zwei Kanten sind adjazent, wenn sie einen gemeinsamen Knoten haben. Die Idee einer Adjazenzmatrix ist ganz einfach: In Form einer Matrix wird festgehalten, ob zwei Knoten durch eine Kante verbunden sind. Die Adjazenzmatrix enthält also die Information über die Kanten. Repräsentiert wird sie durch ein zweidimensionales Array von booleschen Werten: private readonly bool[,] adjacency = new bool[VertexMaxCount,VertexMaxCount]; [Abb. 1] Ein Graph mit zugehöriger Adjazenzmatrix. 72 Eine Kante zwischen den Knoten 5 und 7 wird also dargestellt, indem im Array an Position [5, 7] der Wert true steht. Knoten werden durch Integerzahlen repräsentiert und können daher als Index in der Adjazenzmatrix verwendet werden. Ein Beispiel für einen Graphen und die zugehörige Adjazenzmatrix zeigt Abbildung 1. Die Adjazenzmatrix mag dem ein oder anderen Leser etwas verschwenderisch mit dem Speicherplatz umgehen. Allemal, wenn ein Graph nur wenige Kanten enthält und dadurch die meisten Einträge in der Matrix auf false stehen. Hier sei der Hinweis auf das Prinzip „Vorsicht vor Optimierungen“ erlaubt [2]. Wenn nicht gerade Graphen mit Tausenden von Knoten bearbeitet werden sollen, ist der Speicherbedarf vernachlässigbar. Im Vordergrund steht die Verständlichkeit der Implementation, und die ist hier definitiv gegeben. Um beispielsweise zu ermitteln, ob eine Kante vom Knoten v zum Knoten w existiert, genügt es, in der Adjazenzmatrix nachzuschauen: if(adjacency[v, w]) { ... } Wer sich dennoch Sorgen um den Speicherplatz macht, kann auch die Klasse BitArray aus dem .NET Framework verwenden. Allerdings muss man sich die Matrix dann selbst zusammenbasteln, da BitArrays nur eindimensional sind. In meiner Implementation habe ich die Adjazenzmatrix mit einer fixen Größe von willkürlich 50 Knoten angelegt. Es ist ein Leichtes, dies durch einen zusätzlichen Konstruktor konfigurierbar zu machen. Und natürlich könnte man das Array bei Bedarf auch in der Größe anpassen. Dazu muss lediglich geprüft werden, ob noch Platz für den zu ergänzenden Knoten ist. Da dann allerdings ein Umkopieren der Werte erforderlich ist, sollte man gut überlegen, ob es nicht besser ist, gleich die „richtige“ Größe zu verwenden. API und Implementation des Graphen habe ich aus dem erwähnten Buch übernommen. Allerdings habe ich dabei einige der Bezeichner geändert, weil mir diese im Original zu stark abgekürzt waren. Das erschwert für mich die Lesbarkeit, daher bevorzuge ich ausgeschriebene Begriffe. Lediglich bei den Eigenschaften für die Anzahl der Knoten und Kanten habe ich es bei V und E in Großbuchstaben belassen, weil dies die in der Literatur allgemein verwendeten Symbole sind (abgeleitet von Vertex und Edge). Aber auch über diese Bezeichner ließe sich natürlich reden. Listing 1 zeigt die Implementation der Datenstruktur. Die beiden Properties V und E liefern jeweils die Anzahl von Knoten und Kanten zurück. Zu beachten ist dabei, dass V jeweils die maximale Anzahl möglicher Knoten im Graph liefert. Das liegt daran, dass in der Adjazenzmatrix lediglich die Kanten verwaltet werden. Diese werden beim Einfügen und Löschen zusätzlich noch gezählt, damit für die Ermittlung ihrer Anzahl E kein Zählen in der Matrix erforderlich ist. Für das Hinzufügen und Löschen von Kanten wird die Datenklasse Edge verwendet. Diese hat dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Listing 1 Listing 2 Listing 3 Die Datenstruktur für einen Graphen. Graphen verändern. Benachbarte Knoten traversieren. public class Graph { private const int VertexMaxCount = 50; private readonly bool[,] adjacency = new bool[VertexMaxCount,VertexMaxCount]; private int edgeCount; public int V { get { return VertexMaxCount; } } public int E { get { return edgeCount; } } public void Insert(Edge e) { if (adjacency[e.v, e.w]) { return; } adjacency[e.v, e.w] = true; edgeCount++; } public void Remove(Edge e) { if (!adjacency[e.v, e.w]) { return; } adjacency[e.v, e.w] = false; edgeCount--; } public bool Edge(int v, int w) { return adjacency[v, w]; } public IEnumerable<int> AdjacentVertices(int v) { for (var i = 0; i < VertexMaxCount; i++) { if (Edge(v, i)) { yield return i; } } } } public class Edge { public int v { get; set; } public int w { get; set; } } lediglich zwei Properties für Start- und Zielknoten. Auch hier habe ich es bei den Kleinbuchstaben v und w belassen, da diese in der Literatur sehr oft als Symbole für Knoten verwendet werden. Neben den Methoden zum Verändern des Graphen stehen die beiden Methoden Edge und AdjacentVertices zur Verfügung. Die Methode Edge liefert für zwei Knoten die Information, ob diese durch eine Kante verbunden sind. Dazu ist lediglich ein Zugriff auf die Adjazenzmatrix erforderlich, bei dem die beiden Knoten als Indizes verwendet werden. Der Laufzeitaufwand ist somit konstant O(1). Details zur sogenann- www.dotnetpro.de dotnetpro.dojos.2011 public class GraphTests { private Graph g; [SetUp] public void Setup() { g = new Graph(); } [Test] public void Edge_is_retrievable() { g.Insert(new Edge {v = 0, w = 1}); Assert.That(g.Edge(0, 1), Is.True); Assert.That(g.E, Is.EqualTo(1)); } [Test] public void Edge_can_be_removed() { g.Insert(new Edge {v = 0, w = 1}); g.Remove(new Edge {v = 0, w = 1}); Assert.That(g.Edge(0, 1), Is.False); Assert.That(g.E, Is.EqualTo(0)); } } ten O-Notation – auch Landau-Symbol genannt – finden Sie unter [3]. Mit AdjacentVertices können zu einem gegebenen Knoten v sämtliche adjazenten Knoten ermittelt werden. Das sind Knoten, die durch eine von v ausgehende Kante mit v verbunden sind sind. Dabei kommt ein Iterator zum Einsatz, den ich mit yield return implementiert habe. Der Laufzeitaufwand für diese Methode beträgt O(n), ist also linear. Das bedeutet, dass die Laufzeit linear mit der Größe der Adjazenzmatrix ansteigt. Natürlich habe ich zu dieser Datenstruktur einige automatisierte Tests erstellt. Diese können gleichzeitig auch als Beispiele für die Verwendung des APIs dienen. Listing 2 zeigt ein Beispiel für die Veränderung eines Graphen. Listing 3 zeigt einen Test zur Traversierung der benachbarten Knoten. Der erste Test zeigt, dass die Aufzählung der adjazenten Knoten leer ist, wenn keine Kanten hinzugefügt wurden. Im zweiten Test sieht man, dass die über Kanten unmittelbar erreichbaren Zielknoten aufgelistet werden. Im Rausch der Tiefe Dieses Beispiel führt uns wieder zur Frage aus der Aufgabenstellung zurück: Wie kann man ermitteln, ob es eine Verbindung zwischen zwei Knoten im Graph gibt, die über mehrere Kanten verläuft? Da hilft die Tiefensuche: Man besucht, ausgehend vom Startknoten, so lange alle erreichbaren [Test] public void Adjacent_vertices_without_edge() { Assert.That(g.AdjacentVertices(0), Is.Empty); } [Test] public void Adjacent_vertices() { g.Insert(new Edge {v = 1, w = 1}); g.Insert(new Edge {v = 1, w = 3}); g.Insert(new Edge {v = 1, w = 7}); Assert.That(g.AdjacentVertices(1), Is.EqualTo(new[] {1, 3, 7})); } Knoten, bis man entweder den Zielknoten gefunden hat oder es nicht mehr weitergeht. Um sich dabei nicht im Kreis zu drehen, werden alle Knoten, die einmal besucht wurden, markiert. Wird ein solcher Knoten während der weiteren Suche erneut erreicht, wird dieser Pfad nicht weiter verfolgt. Listing 4 zeigt den in der Klasse PathSearch umgesetzten Algorithmus. Die eigentliche Suche nach einem Pfad von v nach w übernimmt die rekursive Methode SearchRecursive. Ihr Abbruchkriterium ist erreicht, wenn die beiden Knoten v und w dieselben sind, denn ein Weg von einem Knoten zu sich existiert natürlich immer. Danach wird der Startknoten dieser Suche markiert, damit er später nicht erneut berücksichtigt wird. Dann werden in einer Schleife alle adjazenten Knoten besucht, falls das nicht bereits zuvor geschehen ist. Beim Markieren der schon besuchten Knoten ist es wieder nützlich, dass die Knoten als Zahlen repräsentiert werden. Dadurch kann nämlich der Knoten selbst als Index in ein boolesches Array verwendet werden. Die Implementation des Algorithmus ist getrennt von der Implementation der Datenstruktur. Im Konstruktor werden die nötigen Angaben übergeben: der Graph, in dem gesucht werden soll, und die beiden Knoten, zwischen denen ein Pfad gesucht werden soll. Das Ergebnis der Suche wird in einem Feld abgelegt und kann über die Eigenschaft Exists abgefragt werden, wie der Test in Listing 5 zeigt. Auf diese Weise ist es übrigens auch leicht, den Algorithmus so zu erweitern, dass der gefundene Pfad auch ermittelt wird statt nur seine Existenz zu überprü- 73 LÖSUNG Listing 4 In Verzweigungen abtauchen. public class PathSearch { private readonly Graph g; private readonly bool found; private readonly bool[] visited; public PathSearch(Graph g, int v, int w) { this.g = g; visited = new bool[g.V]; found = SearchRecursive(v, w); } private bool SearchRecursive(int v, int w) { if (v == w) { return true; } visited[v] = true; foreach (var t in g.AdjacentVertices(v)) { if (visited[t]) { continue; } if (SearchRecursive(t, w)) { return true; } } return false; } public bool Exists { get { return found; } } } Listing 5 Ergebnis der Suche ablegen. [Test] public void Existing_path() { g.Insert(new Edge{v = 0, w = 1}); g.Insert(new Edge{v = 1, w = 2}); g.Insert(new Edge{v = 2, w = 3}); var path = new PathSearch(g, 0, 3); Assert.That(path.Exists, Is.True); } fen. Die Klasse kann dazu einfach mit einer weiteren Eigenschaft versehen werden, die während der Suche die traversierten Knoten aufsammelt. Natürlich wäre es auch denkbar, den Algorithmus nicht direkt von der Klasse Graph abhängig zu machen. Dazu müsste Graph nur mit einem Interface versehen werden. So könnten unterschiedliche Implementationen der Datenstruktur vom selben Algorithmus verwendet werden. Im Sinne von KISS, Keep it simple, stupid [2], habe ich darauf aber verzichtet, denn zurzeit habe ich nur eine einzige Datenstruktur Graph implementiert. Bei Bedarf ist die Umstellung auf ein Interface keine große Tat. 74 Visualisierung Ich wollte dann noch prüfen, ob der Algorithmus auch mit Kreisen (Zyklen) umgehen kann. Darunter versteht man einen Graphen, in dem zwei Knoten so über Kanten verbunden sind, dass man bei der Traversierung wieder am Ursprungsknoten ankommt. Natürlich dürfen dabei mehrere andere Knoten traversiert werden. Der Test dazu war schnell erstellt. Doch kam der Wunsch auf, den zu testenden Graphen visualisieren zu können: So ist sichergestellt, dass die Testdaten tatsächlich den gewünschten Graphen repräsentieren. Dies visuell zu prüfen ist eben viel einfacher, als eine Liste von Kanten zu interpretieren. Also habe ich mir die MSAGL-Bibliotheken aus den MSDN Subscription Downloads besorgt und in ein neues Projekt eingebunden [4]. MSAGL verwendet zur Visualisierung eine eigene Graph-Klasse. Also galt es zunächst, meine Repräsentation eines Graphen in die Repräsentation aus dem MSAGL-Framework zu überführen Dies habe ich als Extension-Methode implementiert. Dadurch bleibt meine GraphKlasse weiterhin frei von unnötigen Abhängigkeiten, siehe Listing 6. In einer Schleife werden alle Knoten des Graphen durchlaufen. Für jeden Knoten werden dann die adjazenten Knoten ermittelt. Für je zwei adjazente Knoten wird anschließend eine Kante im MSAGL-Graphen angelegt. Ein Knoten wird durch MSAGL standardmäßig als Kästchen visualisiert; deshalb ändere ich hier auch gleich den Shape zu einem Kreis. Danach habe ich ein Windows-FormsProjekt erstellt und darin eine neue Form angelegt. In die Form habe ich ein MSAGLGViewer-Control eingefügt und eine Methode ergänzt, mit welcher der zu visualisierende Graph an die Form übergeben [Abb. 2] Zyklische Bezüge visualisieren. wird. Listing 7 zeigt, wie ein Graph nun angezeigt werden kann. Auch diese Methode habe ich als Extension-Methode implementiert. Somit kann ich nun den nicht kreisfreien Graphen im Test visualisieren, siehe Listing 8. Das Ergebnis der Visualisierung zeigt Abbildung 2. Natürlich sollte die Visualisierung nicht in einem automatisierten Test aufgerufen werden. Nach der visuellen Kontrolle meiner Testdaten habe ich den Aufruf g.ShowGraph() daher auskommentiert. Wer es ganz richtig machen möchte, implementiert den Viewer als Visual Studio Debugger Extension. Dann kann jeder Graph im Debugger zur Kontrolle angezeigt werden. Eine solche Debugger Extension zu realisieren ist nicht schwer. Topologische Sortierung Nun sind wir mithilfe der Pfadsuche in der Lage zu ermitteln, ob ein Pfad von einem Startknoten zu einem Zielknoten existiert. Damit können wir beispielsweise ermitteln, ob eine Assembly von einer anderen Assembly abhängig ist, auch wenn sich die- Listing 6 Die MSAGL-Bibliothek einbeziehen. public static class GraphExtensions { public static Microsoft.Msagl.Drawing.Graph ToMsaglGraph(this Graph g) { var msaglGraph = new Microsoft.Msagl.Drawing.Graph(); for (var v = 0; v < g.V; v++) { foreach (var w in g.AdjacentVertices(v)) { var e = msaglGraph.AddEdge(v.ToString(), w.ToString()); e.SourceNode.Attr.Shape = Shape.Circle; e.TargetNode.Attr.Shape = Shape.Circle; } } return msaglGraph; } } dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Listing 7 Listing 9 Listing 10 Einen Graphen visualisieren. Umgekehrte topologische Sortierung. Graphen testen. public static void ShowGraph(this Graph g) { var viewer = new Viewer(); viewer.SetGraph(g); viewer.ShowDialog(); } Listing 8 Zyklische Bezüge visualisieren. [Test] public void Path_with_circles() { g.Insert(new Edge{v = 0, w = 1}); g.Insert(new Edge{v = 1, w = 2}); g.Insert(new Edge{v = 2, w = 3}); g.Insert(new Edge{v = 1, w = 0}); g.Insert(new Edge{v = 2, w = 0}); g.Insert(new Edge{v = 3, w = 0}); g.ShowGraph(); var path = new PathSearch(g, 0, 3); Assert.That(path.Exists, Is.True); } se Abhängigkeit über mehrere Assemblies hinweg ergibt. Doch wie können wir die Build-Reihenfolge mehrerer Projekte ermitteln, die untereinander Abhängigkeiten haben? Die Lösung liegt in der topologischen Sortierung eines entsprechenden Graphen. Unter der topologischen Sortierung eines Graphen versteht man eine Knotenreihenfolge, bei der jeder Knoten vor allen Knoten angeordnet ist, auf die er mittels Kanten verweist. Für das Bestimmen der BuildReihenfolge benötigen wir die umgekehrte topologische Sortierung: Projekte, die von anderen benötigt werden, müssen vor diesen übersetzt werden. Der Algorithmus zur umgekehrten topologischen Sortierung basiert, wie schon die Pfadermittlung, auf einer Tiefensuche. Auch diesen Algorithmus habe ich wieder als eigenständige Klasse implementiert, siehe Listing 9. Auch hier ist die Tiefensuche wieder als Rekursion implementiert. Damit Knoten nicht mehrfach besucht werden, wird das Array pre mit -1 initialisiert. Nur Knoten, für die der Initialwert noch im Array steht, werden besucht. Die beiden Arrays order und relabel nehmen das Ergebnis der Sortierung auf. ❚ Order liefert für einen gegebenen Index die Knotennummer. www.dotnetpro.de dotnetpro.dojos.2011 public class ReverseTopologicalSort { private readonly Graph g; private int cnt; private int tcnt; private readonly int[] pre; private readonly int[] relabel; private readonly int[] order; public ReverseTopologicalSort(Graph g) { this.g = g; pre = new int[g.V]; relabel = new int[g.V]; order = new int[g.V]; for (var i = 0; i < g.V; i++) { pre[i] = -1; relabel[i] = -1; order[i] = -1; } for (var i = 0; i < g.V; i++) { if (pre[i] == -1) { SortReverse(i); } } } private void SortReverse(int v) { pre[v] = cnt++; foreach (var w in g.AdjacentVertices(v)) { if (pre[w] == -1) { SortReverse(w); } } relabel[v] = tcnt; order[tcnt++] = v; } public int Order(int v) { return order[v]; } public int Relabel(int v) { return relabel[v]; } } ❚ Relabel liefert zu einer gegebenen Knotennummer den Index des Knotens. Listing 10 zeigt einen Test des Algorithmus für einen Graphen, bei dem drei Knoten hintereinander angeordnet sind. Man sieht bereits, dass die beiden Methoden Order und Relabel invers zueinander sind. Natürlich gibt es für manche Graphen mehrere mögliche Ergebnisse. Schon bei einem Knoten mit zwei Nachfolgern stellt sich die Frage, welcher Nachfolger zuerst besucht werden soll. Hier gibt es also zwei mögliche Ergebnisse bei der topologischen Sortierung. Für die Reihenfolge beim Übersetzen von Visual-Studio-Projekten spielt das natürlich keine Rolle. [Test] public void Graph_with_two_edges_in_line() { g.Insert(new Edge {v = 0, w = 1}); g.Insert(new Edge {v = 1, w = 2}); var sut = new ReverseTopologicalSort(g); Assert.That(sut.Order(0), Is.EqualTo(2)); Assert.That(sut.Order(1), Is.EqualTo(1)); Assert.That(sut.Order(2), Is.EqualTo(0)); Assert.That(sut.Relabel(0), Is.EqualTo(2)); Assert.That(sut.Relabel(1), Is.EqualTo(1)); Assert.That(sut.Relabel(2), Is.EqualTo(0)); } Sich im Kreis drehen Beim Ermitteln der Build-Reihenfolge bleibt abschließend noch die Frage, ob zyklische Abhängigkeiten existieren. Wenn A von B abhängt und B von A, gibt es keine BuildReihenfolge, bei der jedes Projekt lediglich einmal übersetzt wird. Solche Situationen sollten erkannt werden, um Referenzen zu verhindern, die zu Kreisen führen würden. Die Vorgehensweise ist demnach wie folgt: Zunächst wird ein Graph mit den bereits vorhandenen Projekten und Referenzen erzeugt. Danach wird die neu hinzuzufügende Referenz in den Graphen eingefügt. Bevor die Referenz tatsächlich in das Visual-Studio-Projekt aufgenommen wird, muss der Graph auf Kreisfreiheit überprüft werden. Würde durch die zusätzliche Referenz ein Kreis entstehen, muss diese Referenz logischerweise abgelehnt werden. Die Lösung des Problems kann man zurückführen auf die Frage, ob zwischen zwei Knoten eine sogenannte starke Verbindung existiert.Wenn für zwei Knoten v und w eine starke Verbindung existiert, bedeutet das, dass auch eine Verbindung zwischen w und v existiert. Ist das der Fall, liegt ein Zyklus (oder auch Kreis) vor. Die Menge von zusammenhängenden Knoten in einem Graph, die über starke Verbindungen erreichbar sind, werden starke Komponenten genannt. Und das führt uns zu einem Algorithmus für die Frage nach der Kreisfreiheit eines Graphen: Wenn die Anzahl starker Komponenten in einem Graph der Anzahl seiner Knoten entspricht, ist keiner der Knoten mit einem anderen stark verbunden. Das heißt, es liegen keine Kreise vor. Um herauszufinden, ob ein Graph Kreise enthält, ermittelt man also die Anzahl der starken Komponenten. Ist diese gleich der Anzahl der Knoten, ist der Graph kreisfrei. 75 LÖSUNG Listing 11 Auf kreisfreie Graphen testen. [Test] public void Graph_without_cycles() { g.Insert(new Edge{v = 0, w = 1}); g.Insert(new Edge{v = 1, w = 2}); g.Insert(new Edge{v = 2, w = 3}); var sc = new StrongComponents(g); Assert.That(sc.Count, Is.EqualTo(g.V)); Assert.That(sc.StronglyReachable(0, 3), Is.False); } [Test] public void Graph_with_cycle_over_2_vertices() { g.Insert(new Edge{v = 0, w = 1}); g.Insert(new Edge{v = 1, w = 0}); var sc = new StrongComponents(g); Assert.That(sc.Count, Is.EqualTo(g.V - 1)); Assert.That(sc.StronglyReachable(0, 1), Is.True); } Im Beispielcode auf der Heft-CD ist der Algorithmus von Tarjan in der Klasse StrongComponents implementiert. Er ist ebenfalls aus Sedgewick [1] entnommen. Zwei Tests sollen das API demonstrieren, siehe Listing 11. Das erste Beispiel betrifft einen kreisfreien Graphen. In diesem Fall ist die Anzahl der starken Komponenten sc.Count gleich der Anzahl der Knoten g.V. Zwischen den beiden Knoten besteht keine starke Verbindung, da sie nur in einer Richtung verbunden sind. Im zweiten Beispiel enthält der Graph zwei Knoten, die in beiden Richtungen miteinander verbunden sind. Somit entspricht die Anzahl der starken Komponenten nicht der Anzahl der Knoten. Der Algorithmus liefert uns also die Aussage, dass der Graph nicht kreisfrei ist. mit negativen Gefühlen verbindet, sei getröstet: Die Algorithmen konnte ich unverändert bei Sedgewick „abschreiben“. Die Übersetzung von Java nach C# war leicht. Das hat für mich wieder bestätigt, dass man als .NET-Entwickler bei der Suche nach Literatur durchaus Java-Literatur in den Blick nehmen sollte, von Java-spezifischen Themen vielleicht abgesehen. Graphenalgorithmen sind grundlegend für viele Problemstellungen. Daher sollte man zumindest die wichtigsten Begriffe und Konzepte kennen. Bei der Umsetzung eines vorhandenen Algorithmus geschieht dies ganz praxisnah nebenbei. Befasst man sich mit Algorithmen und Datenstrukturen regelmäßig im Rahmen der persönlichen Weiterbildung, ist man für zukünftige Herausforderungen gerüstet. [ml] Fazit [1] Robert Sedgewick, Algorithms in Java, Part 5, Graph Algorithms, ISBN 0-201-36121-3 [2] http://www.clean-code-developer.de/ Roter-Grad.ashx [3] http://de.wikipedia.org/wiki/Landau-Symbole [4] http://research.microsoft.com/ en-us/projects/msagl/ Ich habe mich während dieser Übung an mein Studium erinnert. Die Vorlesungen zu Graphenalgorithmen waren immer sehr lehrreich, weil grundlegende Dinge wie Rekursion, Tiefensuche etc. erklärt wurden. Wer solche gedanklichen „Zeitreisen“ eher n e h c u s ? r e e l Si k c i w t n E .NE T Mit uns finden Sie Ihren Wunschkandidaten! Neue Mediengesellschaft Ulm mbH Ihre Ansprechpartner für den Stellenmarkt Angelika Hochmuth Anzeigenleitung Tel: 089 / 74 11 7 - 1 25 [email protected] 76 dotnetpro.dojos.2011 www.dotnetpro.de AUFGABE Eine interaktive Anwendung mit Datenflüssen modellieren Wie fließen die Daten? Software modellieren: Ja, dem gehört die Zukunft. Aber was soll man eigentlich genau modellieren? Datenflüsse oder Abhängigkeiten von Funktionseinheiten? Stefan, kannst du dazu eine Übung stellen? B ei einer algorithmischen Fragestellung liegt es nahe, Datenflüsse zu modellieren.Wer Datenflüsse modelliert, verdeutlicht den Ablauf der Anwendung. Bei der Modellierung von Abhängigkeiten ist das hingegen meistens nicht der Fall. Zwar sieht man bei einem Abhängigkeitsdiagramm, dass eine Funktionseinheit die Dienste anderer Funktionseinheiten in Anspruch nimmt und damit von diesen Funktionseinheiten abhängig wird. Wie die Interaktion der Funktionseinheiten aber genau aussieht, lässt sich meist nur erahnen. Für Aufgabenstellungen ohne Benutzerinteraktion sind Datenflussdiagramme naheliegend. Das Zerlegen einer Zeichenkette in Konfigurationswerte, die in ein Dictionary übernommen werden, wäre ein Beispiel. Doch kann man auch eine interaktive Anwendung auf diese Weise modellieren? Um diese Frage zu beantworten, nehmen wir diesen Monat eine kleine To-do-Listenverwaltung auf der To-do-Liste. Im Vordergrund stehen die Modellierung und Implementation der Benutzerinteraktionen. Ziel ist, die GUI möglichst frei von Logik zu halten. Somit müssen alle Entscheidungen, die sich auf die GUI auswirken, außerhalb der GUI getroffen werden. Diese Form der Interaktion lässt sich mit Datenflüssen sehr gut modellieren. Doch diese Denkweise fällt vor allem eingefleischten OOP-Verfechtern schwer. Wie sehen die Anforderungen an die Anwendung aus? Ich habe dazu eine Featureliste erstellt. Die Features können bei einer konsequenten Datenflussmodellierung einzeln modelliert werden. Das Zusammenfügen der dabei entstandenen Modelle ist bei Datenflussdesigns kein Problem. Das liegt daran, dass die beteiligten Funktionseinheiten keine Abhängigkeiten mehr haben. F3: Vorhandenes To-do zum Bearbeiten öffnen. Interaktion: Doppelklick auf ein To-do. Feedback: To-do wird im Bearbeiten-Modus angezeigt und kann geändert werden. F4: To-dos persistieren. Interaktion: Beenden und Starten der App. Feedback: Nach Starten der Anwendung werden die To-dos aus dem vorhergehenden Lauf wieder angezeigt. Ressource: Datei todoliste.daten. Featureliste Da die einzelnen Features möglicherweise recht umfangreich sind, sollte man sich Gedanken machen, ob man sie weiter zerlegen kann. Bei dieser Zerlegung in sogenannte Featurescheiben oder Slices ist es wichtig, weiterhin bei Längsschnitten zu bleiben, also vertikal zu zerlegen. Würde man Feature F1 beispielsweise in den GUI-Anteil und den „Rest“ zerlegen, so wäre der GUI-Anteil kein eigenständiger Längsschnitt. Besser ist es, die Zerlegung so zu wählen, dass sie durch alle Funktionseinheiten hindurch verläuft. Dann wird ein Teil des GUIs implementiert, aber auch ein Teil des „Rests“, sodass eine voll funktionsfähige Teilfunktionalität zur Verfügung steht. Weitere Informationen zum Entwicklungsprozess siehe [1]. Abbildung 1 zeigt eine mögliche Benutzerschnittstelle. Die Aufgabe für diesen Monat besteht darin, die Features zu modellieren und anschließend zu implementieren. Bei der Implementation mag eine Beschränkung auf Featurescheiben sinnvoll sein, damit man nicht zu viel Zeit in ein einzelnes Feature investiert. Am Ende ist es spannender, von mehreren Features jeweils einen kleinen Ausschnitt zu implementieren, anstatt nur ein einziges Feature komplett zu implementieren. Auch in der Praxis empfiehlt sich diese Vorgehensweise, um möglichst früh Feedback vom Kunden zu erhalten. Happy modeling! [ml] F1: Ein neues To-do hinzufügen. Interaktion: Button Neu wird angeklickt. Feedback: Das neue To-do wird in der Liste der To-dos angezeigt und ist im Bearbeiten-Modus. F2: Bearbeiten eines To-dos beenden. Interaktion: Eingabetaste oder Anklicken eines anderen To-dos. Feedback: Das To-do wird im normalen Modus angezeigt. [1] Ralf Westphal, Elf Schritte bis zum Code, Von den Anforderungen zum fertigen Programm, Teil 1, dotnetpro 10/2010, Seite 126 ff., www.dotnetpro.de/A1010ArchitekturKolumne Teil 2, dotnetpro 11/2010, Seite 130 ff., www.dotnetpro.de/A1011Architektur Teil 3, dotnetpro 12/2010, Seite 130 ff., www.dotnetpro.de/A1012Architektur www.dotnetpro.de dotnetpro.dojos.2011 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 dotnetpro finden. Übung macht den Meister. Also − los geht’s. Aber Sie wollten doch nicht etwa sofort Visual Studio starten… [Abb. 1] So könnte die To-do-Liste aussehen. 77 Wer übt, gewinnt dnpCode: A1104DojoAufgabe LÖSUNG Eine interaktive Anwendung mit Datenflüssen modellieren Wissen, was zu tun ist MVVM-Pattern? Kennt man. Flow Design? Schon mal gehört. Event-Based Components? Klar, das ist die Spezialität von Ralf und Stefan. Aber alles zusammen auf einmal? Ist noch nicht da gewesen. Geht aber, auch wenn Stefan bei der Umsetzung ins Schwitzen kam. dnpCode: A1105DojoLoesung Stefan Lieser ist Softwareentwickler aus Leidenschaft. Nach seinem Informatikstudium mit Schwerpunkt auf Softwaretechnik hat er sich intensiv mit Patterns und Principles auseinandergesetzt. Er arbeitet als Berater und Trainer, hält zahlreiche Vorträge und hat gemeinsam mit Ralf Westphal die Initiative Clean Code Developer ins Leben gerufen. Sie erreichen ihn unter [email protected] oder lieser-online.de/blog. [Abb. 1] ViewModel im Datenfluss. 78 D as Modellieren der ToDo-Listenanwendung in Form eines Flow Designs ist eine nützliche Übung. Es stellt sich die herausfordernde Aufgabe, das MVVM-Pattern mit Flow Design und Event-Based Components (EBC) unter einen Hut zu bringen. Hier sind viele Buzzwords vereint, da tut Aufklärung not. Das Kürzel MVVM steht für Model-ViewViewModel. Das Pattern dient der Implementation grafischer Benutzerschnittstellen. Die Grundidee besteht darin, ein ViewModel zu definieren, welches die View optimal bedient. Als View wird hierbei die Benutzerschnittstelle bezeichnet. Das ViewModel enthält alle Daten, die von der View anzuzeigen sind. Das Ziel dabei ist es, die View so dünn wie möglich zu halten. Sie soll die Daten des ViewModels nicht deuten müssen. Der Grund liegt zum einen in der Testbarkeit. Views sind typischerweise schwierig automatisiert zu testen, daher sollte dort möglichst kein Code untergebracht werden. Zum anderen darf die View keine Domänenlogik enthalten. Es ist Aufgabe der Domänenlogik, die Daten im ViewModel so aufzubereiten, wie die View sie benötigt. Das ViewModel wird an die View übergeben. Es gibt also eine Abhängigkeit der View vom ViewModel. Für WPF- und Silverlight-Anwendungen bedeutet dies vor allem, dass das ViewModel optimal für Data Binding geeignet sein sollte. Dazu sollten beispielsweise die Eigenschaften des ViewModels die INotifyPropertyChanged-Schnittstelle bedienen. Statt also wie beim Model-View-Controller-(MVC)- oder Model-View-Presenter-(MVP)-Pattern der View jeweils mitzuteilen, welche Änderungen durchgeführt werden sollen, wird bei MVVM das ViewModel so an die View gebunden, dass direkt das ViewModel manipuliert werden kann. Durch das Data Binding werden bei Änderungen am ViewModel die notwendigen Aktualisierungen der View durch die Infrastruktur übernommen. Auf der anderen Seite des ViewModels steht das Model. Das Model steht für die eigentliche Geschäftslogik. Die Geschäftslogik soll frei sein von Abhängigkeiten zur View und ihren technischen Details. Als Mittler zwischen Model undView steht das ViewModel. Das Model kann das ViewModel erzeugen bzw. verändern, worauf die View mit einer Aktualisierung der Darstellung reagiert. Hochzeit von MVVM und Flow Design Die Herausforderung beim „Verheiraten“ von MVVM mit Flow Design liegt in der Frage, wie Änderungen am ViewModel modelliert werden. Wird in der View vom Benutzer eine Interaktion gestartet, sind für die Abarbeitung der zugehörigen Logik in der Regel Informationen aus dem ViewModel erforderlich. Umgekehrt führt die Abarbeitung der Geschäftslogik meist zu Änderungen, die in der View visualisiert werden müssen. Eine Möglichkeit wäre, das ViewModel als Ergebnis einer Änderung als Datenfluss zur View zu übertragen, so wie es Abbildung 1 zeigt. Damit würde die View allerdings jedes Mal eine neue Instanz des ViewModels erhalten, was das Data Binding ad absurdum führen würde. Sendet man immer dieselbe Instanz des ViewModels, stellt sich die Frage, wie man dies im Modell deutlich macht. Abbildung 2 zeigt dazu ein Beispiel. Die Aktion Neues ToDo erzeugen liefert an die View ein geändertes ViewModel, in das ein zusätzliches ToDo eingefügt wurde. Daraus ergeben sich nun gleich zwei Fragen: ❚ Woher kennt die Aktion Neues ToDo erzeugen den vorhergehenden Zustand desViewModels? ❚ Wie stellt dieView sicher, dass das Data Binding funktioniert? [Abb. 2] Neues ToDo einfügen. dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG Die Frage nach dem Zustand führt zur Lösung: Das ViewModel ist ein Zustand, der von der View und der Aktion gemeinsam verwendet wird. Beide sind von diesem Zustand abhängig. Wenn dem so ist, stellt sich erneut die Frage, ob es sinnvoll ist, das ViewModel als Datenfluss zur View zu übertragen. Wenn nämlich View und Aktion ein gemeinsames ViewModel verwenden, kann die Aktion das ViewModel manipulieren und muss der View keine Daten mehr in Form eines Datenflusses liefern. Schließlich erfolgt die Aktualisierung der View durch das Data Binding. Für solche Abhängigkeiten haben Ralf Westphal und ich die Notation des Flow Designs um eine weitere Pfeilart ergänzt. Ein Pfeil mit einem Punkt am Ende steht für eine Abhängigkeit. Abbildung 3 zeigt, wie man das ViewModel als gemeinsame Abhängigkeit zwischen die View und eine Aktion stellen kann. Abbildung 4 zeigt erneut das Feature Neues ToDo erzeugen, diesmal jedoch mit einer Abhängigkeit anstelle eines Datenflusses. Auf diese Weise ist es nun bequem möglich, die Abhängigkeit von einem Zustand zu modellieren. Durch die Modellierung des gemeinsamen Zustands als Abhängigkeit gehen die Datenflüsse in den folgenden Abbildungen jeweils vom GUI aus zu den einzelnen Aktionen. Die Aktionen ändern bei Bedarf das ViewModel, und per Data Binding gelangen diese Änderungen zum GUI. Features realisieren Beginnen wir mit dem ersten Feature : F1: Ein neues ToDo hinzufügen. Das Modell dazu ist in Abbildung 4 zu sehen. Das Feature ist im Prinzip recht simpel zu modellieren: Ausgehend von einem Datenfluss vom GUI zur Aktion Neues ToDo erzeugen wird das ViewModel um ein neues ToDo ergänzt, fertig. Durch die gemeinsame Abhängigkeit von View und Aktion vom ViewModel wird die Änderung im ViewModel per Data Binding in der View visualisiert. Bleibt noch die Frage zu klären, auf welchem Weg GUI und Aktion das ViewModel initial erhalten. Es muss einmal instanziert und in die beiden Funktionseinheiten hineingereicht werden. Ich habe mich entschieden, dies jeweils im Konstruktor von GUI und Aktion zu realisieren. Dazu erhalten die Konstruktoren einen Parameter vom Typ SharedState<T>. Dass beide Funktionseinheiten vom ViewModel abhängig sind, geht aus dem Flow-Design-Modell hervor. Wie diese Abhängigkeit initialisiert www.dotnetpro.de dotnetpro.dojos.2011 [Abb. 3] View und Aktion sind vom ViewModel abhängig. [Abb. 4] Neues ToDo erzeugen. [Abb. 5] Geöffnetes und geschlossenes ToDo. wird, ist ein Implementationsdetail. Nach der Modellierung habe ich begonnen, das Feature zu implementieren. Mir war klar, dass ich am GUI die meiste Zeit zubringen werde. Allerdings habe ich mich zunächst konsequent mit einer Minimalimplementation des GUIs zufriedengegeben und die restlichen Funktionseinheiten realisiert, damit am Ende ein Längsschnitt fertig wird statt nur ein schickes GUI. Nach den Minimalimplementationen standen App-Projekt und Buildskript an. Das App-Projekt ist dafür zuständig, die benötigten Funktionseinheiten zu instanzieren und alles zusammenzustecken (Build und Bind). Und da es bei komponentenorientierter Vorgehensweise nicht eine einzige Solution gibt, die das gesamte Programm ausspuckt, muss ein Buildskript her, welches alle Solutions in der richtigen Reihenfolge übersetzt. Das hört sich aufwendiger an, als es in der Praxis ist. Denn das Buildskript besteht aus den immer gleichen vier Schritten : 1. Übersetzen der Kontrakte, 2. Übersetzen aller Komponenten in beliebiger Reihenfolge, 3. Ausführen der Tests, 4. Übersetzen der App. Buildskripte erstelle ich nach wie vor mit FinalBuilder [1]. Das Übersetzen der Komponenten erfolgt in einer Schleife, in der einfach alle Komponentenwerkbänke aufgesammelt werden. Da die Buildreihenfolge der Komponenten egal ist, muss dieser Teil beim Hinzufügen weiterer Komponen- ten nicht angepasst werden. Dadurch ist das Erstellen des Buildskripts ein einmaliger Vorgang zu Beginn des Projekts. Das App-Projekt kann erst erstellt werden, wenn von allen Komponenten zumindest eine Minimalimplementation vorhanden ist. Das liegt daran, dass die App alle Komponenten binär referenzieren muss, um sie instanzieren zu können. Aus diesem Grund erstelle ich von allen Komponenten zunächst eine sogenannte Tracer-BulletImplementation. Die so implementierten Funktionseinheiten machen noch nicht wirklich etwas, außer Traceausgaben zu produzieren. So sind zwei Fliegen mit einer Klappe geschlagen: Zum einen kann das App-Projekt erstellt werden, zum anderen kann man anhand der Traceausgaben bereits feststellen, ob die einzelnen Funktionseinheiten korrekt zusammenspielen. Nachdem ich die einzelnen Funktionseinheiten implementiert hatte, habe ich begonnen, das GUI aufzumotzen. Ich möchte erreichen, dass ein ToDo in der Liste in zwei verschiedenen Zuständen angezeigt werden kann : ❚ geöffnet zur Bearbeitung, ❚ geschlossen zum Anzeigen. Abbildung 5 zeigt die Form mit je einem geöffneten und einem geschlossenen ToDo. Im geöffneten Zustand soll der Text des Eintrags editiert werden können. Ferner soll man das ToDo mit einer Checkbox als erledigt kennzeichnen können. In einem späteren Schritt soll auch die Eingabe von Tags möglich sein. Somit stand schnell die 79 LÖSUNG Listing 1 Einen Data Trigger verwenden. <Style TargetType="{x:Type ListViewItem}"> <Setter Property="IsSelected" Value="{Binding Mode=TwoWay, Path=IsSelected}"/> <Style.Triggers> <DataTrigger Binding="{Binding InBearbeitung}" Value="true"> <Setter Property="ContentTemplate" Value="{StaticResource OpenedItem}" /> </DataTrigger> <DataTrigger Binding="{Binding InBearbeitung}" Value="false"> <Setter Property="ContentTemplate" Value="{StaticResource ClosedItem}" /> </DataTrigger> </Style.Triggers> </Style> Idee im Raum, dazu eine ListView mit zwei unterschiedlichen DataTemplates zu verwenden. Die Frage war nur, wie man es erreicht, dass das DataTemplate in Abhängigkeit von einer Eigenschaft im ViewModel ausgewählt wird. Denn meine Idee war, dass jedes einzelne ToDo eine Eigenschaft InBearbeitung erhalten soll. Abhängig davon, ob diese Eigenschaft auf true oder false gesetzt ist, soll das passende DataTemplate ausgewählt werden. Die Lösung liegt in der Verwendung eines Data Triggers, der an die Eigenschaft im ViewModel gebunden wird. Listing 1 zeigt den relevanten XAML-Ausschnitt. Um das Tüfteln an der Form zu beschleunigen, braucht man einen Testrahmen. Müsste man erst die gesamte Anwendung vollständig übersetzen, um die Form in Aktion sehen zu können, wäre die Entwicklung stark ausgebremst. Die Komponentenorientierung will ja gerade erreichen, dass die einzelnen Komponenten isoliert entwi- Listing 2 Das GUI testen. [SetUp] public void Setup() { toDoListe = new ToDoListe(); toDoListe.ToDos.Add(new ToDo { Text = "ToDo Nummer 1", IsSelected = true}); // ... state = new SharedState<ToDoListe>(); state.Write(toDoListe); sut = new Main(state); sut.ToDo_schliessen += delegate { }; } [Test, RequiresSTA, Explicit] public void Liste_mit_ToDos_anzeigen() { sut.ShowDialog(); } 80 ckelt werden können. Das soll auch für das GUI möglich sein. Folglich habe ich in der GUI-Solution neben dem Projekt mit der Implementation noch ein Testprojekt angelegt. In einem normalen NUnit-Test instanziere ich die Form und fülle sie mit entsprechenden Testdaten. So ist ein zügiges Arbeiten möglich, weil zur visuellen Kontrolle lediglich ein Test gestartet werden muss. Listing 2 zeigt einen der GUI-Tests. Da dieser Test mit ShowDialog eine WPFForm öffnet und erst weiterläuft, wenn die Form wieder geschlossen ist, habe ich das NUnit-Attribut Explicit ergänzt. Es sorgt dafür, dass der Test nur ausgeführt wird, wenn er explizit gestartet wird. Beim Ausführen aller Tests der Assembly, beispielsweise auf dem Continuous-Integration-Server, werden die expliziten Tests ignoriert. Des Weiteren habe ich das Attribut RequiresSTA ergänzt, damit den Anforderungen von Windows Forms bzw. WPF entsprochen wird. Das Austüfteln des XAML-Codes hat am Ende doch einige Zeit in Anspruch genommen. Durch die klare Trennung des GUIs vom Rest der Anwendung, durch Einsatz der Komponentenorientierung, wäre es jedoch leicht möglich gewesen, diese Details von einem erfahrenen WPF-Entwickler vornehmen zu lassen. Dazu hätte dieser nicht die gesamte Anwendung benötigt, sondern lediglich die Visual-Studio-Solution mit der WPF-Form und dem zugehörigen Testrahmen. ToDos bearbeiten Wenn ein neues ToDo in die Liste aufgenommen wird, befindet es sich im Modus Bearbeiten. Dazu ist die Eigenschaft InBearbeitung im ViewModel auf true gesetzt. Damit immer nur ein einzelnes ToDo geöffnet dargestellt wird, muss die View einen Event auslösen, sobald ein anderes To- do selektiert wird. Daraufhin muss die InBearbeitung-Eigenschaft des selektierten Elements auf false geändert werden. Durch Data Binding sorgt WPF dann dafür, dass das Listenelement mit dem anderen DataTemplate im geschlossenen Zustand visualisiert wird. Umgekehrt muss es möglich sein, ein vorhandenes ToDo zur Bearbeitung zu öffnen. Laut Feature F3 soll dazu ein Doppelklick auf ein ToDo-Element als Interaktion verwendet werden. Auch hier ist also in der View ein Event auszulösen, wenn ein Eintrag der Liste per Doppelklick ausgewählt wird. Die Umsetzung der Logik ist trivial: Es muss lediglich die InBearbeitung-Eigenschaft angepasst werden. Persistenz Das nächste Feature hat es in sich: Die ToDo-Einträge sollen von einem zum anderen Programmstart erhalten bleiben. Dazu habe ich zunächst identifiziert, bei welchen bereits umgesetzten Features die ToDo-Liste so geändert wird, dass die Daten erneut persistiert werden müssen. Das ist bei folgenden Interaktionen der Fall: ❚ Ein neues ToDo wird angelegt. ❚ Der Text eines vorhandenen ToDos wird geändert. ❚ Die Erledigt-Eigenschaft eines vorhandenen ToDos wird geändert. Anschließend habe ich im vorhandenen Modell nachgesehen, ob diese drei Interaktionen dort bereits sichtbar sind. Bei zweien ist das der Fall, lediglich das Ändern der Erledigt-Eigenschaft über die Checkbox ist nicht im Modell sichtbar, da dies vollständig mittels Data Binding gelöst ist. Daher habe ich im GUI einen ausgehenden Datenfluss ergänzt, der mitteilt, dass ein ToDo geändert wurde. Ausgehend von den Aktivitäten ToDo geändert, ToDo-Bearbeitung beenden und Neues ToDo ergänzen wird die Aktivität ViewModel in ToDo-Liste übersetzen gestartet. Sie mappt das ViewModel auf ein Datenmodell für die Persistenz. Beide Modelle sind strikt zu trennen, damit es nicht zu Abhängigkeiten zwischen View und Persistenz kommt. Zum Mappen des ViewModels auf das Datamodell habe ich das Open-SourceFramework AutoMapper [2] verwendet. Als letzter Schritt muss das Datenmodell persistiert werden. Dazu habe ich es nach XML serialisiert. Weil der XML Serializer aus dem .NET Framework nur mit konkreten Typen umgehen kann und beispielsweise nicht mit IEnumerable<T> klarkommt, dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG [Abb. 6] Die ToDoListenlogik. habe ich dazu das Open-Source-Framework sharpSerializer verwendet [3]. Wenn der XML Serializer aus dem .NET Framework Sie mal wieder ärgert, sollten Sie sharpSerializer ausprobieren. Zum Speichern gehört auch das Laden der ToDo-Liste. Die Implementation dieser Funktionseinheit ging schnell von der Hand. Eingesetzt wird sie im Mainboard der Anwendung. Das Mainboard bietet eine Run-Methode, die beim Starten der App aufgerufen wird. Auch dieser Schritt lässt sich im Rahmen von Flow Design generalisieren. Nach Build, Bind und Inject folgt Init. In der Beispielanwendung, die Sie auf der beiliegenden Heft-DVD finden, wird die Run-Methode des Mainboards allerdings direkt aufgerufen. Spaßeshalber habe ich noch einen weiteren Persistenzmechanismus implementiert. Statt die ToDo-Liste in eine Datei zu schreiben, kann man sie auch in einer Amazon-SimpleDB-Datenbank ablegen. Das bringt beim Speichern allerdings Latenzzeiten mit sich. Damit entstand der Wunsch, das Speichern in den Hintergrund zu verlagern. Zusätzlich sollten mehrere Speicheraufträge, die in kurzer zeitlicher Folge eintreffen, zu einem einzigen zusammengefasst werden. Das Speichern soll so lange verzögert werden, bis für eine gewisse Zeit keine Änderungen mehr eintreffen. Die Umsetzung dieser Anforderung ist mit Flow Design und EBC ein Leichtes. Zunächst habe ich die Funktionseinheit ToDoListe speichern von einem Bauteil in eine Platine geändert. Anschließend habe ich in der Platine vor das eigentliche Speichern einen Standardbaustein Throttle eingesetzt. Dieser verzögert einen eingehenden Datenfluss für fünf Sekunden. Trifft das Ereignis in diesem Zeitraum mehrfach ein, wird der interne Timer dadurch wieder zurückgesetzt. Diesen Standardbaustein sowie einige andere finden Sie im OpenSource-Projekt ebclang [4]. www.dotnetpro.de dotnetpro.dojos.2011 Um die Benutzerschnittstelle über das Speichern zu informieren, liefert die Platine drei Datenflüsse, die anzeigen, dass mit dem Speichern begonnen wurde, dass es beendet ist bzw. dass ein Fehler aufgetreten ist. Damit diese Datenflüsse im GUI verwendet werden können, um eine Meldung in der Statusleiste auszugeben, müssen sie auf den GUIThread synchronisiert werden. Auch das übernehmen Standardbausteine innerhalb der Platine. Abbildung 6 zeigt den Kern der Anwendung, die ToDo-Listenlogik. Das in der ToDo-App eingesetzte Tooling ist Open Source und unter [4] zu finden. Die Platinen sind im Contracts-Projekt jeweils in XML-Dateien beschrieben. Aus diesen ebc.xml-Dateien wird durch den ebc.compiler C#-Code generiert. Ein Vorteil dieser Vorgehensweise liegt darin, dass die ebc.xml-Dateien visualisiert werden können. Dadurch ist eine visuelle Kontrolle möglich, die sicherstellt, dass die Modellierung korrekt in die ebc.xml-Dateien übernommen wurde. Zurzeit sehen die generierten Graphen noch nicht so richtig schick aus. Aber Ralf Westphal und ich arbeiten bereits an einem Nachfolger. ILmerge Als die Anwendung in Grundzügen fertig war, dachte ich mir, es sei eine gute Idee, alle DLLs der App mithilfe von ILmerge zu einer einzigen EXE-Datei zusammenzufassen. Das habe ich schon öfter gemacht und bin dabei nie auf Probleme gestoßen. Diesmal aber schon. Der Grund: ILmerge versagt seinen Dienst, sobald WPF-Assemblies mit im Spiel sind. Das liegt nicht an ILmerge, sondern daran, dass WPF beim Laden von Ressourcen vollqualifizierte Assemblynamen verwendet. Diese ändern sich dummerweise durch ILmerge. Aber da es ja darum ging, etwas zu lernen, habe ich mich nicht damit abgefunden, sondern nach Abhilfe gesucht. Die Lösung findet sich im Hinweistext zu ILmerge, in dem dasWPF-Problem beschrieben wird. Hier steht auch gleich der Link zu einer Lösung [5]. Allerdings funktionierte die dort beschriebene Lösung nicht auf Anhieb. Das lag daran, dass bei mehrfachem Laden derselben Assembly mehrere Instanzen der Assembly geliefert wurden. Ein kleiner Cache, implementiert mit einem Dictionary, schafft Abhilfe. Die ILmerge-Alternative basiert darauf, dass alle erforderlichen DLLs als Ressource in das EXE-Projekt eingebettet werden. Ein Assembly-Loader, der die Assemblies aus der Ressource holt, sorgt dafür, dass die Anwendung komplett in der EXEDatei enthalten ist. Wer sich das Verfahren genauer anschauen mag, sollte die Solution im Verzeichnis source.app öffnen. Fazit Die Umsetzung der kompletten ToDo-App hat länger gedauert als geplant. Das lag an drei Bereichen: ❚ Das Modellieren mit MVVM und Abhängigkeiten brauchte noch etwas Feinschliff. ❚ Der XAML-Code des GUIs ist recht aufwendig geraten. ❚ Es gab Probleme mit ILmerge. Trotz der Herausforderungen hatte ich zwischendurch immer wieder Versionen, die ich hätte liefern können. Konsequentes Fokussieren auf Längsschnitte sowie schrittweises Verfeinern sind Schlüssel zum entspannten Arbeiten. [ml] [1] Best of Breed, Die Lieblingstools der dotnetpro-Autoren, dotnetpro 3/2011, S. 16, www.dotnetpro.de/A1103LieblingsTools [2] AutoMapper, http://automapper.codeplex.com/ [3] sharpSerializer, http://www.sharpserializer.com/ [4] Event-Based Components Tooling, http://ebclang.codeplex.com/ [5] Microsoft Research, ILMerge, http://research.microsoft.com/enus/people/mbarnett/ilmerge.aspx 81 GRUNDLAGEN MVVM und Flow-Design kombinieren Alles unter einem Hut Das Data Binding von WPF bietet beeindruckende Möglichkeiten. MVVM ist für WPF-Anwendungen das geeignete Konzept. Flow-Design erlaubt eine sehr natürliche Art der Modellierung. Und Event-Based Components stellen ein universales Konzept für Modellierung und Implementierung. dotnetpro zeigt, wie Sie diese Konzepte gemeinsam nutzen können. Auf einen Blick Stefan Lieser ist Berater und Trainer und hat mit Ralf Westphal die Initiative „Clean Code Developer“ ins Leben gerufen. Sie erreichen ihn unter [email protected] oder unter lieser-online.de/blog. Inhalt ➡ Flow-Design für den Entwurf einer grafischen Oberfläche verwenden. ➡ Model und View in Abhängigkeit von einer einzelnen Instanz des ViewModels modellieren. ➡ Die Phasen des Konzepts der Event-Based Components bei der Implementierung umsetzen. dnpCode A1105MVVM 82 Alles ist im Fluss ür Anwendungen mit An dieser Stelle kommt das einer visuellen BenutFlow-Design ins Spiel. Flowzerschnittstelle stellt Design ist eine Art der Modelsich die zentrale Frage, wie die lierung, bei der Datenflüsse die Daten in die View gelangen. zentrale Rolle spielen. Die DaDie gleiche Frage stellt sich auf ten fließen hier zwischen dem Rückweg: Wie gelangen Funktionseinheiten, die in der die vom Benutzer eingegebeRegel in Form einer Aktion benen Daten von der View zur schrieben sind. Wenn also beiProgrammlogik? Ferner: Wie spielsweise in einer Anwenwerden Kommandos des Bedung zur Pflege von Kundennutzers mitgeteilt? Seit das Dadaten eine Änderung an einer ta Binding mit der Einführung Kundenadresse vorgenommen von WPF deutlich an Leiswerden soll, fließen die geäntungsfähigkeit gewonnen hat, derten Adressdaten von der liegt es auf der Hand, diese Benutzerschnittstelle zu einer Leistungsfähigkeit auch auszuFunktionseinheit Kundenadresnutzen. Zwar verfügte auch se ändern. Von dort fließen die schon Windows Forms über Daten weiter zu einer Aktion Data-Binding-Funktionalität, [Abb. 1] Ein Datenfluss mit GUI. Geänderte Kundendaten perdoch hat sich der Einsatz nicht sistieren, um in einer Datenwirklich durchgesetzt. bank abgelegt zu werden. Abbildung 1 zeigt das Wer bei WPF oder Silverlight die Möglichkeiten Modell dazu. des Data Bindings einmal gesehen hat, wird auf Dieser Fluss von Daten und das Aneinanderihren Einsatz nicht verzichten wollen. So kann reihen von Aktionen ist eine sehr natürliche Art beispielsweise eine Textbox mit einer Dropder Modellierung. Das liegt daran, dass es einem downliste verbunden werden, und der DatenDenken in Prozessschritten sehr nahe kommt. austausch erfolgt in beiden Richtungen. Wer das Denn wenn wir uns als Entwickler eine Anfordeohne Data Binding selbst implementieren müssrung wie das skizzierte Ändern einer Kundente, wäre längere Zeit beschäftigt. adresse anschauen, analysieren wir meist geAkzeptieren wir also für den Moment, dass das danklich, aus welchen Prozessschritten das Model-View-ViewModel-Pattern (MVVM) im ZuFeature besteht. Diese Prozessschritte sofort als sammenhang mit WPF und Silverlight gesetzt ist, Modell zu verwenden, liegt nahe. auch wenn eine detailliertere Betrachtung des Die Umsetzung solcher Flow-Designs in eine Patterns erst später folgt [1] [2]. Trotz MVVM Implementation ist mithilfe von Event-Based bleibt aber die Frage offen, wie der Rest der AnComponents (EBC) möglich. Dabei liegt der growendung strukturiert wird. Schließlich muss irße Vorteil in der Tatsache, dass das Modell 1:1 in gendwer das ViewModel mit Daten befüllen beCode übersetzt werden kann. Man findet also alziehungsweise vom Benutzer eingegebene Dale Artefakte des Modells leicht im Code wieder. ten verarbeiten. Zu glauben, mit MVVM wäre der Und umgekehrt gilt auch, dass alle Artefakte der gesamte Aufbau einer Anwendung bereits vorgeImplementation leicht im Modell zu verorten geben, ist ein Trugschluss, denn die Anwensind. Damit kann ein Flow-Design-Modell tatdungslogik dürfte den Hauptteil jeder nicht trisächlich die vornehmste Aufgabe eines Modells vialen Anwendung ausmachen. MVVM ist nur übernehmen, die darin besteht, zu abstrahieren. ein Pattern für bestimmte Teile der Anwendung Das Modell ist eine abstrakte Darstellung der Imund gibt noch nicht das Modell für den wesentplementation. Damit dient das Modell einerseits lich umfangreicheren Rest der Anwendung vor. F dotnetpro.dojos.2011 www.dotnetpro.de GRUNDLAGEN als Vorlage für die Implementation, andererseits auch als Dokumentation derselben. Ohne den Einsatz von MVVM ist das Einbeziehen der Benutzerschnittstelle in das Flow-Design ganz leicht: Daten fließen von der Benutzerschnittstelle zur Logik und werden dort verarbeitet. Und in vielen Fällen fließt aus der Logik ein Resultat zurück zur Benutzerschnittstelle, um dort visualisiert zu werden. Doch wie bezieht man nun Data Binding und ViewModels in das Flow-Design ein? ViewModels Dazu muss zunächst geklärt werden, was es mit dem ViewModel auf sich hat. Ein ViewModel enthält alle Daten, die eine View darstellen soll. Das ViewModel hat keine Abhängigkeit zu irgendeiner Infrastruktur wie etwa WPF oder Silverlight. Es sind einfache Klassen, die sich gut automatisiert testen lassen. Weil die Testbarkeit ein unverzichtbares Kriterium bei der modernen Softwareentwicklung darstellt, ist es wichtig, diesen Vorteil deutlich herauszustellen. Wenn das Ergebnis einer Logikoperation direkt in einer WPF-View angezeigt wird, ist es ungleich schwerer, dies automatisiert zu testen. Ist das Ergebnis der Operation jedoch ein ViewModel, das quasi aus der Operation hinausfließt, sind diese Tests sehr leicht zu automatisieren. Daraus folgt eine wichtige Erkenntnis: Views sollen die Daten nicht deuten müssen. Dann wäre nämlich in der View Code enthalten, der getestet werden muss. Liegen die Daten aber bereits im ViewModel fix und fertig aufbereitet vor, sodass die View diese Daten lediglich anzeigt, ohne sie erst deuten zu müssen, ist das Testen einfach. Dazu ein Beispiel: Häufig sind Controls in einer View nur unter bestimmten Umständen eingeschaltet. Es kann sein, dass ein Control zwar immer sichtbar ist, die Eingabe jedoch deaktiviert ist. Es kann aber auch sein, dass eine Gruppe von Controls nur in einem bestimmten Kontext angezeigt wird. Hier stellt sich also die Frage, wer dafür verantwortlich ist, Controls abhängig von einem Zustand auszublenden. Würde dies durch die View bewerkstelligt, indem dort eine entsprechende Prüfung des Zustands mit anschließendem Deaktivieren der Controls programmiert ist, wäre dies problematisch in Bezug auf die Testbarkeit. Viel leichter zu testen ist ein solches Szenario, wenn die zu testenden Daten bereits im ViewModel vorhanden sind. Dort sollte also für dieses Szenario für jedes Control eine Eigenschaft vorhanden sein, die anzeigt, ob das www.dotnetpro.de dotnetpro.dojos.2011 Listing 1 Ein Control ein- und ausblenden. public class ViewModel : INotifyPropertyChanged { private bool kontonummerEnabled; public bool KontonummerEnabled { get { return kontonummerEnabled; } set { kontonummerEnabled = value; PropertyChanged(this, new PropertyChangedEventArgs("KontonummerEnabled")); } } public event PropertyChangedEventHandler PropertyChanged = delegate { }; } [Abb. 2] ViewModel als Datenfluss austauschen. Control deaktiviert werden muss beziehungsweise gar nicht angezeigt werden soll. Listing 1 zeigt dazu ein Beispiel. Mittels ViewModel und Data Binding lässt sich in der View der Effekt erzielen, dass das Control nur aktiviert ist, wenn die Eigenschaft im ViewModel true ist. <TextBox IsEnabled="{Binding Path=KontonummerEnabled}" /> Der Vorteil dieser Vorgehensweise liegt darin, dass die Deutung des Zustands bereits durch die Logikeinheit erfolgt. Ferner lässt sich das Ergebnis im ViewModel ablesen und damit leicht automatisiert testen. Auch die View lässt sich auf diese Weise leicht testen. Im Test können einfach ViewModels mit unterschiedlichen Daten instanziert und an die View übergeben werden. Anschließend wird die View mit ShowDialog angezeigt und visuell überprüft. So ist sichergestellt, dass auch die in XAML formulierten Data Bindings schnell überprüft werden können. Es ist nicht mehr notwendig, die komplette Anwendung zu starten und mit Testdaten zu füttern, um die unterschiedlichen visuellen Effekte der Views zu überprüfen. View und Logik verbinden Das ViewModel bildet die Verbindung zwischen View und Logik. Im MVVM-Pattern wird die Logik als Modell bezeichnet. Das finde ich irreführend, weil im Rahmen des Entwurfs das Ergebnis der Modellierung ebenfalls Modell heißt. Daher spreche ich lieber von Logik. Eigentlich müsste es dann also Logik-View-ViewModel heißen. View und Logik sind beide vom ViewModel abhängig. Dadurch wird eine saubere Trennung zwischen View und Logik erreicht, denn zwischen ihnen gibt es keine direkte Abhängigkeit. Allerdings bleibt noch die Frage zu klären, wie beim FlowDesign das ViewModel konkret von View und Logik zu verwenden ist. Eine Möglichkeit wäre, das ViewModel als einen Datenfluss zwischen View und Logik aufzufassen. Das bedingt allerdings, dass View und Logik entweder jeweils ein neues ViewModel instanzieren oder einer von beiden dieses als Zustand hält. In beiden Fällen wird dies aus einem reinen Datenflussdiagramm nicht deutlich. Abbildung 2 zeigt das an einem Beispiel. Hier ist nicht ersichtlich, ob es sich beim Datenfluss um ein und dasselbe ViewModel handelt oder ob es jeweils eine neue Instanz ist. Abhilfe schafft eine Erweiterung der Flow-Design-Diagramme. Das ViewModel stellt eine Abhängigkeit dar. SowohlView als auch Logik sind vom ViewModel abhängig. Und wenn sich beide auf dieselbe Instanz beziehen würden, wäre es nicht mehr nötig, das ViewModel in Form eines Datenflusses zwischen beiden zu transportieren. Abbildung 3 zeigt das im Beispiel. Der Datenfluss von View zu Logik dient nun lediglich dazu, die Logik über die Interaktion zu informieren, damit dort die entsprechende 83 GRUNDLAGEN [Abb. 3] View und Logik sind vom ViewModel abhängig. [Abb. 4] MVVM mit Flow verheiratet. Funktionalität ausgeführt wird. Der Datenfluss trägt allerdings keine Daten mehr, was durch das leere Klammerpaar ausgedrückt wird. Stattdessen sind nun View und Logik vom ViewModel abhängig, was durch die „Pfeile“ mit Kringel am Ende angezeigt wird. Ein weiterer Vorteil dieser Vorgehensweise liegt darin, dass das ViewModel leicht eine Instanz sein kann, die nur einmalig beim Öffnen der View erzeugt wird. Damit wird das Data Binding erleichtert, da das Binden nur einmalig erfolgen muss. Fließt jeweils eine neue Instanz des ViewModels von der Logik zur View, muss dieses jeweils neu gebunden werden. Dieser Nachteil wird beseitigt, wenn View und Logik die ganze Zeit auf demselben ViewModel arbeiten. Um zu sehen, wie das ViewModel zu den beiden davon abhängigen Funktionseinheiten, View und Logik, gelangt, ist es notwendig, darzustellen, wie Funktionseinheiten instanziert und verbunden werden. Das Konzept der Event-Based Components sieht mehrere Phasen vor, die durchlaufen werden, bevor Daten zwischen den Funktionseinheiten fließen können: ❚ Build, ❚ Bind, ❚ Inject, ❚ Config, ❚ Run. Die Build-Phase ist dafür verantwortlich, Funktionseinheiten zu instanzieren. Für Bauteile ist dies ganz einfach, da die Konstruktoren von Bauteilen keine Parameter haben. Das liegt daran, dass Bauteile keine Abhängigkeiten aufweisen. Bei Platinen ist es jedoch erforderlich, die Funktionseinheiten, die von der Platine verbunden werden, vor der Platine zu instanzieren, da diese im Konstruktor an die Platine übergeben werden. Die Build-Phase kann entweder manuell ausprogrammiert werden oder von einem Dependency-Injection-Container wie StructureMap übernommen werden. 84 Aufgabe der Platinen ist es, Funktionseinheiten zu verbinden, indem Input- und Output-Pins zusammengesteckt werden. Dazu müssen Input-Pin-Methoden an Output-Pin-Events gebunden werden. Insofern erfolgt die Bind-Phase innerhalb der Build-Phase in den jeweiligen Konstruktoren der Platinen. In der Inject-Phase geht es darum, die Abhängigkeiten in die betroffenen Funktionseinheiten zu injizieren. Hier ist es wichtig, zu differenzieren. Es geht nicht um die Abhängigkeiten von Platinen zu den in ihnen enthaltenen Funktionseinheiten. Diese sind ja bereits durch die Build-Phase abgehandelt und wurden durch Konstruktorinjektion aufgelöst. In der Inject-Phase geht es um Abhängigkeiten, die explizit als solche modelliert wurden. Diese werden nun durch Aufruf einer Methode in die abhängigen Funktionseinheiten hineingereicht. In der vorletzten Phase, der Config-Phase, sind alle Funktionseinheiten bereits betriebsbereit. Sie sind verdrahtet und ihre Abhängigkeiten sind erfüllt. Bevor die Anwendung am sogenannten Entry Point startet, mögen manche Funktionseinheiten noch eine Konfiguration oder Initialisierung benötigen. So könnte es beispielsweise notwendig sein, die Daten der Anwendung durch einen Persistenzmechanismus zu laden. Zum Schluss ist eine Funktionseinheit dafür zuständig, die Ausführung der Anwendung zu beginnen. Meist ist dies das Hauptformular der Anwendung. Diese Funktionseinheit stellt eine Run-Methode bereit. Für die Phasen Inject, Config und Run sollten Interfaces verwendet werden, um diese Aspekte bei beliebigen Funktionseinheiten definieren zu können. Listing 2 zeigt das Interface für die Inject-Phase. In der Inject-Phase müssen alle Funktionseinheiten aufgesucht werden, die IDependsOn<T> implementieren. Diesen muss dann die er- forderliche Abhängigkeit durch Aufruf der Inject-Methode übergeben werden. Nun haben wir für die gemeinsame Verwendung eines ViewModels alles zusammen. Beim Programmstart werden alle Funktionseinheiten instanziert und die Datenflüsse verbunden. Anschließend werden die ViewModels an den entsprechenden Stellen injiziert und die Anwendung kann loslaufen. Die View weist das ViewModel dem DataContext zu. Dadurch werden die im XAML-Code definierten Bindings wirksam. Ändert eine Funktionseinheit Daten im ViewModel, werden diese Änderungen durch das Data Binding visualisiert. Ändert der Benutzer gebundene Daten über die Listing 2 Interface für die Inject-Phase. public interface IDependsOn<T> { void Inject(T independent); } Listing 3 Interface für Commands. public interface ICommand { event EventHandler CanExecuteChanged; bool CanExecute(object parameter); void Execute(object parameter); } Listing 4 Interface eines Kommandos. public interface IFlowCommand { event Action ExecuteAction; void SetCanExecute(bool newValue); } dotnetpro.dojos.2011 www.dotnetpro.de GRUNDLAGEN entsprechenden Controls, landen die Änderungen im ViewModel. Doch genau an dieser Stelle fehlt noch ein kleiner Baustein : Wie signalisiert eine View der zugehörigen Logik eine Interaktion? Wie sind Buttons, Toolbars und Menüs an die Logik anzubinden? Kommandos WPF verwendet das Konzept der Commands. Dahinter steht das Interface ICommand, siehe Listing 3. Ein Kommando muss zunächst eine Execute-Methode bereitstellen, die von WPF aufgerufen wird, um das Kommando auszuführen. Natürlich sollte der Code, der für das Kommando auszuführen ist, auf keinen Fall im Kommando abgelegt werden. Das wäre nicht viel besser, als würde die Logik direkt in der View untergebracht. Um den IsEnabledZustand eines Buttons oder Menüpunkts passend setzen zu können, ruft WPF die CanExecute-Funktion auf. Diese muss true liefern, wenn das Kommando ausgeführt werden kann. Ändert sich der Zustand von CanExecute, muss dies durch CanExecuteChanged signalisiert werden. Ein Kommando, welches ICommand implementiert, kann in WPF per Data Binding gebunden werden. Für einen Button sieht das beispielsweise so aus: Listing 5 ICommand und IFlowCommand nutzen. public class FlowCommand : ICommand, IFlowCommand { private bool canExecute = true; public void Execute(object parameter) { ExecuteAction(); } public bool CanExecute(object parameter) { return canExecute; } public event EventHandler CanExecuteChanged = delegate { }; public event Action ExecuteAction = delegate { }; public void SetCanExecute(bool newValue) { canExecute = newValue; CanExecuteChanged(this, EventArgs.Empty); } } Listing 6 Kommandos mit im ViewModel ablegen. public class ViewModel : INotifyPropertyChanged { private bool kontonummerEnabled; private string kontonummer; public ViewModel() { ÜberweisungCmd = new FlowCommand(); } public FlowCommand ÜberweisungCmd { get; private set; } public bool KontonummerEnabled { get { return kontonummerEnabled; } set { kontonummerEnabled = value; PropertyChanged(this, new PropertyChangedEventArgs("KontonummerEnabled")); } } public string Kontonummer { get { return kontonummer; } set { kontonummer = value; PropertyChanged(this, new PropertyChangedEventArgs("Kontonummer")); } } <Button Command="{Binding Path=ÜberweisungCmd}">Überweisung </Button> Doch wie bringt man ein Kommando mit einem Flow zusammen? Dazu muss das Kommando einen Ausgang haben, an dem ein Datenfluss beginnen kann. Ausgehende Flüsse werden bei EBC mit Events implementiert. Folglich muss das Kommando bei Aufruf der Execute-Methode einen Event auslösen, der dann einen Datenfluss startet. Umgekehrt muss es möglich sein, durch einen eingehenden Datenfluss zu bestimmen, ob ein Kommando weiterhin eingeschaltet ist. Eingehende Datenflüsse werden bei EBC durch Methoden realisiert. Somit sieht das Interface eines Kommandos aus EBC-Sicht so aus, wie es Listing 4 zeigt. ExecuteAction ist der vom Kommando ausgehende Datenfluss, der initiiert werden muss, wenn WPF die Execute-Methode aufruft. SetCanExecute ist der eingehende Datenfluss, mit dem ein Logikbauteil das Kommando ein- oder ausschalten kann. Eine Implementation der beiden Interfaces ICommand und IFlowCommand zeigt Listing 5. Damit kann das Kommando nun sowohl WPF-konform verwendet werden als auch www.dotnetpro.de dotnetpro.dojos.2011 } Listing 7 Datenflüsse verdrahten. public class Mainboard { public Mainboard(MainWindow mainWindow, Logik logik, FlowCommand überweisungCmd) { überweisungCmd.ExecuteAction += logik.ÜberweisungAktivieren; logik.ÜberweisungAktiviert += überweisungCmd.SetCanExecute; } } 85 GRUNDLAGEN in einem Datenfluss stehen. Für die WPFSeite ist ICommand zuständig, für die EBCSeite IFlowCommand. Für das Data Binding ist es sinnvoll, die Kommandos mit im ViewModel abzulegen. So kann der DataContext der Form für alle Bindings verwendet werden. Das ViewModel für ein einfaches Beispiel zeigt Listing 6. Auf diese Weise können in der XAMLDatei die Eigenschaften ÜberweisungCmd, Kontonummer und KontonummerEnabled gebunden werden. Listing 8 Build, Bind und Inject. var mainWindow = new MainWindow(); var logik = new Logik(); var viewModel = new ViewModel(); var mainBoard = new Mainboard(mainWindow, logik, viewModel.ÜberweisungCmd); mainWindow.Inject(viewModel); logik.Inject(viewModel); Auf der EBC-Seite werden View und Logik sowie das Kommando in eine Platine injiziert. Die Platine kann dann die Datenflüsse verdrahten, siehe Listing 7. Hier wird das Kommando so verdrahtet, dass es mit dem Eingang ÜberweisungAktivieren der Logik verbunden ist. Die Logik-Funktionseinheit kann daraufhin die notwendige Funktionalität ausführen. Durch die Verbindung von ÜberweisungAktiviert zu SetCanExecute wird nach Ausführung des Kommandos durch die Logik bestimmt, ob das Kommando nach wie vor eingeschaltet bleibt. Nach Build und Bind wird das ViewModel in die betroffenen Bauteile injiziert. Build, Bind und Inject sehen damit so aus wie in Listing 8. Abbildung 4 zeigt, wie die Funktionseinheiten zusammenspielen. Die View ist abhängig vom ViewModel. Dies ist notwendig, damit die View per Data Binding auf das ViewModel zugreifen kann. Auch die Logik ist vom ViewModel abhängig, damit sie Daten zur Visualisierung im ViewModel ablegen und Benutzereingaben von dort entnehmen kann. Zusätzlich zu den Abhängigkeiten steht ein Teil des ViewModels, nämlich die darin enthaltenen Kommandos, im Flow. Dabei können Datenflüsse zum einen von Kommandos ausgehen, um eine Benutzerinteraktion zu signalisieren. Zum anderen können sie im Kommando enden, um ein Kommando ein- oder auszuschalten. Fazit Durch die „Hochzeit“ zwischen MVVM und Flows steht einer WPF-konformen Umsetzung von Anwendungen, die auf Datenflüssen basieren, nichts mehr im Weg. So können Sie die tollen Möglichkeiten des Data Bindings voll ausschöpfen. Und natürlich können so auch die tollen Möglichkeiten des Flow-Designs und der Umsetzung mit Event-Based Components zum Einsatz kommen. [ml] [1] Torsten Zimmermann, Ein Rahmen für Feinheiten, Die Funktionsweise von MVVM-Frameworks für WPF, dotnetpro 1/2011, Seite 14 ff., www.dotnetpro.de/A1101FrameworkTest [2] Torsten Zimmermann, Geschickt verbunden, Microsofts Framework für das Entwurfsmuster MVVM, dotnetpro 1/2011, Seite 40 ff., www.dotnetpro.de/A1101WAF AUFGABE Synchronisation über die Cloud Was steht in den Wolken? Über die Cloud wurde genügend spekuliert. Es wird Zeit, sie konkret anzuwenden. Stefan, kannst du zu diesem wolkigen Thema eine möglichst handfeste Übung stellen? D ie Idee zu dieser Übung hängt mit der Aufgabe im vorhergehenden Heft zusammen, deren Lösung auf den folgenden Seiten präsentiert wird. Dort geht es um eine To-do-Listenanwendung, die ihre Daten lokal in einer Datei persistiert. Dass die Daten lokal persistiert werden, hat den großen Vorteil, dass sie auch dann zur Verfügung stehen, wenn das entsprechende Gerät mal gerade nicht mit dem Internet verbunden ist. Dennoch besteht oft der Wunsch, die Daten über die Cloud mit einem anderen Gerät zu synchronisieren. Diese Möglichkeit fehlt mir beispielsweise bei Things [1], einer To-do-Listenanwendung für Mac und iPhone/iPad. Leider kommt der Hersteller dieser Software schon seit mehreren Monaten nicht dem Versprechen nach, eine Synchronisation über die Cloud anzubieten. Zwar kann man die Daten per WLAN synchronisieren. Viel einfacher wäre aber eine Synchronisation über einen Service in der Cloud: Dann könnte die Software im Hintergrund selbstständig synchronisieren, und zwar unabhängig davon, ob die betreffenden Geräte gerade eingeschaltet sind oder nicht. Durch die Wolke stechen Doch wie schwierig ist die Realisierung einer solchen Synchronisation? Das herauszufinden ist die Übung für diesen Monat. Es geht dabei um einen sogenannten Spike. Ein Spike hat den Erkenntnisgewinn zum Ziel. Es geht also nicht darum, einen produktionsreifen Synchronisationsdienst zu entwickeln, sondern darum herauszufinden, wie ein solcher technisch realisierbar wäre. Um Daten über die Cloud zu synchronisieren, benötigt man einen Datenspeicher in der Cloud. Dieser ist von allen Clients aus erreichbar und kann somit verwendet werden, um darüber Daten auszutauschen. Auf der einen Seite schreibt ein Client seine Änderungen in diesen Cloudspeicher. Auf der anderen Seite holt sich ein anderer Client aus dem Cloudspeicher die Änderungen des ersten Clients. Beide Clients arbeiten also mit denselben Daten. Am Ende darf allerdings kein Client die Änderungen des anderen www.dotnetpro.de dotnetpro.dojos.2011 überschreiben. Findet ein Client in der Cloud neue oder geänderte Daten, müssen diese in den lokalen Speicher eingepflegt werden. Ferner muss der Client seine lokalen Änderungen an den Cloudspeicher melden. Natürlich können beim Synchronisieren Konflikte entstehen. Das passiert, wenn beide Clients dieselben Daten ändern und dann synchronisieren. Solche Konflikte sollten mindestens erkannt werden. Die Lösung des Konflikts könnte darin bestehen, den Anwender entscheiden zu lassen, welchen Stand er als aktuell akzeptieren möchte. Treten solche Konflikte häufig auf, könnte die Lösung darin bestehen, die Änderungen zusammenzufassen. Dies ist allerdings relativ aufwendig und vor allem nicht generisch lösbar. Es wird hier nicht weiter betrachtet. Als Cloudspeicher kann beispielsweise Amazon SimpleDB [2] zum Einsatz kommen. Dieser Service bietet ausreichend kostenlose Kapazitäten, um damit zu experimentieren. Mit dem Open-Source-Framework Simple Savant [3] steht ferner ein einfach zu bedienendes API zur Verfügung. Wer mag, kann auch eine Lösung mit Windows Azure versuchen [4]. Bleibt am Ende die Frage, wie man die Synchronisation algorithmisch löst. Klar ist, dass jeder Datensatz über eine eindeutige ID verfügen muss. Nimmt man dazu einen GUID, ist sichergestellt, dass die IDs auch über mehrere Clients hinweg eindeutig sind. So ist es schon mal einfach zu erkennen, ob ein Datensatz in der Cloud und/oder lokal vorhanden ist. Für die Synchronisation der Änderungen sind Fälle interessant, in denen sowohl in der Cloud als auch lokal eine Version der Daten vorliegt. Um dann zu erkennen, in welcher Richtung ein Update erfolgen muss, benötigt man eine Versionsnummer der Daten. Viel Spaß beim Tüfteln! [ml] 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 dotnetpro finden. Übung macht den Meister. Also − los geht’s. Aber Sie wollten doch nicht etwa sofort Visual Studio starten… [1] Cultured Code, Things, http://culturedcode.com/things/ [2] Amazon SimpleDB, http://aws.amazon.com/de/simpledb/ [3] Simple Savant, http://simol.codeplex.com/ [4] Windows Azure, http://www.microsoft.com/ germany/net/WindowsAzure/ 87 Wer übt, gewinnt dnpCode: A1105DojoAufgabe LÖSUNG Synchronisation über die Cloud Nicht ohne meine Wolke Seine Daten will man am liebsten überall von verschiedenen Geräten aus verfügbar haben. Kein Problem, wenn man sie über die Cloud synchronisiert. Und das ist gar nicht so schwer. dnpCode: A1106DojoLoesung Stefan Lieser ist Softwareentwickler aus Leidenschaft. Nach seinem Informatikstudium mit Schwerpunkt auf Softwaretechnik hat er sich intensiv mit Patterns und Principles auseinandergesetzt. Er arbeitet als Berater und Trainer, hält zahlreiche Vorträge und hat gemeinsam mit Ralf Westphal die Initiative Clean Code Developer ins Leben gerufen. Sie erreichen ihn unter [email protected] oder lieser-online.de/blog. D ie Synchronisierung von Daten wird immer wichtiger. Immer mehr Anwender nutzen Smartphones und Tablets, die ihrerseits immer leistungsfähiger werden. Damit wächst der Wunsch, alle relevanten Daten auch offline, also ohne Verbindung zum Internet, auf dem Gerät zur Verfügung zu haben. Aber natürlich müssen die Daten mit anderen Geräten synchronisiert werden, da die meisten Nutzer von Smartphones wohl zusätzlich einen Arbeitsplatzrechner und/oder ein Notebook verwenden. Ein zweiter Trend verstärkt die Nachfrage nach Synchronisationslösungen: die Cloud. Dienste in der Cloud werden ebenfalls immer leistungsfähiger, gleichzeitig sinken die Preise. Was liegt also näher, als die Synchronisation der Daten über die Cloud auszuführen? Die Idee dabei: Alle beteiligten Geräte synchronisieren sich mit einem Dienst, der in der Cloud läuft, siehe Abbildung 1. Dadurch muss man nicht mehr zwei Geräte miteinander verbinden, um Daten zu synchronisieren. Denn das ist lästig. Wenn ich mit dem Notebook arbeite, möchte ich alle Änderungen von dort in die Cloud synchronisieren. Anschließend „Deckel zu“ und Smartphone raus: Die Daten sollen nun von der Cloud zum Smartphone übertragen werden. Dabei kann zwischen dem Wechsel von einem zum anderen Gerät auch mal ein längerer Zeitraum vergehen. Das ist komfortabler, als wenn man zum Synchronisieren beide Geräte gleichzeitig verfügbar haben muss. Damit sich die Geräte synchronisieren können, müssen die Daten über die Cloud erreich- [Abb. 1] Daten über einen Cloud-Service synchronisieren. 88 bar sein. Das bedingt noch nicht, dass sie in der Cloud gespeichert werden. Ein Webservice, der über das Internet erreichbar ist und der seine Daten auf einem firmeneigenen Server ablegt, kann den Zweck ebenso erfüllen. Damit die Lösung hier nicht zu umfangreich ausfällt, habe ich mich jedoch dazu entschieden, direkt auf einen Datenspeicher in der Cloud zu setzen. Dadurch ist es nicht erforderlich, eine eigene Infrastruktur in der Cloud aufzubauen. Der Datenspeicher muss lediglich über das Internet erreichbar sein. Ein zusätzlicher Webservice entfällt. Die Clients greifen zum Synchronisieren direkt auf den Speicherservice in der Cloud zu. Amazon SimpleDB Wie in der Aufgabenstellung angedeutet, setzte ich bei meiner Lösung auf Amazon SimpleDB. Amazon bietet kostenlos ein ausreichend großes monatliches Kontingent an, sodass bei den Experimenten keine Kosten anfallen. Man muss sich lediglich für die Nutzung von SimpleDB bei Amazon anmelden [1]. Als API habe ich das Open-Source-Framework Simple Savant [2] verwendet. Es vereinfacht das Speichern und Laden von Objekten im SimpleDB Storage. Synchronisation Doch ehe wir zu den Details des Cloud-Speichers kommen, muss eine Strategie entwickelt werden, nach der die Synchronisation erfolgen soll. Es ist naheliegend, dass die einzelnen Datensätze einen eindeutigen Identifier benötigen. Und natürlich ist [Abb. 2] Synchronisieren als Flow. dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG es naheliegend, dazu den .NET-Datentyp Guid zu verwenden. Der Vorteil: Das Erzeugen einer ID für einen neuen Datensatz kann dezentral erfolgen, da der Guid-Algorithmus sicherstellt, dass die generierten IDs eindeutig sind. Für den Synchronisationsvorgang habe ich die einzelnen zu synchronisierenden Datensätze zusätzlich mit einer Versionsnummer versehen. Durch diese Nummer kann erkannt werden, ob Daten zwischenzeitlich auf einem anderen Client aktualisiert wurden: Ist die lokal vorhandene Version kleiner als die in der Cloud, muss offensichtlich eine Übertragung von der Cloud in den lokalen Speicher erfolgen. Damit das Verfahren funktioniert, muss sichergestellt werden, dass die Versionsnummer beim Speichern einer Änderung in der Cloud jeweils erhöht wird. Nur beim Speichern in der Cloud darf die Versionsnummer verändert werden! So können Clients leicht feststellen, ob in der Cloud eine neuere Version eines Datensatzes existiert. Allerdings genügt die Versionsnummer noch nicht, um alle möglichen Fälle abzudecken. Zusätzlich muss jeder Client an den Datensätzen festhalten, ob sie lokal geändert wurden. Sind die Versionsnummern nämlich gleich und liegen lokale Änderungen vor, müssen diese zur Cloud übertragen werden. Alle Szenarien sind in der Tabelle 1 abgebildet. Die beiden ersten Fälle sind einfach: Wenn Daten lokal, aber nicht entfernt vorhanden sind, muss der Datensatz in die Cloud übertragen und dort eingefügt werden. Sind die Daten umgekehrt nur in der Cloud, aber nicht lokal vorhanden, müssen sie lokal eingefügt werden. Der dritte Fall liegt vor, wenn Daten lokal geändert wurden. Beide Versionsnummern sind gleich, das heißt, lokal lag vor den Änderungen der Stand vor, der derzeit in der Cloud liegt. Daher müssen die lokalen Änderungen in die Cloud übertragen werden. Wurden Daten auf einem anderen Client geändert und in die Cloud übertragen, liegt der vierte Fall vor: Die entfernte Versionsnummer ist höher als die lokale, und es gibt lokal keine Änderungen. In diesem Fall werden die Änderungen aus der Cloud lokal übernommen. Im fünften Fall sind die Versionsnummern gleich, und es liegen lokal keine Änderungen vor. In diesem Fall ist nichts zu tun. Beim sechsten Fall kommt es zu einem Konflikt: Hier liegen Änderungen sowohl lokal wie entfernt vor. Die lokalen Änderungen werden am Changed Flag erkannt, www.dotnetpro.de dotnetpro.dojos.2011 [Tabelle 1] Mögliche Fälle bei der Synchronisation. die entfernten daran, dass entfernt eine höhere Versionsnummer als lokal vorliegt. In diesem Konfliktfall müssen die Änderungen zusammengeführt werden, sofern dies möglich ist. Im einfachsten Fall wird der Anwender informiert und um eine Entscheidung gebeten, welche Daten herangezogen werden sollen. Zuletzt bleiben noch zwei Fälle in der Tabelle übrig, die nicht eintreten können, wenn alles richtig implementiert ist: Die lokale Versionsnummer ist größer als die entfernte. Dieser Fall kann nicht eintreten, sofern die Versionsnummer nur beim Spei- chern von Daten auf dem entfernten System erhöht wird. Löschen Die Tabelle sieht einfach und übersichtlich aus, die Implementation dürfte eigentlich keine Schwierigkeiten bereiten. Doch der Teufel steckt im Detail: Das Verfahren eignet sich nicht für das Synchronisieren von Löschungen. Überhaupt ist das Löschen von Daten die größte Herausforderung beim Synchronisieren. Dazu ein Beispiel: Zunächst wird auf dem Client ein Datensatz angelegt und in Listing 1 Definition der Platine. <?xml version="1.0" encoding="utf-8"?> <board name="ToDo_Liste_synchronisieren" implements="IToDo_Liste_synchronisieren"> <using namespace="ebcpatterns"/> <using namespace="ebcpatterns.infrastructure"/> <using namespace="todo.contracts.datamodel"/> <external name="IToDo_Liste_in_ViewModel_uebersetzen"/> <external name="ILokale_ToDo_Liste_laden"/> <external name="IToDo_Liste_speichern"/> <wire from="this" type="" to="Lokale_ToDo_Liste_laden"/> <wire from="this" type="" to="Remote_ToDo_Liste_laden"/> <wire from="Lokale_ToDo_Liste_laden" type="ToDoListe" to="(ResetJoin{ToDoListe,ToDoListe,Tuple{ToDoListe,ToDoListe}})join.Input1"/> <wire from="Remote_ToDo_Liste_laden" type="ToDoListe" to="(ResetJoin{ToDoListe,ToDoListe,Tuple{ToDoListe,ToDoListe}})join.Input2"/> <wire from="(ResetJoin{ToDoListe,ToDoListe,Tuple{ToDoListe,ToDoListe}})join.Output" type="Tuple{ToDoListe,ToDoListe}" to="ToDo_Listen_synchronisieren"/> <wire from="ToDo_Listen_synchronisieren" type="ToDoListe" to="Remote_ToDo_Liste_speichern"/> <wire from="ToDo_Listen_synchronisieren" type="ToDoListe" to="ToDo_Liste_in_ViewModel_uebersetzen"/> <wire from="ToDo_Listen_synchronisieren" type="ToDoListe" to="ToDo_Liste_speichern"/> <dependency from="ToDo_Listen_synchronisieren" to="SharedState{ToDoListe}" /> </board> 89 LÖSUNG Listing 2 chronisieren entfernt werden, wenn sichergestellt ist, dass diese Daten beim Synchronisieren von der Cloud zum Client nicht wieder auf dem Client angelegt werden. Key und Passwort auslesen. public class Secrets { public string AwsAccessKeyId { get; set; } public string AwsSecretAccessKey { get; set; } public static Secrets LoadFromFile(string filename) { var serializer = new SharpSerializer(); return (Secrets)serializer.Deserialize(filename); } public void SaveToFile(string filename) { var serializer = new SharpSerializer(); serializer.Serialize(this, filename); } } Listing 3 Das Simple-Savant-API initialisieren. var secrets = Secrets.LoadFromFile("aws.secrets"); var config = new SavantConfig { ReadConsistency = ConsistencyBehavior.Immediate }; Savant = new SimpleSavant(secrets.AwsAccessKeyId, secrets.AwsSecretAccessKey, config); Listing 4 Festlegen der zu speichernden Attribute. var itemNameMapping = AttributeMapping.Create("Id", typeof(Guid)); itemMapping = ItemMapping.Create(domainName, itemNameMapping); itemMapping.AttributeMappings.Add(AttributeMapping.Create("Text", typeof(string))); itemMapping.AttributeMappings.Add(AttributeMapping.Create("Erledigt", typeof(bool))); itemMapping.AttributeMappings.Add(AttributeMapping.Create("Version", typeof(int))); itemMapping.AttributeMappings.Add(AttributeMapping.Create("Deleted", typeof(bool))); die Cloud synchronisiert. Damit steht der Datensatz nun mit der gleichen Versionsnummer lokal und entfernt zur Verfügung. Wird er nun lokal gelöscht und anschließend synchronisiert, landen wir bei Fall zwei der Tabelle: Der Datensatz wird aus der Cloud wieder zum Client übertragen, das Löschen damit rückgängig gemacht. Ich habe die Synchronisation in die ToDo-Listenanwendung integriert, die ich für die Übung aus dem vorhergehenden Heft erstellt habe. Für das Synchronisieren der Löschungen habe ich die Anwendung zunächst so umgestellt, dass das Löschen nicht hart auf den Daten ausgeführt wird, sondern nur soft, indem ein Marker in den Daten gesetzt wird. BeimVisualisieren ignoriert der Client alle als gelöscht markierten Daten. Allerdings werden die gelöschten Daten weiterhin in der Datendatei abge- 90 legt. Damit ist das Problem der Löschsynchronisation auf das normale Synchronisieren von Änderungen zurückgeführt. Auf dem Client führt beim Synchronisieren von Löschungen ohnehin kein Weg daran vorbei, über die Löschungen bis zum nächsten Synchronisieren Buch zu führen. Insofern ist das softe Löschen eine gute Vorbereitung für eine leistungsfähigere Implementation. Schließlich würden sich im Laufe der Zeit einige Daten ansammeln, wenn die als gelöscht markierten Daten nie entfernt würden. In der vorliegenden Implementation werden einfach alle als gelöscht markierten Daten dauerhaft gespeichert und synchronisiert. In einem späteren Schritt kann man dies so erweitern, dass die als gelöscht markierten Daten nur in der Cloud gehalten werden. Auf den Clients könnten die Löschungen nach dem Syn- Show me your Code! Doch nun zur Implementation. Zunächst habe ich die Synchronisation der ToDoListe auf relativ hohem Abstraktionsniveau durch einen Flow abgebildet. In diesem Flow werden die lokalen sowie die entfernten Daten gelesen und dann zu einer synchronisierten Liste zusammengefasst. Diese zusammengefasste Liste wird anschließend lokal und in der Cloud gespeichert. Ferner wird sie in das ViewModel gemappt, um im GUI visualisiert zu werden. Abbildung 2 zeigt den Flow. Alle Pfeile in der Abbildung repräsentieren Datenflüsse, Abhängigkeiten sind durch eine Verbindungslinie mit einem Punkt am Ende dargestellt. Die Abbildung ist aus einer XMLDatei generiert. Gleichzeitig werden aus dieser Datei die Interfaces für alle Funktionseinheiten und die Implementation der Platinen generiert. Das dabei verwendete Tooling ist im Open-Source-Projekt ebclang unter [3] zu finden. Listing 1 zeigt die Definition der Platine. Das Synchronisieren beginnt, indem die lokale und die entfernte ToDo-Liste geladen werden. Die beiden Ergebnisse der Ladevorgänge werden durch einen Join-Baustein zu einem Tuple zusammengefasst. Ganz wichtig bei diesem Join: Erst wenn beide Eingänge über Daten verfügen, wird der Datenstrom am Ausgang in Gang gesetzt. Bei einem erneuten Durchlaufen der Synchronisation müssen wieder beide Eingänge des Joins anliegen. Daher wird hier ein ResetJoin-Baustein aus dem ebclangProjekt verwendet. Bei diesem müssen jedes Mal beide Eingänge anliegen, bevor der Ausgang freigeschaltet wird. So ist sichergestellt, dass bei jedem Synchronisationsvorgang sowohl die lokalen als auch die entfernten Daten ermittelt werden. Der zweite wichtige Aspekt des Flows liegt in der Abhängigkeit des Bauteils ToDo_Listen_synchronisieren von SharedState<ToDoListe>. Diese Abhängigkeit ist im Kontext der ToDo-Listenanwendung erforderlich, damit das Modell der ToDo-Liste im Speicher aktualisiert wird. Dieses Modell repräsentiert den gesamten Zustand der Anwendung. Bauteile implementieren Nachdem das Flow-Design der Synchronisation erstellt war, habe ich begonnen, die 6. 2011 www.dotnetpro.de LÖSUNG Listing 5 Die ToDo-Liste aus dem Cloud-Speicher lesen. private IEnumerable<ToDo> LoadToDos() { using (new ConsistentReadScope()) { var selectStatement = new SelectCommand(simpleDb.ItemMapping, string.Format("select * from {0}", simpleDb.DomainName)); var results = simpleDb.Savant.SelectAttributes(selectStatement); foreach (var propertyValues in results) { var toDo = (ToDo)PropertyValues.CreateItem(simpleDb.ItemMapping, typeof(ToDo), propertyValues); Trace.TraceInformation(" Id: {0}", toDo.Id); yield return toDo; } } } einzelnen Bauteile zu implementieren. Das Laden der lokalen ToDo-Liste aus einer Datei war schon fertig und konnte wiederverwendet werden. Auch das lokale Speichern und das Übersetzen in das ViewModel hatte ich bereits im Rahmen der ToDo-Anwendung implementiert. Wer die Lösung der ToDo-Listenanwendung im vorhergehenden Heft studiert hat, sollte sich die aktuelle Version der Anwendung noch mal anschauen − es hat sich einiges geändert. Zur Implementation standen also an: ❚ Laden und Speichern der entfernten ToDo-Liste in einer SimpleDB-Datenbank in der Cloud. ❚ Das eigentliche Synchronisieren zweier Listen. Über den Wolken Beginnen wir beim Laden und Speichern in der Cloud. Um Amazons SimpleDB nutzen zu können, muss man bei jedem APIAufruf einen Key und ein Passwort übergeben. Simple Savant erwartet beide als Konstruktorparameter, sodass man die Angaben nur an einer Stelle hinterlegen muss. Natürlich haben solch vertrauliche Daten nichts im Quellcode zu suchen, sondern müssen in eine Konfigurationsdatei ausgelagert werden. Und tun Sie sich gleich den Gefallen, den Dateinamen in die IgnoreListe der Versionsverwaltung aufzunehmen. Sonst erhöht sich das Risiko, dass Ihre Zugangsdaten plötzlich nicht mehr so geheim sind, wie sie es verdient haben. Für das Lesen der Zugangsdaten habe ich eine Klasse implementiert, die einen XML-Serialisierer verwendet, um die Daten aus der Datei zu lesen, siehe Listing 2. Damit ist die Initialisierung der Simple Savant API ganz einfach, siehe Listing 3. Danach wird definiert, welche Attribute in den SimpleDB-Datensätzen abgelegt wer- www.dotnetpro.de 6. 2011 den sollen, siehe Listing 4. Anschließend können Sie bereits Objekte im Cloud-Speicher ablegen. Das Lesen der ToDo-Liste aus dem Cloud-Speicher zeigt Listing 5. Das Schreiben der Daten in den CloudSpeicher sieht ähnlich aus. Den kompletten Quellcode für den Cloud-Speicherzugriff finden Sie auf der beiliegenden Heft-DVD. Er befindet sich innerhalb des Projektverzeichnisses in der Solution source\todo.simpledbadapter\todo.simpledbadapter.sln. Ferner wird in der kommenden dotnetpro ein Artikel zum Einsatz von SimpleDB erscheinen. Listing 6 ToDo-Listen synchronisieren. public void Process(Tuple<ToDoListe, ToDoListe> message) { Trace.TraceInformation("ToDo_Listen_synchronisieren.Process"); Trace.TraceInformation(" Local count: {0}", message.Item1.ToDos.Count()); Trace.TraceInformation(" Remote count: {0}", message.Item2.ToDos.Count()); var distinctIds = GetDistinctIds(message.Item1.ToDos, message.Item2.ToDos); var pairs = GetPairs(distinctIds, message.Item1.ToDos, message.Item2.ToDos); var actions = GetSyncActions(pairs); var result = GetResult(actions); sharedState.Write(result); Result(result); } Listing 7 LINQ nutzen. internal static IEnumerable<Guid> GetDistinctIds(IEnumerable<ToDo> localToDos, IEnumerable<ToDo> remoteToDos) { return (from l in localToDos select l.Id) .Union(from r in remoteToDos select r.Id) .Distinct() .ToList(); } internal static IEnumerable<Tuple<ToDo, ToDo>> GetPairs(IEnumerable<Guid> distinctIds, IEnumerable<ToDo> localToDos, IEnumerable<ToDo> remoteToDos) { return (from id in distinctIds let local = localToDos.FirstOrDefault(x => x.Id == id) let remote = remoteToDos.FirstOrDefault(x => x.Id == id) select new Tuple<ToDo, ToDo>(local, remote)) .ToList(); } 91 LÖSUNG Listing 8 Das Ermitteln der Synchronisationsaktion testen. [TestFixture] public class SyncResultTests { private readonly Guid guid_1 = new Guid("11111111-1111-1111-1111-111111111111"); private readonly Guid guid_2 = new Guid("22222222-2222-2222-2222-222222222222"); [Test] public void Only_local_data_found() { var syncResult = Syncing.GetSyncResult( new ToDo(), null); Assert.That(syncResult, Is.EqualTo(SyncResult.InsertRemote)); } [Test] public void Only_remote_data_found() { var syncResult = Syncing.GetSyncResult( null, new ToDo()); Assert.That(syncResult, Is.EqualTo(SyncResult.InsertLocal)); } [Test] public void Local_changes() { var syncResult = Syncing.GetSyncResult( new ToDo { Id = guid_1, Version = 1, Changes = true}, new ToDo { Id = guid_1, Version = 1 }); Assert.That(syncResult, Is.EqualTo(SyncResult.UpdateRemote)); } [Test] public void No_local_changes() { var syncResult = Syncing.GetSyncResult( new ToDo { Id = guid_1, Version = 1 }, new ToDo { Id = guid_1, Version = 1 }); Assert.That(syncResult, Is.EqualTo(SyncResult.Nothing)); } [Test] public void Local_and_remote_changes() { Synchronisieren Das Synchronisieren arbeitet auf zwei Listen. Eine enthält den lokalen Stand der Daten, die andere den Stand der Daten in der Cloud. Um die Listen zu synchronisieren, müssen immer je zwei zusammengehörige Einträge verglichen werden. Dabei sind zunächst drei Fälle zu unterscheiden: ❚ ein Datensatz ist nur lokal vorhanden, ❚ ein Datensatz ist nur entfernt vorhanden, ❚ ein Datensatz ist sowohl lokal als auch entfernt vorhanden. Schon bei dieser ersten Überlegung wird klar, dass der komplette Vorgang der Synchronisation der beiden Listen nicht sinnvoll in einer einzigen Methode unterzubringen ist. Ferner ist die Aufgabenstellung zu kompliziert, um sofort draufloszucodieren. Nachdenken hilft bekanntlich, also habe ich vor der Implementation einen weiteren Flow entworfen. Abbildung 3 zeigt, wie die beiden Listen synchronisiert werden. 92 var syncResult = Syncing.GetSyncResult( new ToDo {Id = guid_1, Version = 1, Changes = true}, new ToDo {Id = guid_1, Version = 2}); Assert.That(syncResult, Is.EqualTo(SyncResult.Conflict)); } [Test] public void Remote_changes() { var syncResult = Syncing.GetSyncResult( new ToDo { Id = guid_1, Version = 1 }, new ToDo { Id = guid_1, Version = 2 }); Assert.That(syncResult, Is.EqualTo(SyncResult.UpdateLocal)); } [Test] public void Invalid_local_version_with_local_changes() { Assert.Throws<InvalidOperationException>(() => Syncing.GetSyncResult( new ToDo {Version = 2, Changes = true}, new ToDo {Version = 1})); } [Test] public void Invalid_local_version_without_local_changes() { Assert.Throws<InvalidOperationException>(() => Syncing.GetSyncResult( new ToDo {Version = 2}, new ToDo {Version = 1})); } [Test] public void Different_ids() { Assert.Throws<InvalidOperationException>(() => Syncing.GetSyncResult( new ToDo {Id = guid_1}, new ToDo {Id = guid_2} )); } } Im ersten Schritt (GetDistinctIds) wird eine Liste aller IDs gebildet, die in den beiden Datenlisten auftreten. Die Liste der IDs wird im zweiten Schritt (GetPairs) verwendet, um jeweils Paare von Datensätzen zu bilden. Dabei werden jeweils die Datensätze aus der lokalen und entfernten Liste mit gleicher ID zusammengestellt. Der dritte Schritt (GetSyncActions) ermittelt zu jedem der Paare die Aktion, die auszuführen ist. Dabei werden die in der Tabelle gezeigten Regeln umgesetzt. Zuletzt (GetRe- sult) werden die ermittelten Aktionen auf das zugehörige Paar angewandt, und es entsteht eine synchronisierte Liste. Der dargestellte Datenfluss ist kein astreiner Flow, das sei hier zugestanden. Die Umsetzung habe ich mit Methoden gelöst, siehe Listing 6. Der Vorteil dieser Aufteilung auf Methoden liegt in jedem Fall in der Testbarkeit. Die einzelnen Schritte der Synchronisation lassen sich isoliert testen. Am Ende genügen dann wenige Integrationstests, die den Datenfluss vollständig durchlaufen. [Abb. 3] Zwei Listen synchronisieren. dotnetpro.dojos.2011 www.dotnetpro.de LÖSUNG In den einzelnen Methoden habe ich LINQ wieder einmal schätzen gelernt. GetDistinctIds und GetPairs sind mit LINQ schnell implementiert, siehe Listing 7. Die Musik spielt dann am Ende an zwei Stellen: beim Ermitteln der Synchronisationsaktion und beim Zusammenstellen der Ergebnisliste. Die Synchronisationsaktion habe ich testgetrieben stur nach der Tabelle umgesetzt: ersten Fall der Tabelle genommen, in einem Test abgebildet, implementiert; zweiten Fall abgebildet, implementiert. Und so fort. Das ging leicht von der Hand. Und wie sich später zeigte, war dieser Teil auch von Anfang an korrekt. Auch die anderen Einzelteile waren sofort korrekt. Lediglich beim Join habe ich anfangs den falschen verwendet, weshalb mehrfaches Synchronisieren nicht auf Anhieb funktionierte. Listing 8 zeigt die Tests für das Ermitteln der Synchronisationsaktion. Die Umsetzung besteht dann nur aus ein paar Bedingungen, die in der richtigen Reihenfolge überprüft werden müssen, siehe Listing 9. Für das Zusammenstellen der Ergebnisliste müssen nun die Aktionen jeweils pro Datenpaar ausgeführt werden, siehe Listing 10. Je nach ermittelter Aktion wird der lokale oder der entfernte Datensatz in die Ergebnisliste übernommen. Synchronisationskonflikte habe ich hier nicht berücksichtigt, da dazu eine Interaktion mit dem Benutzer erforderlich ist. Die Konflikte müssten also vor dem Bilden der Ergebnisliste behandelt werden. Doch die Übung ist ohnehin wieder etwas länglich geraten, daher habe ich die Konfliktbehandlung weggelassen. Fazit Das Synchronisieren von Datenbeständen mehrerer Clients mittels eines Cloud-Speichers ist kein Hexenwerk. Die Lösung hat wenige Stunden in Anspruch genommen. Sie ist sicher noch nicht robust genug, um in ein Produkt aufgenommen zu werden. Doch der Schritt dahin ist nicht wirklich aufwendig. Für den Einsatz einer Synchronisation in einem Produkt will man dem Benutzer sicher auch nicht zumuten, seine AmazonSimpleDB-Credentials zurVerfügung zu stellen. Das heißt, dass man einen Webservice ergänzen müsste, über den die Synchronisation erfolgt. Auch das ist nicht mit Zauberei verbunden. Ich frage mich also am Ende weiterhin, wieso einzelne Produkte trotz lange zurückliegender Ankündigung immer noch keine Cloud-Synchronisation anbieten. An der technischen Herausforderung kann es jedenfalls nicht liegen. www.dotnetpro.de dotnetpro.dojos.2011 Listing 9 Datensätze synchronisieren. public class Syncing { public static SyncResult GetSyncResult(ToDo local, ToDo remote) { if (remote == null) { return SyncResult.InsertRemote; } if (local == null) { return SyncResult.InsertLocal; } if (local.Id != remote.Id) { throw new InvalidOperationException("Ids must be the same"); } if (local.Version == remote.Version) { if (local.Changes) { return SyncResult.UpdateRemote; } return SyncResult.Nothing; } if (local.Version < remote.Version) { if (local.Changes) { return SyncResult.Conflict; } return SyncResult.UpdateLocal; } throw new InvalidOperationException("Version numbers are corrupted"); } } Listing 10 Ergebnisliste zusammenstellen. private static IEnumerable<ToDo> GetResultList(IEnumerable<Tuple<SyncResult, Tuple<ToDo, ToDo>>> tuples) { foreach (var tuple in tuples) { if (tuple.Item1 == SyncResult.InsertLocal || tuple.Item1 == SyncResult.UpdateLocal) { Trace.TraceInformation(" Local Id: {0}, Sync: {1}", tuple.Item2.Item2.Id, tuple.Item1); tuple.Item2.Item2.Changes = false; yield return tuple.Item2.Item2; } else if (tuple.Item1 == SyncResult.InsertRemote || tuple.Item1 == SyncResult.UpdateRemote) { Trace.TraceInformation(" Remote Id: {0}, Sync: {1}", tuple.Item2.Item1.Id, tuple.Item1); tuple.Item2.Item1.Version++; tuple.Item2.Item1.Changes = false; yield return tuple.Item2.Item1; } else if (tuple.Item1 == SyncResult.Nothing) { Trace.TraceInformation( " Nothing Id: {0}, Sync: {1}", tuple.Item2.Item1.Id, tuple.Item1); yield return tuple.Item2.Item1; } } } Ein weiterer Aspekt ist mir bei der Übung erneut aufgefallen: Durch die Modellierung und Umsetzung mit Flows lässt sich die Anwendung einfach erweitern. Die Wiederverwendung einzelner Bausteine war problemlos möglich, und die Integra- tion der zusätzlichen Funktionalität war leicht. [ml] [1] http://aws.amazon.com/de/simpledb [2] http://simol.codeplex.com [3] http://ebclang.codeplex.com 93 Impressum Imprint Dojos für Entwickler Stefan Lieser, Tilman Börner published by: epubli GmbH, Berlin, www.epubli.de Copyright: © 2012 Stefan Lieser, Tilman Börner GRUNDLAGEN Coding Dojo : Mit Spaß lernen Lass uns einen lernen gehen Auch nach der Ausbildung gehört Lernen zum Berufsbild des Softwareentwicklers. Aber Lernen kann auch richtig Spaß machen: In lockerer Runde eine Programmieraufgabe lösen macht Laune. W as fällt Ihnen bei folgenden zwei Begriffen ein: Übung und Meister? Genau, das ist der Spruch, den wir suchen. Auch bei der Programmierung gilt, dass stetes Trainieren wichtig ist. Ja, man kann sogar sagen, dass Lernen wie in keiner anderem Branche überlebenswichtig ist. In kaum einem anderen Gebiet dreht sich das Rad mit neuen Technologien so schnell wie in der heiligen Softwareentwicklung. „Ja, das stimmt, aber ich übe jeden Tag, wenn ich für unsere Kunden Software schreibe“, mag Ihr Kommentar lauten. Das ist aber nicht richtig. Wer lernen will, muss spielen und ausprobieren. Spielen heißt im Fall von Programmieren schlicht, sich auch mal an etwas anderem versuchen, was nicht zur täglichen Arbeit gehört. Benötigen Sie in Ihrer täglichen Arbeit je Asynchronizität? Oder wie viele Male haben Sie schon die Rx [1] benutzt? Sind Sie fit mit Event-Based Components, oder wissen Sie, wie Sie Code tatsächlich clean machen? Mit TDD alles klar? Projekte, mit denen Sie im weitesten Sinne Ihr Einkommen verdienen, sind meist viel zu groß, um einen sinnvollen Anreiz für das Spielen zu geben. Eine Lernübung hingegen sollte vom Umfang überschaubar sein und nicht mehr als ein, zwei oder drei Stunden in Anspruch nehmen. Aufgaben stellen ist nicht einfach Der gravierendste und am häufigsten von Softwareentwicklern begangene Fehler liegt in folgenden Sätzen: „Das sind nur ein paar Zeilen Code“ oder „Das haben wir gleich“. Meist werden Aufwände komplett unterschätzt. Mit dem Ausdenken von Übungsaufgaben verhält es sich da nicht anders. „Aufgaben zu finden ist doch nicht schwer.“ Dieser Satz ist falsch, denn es müssen so viele Randbedingungen eingehalten werden: Idee, Machbarkeit, Dauer und so weiter. Finden Sie eine Aufgabe, die in etwa zwei bis drei Stunden lösbar ist, haben Sie eine sogenannte Kata erfunden. Wollen Sie sich die Mühe aber nicht machen, eigene Katas zu erfinden, können Sie auch welche aus dem Internet verwenden [2], [3]. Weiter unten listen wir die bekanntesten Katas auf. Kata und Dojo Der Begriff Kata ist der fernöstlichen Kampfkunst entlehnt und bedeutet so viel wie Übung. Wer www.dotnetpro.de dotnetpro.dojos.2011 Kampfsportarten lernt, läuft Katas. Das sind strikt vorgegebene Bewegungsfolgen, die Koordination, Bewegung und Exaktheit trainieren sollen. Insofern passt der Begriff eigentlich nicht hundertprozentig, denn es geht bei einer Coding Kata nicht darum, die gleiche Aufgabe immer und immer wieder zu lösen, sondern um das Eruieren von Neuem. Auch der Begriff Dojo kommt aus Fernost. Er bezeichnet den Platz, an dem Kampfsportarten geübt werden. In der Softwareentwicklung ist mit Coding Dojo nicht der Raum an sich gemeint, sondern das Event des Zusammenkommens und Lösens der Aufgabe. Es geht los – fast Am Anfang eines Coding Dojos bestimmt die Gruppe, welche Aufgabe gelöst werden soll. Sowohl Moderator als auch Teilnehmer können Vorschläge unterbreiten. Die Anwesenden einigen sich auf eine Kata, und los geht’s. Na ja, noch nicht ganz. Was gern vergessen wird, ist, zu prüfen, ob die Kata auch komplett von allen verstanden wurde. Was sind die Anforderungen? Wie sieht beispielsweise ein Use Case aus? Welche Nebenbedingungen sind zu erfüllen? Was ist eigentlich zu erzeugen? Ist das Ergebnis eine Klasse oder eine Methode? Erst wenn die Aufgabenstellung klar ist, sollte es losgehen. Und das heißt: programmieren nach Test-Driven Development. Das wiederum bedeutet, dass es eine Schnittstelle gibt, deren Funktionalität über Unit-Tests getestet wird. Es muss somit keine Kompilierung laufen, nur ein Testframework und ein Testrunner sind nötig. Auch das ist wieder so eine Randbedingung der Aufgabenstellung: Die Kata darf keine Abhängigkeiten haben. Sie muss losgelöst getestet werden können. Auf einen Blick Tilman Börner ist Diplomphysiker und Chefredakteur der dotnetpro. Programmieren ist für ihn ein kreativer Akt, für den er leider viel zu wenig Zeit hat. Inhalt ➡ In Coding Dojos löst man gemeinsam eine Programmieraufgabe per TDD. ➡ Die populärsten Übungsaufgaben vorgestellt. ➡ Spaß und Spiel gehören unbedingt mit zum Lernen. dnpCode A11DOJOKatas Die Spielarten Es gibt verschiedene Arten von Coding Dojos, über deren Sinn oder Unsinn schon trefflich gestritten wurde. Oft läuft ein Dojo aber so ab: Die Aufgabe wird an einem Rechner gelöst, dessen Bild per Beamer an die Wand projiziert wird. Es gibt einen Code Monkey, der im Prinzip das in die Tasten klopft, was die Runde der Anwesenden wünscht. In einer anderen Art des Coding Dojos kann jeder, der möchte, für eine gewisse Zeit am Dojo- 95 GRUNDLAGEN _Coding Dojo : Mit Spaß programmieren lernen [Abb. 1] Coding Dojo auf der .NET DevCon 2011 in Nürnberg mit Ilker Cetinkaya (stehend). Rechner sitzen. Damit ist er in der Lage, die Aufgabe so zu lösen, wie er möchte. Meist sorgt eine Zeitbegrenzung dafür, dass möglichst viele mal ihre Ideen zeigen können. Der Nachteil: Der Nachfolgende kann den Code des Vorgängers löschen und durch eigenen ersetzen. In beiden Formen ist es von Vorteil, wenn es einen Moderator gibt, der versucht, einen gewissen Konsens im einen Fall und eine gewisse Stringenz im anderen Fall durchzusetzen. Vor allem muss der Moderator auf die Zeit achten, denn Diskussionen unter Programmierern ufern bekanntlich schnell aus. Die Teilnehmer denken sich nun einen ersten Test aus, der die Schnittstelle gemäß einer Anforderung überprüft. Eine erste Implementierung des System Under Test (SUT) muss dann den ersten Test grün machen. Weitere Anforderungen werden in Form von Unit-Tests beschrieben und das SUT so implementiert, dass die Tests alle grün werden. Ein möglicher Streitpunkt ist, wann eine Refaktorisierung erfolgen soll und ob das überhaupt zu den Aufgaben des Coding Dojos gehört. Wenn Code nach TDD wächst, empfiehlt es sich nach gewissen Schritten, den Code so umzustrukturieren, dass er wieder lesbarer wird. Aber ist das tatsächlich eine Anforderung, die an die Software gestellt wird, oder soll der Code 96 nur die funktionale Ebene erfüllen, und es ist egal, wie er aussieht? Coding Dojo = Spaß + Lernen Dialog und Diskussion gehören zum Coding Dojo. Aber Vorsicht vor zu langen Diskussionen. Meistens entzünden sie sich an kleinen Dingen wie Benennungen von Methoden. Hier muss der Moderator rechtzeitig eingreifen – obwohl auch die richtige Benennung von Methoden und Variablen durchaus geübt werden muss. Schließlich ist die vorgegebene Zeit um, und die Aufgabe ist nicht gelöst. War das Coding Dojo deshalb ein Misserfolg? Diese Frage kann nur jeder für sich beantworten. Schließlich und endlich geht es ja darum, etwas mitzunehmen und etwas mit Spaß zu lernen (siehe auch Abbildung 2, Das Code Kata Manifesto). Und das kann schlicht die Erkenntnis sein, dass ein Coding Dojo manchmal einfach zu chaotisch verläuft. Die Klassiker : Ausgewählte Katas Kata BankOCR In dieser Kata geht es um das Lesen von Zahlen: Gesucht ist ein Algorithmus, der aus einer Eingabe eine Zahl erzeugt. Die Eingabe sind in dem Fall Strings, die Ausgabe soll ein Ganzzahlenformat sein. Wie der Name schon sagt, soll die Zahl per Optical Character Recognition (OCR) erkannt werden. Die Eingabe ist die Darstellung der Zahl in Form einer sogenannten Siebensegmentanzeige. Die Ziffern von 0 bis 9 würden dann so aussehen: _ _ _ _ _ _ _ _ | | | _| _||_||_ |_ ||_||_| |_| ||_ _| | _||_| ||_| _| Diese Zahl lässt sich durch drei Zeilen Text darstellen. Darin markieren Unterstriche, vertikale Striche und Leerzeichen die einzelnen Segmente einer Ziffer. Nach den drei Zeilen kommt eine Leerzeile, die eine Zahl von der nächsten trennt. Jede Ziffer besteht damit aus drei Zeilen und ist drei Zeichen breit. Die Ziffern sind innerhalb des Rechtecks aus drei Zeilen und drei Zeichen rechtsbündig ausgerichtet. Das bedeutet, die 1, die ja nur ein Zeichen breit ist, erhält links noch zwei Leerzeichen. Jede Zahl ist 9 Ziffern breit. Damit ist jede Zahl 27 Zeichen breit und drei Zeilen hoch. dotnetpro.dojos.2011 www.dotnetpro.de GRUNDLAGEN Aufgabe: Schreiben Sie einen Algorithmus, der aus einer Eingabe, die aus Strings besteht, Zahlen erzeugt. Die Menge der zu erkennenden Zahlen ist nicht besonders groß. Gehen Sie von 500 Zahlen aus. Die Zahlen sind alle fehlerfrei. Eine Fehlererkennung ist im ersten Schritt nicht nötig. Diese Kata lässt sich noch erweitern, etwa indem über eine Checksumme geprüft wird, ob die Zahl in Ordnung ist. Meist reicht aber für den ersten Teil die Zeit gerade so. Kata FizzBuzz Diese Kata geht auf ein Spiel zurück, bei dem die Konzentration eine wichtige Rolle spielt. Eine Gruppe Menschen steht zusammen und zählt der Reihe nach oben. 1, 2, 3 … und so weiter. So weit ist das noch einfach. Jetzt kommt aber die Verschärfung: Ist eine Zahl durch drei teilbar, ruft der Mensch statt des Zahlenwerts „Fizz“, ist sie durch fünf teilbar, dann „Buzz“, und ist sie durch drei und fünf teilbar, dann muss derjenige „FizzBuzz“ rufen. Sie dürfen sich selbst ausmalen, was derjenige machen muss, der nicht richtig Zahl, Fizz, Buzz oder FizzBuzz ruft. Aufgabe : Schreiben Sie einen Algorithmus, der jedes Mal, wenn er aufgerufen wird, entweder eine Zahl, Fizz, Buzz oder FizzBuzz zurückgibt. Startpunkt soll bei 1 liegen. Eine Folge von Aufrufen gibt also das Folgende zurück: 1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz … Kata Potter In dieser Kata geht es um den Verkauf von Büchern. Genauer gesagt: von Harry-Potter-Büchern. Eine Buchhandlung möchte als Werbeaktion besondere Buchbundles anbieten. Frei nach dem Motto: Kauf zwei, bekomm das dritte geschenkt. Aber so einfach geht es hier nicht zu. Ein Buch aus der Harry-Potter-Reihe soll acht Euro kosten. So weit wäre die Kata zwar sehr einfach, die Werbeaktion aber ein Fiasko. Also kommt Rabatt ins Spiel. Wer zwei verschiedene Bücher aus der Reihe erwirbt, erhält 5 Prozent Rabatt auf die beiden Bücher. Wer drei verschiedene Bücher kauft, erhält 10 Prozent Rabatt. Bei vier verschiedenen Büchern sind es schon 20 Prozent Rabatt. Kauft ein Kunde fünf verschiedene Bücher aus der Reihe, bekommt er 25 Prozent Rabatt. Knifflig wird es, wenn die Bücher nicht unterschiedlich sind. Wer beispielsweise drei Exemplare kauft, wobei es sich aber www.dotnetpro.de dotnetpro.dojos.2011 [Abb. 2] Das Code Kata Manifesto fasst alle wichtigen Regeln für eine Kata zusammen. nur um zwei verschiedene handelt (also ein Buch kauft er zweimal), erhält er nur auf die zwei verschiedenen 5 Prozent Rabatt. Das doppelte Buch kostet weiterhin 8 Euro. Aufgabe : Schreiben Sie einen Algorithmus, der für beliebig viele Exemplare den Preis berechnet, wobei der den maximalen Rabatt geben soll. Bei dieser Aufgabe ist es extrem wichtig, sich durch einige Fälle mit der Problematik vertraut zu machen. Die Optimierung bezüglich geringstem Preis ist nicht so einfach, wie das auf den ersten Blick scheint. Kata Tennis Tennisspieler sind anders. Sie spielen auf Sand oder Rasen, zu zweit oder zu viert und hauen sich mit Schlägern den Ball gegenseitig um die Ohren. Am seltsamsten ist aber die Zählweise, die den Punktestand in einem Tennismatch festhält. 0, 15, 30, 40, Spiel lautet die Zählweise. Erreichen die Gegner beide den Punktestand 40, entsteht ein sogenannter Einstand. Wer nach dem Einstand einen Ballwechsel für sich entscheiden kann, erhält den „Vorteil“. Aber er hat das Spiel noch nicht gewonnen. Erst mit einem erneuten gewonnenen Ballwechsel gewinnt er das Match. Verliert er den Ballwechsel, herrscht wieder Einstand. Wie gesagt: Das Tennisspiel ist seltsam. Nichtsdestotrotz gibt das eine schöne Aufgabe für einen Coding Dojo. Aufgabe : Schreiben Sie einen Algorithmus, der die Zählweise des Tennis nachahmt. Dabei bekommt Spieler A oder Spieler B den Gewinn des Ballwechsels zugesprochen. Der Algorithmus soll den aktuellen Spielstand zurückgeben. Kata Römische Zahlen Das darf nicht fehlen: Wenn es seltsame Zahlensysteme gibt, dann gehört das römische mit dazu. Gesehen haben Sie solche Zahlen sicher schon, vor allem als Jahreszahlangabe auf Gräbern oder Häusern. MCMLI steht etwa für 1951. Die Zahlen sind so aufgebaut: Es gibt Zeichen, mit denen sich alle Zahlen darstellen lassen. Diese Zeichen sind: I, V, X, L, C, D, M gemäß 1, 5, 10, 50, 100, 500, 1000. Eine Zahl setzt sich nun aus mehreren solcher Zeichen zusammen: I steht für 1 II steht für 2 III steht für 3 IV steht für 4 V steht für 5 VI steht für 6 VII steht für 7 VIII steht für 8 IX steht für 9 X für 10. Aufgabe : Schreiben Sie einen Algorithmus, der eine dezimale Zahl als String einer römischen Zahl zurückgibt. Achten Sie darauf, dass die römische Zahl auch valide 97 GRUNDLAGEN _Coding Dojo : Mit Spaß programmieren lernen ist. IM beispielsweise für 999 ist falsch. Richtig wäre CMXCIX. Kata Taschenrechner Römische Zahlen Wer konvertieren kann, kann auch rechnen. Somit kann man die Kata Römische Zahlen so erweitern, dass daraus ein Taschenrechner wird. Aber Vorsicht : Ist das Konvertieren überhaupt nötig ? Oder lässt sich das ganze auch ohne Konversion erledigen. Es gibt nämlich die folgende Erleichterung : Der Taschenrechner soll nur addieren. Subtraktion, Division und Multiplikation sind nicht zu implementieren. Aufgabe : Schreiben Sie einen Algorithmus, der zwei römische Zahlen addiert. Also: VIII + LXX = LXXVIII oder CXI + XII = CXXIII Kata Spiel des Lebens Das „Spiel des Lebens“ ist wohl ein Klassiker, den John Horton Conway schon 1970 erfunden hat. Auf Basis einer Verteilung von Zellen auf einem zweidimensionalen Raster wird die nächste Generation an Zellen berechnet. Welche Zelle in der nächsten Generation weiterlebt, stirbt oder von den Toten aufgeweckt wird, richtet sich danach, wie viele Zellen sie als Nachbarn hat. Nach folgenden Bedingungen berechnet sich die nächste Generation: 1. Jede Zelle, die weniger als zwei Nachbarn hat, stirbt wegen Vereinsamung. 2. Jede Zelle, die mehr als drei Nachbarn hat, stirbt wegen Überbevölkerung. 3. Jede Zelle, die zwei oder drei Nachbarn hat, lebt auch in der nächsten Generation weiter. 4. Jede tote Zelle mit genau drei Nachbarn wird reanimiert und lebt in der nächsten Generaton wieder. Besonders zu beachten ist die Bedingung 4 : Wir werden zu Frankensteins, die Leben erschaffen können. Hier ein Beispiel für drei aufeinanderfolgende Generationen. Drei Zellen in einer Reihe werden zu einem Oszillator, der von einer Generation zur nächsten die Ausrichtung von vertikal zu horizontal und wieder zurück bildet. Generation 1 ........ ....*... ....*... ....*... 98 [Abb. 3] Eine Kata von Ilker Cetincayas Website [4]. Ilker hat die Coding Dojos in Deutschland in die .NET-Community gebracht. Generation 2 ........ ........ ...***.. ........ Generation 3 ........ ....*... ....*... ....*... Aufgabe : Schreiben Sie einen Algorithmus, der aus einem zweidimensionalen Array mit lebenden und toten Zellen auf Basis der vier Regeln die nächste Generation berechnet. Der Algorithmus soll ein zweidimensionales Array zurückgeben, das dann wieder zur Berechnung der nächsten Generation verwendet werden kann. Hinweis : Teilen Sie die Randbereiche ab. Diese bedürfen einer eigenen Behandlung. Kata Anagram Gegeben sei ein Wort, das der Schlüssel zum Schloss eines sagenhaften Schatzes ist. Nur leider sind die Buchstaben dieses Wortes durcheinandergeraten. Sie müssen so lange probieren, bis Sie die richtige Kombination gefunden haben. Wäre das Wort etwa ei, dann ist das schnell erledigt, denn es gibt nur noch ie als Permutation. Bei „ein“ sind es schon sechs mögliche : ein eni ine ien nei nie Aufgabe : Schreiben Sie einen Algorithmus, der von einem eingegebenen Wort alle Buchstabenpermutationen erzeugt. Eingabe ist ein String, Ausgabe ist eine Liste von Strings. Aber aufpassen: Die Zahl der Permutationen wächst mit der Fakultät der Zahl der Buchstaben. Also sind es bei einem Wort mit vier Buchstaben schon 24 mögliche Wörter. [tib] [1] http://msdn.microsoft.com/enus/data/gg577609 [2] http://codingkata.org/ [3] http://codingdojo.org/ [4] ilker.de/code-kata-pickakin dotnetpro.dojos.2011 www.dotnetpro.de #1 dotnetpro.de facebook.de/dotnetpro twitter.com/dotnetpro_mag gplus.to/dotnetpro