Sie sind auf Seite 1von 98

#1

Stefan Lieser, Tilman Brner

Dojos fr Entwickler
15 Aufgaben und Lsungen in .NET

EINLEITUNG

Wer bt, gewinnt

in Profimusiker bt tglich mehrere Stunden. Er bt Fingerfertigkeit, Phrasierung, Ansatz beziehungsweise Haltung, Intonation und VomBlatt-Spielen. Als Hilfsmittel verwendet er Tonleitern, Etden, Ausschnitte von Stcken und Unbekanntes. Ohne ben knnte er die Qualitt seines Spiels nicht halten, geschweige denn verbessern. ben gehrt fr 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 Auffhrung, sprich das Program-mieren, mit dem Sie sich Ihr Einkommen verdienen. Gemeint sind die Etden, das ben von Fingerfertigkeit, Intonation, Ansatz und Vom-Blatt-Spielen. Wie sehen diese Aufgaben denn bei einem Programmierer aus? Freilich liee sich die Analogie bis zum Abwinken auslegen. Hier mag ein kleiner Ausschnitt gengen: Sie knnten als Etde zum Beispiel trainieren, dass Sie immer erst den Test schreiben und dann die Implementation der Methode, die den Test erfllt. Damit verwenden Sie knftig nicht immer wieder den falschen Fingersatz, sondern immer gleich die richtige Reihenfolge: Test Implementation. Klar, ben ist zeitraubend und manchmal nervttend vor allem fr die, die zuhren. Aber ben kann auch Spa machen. Kniffeln, eine Aufgabe lsen und dann die eigene Lsung mit einer anderen Lsung vergleichen. Das ist der Grundgedanke beim dotnetpro.dojo. In jeder Ausgabe stellt dotnetpro eine Aufgabe, die in maximal drei Stunden zu lsen sein sollte. Sie investieren einmal pro Monat wenige Stunden und ge-

winnen dabei jede Menge Wissen und Erfahrung. Den Begriff Dojo hat die dotnetpro nicht erfunden. Dojo nennen die Anhnger fernstlicher Kampfsportarten ihren bungsraum. Aber auch in der Programmierung hat sich der Begriff eines Code Dojo fr eine bung eingebrgert.

Der Spruch bung macht den Meister ist abgedroschen, weil oft bemht, weil einfach richtig. Deshalb finden Sie in diesem Sonderheft 15 dotnetpro.dojos, also bungsaufgaben inklusive einer Musterlsung und Grundlagen.

Das knnen Sie gewinnen


Der Gewinn lsst sich in ein Wort fassen: Lernen. Das ist Sinn und Zweck eines Dojo. Sie knnen/ drfen/sollen lernen. Einen materiellen Preis loben wir nicht aus. Ein dot-netpro.dojo ist kein Contest. Dafr gilt aber : T Falsche Lsungen gibt es nicht. Es gibt mglicherweise elegantere, krzere oder schnellere, aber keine falschen. T Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das knnen Sie, indem Sie Ihre Lsung mit der vergleichen, die Sie eine Ausgabe spter in der dotnetpro finden.

Wer stellt die Aufgabe? Wer liefert die Lsung?


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 fr Qualitt der resultierenden Software sorgen. Er denkt sich die Aufgaben aus und gibt dann auch seine Lsung 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 Lsungen gibt es nicht.

www.dotnetpro.de dotnetpro.dojos.2011

INHALT

15 Aufgaben und Lsungen


5 Aufgabe 1 : Vier gewinnt
Ein Spielfeld, zwei Spieler und jede Menge Spa beim Programmieren : Das kleine Brettspiel ist genau das Richtige zum Warmwerden.

66 Aufgabe 12 : Twitter
Es treten auf: mehrere Threads, eine Synchronisation, ein Timer, ein Control wahlweise in WPF-, Windows-Forms- oder Silverlight-Qualitt und ein API. Fertig ist das Twitter-Band.

9 Aufgabe 2 : Data Binding


Knpfe Kontrollelement an Eigenschaft, und schon wirkt der Zauber: Vernderungen der Eigenschaft spiegeln sich im Control wider und auch andersherum.

71 Aufgabe 13 : Graphen
Entwerfen Sie ein API fr 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, Mller, Schulze ganze 250 000 Mal: Fr 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 grten Wert, Mittelwert und die Quartile.

31 Aufgabe 6 : RavenDB
Computer aus, Daten weg? Von wegen: Eine Persistenzschicht sorgt fr deren berleben. Mit RavenDB braucht man dafr auch keinen SQL-Server.

77 Aufgabe 14 : ToDo, MVVM und Datenfluss


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

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.

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 ...

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.

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.

50 Aufgabe 9 : Event-Based Components


Was, bitte schn, 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 fr dich. Aus Wurzel, Zweig und Blatt und den Interfaces ITree<T> und INode<T>. Und Sie drfen ihn erklettern.

95 Klassische Katas
Sie heien Kata Potter, Kata BankOCR oder Kata FizzBuzz: An klassischen Programmieraufgaben gibt es inzwischen schon ganze Kataloge. Tilman Brner stellt die wichtigsten vor.

61 Aufgabe 11 : LINQ
Frage: Wie heit die bekannteste Abfragesprache? Richtig: SQL. Aber in dieser Aufgabe geht es um eine andere: Language Integrated Query.

Impressum
94 Impressum

dotnetpro.dojos.2011 www.dotnetpro.de

AUFGABE Stefan, vielleicht sollten wir erst einmal mit etwas Einfacherem anfangen. Vielleicht wre ein kleines Spiel zum Warmwerden genau das Richtige. Fllt dir dazu eine Aufgabe ein?

Wer bt, gewinnt

lar, knnen wir machen. Wie wre 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 Benutzeroberflche. 4 gewinnt wird mit einem aufrecht stehenden Spielfeld von sieben Spalten gespielt. In jede Spalte knnen von oben maximal sechs Spielsteine geworfen werden. Ein Spielstein fllt 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.

Die Abbildungen 1 und 2 zeigen, wie eine Oberflche aussehen knnte. Ist die Spalte, in die der Spieler seinen Stein legen mchte, bereits ganz mit Steinen gefllt, 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 zugehrigen automatisierten Tests. Die Benutzerschnittstelle des Spiels steht eher im Hintergrund. Ob animierte WPFOberflche, WinForms, ASP .NET oder Konsolenanwendung, das ist nicht wichtig. Im Vordergrund soll eine Lsung stehen, die leicht in eine beliebige Oberflchentechnologie integriert werden kann. Evolvierbarkeit und Korrektheit sollen hier also strker bewertet werden als eine superschicke Oberflche. Im nchsten Heft zeigen wir eine exemplarische Musterlsung. Die Lsung kann es in einem solchen Fall bekanntlich eh nicht geben. Damit mchte ich Sie, lieber Leser, noch mal ermutigen, sich der Aufgabe anzunehmen. Investieren Sie etwas Zeit, und erarbeiten Sie eine eigene Lsung. Die knnen Sie dann spter mit der hier vorgestellten vergleichen. Viel Spa !

In jeder dotnetpro finden Sie eine bungsaufgabe von Stefan Lieser, die in maximal drei Stunden zu lsen sein sollte. Wer die Zeit investiert, gewinnt in jedem Fall wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen. Es gilt : T Falsche Lsungen gibt es nicht. Es gibt mglicherweise elegantere, krzere oder schnellere Lsungen, aber keine falschen. T Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das knnen Sie, indem Sie Ihre Lsung mit der vergleichen, die Sie eine Ausgabe spter in der dotnetpro finden. bung macht den Meister. Also los gehts. Aber Sie wollten doch nicht etwa sofort Visual Studio starten

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 fhrten, ermittelt. Bei einer grafischen Benutzeroberflche knnten die vier Steine dazu farblich markiert oder eingerahmt werden. Bei einer Konsolenoberflche knnen 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 Benutzeroberflche knnen die Spalten je durch einen Button gewhlt werden. Wird das Spiel als Konsolenanwendung implementiert, gengt die Eingabe der jeweiligen Spaltennummer per Tastatur.

[Abb. 1 und 2] Eine mgliche Oberflche (links) und die Anzeige der siegreichen vier Steine (rechts). Aber auf die Oberflche kommt es bei dieser bung nicht an.

www.dotnetpro.de dotnetpro.dojos.2011

LSUNG
Eine bung, bei der Sie nur gewinnen konnten

O
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 frh gezuckt hat und sofort mit der Codeeingabe begonnen hat, wird es vielleicht gemerkt haben: Die Aufgabe luft aus dem Ruder, wchst einem ber den Kopf. Das ging mir nicht anders. Frher. 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 lsen, 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 fhren. Zwei Spieler legen abwechselnd gelbe und rote Spielsteine in ein 7 x 6 Felder groes Spielfeld. Derjenige, der als Erster vier Steine seiner Farbe nebeneinander liegen hat, hat das Spiel gewonnen. Hier hilft es, sich mgliche Vierergruppen aufzumalen, um zu erkennen, welche Konstellationen im Spielfeld auftreten knnen. Nachdem ich das Problem durchdrungen habe, zeichnet sich eine algorithmische Lsung 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 Abhngigkeiten die ver6

Vier gewinnt. Eine Lsung.


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?

schiedenen Einheiten verbinden, dann steht auf oberster Ebene das gesamte Spiel. Es zerfllt in weitere Funktionseinheiten, die eine Ebene tiefer angesiedelt sind. Diese knnen wiederum zerlegt werden. Bei der Zerlegung knnen zwei unterschiedliche Flle betrachtet werden: T vertikale Zerlegung, T 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. Kmmert sich eine Funktionseinheit um mehr als eine Sache, wird sie horizontal zerlegt. Wre es beispielsweise mglich, einen Spielzustand in eine Datei zu speichern, knnte das Speichern im ersten Schritt in der Funktionseinheit Spiellogik angesiedelt sein. Dann stellt man jedoch fest, dass diese Funktionseinheit fr mehr als eine Verantwortlichkeit zustndig wre, 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 zerfllt 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 zerfllt 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 nchsten 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 Nchster am Zug ist. Fr 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 nchste gelegte Stein fllt. 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 T Steine legen, T nachhalten, wo bereits Steine liegen, T erkennen, ob vier Steine zusammen liegen.

Vom Problem zur Lsung


Nun wollen Sie sicher so langsam auch mal Code sehen. Doch vorher muss noch geklrt werden, was aus den einzelnen Funktionseinheiten werden soll. Werden sie jeweils eine Klasse? Eher nicht, denn dann wren Spiellogik und Benutzerschnittstelle nicht ausreichend getrennt. Somit werden Benutzerschnittstelle und Spiellogik mindestens eigenstndige Komponenten. Die Funktionseinheiten innerhalb der Spiellogik hngen 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 lauffhigen 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

LSUNG
Komponenten zusammengefhrt werden. So entstehen am Ende drei Komponenten. Abbildung 3 zeigt die Solution fr die Spiellogik. Sie enthlt zwei Projekte: eines fr die Tests, ein weiteres fr die Implementierung. Die Funktionseinheit Spielzustand zerfllt 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 Flle zu unterscheiden: Die Spalte ist leer, enthlt 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 hchste 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 gefllte Spalte zu legen? Eine Mglichkeit wre: Sie stellen eine Methode bereit, die vor dem Platzieren eines Steins aufgerufen werden kann, um zu ermitteln, ob dies in der betreffenden Spalte mglich ist. Der Code she dann ungefhr so aus :
if(spiel.KannPlatzieren(3)) { spiel.LegeSteinInSpalte(3); }
[Abb. 1] Die Aufgabe in Teile zerlegen: erster Schritt ... [Abb. 2] ... und zweiter Schritt.

[Abb. 3] Aufbau der Solution.

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 dont ask verstt. 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, msste mit KannPlatzieren() geprft werden, ob dies berhaupt mglich ist. Nach dem Tell dont ask-Prinzip sollte man Klassen so erstellen, dass man den Objekten der Klasse mitteilt, was zu tun ist statt vorher nachfragen zu mssen, 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 knnte sein, die Methode LegeSteinInSpalte() mit einem Rckgabewert auszustatten. War das Platzieren erfolgreich, wird true geliefert, ist die Spalte bereits voll, wird false geliefert. In

dem Fall msste sich der Verwender der Methode mit dem Rckgabewert befassen. Am Ende soll der Versuch, einen Stein in eine bereits gefllte Spalte zu platzieren, dem Benutzer gemeldet werden. Also msste der Rckgabewert bis in die Benutzerschnittstelle transportiert werden, um dort beispielsweise eine Messagebox anzuzeigen. Die Idee, die Methode mit einem Rckgabewert auszustatten, verstt jedoch ebenfalls gegen ein Prinzip, nmlich 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 verndert. Fr 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 enthlt und dabei den Zustand nicht verndert. Wrde die Methode LegeSteinInSpalte() einen Rckgabewert haben, wre sie dadurch gleichzeitig eine Query. Nach diesen berlegungen bleibt nur eine Variante brig: Die Methode LegeSteinInSpalte() sollte eine Ausnahme auslsen, wenn das Platzieren nicht mglich ist. Die Ausnahme kann in der Benutzerschnittstelle abgefangen und dort in einer entsprechenden Meldung angezeigt werden. Damit entfllt die Notwendigkeit, einen Rckgabewert aus der Spiellogik bis in die Benutzerschnittstelle zu transportieren. Ferner sind die Prinzipien Tell dont ask und Command/Query Separation eingehalten.

stands gelst: 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 zusammenhngende Steine knnen 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 mssen, es darf sich also kein gegnerischer Stein dazwischen befinden. Ich habe zuerst versucht, diese Vierergruppenerkennung direkt auf dem zweidimensionalen Array zu lsen. Dabei habe ich festgestellt, dass das Problem in zwei Teilprobleme zerlegt werden kann : T Ermitteln der Indizes benachbarter Felder. T Prfung, ob vier benachbarte Felder mit Steinen gleicher Farbe besetzt sind. Fr 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 anschlieend verwendet, um im Spielfeld zu ermitteln, ob die betreffenden Felder alle

Vier Steine finden


Nun sind mit dem zweidimensionalen Array und der Methode LegeSteinInSpalte() bereits zwei Teilprobleme des Spielzu-

www.dotnetpro.de dotnetpro.dojos.2011

LSUNG
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); } }

fhrt, 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 gefhrt haben. Da die Methode AlleVierer() ein IEnumerable liefert und SelbeFarbe() dies als ersten Parameter erwartet, knnen die beiden Extension Methods hintereinander geschrieben werden. Da das Spielfeld in beiden Methoden bentigt wird, verfgt 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 fr Vertikal und Diagonal }

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 mssen die einzelnen Funktionseinheiten nur noch gemeinsam verwendet werden. Die dafr verantwortliche Klasse heit VierGewinntSpiel. Sie ist public und reprsentiert nach auen die Komponente. Die Klasse ist fr die Spielregeln zustndig. 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; }

Steine derselben Farbe enthalten. Die betreffenden Klassen heien HorizontalerVierer, VertikalerVierer, DiagonalHochVierer und DiagonalRunterVierer. Listing 1 zeigt exemplarisch die Klasse HorizontalerVierer . Zunchst fllt auf, dass die Klasse internal ist. Sie wird im Rahmen der Spiellogik nur intern bentigt, daher soll sie nicht auerhalb der Komponente sichtbar sein. Damit Unit-Tests fr die Klasse mglich sind, habe ich auf der Assembly das Attribut InternalsVisibleTo gesetzt. Dadurch kann die Assembly, welche die Tests enthlt, 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 anschlieend in einem Flow aus zwei Schritten. Im ersten Schritt wird aus einem Spielfeld die Liste der mglichen Vierergruppen bestimmt. Im zweiten Schritt wird aus dem Spielfeld und den mglichen Vierergruppen ermittelt, ob eine der Vierergruppen Steine derselben Farbe enthlt. Die beiden Schritte des Flows sind als Extension Methods realisiert. Dadurch sind sie leicht isoliert zu testen. Anschlieend knnen sie hintereinander ausge-

Es wird also ein entsprechender Spielstein gelegt und anschlieend ermittelt, wer am Zug ist. Etwas spter folgt dann die Auswertung eines mglichen Gewinners:
if (spielbrett.Gewinner == Spieler.Rot){ Zustand = Zustaende.RotHatGewonnen; }

Auch diese Methode ist internal, da sie auerhalb der Komponente nicht bentigt wird. In zwei geschachtelten Schleifen werden die Anfangsindizes von horizontalen Vierergruppen ermittelt. Fr jeden Anfangsindex wird mit yield return eine Instanz eines HorizontalerVierers geliefert. Dieser bernimmt das Ermitteln der drei anderen Indizes. Eine Alternative zur gezeigten Methode wre, die mglichen Vierer als Konstanten zu hinterlegen. Es wrde dann die Berechnung in AlleVierer() entfallen, ferner die

Die Ermittlung eines Gewinners erfolgt also im Spielbrett, whrend hier nur der Zustand des Spiels verwaltet wird. Fazit: Die richtigen Vorberlegungen sind der Schlssel zu einer erfolgreichen Implementierung. [ml]

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

Listing 2 Prfen 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
DataBinding ist eine tolle Sache: Objekt an Formular binden und wie von Zauberhand stellen die Controls die Eigenschaftswerte des Objekts dar. DataBinding ist aber auch knifflig. Stefan, kannst du dazu eine Aufgabe stellen?

Wer bt, gewinnt

ataBinding ist beliebt. Lstig daran ist: Man muss die INotifyPropertyChanged-Schnittstelle implementieren. Sie fordert, dass bei nderungen an den Eigenschaften eines Objekts das Ereignis PropertyChanged ausgelst wird. Dabei muss dem Ereignis der Name der genderten 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 fr eine einzelne Eigenschaft zu prfen ist nicht schwer. Man bindet einen Delegate an den PropertyChanged-Event und prft, ob er bei nderung der Eigenschaft aufgerufen wird. Auerdem ist zu prfen, ob der bergebene Name der Eigenschaft korrekt ist, siehe Listing 3. Um zu prfen, ob der Delegate aufgerufen wurde, erhhen Sie im Delegate beispielsweise eine Variable, die auerhalb definiert ist. Durch diesen Seiteneffekt knnen Sie berprfen, ob der Event beim ndern der Eigenschaft ausgelst und dadurch der Delegate aufgerufen wurde. Den Namen der Eigenschaft prfen Sie innerhalb des Delegates mit einem Assert. Solche Tests fr jede Eigenschaft und jede Klasse, die INotifyPropertyChanged implementiert, zu schreiben, wre keine Lsung, weil Sie dabei Code wiederholen wrden. Da die Eigenschaften einer Klasse per Reflection ermittelt werden knnen, ist es nicht schwer, den Testcode so zu verallgemeinern, dass damit alle Eigenschaften einer Klasse getestet werden knnen. Also lautet in diesem Monat die Aufgabe: Implementieren Sie eine Klasse zum automatisierten Testen der INotifyPropertyChanged-Logik. Die zu implementierende Funktionalitt ist ein Werkzeug zum Testen von ViewModels. Dieses Werkzeug soll wie folgt bedient werden:
NotificationTester.Verify<MyViewModel>();

geben. Die Prfung soll so erfolgen, dass per Reflection alle Eigenschaften der Klasse gesucht werden, die ber einen Setter und Getter verfgen. Fr diese Eigenschaften soll geprft werden, ob sie bei einer Zuweisung an die Eigenschaft den PropertyChanged-Event auslsen und dabei den Namen der Eigenschaft korrekt bergeben. Wird der Event nicht korrekt ausgelst, muss eine Ausnahme ausgelst werden. Diese fhrt bei der Ausfhrung des Tests durch das Unit-Test-Framework zum Scheitern des Tests. Damit man wei, fr 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, fr die der Test fehlschlug. In einer weiteren Ausbaustufe knnte das Werkzeug dann auch auf Klassen angewandt werden, die ebenfalls per Reflection ermittelt wurden. Fasst man beispielsweise smtliche ViewModels in einem bestimmten Namespace zusammen, kann eine Assembly nach ViewModels durchsucht werden. Damit die so gefundenen Klassen berprft werden knnen, muss es mglich sein, das Testwerkzeug auch mit einem Typ als Parameter aufzurufen :
NotificationTester.Verify (typeof(MyViewModel));

In jeder dotnetpro finden Sie eine bungsaufgabe von Stefan Lieser, die in maximal drei Stunden zu lsen sein sollte. Wer die Zeit investiert, gewinnt in jedem Fall wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen. Es gilt : T Falsche Lsungen gibt es nicht. Es gibt mglicherweise elegantere, krzere oder schnellere Lsungen, aber keine falschen. T Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das knnen Sie, indem Sie Ihre Lsung mit der vergleichen, die Sie eine Ausgabe spter in dotnetpro finden. bung macht den Meister. Also los gehts. Aber Sie wollten doch nicht etwa sofort Visual Studio starten

Im nchsten Heft finden Sie eine Lsung des Problems. Aber versuchen Sie sich zunchst 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)); }

Die Klasse, die auf INotifyPropertyChangedSemantik geprft werden soll, wird als generischer Typparameter an die Methode ber-

www.dotnetpro.de dotnetpro.dojos.2011

LSUNG
INotifyPropertyChanged-Logik automatisiert testen

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

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 fr alle Eigenschaften einer Klasse automatisiert ausgefhrt wird? Im Kern basiert die Lsung auf folgender Idee: Suche per Reflection alle Properties einer Klasse und fhre den Test fr 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.

sind zustandslos, das heit, sie erledigen ihre Aufgabe ausschlielich mit den Daten aus ihren Argumenten. Das hat den Vorteil, dass mehrere Flows asynchron ausgefhrt werden knnen, ohne dass dabei die Zugriffe auf den Zustand synchronisiert werden mssten. Ferner lassen sich zustandslose Funktionen sehr schn automatisiert testen, weil das Ergebnis eben nur von den Eingangsparametern abhngt.

Einer nach dem anderen

Suche den Flow!


Ich mchte versuchen, die Aufgabenstellung mit einem Flow zu lsen. Doch dazu sollte ich ein klein wenig ausholen und zunchst erlutern, 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 zurck. Dieses Ergebnis geht in die nchste Funktion, auch diese berechnet damit wieder etwas und liefert ihr Ergebnis an die nchste 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 input = "input"; x1 = A(input); x2 = B(x1); result = C(x2);

Ein Detail ist bei der Realisierung von Flows ganz wichtig: Weitergereicht werden sollten nach Mglichkeit jeweils Daten vom Typ IEnumerable<T>. Dadurch besteht nmlich die Mglichkeit, auf diesen Daten mit LINQ zu operieren. Ferner knnen die einzelnen Flowstages dann beliebig groe Datenmengen verarbeiten, da bei Verwendung von IEnumerable<T> nicht alle Daten vollstndig im Speicher existieren mssen, sondern Element fr Element bereitgestellt werden knnen. Im Idealfall fliet also zwischen den einzelnen Flowstages immer nur ein einzelnes Element. Es wird nicht etwa das gesamte Ergebnis der ersten Stage berechnet und dann vollstndig weitergeleitet. Im Beispiel von Listing 2 fhrt die Verwendung von yield return dazu, dass der Compiler einen Enumerator erzeugt. Dieser Enumerator liefert nicht sofort die gesamte Aufzhlung, sondern stellt auf Anfrage Wert fr Wert bereit. Bei Ausfhrung der Methode Flow() werden also zunchst nur die einzelnen Aufzhlungen 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 Aufzhlung x2 das erste Element. Dadurch kommt B ins Spiel und entnimmt ihrerseits der Aufzhlung x1 den ersten Wert. Dies

O
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 Mglichkeit, unendlich groe 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 Schlsselwort this ergnzt werden, siehe Listing 3. Natrlich mssen die Parameter und Return-Typen der Flowstages zueinander passen.

Lsungsansatz
Der erste Schritt des INotifyPropertyChanged-Testers besteht darin, die zu testenden Properties des Typs zu ermitteln. Anschlieend muss er jedem dieser Properties einen Wert zuweisen, um zu prfen, ob der Event korrekt ausgelst wird. Zum Zuweisen eines Wertes bentigen Sie zur Laufzeit einen Wert vom Typ der Property. Wenn Sie auf eine string-Property stoen, mssen 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 mglich. Das Instanzieren der Werte scheint eine eigenstndige Funktionseinheit zu sein, denn die Aufgabe ist recht umfangreich. Wenn Sie die Properties und ihren jeweiligen Typ gefunden haben, mssen Sie fr jede Property einen Test ausfhren. Jeder

10

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Listing 2 Rckgabedaten 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); } }

An dieser Stelle fragen Sie sich mglicherweise, warum ich die Property-Namen als Strings zurckgebe und nicht etwa eine Liste von PropertyInfo-Objekten. Schlielich stecken in PropertyInfo mehr Informationen, insbesondere der Typ der Property, den ich spter ebenfalls bentige. Ich habe mich dagegen entschieden, weil dies das Testen der nchsten Flowstage deutlich erschwert htte. Denn diese htte dann auf einer Liste von PropertyInfo-Objekten arbeiten mssen. Und da PropertyInfo-Instanzen nicht einfach mit new hergestellt werden knnen, wren die Tests recht mhsam geworden. Nachdem die Property-Namen bekannt sind, kann die nchste Flowstage dazu den jeweiligen Typ ermitteln. Die Flowstage erhlt also eine Liste von Property-Namen sowie den Typ und liefert eine Aufzhlung von Typen.
static IEnumerable<Type> FindPropertyTypes( this IEnumerable<string> propertyNames, Type type)

Im Anschluss muss fr jeden Typ ein Objekt instanziert werden. Diese Objekte werden spter im Test den Properties zugewiesen. Die Flowstage erhlt also eine Liste von Typen und liefert fr jeden dieser Typen eine Instanz des entsprechenden Typs.
static IEnumerable<object> GenerateValues( this IEnumerable<Type> types)

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();

Dann wird es spannend: Die Actions mssen erzeugt werden. Dabei lsst es sich leider nicht vermeiden, die Property-Namen aus der ersten Stage nochmals zu verwenden. Die Ergebnisse der ersten Stage flieen also nicht nur in die unmittelbar nchste Stage, sondern zustzlich auch noch in die Stage, welche die Actions erzeugt. Die Namen der Properties werden bentigt, um mittels Reflection die jeweiligen Setter aufrufen zu knnen.
static IEnumerable<Action<object>> GenerateTestMethods(this IEnumerable<object> values, IEnumerable<string> propertyNames, Type type)

dieser Tests ist eine Action<object>, die auf einer Instanz der Klasse ausgefhrt wird, die zu testen ist. Wenn also die Klasse KundeViewModel berprft werden soll, wird fr jede Property eine Action<KundeViewModel> erzeugt. Sind die Actions erzeugt, mssen sie nur nacheinander ausgefhrt werden. Dabei soll jede Action eine neue Instanz der zu testenden Klasse erhalten. Andernfalls knnte es zu Seiteneffekten beim Testen der Properties kommen.

Funktionseinheiten identifizieren
Die erste Aufgabe ist also das Ermitteln der zu testenden Properties. Eingangsparameter in diese Funktionseinheit ist der Typ, fr den die INotifyPropertyChanged-Implementierung berprft werden soll. Das Ergebnis der Flowstage ist eine Aufzhlung der Property-Namen.
static IEnumerable<string> FindPropertyNames(this Type type)

Der letzte Schritt besteht darin, die gelieferten Actions auszufhren. 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

www.dotnetpro.de dotnetpro.dojos.2011

11

LSUNG
[Abb. 2] Die zu testenden Properties ermitteln.

Listing 4 Eine einfache Testklasse.


public class ClassWithPublicGettersAndSetters { public string StringProperty { get; set;} public int IntProperty { get; set;} }

mchte ich die einzelnen Stages aber isoliert. Denn nur so kann ich die Implementierung Schritt fr Schritt vorantreiben und muss nicht gleich einen Integrationstest fr den gesamten Flow schreiben. Einige Integrationstests sollten am Ende aber auch nicht fehlen. Diese Vorgehensweise hat einen weiteren Vorteil: Um den NotificationTester testen zu knnen, mssen Testdaten her. Da er auf Typen arbeitet, mssen also Testdaten in Form von Klassen erstellt werden. Das ist nicht nur aufwendig, sondern wird auch schnell unbersichtlich. 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 knnen, habe ich ihre Sichtbarkeit auf internal gesetzt. Damit sind die Methoden zunchst nur innerhalb der Assembly, in der sie implementiert sind, sichtbar. Um auch in der Test-Assembly darauf zugreifen zu knnen, muss diese zustzliche Sichtbarkeit ber das Attribut InternalsVisibleTo hergestellt werden :
[assembly:InternalsVisibleTo( "INotifyTester.Tests")]

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 fr vertretbar. UnitTests sind Whitebox-Tests, das heit, 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 erhht, dass die Methoden in Tests angesprochen werden knnen. Eine vollstndige Offenlegung mit public wre mir zu viel des Guten. brigens halte ich es fr keine gute Idee, auf die Interna einer zu testenden Klasse mittels Reflection zuzugreifen. Dabei entziehen sich nmlich die Interna, die ber Reflection angesprochen werden, den Refaktorisierungswerkzeugen. Und wie man sieht, ist internal in Verbindung mit dem InternalsVisibleTo-Attribut vllig ausreichend.

Um diese Funktion testen zu knnen, mssen Testklassen angelegt werden. Das lsst 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 zunchst um das Happy Day Szenario gekmmert, also einen Testfall, der spter bei der Verwendung typisch ist, siehe Listing 4. Als Nchstes folgt eine Klasse, deren Properties private sind. Diese sollen in den Tests unbercksichtigt 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 dafr, dass nur Properties bercksichtigt 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 erhlt als Argumente die Liste der Property-Namen, die bercksichtigt werden sollen, sowie den Typ, zu dem die Properties gehren. Dazu liefert sie jeweils den Typ der Properties. Auch diese Tests bentigen wieder Testklassen. Ich habe einfach die schon vorhandenen Testklassen verwendet. Auch hier ist die Implementierung dank LINQ nicht schwierig.

GenerateValues
Um die Property-Setter spter aufrufen zu knnen, muss jeweils ein Objekt vom Typ der Property erzeugt werden. Diese Aufgabe bernimmt die Funktion GenerateValues. Sie erhlt 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-

FindPropertyNames
Die Namen der Properties werden durch die Flowstage FindPropertyNames geliefert. Dabei entscheidet diese Funktion bereits, welche Properties geprft werden sollen. Es werden nur Properties bercksichtigt, die ber ffentliche Getter und Setter verfgen.

Das Attribut kann prinzipiell in einer beliebigen Quellcodedatei in der Assembly untergebracht werden. blicherweise werden Attribute, die sich auf die Assembly be-

12

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Listing 5 Die zu prfenden Properties finden.
internal static IEnumerable<string> FindPropertyNames(Type type) { return type.GetProperties(PropertyBindingFlags) .Where(propertyInfo => propertyInfo.CanRead) .Where(propertyInfo => propertyInfo.CanWrite) .Select(propertyInfo => propertyInfo.Name); }

Listing 7 Flowstages zusammenstecken.


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)); } internal static object CreateInstance(Type type) { if (type == typeof(string)) { return ""; } return Activator.CreateInstance(type); }

JetBrains ReSharper bezahlt. Der weist nmlich mit der Warnung Access to modified closure auf das Problem hin.

ExecuteTestMethods
Der letzte Schritt im Flow ist die Ausfhrung 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 verfgt, 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. 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. Zunchst mssen zwei Aufzhlungen im Gleichschritt durchlaufen werden. Dazu wird der Enumerator einer der beiden Aufzhlungen ermittelt. Anschlieend 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 htte dies gerne in eine Methode ausgelagert, das ist jedoch durch die Verwendung von yield return nicht mglich. Damit sind wir bei der zweiten Besonderheit der Funktion. Die einzelnen Testmethoden werden jeweils mit yield return zurckgeliefert. Da das Ergebnis der Funktion eine Aufzhlung von Actions ist, liefert das yield return jeweils eine Action in Form einer Lambda Expression. Dabei mssen 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 knnen. Andernfalls wrden am Ende alle Lambda Expressions auf demselben Wert arbeiten, nmlich dem aus dem letzten Schleifendurchlauf. Auch hier macht sich brigens wieder mal der Einsatz von Nun mssen nur noch alle Flowstages zusammengesteckt werden. Das ist einfach, da die Stages als Extension Methods implementiert sind. Dadurch knnen sie hintereinandergereiht werden, wie Listing 7 zeigt. Der Flow wird lediglich dadurch etwas unterbrochen, dass die Namen der Properties in zwei Flowstages bentigt werden. Daher werden diese nach Ausfhrung der ersten Stage in einer Variablen zwischengespeichert, die dann weiter unten wieder in eine andere Stage einfliet.

GenerateTestMethods
Nun stehen alle Informationen zur Verfgung, um fr jede Property eine Testmethode zu erzeugen. Die Funktion GenerateTestMethods erhlt drei Argumente : T die Liste der Werte fr die Zuweisung, T die Liste der Property-Namen, T 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)

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 anschlieende Implementierung ging dafr rasch. Was mir an der Lsung gut gefllt, ist die Tatsache, dass Erweiterungen leicht vorzunehmen sind, weil es klar abgegrenzte Verantwortlichkeiten gibt. Bedarf fr 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]

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 prfen, dass sie jeweils einen bestimmten Aspekt der INotifyPropertyChangedSemantik berprfen. Hier wird es schon

[1] Stefan Lieser, Zauberwort, INotifyPropertyChanged-Logik automatisiert testen, dotnetpro 4/2010, S. 107, www.dotnetpro.de/A1004dojo

www.dotnetpro.de dotnetpro.dojos.2011

13

AUFGABE
Testdaten automatisch generieren

Meier, Mller, Schulze


Nach wie vor spielt die klassische Forms over Data-Anwendung eine groe Rolle. Daten aus einer Datenbank sollen per Formular bearbeitet werden. Wenn diese Applikationen getestet werden, spielen Testdaten eine zentrale Rolle. Mglichst viele sollten es sein und mglichst realistisch geformt noch dazu. Stefan, fllt dir dazu eine bung ein?

dnpCode: A1005dojo

Wer bt, gewinnt

In jeder dotnetpro finden Sie eine bungsaufgabe von Stefan Lieser, die in maximal drei Stunden zu lsen sein sollte. Wer die Zeit investiert, gewinnt in jedem Fall wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen. Es gilt : T Falsche Lsungen gibt es nicht. Es gibt mglicherweise elegantere, krzere oder schnellere Lsungen, aber keine falschen. T Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das knnen Sie, indem Sie Ihre Lsung mit der vergleichen, die Sie eine Ausgabe spter in dotnetpro finden. bung macht den Meister. Also los gehts. Aber Sie wollten doch nicht etwa sofort Visual Studio starten

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 mssen die Daten seitenweise abgerufen und visualisiert werden. Suchen und Filtern kommen meistens hinzu, und schon stellt sich die Frage, ob der gewhlte Ansatz auch noch funktioniert, wenn mehr als nur eine Handvoll Testdaten in der Datenbank liegen. Solche Tests auf Echtdaten Ihrer Kunden vorzunehmen wre brigens keine gute Idee. Diese unterliegen dem Datenschutz und sollten keinesfalls zu Testzwecken verwendet werden. Und fr eine vllig neue Anwendung stehen natrlich noch gar keine Echtdaten zurVerfgung. Folglich bleibt nur die Mglichkeit, Testdaten zu generieren. Und genau darum geht es in dieser bung: Erstellen Sie eine Bibliothek zum Erzeugen von Testdaten.

statt wie zufllig zusammengewrfelte Zeichenfolgen. Es mssen lediglich einige Straennamen vorgegeben werden. Das Gleiche bietet sich fr die Namen von Personen an. Auch hier kann gut mit einer Liste von Namen gearbeitet werden, aus der dann zufllig Werte ausgewhlt werden. Die Strategie fr die Testdatenerzeugung soll mglichst flexibel sein. Ein Entwickler sollte mit wenig Aufwand einen eigenen Generator ergnzen knnen. Endergebnis der Datenerzeugung soll eine Aufzhlung von Zeilen sein :
IEnumerable<object[]>

Verschiedene Arten von Testdaten

Die generierten Testdaten sollen eine Tabellenstruktur haben. Fr jede Spalte wird definiert, von welchem Typ die Werte sind und wie sie erzeugt werden. Anschlieend gibt man an, wie viele Zeilen generiert werden sollen, und die Testdaten werden generiert. Die Anforderungen an die Daten knnen sehr vielfltig sein. Um hier ausreichend flexibel zu sein, sollen die Daten nach verschiedenen Strategien erzeugt werden knnen. Reine Zufallsdaten sind ein erster Schritt, drften aber in vielen Fllen nicht ausreichen. Zumindest eine Beschrnkung innerhalb vorgegebener Minimum- und Maximumwerte erscheint sinnvoll. Eine weitere Strategie knnte darin bestehen, eine Liste von mglichen Werten vorzugeben, aus denen dann zufllig ausgewhlt wird. So knnten beispielsweise Straennamen generiert werden, die in [Abb. 1] So knnte das GUI fr einen Testdatengenerator den Formularen dann auch aussehen. wie Straennamen aussehen

Die generierten Zeilen knnen dann beliebig verwendet werden. Sie knnen direkt in Tests einflieen oder auch zuerst als Datei gespeichert werden. Hier bietet sich beispielsweise die Speicherung als CSV-Datei an. Auch das Speichern in einer Datenbank ist natrlich ein typisches Szenario. Das konkrete Speichern der Daten sollte unabhngig 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 primr um eine Bibliothek und weniger um eine Benutzerschnittstelle. Wer mag, kann sich aber auch um eine Benutzerschnittstelle kmmern, denn die drfte hier etwas anspruchsvoller sein. Schlielich bentigen die verschiedenen Generatoren unterschiedliche Eingabedaten. Gengen bei einem Zufallsgenerator vielleicht Minimum und Maximum, mssen bei einem anderen Generator Wertelisten eingegeben werden. Hinzu kommt, dass die Eingabedaten von unterschiedlichem Typ sein knnen, wofr unterschiedliche Eingabevalidierungen ntig 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 regelmig ben, statt immer nur zu performen. Schlielich sollte man beim Auftritt keine Fehler machen, nur beim ben ist das zulssig und sogar erwnscht: ohne Fehler keine Weiterentwicklung. Also ben Sie und machen Sie Fehler! [ml]

14

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Testdaten automatisch generieren

Tckisches 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 Oberflchenelemente. Und der Anwender will seine Daten individuell strukturieren knnen.

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 erwhnt. Doch wie geht man generell vor, wenn fr eine Aufgabenstellung eine Architektur entworfen werden soll? Ganz einfach: Man malt den kleinen Knig. Den gibt es immer, denn er ist schlielich derjenige, der die Anforderungen formuliert hat. Er ist der Grund dafr, 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 Knig, der das System bedient. Bei manchen Systemen mag es mehrere unterschiedliche Clients geben, das spielt fr den Testdatengenerator jedoch keine Rolle. Die zweite Kategorie von Elementen in der Umwelt stellen Ressourcen dar. Diese liegen auerhalb des zu erstellenden Systems und sollten daher in das System-Umwelt-Diagramm aufgenommen werden, denn unser System ist von diesen Ressourcen abhn-

gig. Im Fall des Testdatengenerators sind als Ressourcen in der Umwelt CSV-Dateien und Datenbanken denkbar. Irgendwo mssen die generierten Testdaten schlielich hin. Folglich ergnze 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 fr den Architekturentwurf, der irrt. Denn aus diesem Bild wird bereits deutlich, welche Komponenten mindestens entstehen mssen. Den Begriff Komponente verwende ich hier mit einer festen Bedeutung, siehe dazu die Erluterungen im Kasten. Der Kern des Systems sollte gegenber der Umwelt abgeschirmt werden, weil das System die Umwelt nicht kontrollieren kann. Die Umwelt kann sich verndern. Es knnen etwa neue Clients hinzukommen oder auch zustzliche Ressourcen. Folglich mssen auf der Umrandung des Systems Komponenten entstehen, die den Kern des Systems ber definierte Schnittstellen gegenber der Umwelt isolieren. Andernfalls wrde der Kern des Systems immer wieder von nderungen in der Umwelt betroffen sein und wre damit sehr anfllig. Und darin liegt die Bedeutung des System-UmweltDiagramms: Es zeigt, welche Komponenten das System von der Umwelt abschirmen. Fr Clients, die das System verwenden, bezeichnen wir die Komponente, ber wel-

[Abb. 1] SystemUmweltDiagramm, Version 1.

[Abb. 2] System-Umwelt-Diagramm, Version 2.

che der Client mit dem System interagiert, als Portal. In Abhngigkeitsdiagrammen 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 knnen wir aufgrund des System-Umwelt-Diagramms also schon vier Komponenten identifizieren, siehe Abbildung 3: T Portal, T CSV-Adapter, T Datenbank-Adapter, T 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 lsst sich das in Abbildung 4 gezeigte Abhngigkeitsdiagramm ableiten.

Komponente
Eine Komponente ist eine binre Funktionseinheit mit separatem Kontrakt :

Binr bedeutet hier, dass die Komponente an den Verwendungsstellen binr 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 fr 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, nmlich einer fr den Kontrakt und einer fr die Implementierung. Und natrlich gehren Tests dazu also besteht jede Komponente aus mindestens drei Projekten.

Den Kern zerlegen


Nachdem ich diese Komponenten identifiziert hatte, habe ich die Aufgabenstellung

www.dotnetpro.de dotnetpro.dojos.2011

15

LSUNG
[Abb. 3] System-UmweltKomponenten.

[Abb. 4] Abhngigkeitsdiagramm.

[Abb. 6] Abhngigkeitsdiagramm 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: T Erzeugen eines einzelnen Wertes, T Erzeugen einer Zeile, T Erzeugen mehrerer Zeilen. Dabei scheint die Trennung in das Erzeugen einer Zeile und das Erzeugen mehrerer Zeilen auf den ersten Blick mglicherweise etwas merkwrdig. Wenn eine Zeile erzeugt werden kann, gengt doch eine simple Schleife, und schon knnen mehrere Zeilen erzeugt werden. Dennoch halte ich es fr wichtig, diese beiden Funktionseinheiten zu identifizieren. Denn fr die testgetriebene Entwicklung ist es ntzlich, im Vorfeld zu wissen, welche Funktionseinheiten auf einen zukommen. So fllt es nmlich viel leichter, ausreichende Testflle zu finden, sprich: die Anforderungen zu klren. Und bei den Anforderungen liegt die Herausfor-

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 wrde mglicherweise nur eine Funktionseinheit entstehen, die mehrere Datenzeilen erzeugt. Das wrde die testgetriebene Entwicklung unntig erschweren. Nachdem ich fr 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 fr besonders wichtig: bei den Generatoren. Die werden spter sicher immer wieder ergnzt werden. Da ist es hilfreich, wenn dann nicht jeweils die gesamte Anwendung neu bersetzt

werden muss, sondern neue Generatoren mit geringem Aufwand ergnzt werden knnen. In einer weiteren Ausbaustufe wre es sogar denkbar, die Generatoren zur Laufzeit zu laden. Dann knnten spter beliebige zustzliche Generatoren verwendet werden, ohne dass am Testdatengenerator selbst etwas gendert werden muss. Damit sind die Generatoren zunchst einmal eine Komponente. Eine andere Aufteilung wre ebenfalls denkbar, man knnte Generatoren zum Beispiel nach Typ in Komponenten zusammenfassen. Eine Komponente mit Stringgeneratoren, eine fr intGeneratoren et cetera. Zurzeit sind es nur wenige Generatoren, daher habe ich mich dafr 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 dafr zustndig 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

16

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Listing 1 Einen generischen Typparameter verwenden.
public interface IGenerator<T> { T GenerateValue(); }

Listing 2 Spalten definieren.


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; } }

Datenbank liegt auf der Hand, auf diesen habe ich aus Zeitgrnden 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 Abhngigkeiten, wie Abbildung 6 zeigt. Der CsvDataAdapter ist nicht von den anderen Komponenten abhngig, weil er lediglich auf dem gemeinsamen Datenmodell aufsetzt.

Listing 3 Werte erzeugen.


public static IEnumerable<object> GenerateValues(this IEnumerable<ColumnDefinition> columnDefinitions) { return columnDefinitions .Select(x => x.Generator) .Select(x => x.GenerateValue()); }

Einzelne Werte
Fr das Erzeugen eines einzelnen Wertes habe ich mich fr 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 zufllige Werte erzeugt. Genauso kann aber auch ein Generator erstellt werden, der eine Liste von Daten erhlt und daraus zufllig auswhlt. Die Beschreibung der zu erzeugenden Datenzeilen besteht also darin, pro Spalte einen Generator zu definieren. Ferner wird pro Spalte der Name der Spalte bentigt. Im Kontrakt der Generatoren habe ich einen generischen Typparameter verwendet, siehe Listing 1. Dadurch wird bereits zur bersetzungszeit geprft, ob der Rckgabewert 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. Fr die Klasse ColumnDefinition wrde das bedeuten, dass diese ebenfalls einen generischen Typparameter erhlt, siehe Listing 2. So weit, so gut. Doch eine Zeile besteht aus mehreren Spalten. Daher mssen meh-

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 natrlich jede Spalte einen anderen Typ haben kann, muss es mglich 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 mglich. Wrde man die Liste als List<object> definieren, msste die Liste Kovarianz untersttzen. Das tut sie jedoch nicht, Ko- und Kontravarianz stehen erst

www.dotnetpro.de dotnetpro.dojos.2011

17

LSUNG
mit C# 4.0 zur Verfgung. 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 knnen die einzelnen Werte der Spalten erzeugt werden. Um eine ganze Datenzeile zu erzeugen, muss jeder Generator einmal aufgerufen werden, um seinen jeweils nchsten Wert zu liefern. Dies ist bei Verwendung von LINQ ganz einfach, siehe Listing 3. In der Methode wird ber die Aufzhlung 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 Aufzhlung der von den Generatoren gelieferten Werte. Diese Aufzhlung wird spter an den Konstruktor einer Zeile bergeben, siehe Listing 4.

eines Generators fr int-Werte gezeigt werden, der zufllige 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 zufllige 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 fr int-Werte.


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); } }

Mehrere Zeilen
Die Erzeugung mehrerer Zeilen erfolgt in einer for-Schleife. Dabei wird die Schleife so oft durchlaufen, dass die Anzahl der gewnschten Datenstze erzeugt wird. Dabei kommt wieder einmal ein yield return zum Einsatz, siehe Listing 5.

Flow
Und schon wieder konnte ich einen Flow identifizieren. Die Aufzhlung der ColumnDefinitions fliet in die Methode GenerateValues. Heraus kommt eine Aufzhlung mit Werten. Diese wird weitergeleitet in die Methode GenerateLine, die aus den Werten eine Zeile erstellt :
columnDefinitions .GenerateValues() .GenerateLine();

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)); ... } }

Um den Flow so formulieren zu knnen, 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.

Generatoren
Ein Generator ist fr das Erzeugen von Werten eines bestimmten Typs zustndig. Welche Strategie dabei verfolgt wird, ist Sache des Generators. Dies soll am Beispiel

18

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
keit begrndet, den Generator automatisiert testen zu knnen. Wrde der Generator den Zufallszahlengenerator selbst instanzieren, wrde er immer zufllige Werte liefern. Dies soll er natrlich tun, aber im Test bentigen wir die Kontrolle darber, welche Werte zufllig geliefert werden, siehe Listing 7. Die Testmethode ist hier verkrzt dargestellt. Ich rufe im Test so lange Werte ab, bis alle mglichen Werte innerhalb von Minimum und Maximum mindestens einmal geliefert wurden. Der Trick, dass sich der Random-Generator immer gleich verhlt, liegt darin, dass ich ihn im Test immer mit demselben Startwert (Seed) 0 instanziere. Um das zu ermglichen, habe ich einen internal-Konstruktor ergnzt, 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 zufllige 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 verfgt nur ber die beiden Parameter fr 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 Dont Repeat Yourself (DRY) kommt. Der ffentliche Konstruktor knnte ja die Grenzwerte selbst an die Felder zuweisen, dann wrden diese Zuweisungen jedoch an zwei Stellen auftreten. Eine weitere interessante Implementierung bietet der RollingIntGenerator. Er liefert, ausgehend von einem Minimumwert,

Listing 8 Zufallsgenerator fr 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 Zufllig einen Stringwert auswhlen.


internal RandomSelectedStringsGenerator(Random random, params string[] values) { this.random = random; this.values = values; }

immer den nchsten Wert, bis er beim Maximumwert angekommen ist. Dann wird wieder von vorn begonnen. Bei diesem Generator lag die Herausforderung darin, korrekt mit dem grtmglichen int-Wert (int.MaxValue) umzugehen. Ohne UnitTests wre das ein elendiges Rumprobieren geworden. So war es ganz leicht. Fr Stringwerte habe ich einen Generator implementiert, der eine Liste von Strings erhlt und daraus zufllig einen auswhlt. Die zur Verfgung stehenden Strings habe ich im Konstruktor als Parameter-Array definiert, siehe Listing 9. Das ist fr die Unit-Tests ganz angenehm, weil man einfach eine beliebige Liste von Stringwerten bergeben kann :
sut = new RandomSelectedStringsGenerator( new Random(0), "Apfel", "Birne", "Pflaume");

wnschenswert, einen String zu bergeben, der eine Liste von Werten enthlt, die mit Semikolon getrennt sind:
"Apfel; Birne; Pflaume"

Um das zu ermglichen, 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()

Natrlich htte ich das Zerlegen des Strings in die Einzelwerte auch im entsprechenden Generator implementieren knnen. Dann htte der sich aber um mehr als eine Verantwortlichkeit gekmmert. Ferner war die Implementierung so etwas einfacher, da ich mich jeweils auf eine einzelne Aufgabenstellung konzentrieren konnte.

Bei der Verwendung des Generators aus Sicht einer Benutzerschnittstelle ist es

Portal
Das Portal hatte es in sich. Obwohl ich mit WPF schon einiges gemacht habe, fhlte ich mich etwas unsicher, diese sehr dynamische Aufgabenstellung mit WPF anzugehen, und entschied mich daher, das Problem mit Windows Forms zu lsen, weil mir das schneller von der Hand geht. Doch der Reihe nach. Wie die Benutzerschnittstelle des Testdatengenerators ungefhr aussehen knnte, habe ich in der Aufgabenstellung bereits durch ein Mockup angedeutet. Abbildung 8 zeigt, wie mein Ergebnis aussieht.

[Abb. 8] Das fertige Portal.

www.dotnetpro.de dotnetpro.dojos.2011

19

LSUNG
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 fr Spaltendefinitionen variabel sein muss. Im Mockup habe ich daher Schaltflchen vorgesehen, mit denen eine Spaltendefinition entfernt bzw. hinzugefgt werden kann. Die zweite Herausforderung sehe ich im Aufbau der Spaltendefinitionen. Je nach ausgewhltem Generatortyp sind unterschiedliche Eingaben notwendig. Mal sind zwei Textfelder fr Minimum und Maximum erforderlich, mal nur eine fr die Elemente einer Liste. Das heit, dass sich der Aufbau der Benutzeroberflche mit der Wahl des Generatortyps ndert. Um diese beiden Herausforderungen mglichst isoliert angehen zu knnen, habe ich fr die variablen Anteile einer Spaltendefinition mit UserControls gearbeitet. So habe ich fr einen Generator, der Minimum- und Maximumwerte bentigt, ein UserControl erstellt, in dem zwei Textboxen mit zugehrigen Labels zusammengefasst sind. Wird aus der Dropdownliste ein Generator ausgewhlt, muss das zum Generator passende Control angezeigt werden. Ferner muss zum ausgewhlten Generator spter die zugehrige ColumnDefinition erzeugt werden, um damit dann die Daten zu generieren. Diese Informationen habe ich im Portal in einer Datenklasse Spalten-

Definition zusammengefasst. Objekte dieser Klasse werden direkt in der Dropdownliste verwendet. Daher enthlt die SpaltenDefinition auch eine Beschreibung. Diese wird als DisplayMember in der Dropdownliste angezeigt, siehe Listing 10. Die Eigenschaft ControlType enthlt den Typ des zu verwendenden Controls. Gengt ein Textfeld, kann hier typeof(TextBox) gesetzt werden. In komplizierteren Fllen wird der Typ eines dafr implementierten UserControls gesetzt. Um fr den ausgewhlten Generatortyp eine ColumnDefinition erzeugen zu knnen, habe ich eine Eigenschaft ergnzt, die eine Funktion erhlt, die genau dies bewerkstelligt: Sie erzeugt eine ColumnDefinition. Dazu erhlt 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 fr die verfgbaren Generatoren jeweils eine Spaltendefinition erzeugt und in die Item-Liste des Dropdown-Controls gestellt, siehe Listing 11. Interessant hierbei ist die Lambda Expression. Diese erhlt die beiden Parameter columnName und control und erzeugt daraus eine ColumnDefinition mit dem ausgewhlten 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 she eine Lsung mit Generics sicher eleganter aus, ist aber ohne Ko-/Kontravarianz nicht mglich. Wird nun in der Combobox ein anderer Generatortyp ausgewhlt, muss das in der Spaltendefinition angegebene Control angezeigt werden. Um dynamisch die zugehrigen Controls zu finden, fge ich alle Controls, die zu einer Spalte gehren (Plusund Minus-Button, Textfeld fr den Spaltennamen, Combobox, Platzhalter fr UserControl) in ein Panel ein. Um in diesem Panel spter dynamisch das UserControl austauschen zu knnen, fge ich dieses zustzlich in ein weiteres Panel. Dieses dient jeweils als Platzhalter fr 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 fr 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 Ergnzen und Lschen von Spaltendefinitionen. Jede Spaltendefinition verfgt ber die beiden Schaltflchen zum Hinzufgen und Lschen von Spaltendefinitionen. Zurzeit fge ich eine neue Spaltendefinition jeweils ans Ende an, knftig knnte diese aber auch an der betreffenden Position eingefgt werden. Daher habe ich bereits an jeder Spaltendefinition einen Plus-Button vorgesehen. Fr die Aufnahme aller Spaltenbeschreibungen ist im statischen Teil der Form ein Panel zustndig. Wird mit der Minus-Schaltflche versucht, eine Spaltenbeschreibung zu entfernen, mssen die zugehrigen Controls aus die-

20

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
[Abb. 10] Controls im Panel.

sem Panel entfernt werden. Um dies zu vereinfachen, ist das zugehrige Panel an der Tag-Eigenschaft des Buttons gesetzt. So wei der Button, zu welchem Panel er gehrt und kann dieses aus dem umschlieenden Panel entfernen. Wird im Portal die Schaltflche Generieren angeklickt, muss fr 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 enthlt. Ferner befindet sich im Platzhalterpanel ein Control, in dem die Parameter fr den Generator enthalten sind. In der Dropdownliste enthlt das SelectedItem eine SpaltenDefinition, aus der sich die ColumnDefinition erstellen lsst. 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 wrde ich allerdings in einem echten Projekt im Nachhinein ergnzen, da die Logik fr den dynamischen Aufbau des Portals doch recht umfangreich geworden ist. Um hier bei spteren Erweiterungen Fehler auszuschlieen, wrde ich die typischen Bedienungsschritte eines Anwenders automatisiert testen.

idee dabei ist: Man berlsst 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 gehts. Ein solcher Host kann dann sogar generisch sein und in allen Anwendungen verwendet werden.

Wir liefern passgenaue Strategien und Lsungen fr Ihre Inhalte auf iPhone/iPad Android BlackBerry Windows Phone 7 dem mobilen Browser

Denkbare Erweiterungen
Fr wiederkehrende Aufgaben wre es sinnvoll, das Schema der Datengenerierung speichern und laden zu knnen. Dies kann beispielsweise mit dem Lounge Repository [4] erfolgen. In der Architektur wrde dafr ein weiterer Adapter ergnzt, mit dem ein Schema gespeichert und geladen werden kann. Natrlich mssten im Portal entsprechende Anpassungen vorgenommen werden, um entsprechende Menfunktionen zu ergnzen. Des Weiteren wre es denkbar, die Generatoren zur Laufzeit dynamisch zu laden. Damit knnten Entwickler ihre eigenen Generatoren implementieren und verwenden, ohne dazu die gesamte Anwendung bersetzen zu mssen. Mithilfe eines DIContainers wie StructureMap oder des Managed Extensibility Framework MEF [5] sollte auch diese Erweiterung keine groe Hrde darstellen.

Fazit
Bei dieser Aufgabe stellte sich heraus, dass die Benutzerschnittstelle einer Anwendung durchaus einige Zeit in Anspruch nehmen kann. Die eigentliche Funktionalitt war dagegen schnell entworfen und implementiert. Das lag mageblich daran, dass ich mir im Vorfeld einige Gedanken ber die Architektur gemacht hatte. Danach ging die testgetriebene Entwicklung flssig von der Hand. [ml]

Host
Am Ende bentigen wir fr die gesamte Anwendung noch eine EXE-Datei, mit der die Anwendung gestartet werden kann. Aufgabe dieses Hosts ist es, die bentigten Komponenten zu beschaffen und sie den Abhngigkeiten gem zu verbinden. Die Abhngigkeiten sind hier in Form von Konstruktorparametern modelliert. Folglich muss der Host die Komponenten in der richtigen Reihenfolge instanzieren, im Abhngigkeitsbaum von unten nach oben, von den Blattknoten zur Wurzel. Anschlieend bergibt er die Kontrolle an das Portal. Fr die vorliegende Anwendung, bestehend aus einer Handvoll Komponenten, ist diese Aufgabe trivial. Bei greren Anwendungen kostet diese Handarbeit Zeit und sollte automatisiert werden. Die Grund-

[1] Stefan Lieser, Meier, Mller, 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

www.dotnetpro.de dotnetpro.dojos.2011

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, fllt dir dazu eine bung ein?

dnpCode: A1006dojo

Wer bt, gewinnt

In jeder dotnetpro finden Sie eine bungsaufgabe von Stefan Lieser, die in maximal drei Stunden zu lsen sein sollte. Wer die Zeit investiert, gewinnt in jedem Fall wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen. Es gilt : T Falsche Lsungen gibt es nicht. Es gibt mglicherweise elegantere, krzere oder schnellere Lsungen, aber keine falschen. T Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das knnen Sie, indem Sie Ihre Lsung mit der vergleichen, die Sie eine Ausgabe spter in dotnetpro finden. bung macht den Meister. Also los gehts. Aber Sie wollten doch nicht etwa sofort Visual Studio starten

er kennt nicht Minesweeper, das beliebte Spiel, welches zum Lieferumfang von Windows gehrt? 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 enthlt die Beschreibung eines Minesweeper-Spielfeldes. Das Programm erzeugt als Ausgabedatei einen dazu passenden Mogelzettel. Der Aufruf erfolgt folgendermaen:
mogelzettel spiel1.txt mogelzettel1.txt

001211 001121 111121 121010 211011

Der Aufbau der Eingabedatei ist wie folgt: Die erste Zeile enthlt 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 ....*. ...*.. ...... *....* .*....

Fr diese Eingabedatei soll eine Ausgabedatei erzeugt werden. Die Ausgabedatei gibt fr jedes Feld die Anzahl der Minen in der unmittelbaren Nachbarschaft an. Jedes Feld hat maximal acht Nachbarn, folglich knnen maximal acht Minen in der Nachbarschaft eines Feldes vorkommen, am Rand des Spielfeldes sind es natrlich 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 gezhlt werden. Der Mogelzettel gibt also keine direkte Auskunft darber, wo die Minen liegen, sondern nur ber die Anzahl der Minen in den jeweils benachbarten Feldern. Fr das obige Beispiel soll folgende Ausgabedatei erzeugt werden :

Sie drfen 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 Lsung dieser bung drfte Lesern der vorhergehenden bungen inzwischen gelufig sein. Zunchst sollten die Anforderungen geklrt werden. Dazu ist es sinnvoll, Beispiele aufzuschreiben. Eines habe ich Ihnen gegeben, vielleicht definieren Sie weitere, um beispielsweise Randflle zu definieren. Da ich Ihnen als Kunde nicht so ohne Weiteres fr Rckfragen zur Verfgung stehe, treffen Sie gegebenenfalls selber sinnvolle Annahmen. Nach der Klrung der Anforderungen beginnt die Planung. Dieser Phase sollten Sie viel Aufmerksamkeit und Zeit schenken. Je intensiver Sie sich mit dem Problem und seiner Lsung auseinandersetzen, desto besser sind Sie vorbereitet fr die Implementierung. Zerlegen Sie das Gesamtproblem in kleinere Teilprobleme, suchen Sie mgliche Flows. Wenn Sie sich bei der Herangehensweise nicht sicher sind, ob eine Idee tatschlich zur Lsung des Problems fhren wird, lohnt es sich, einen Spike zu erstellen. Ein Spike dient dazu, eine Idee zu berprfen 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 verstoen werden. Es geht um den Erkenntnisgewinn. Es knnte sich dabei schlielich auch herausstellen, dass eine Idee nicht zum Ziel fhrt. Im Anschluss an den Spike beginnen Sie dann testgetrieben mit der Entwicklung des Produktionscodes. Dabei beachten Sie selbstverstndlich alle Prinzipien und Praktiken. Und nun frisch ans Werk! Mogeln Sie aber bitte nur beim Minesweeper-Spielen, nicht bei der Softwareentwicklung. [ml]

22

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Daten verarbeiten und auswerten

So mogeln Sie mit EVA!


Den Mogelzettel fr ein Minesweeper-Minenfeld erstellen Sie nach dem EVA-Prinzip: Eingabe, Verarbeitung, Ausgabe. Die Lsung gestaltet sich nach grndlicher Planung recht einfach. Da im Detail aber Variationen mglich sind, knnen Sie diese bung mit Gewinn auch mehrmals lsen.

D
5 6 ....*. ...*.. ...... *....* .*.... 001211 001121 111121 121010 211011

ie Aufgabenstellung lautete vorigen Monat: Entwickle ein Programm, das einen Mogelzettel fr Minesweeper erstellt [1]. Es soll so aufgerufen werden:

mogelzettel spiel1.txt mogelzettel1.txt

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 Lsungen zu kommen. Schlielich soll ben dem Gewinnen von Erkenntnissen dienen.

[Abb. 2] Entweder Mine fr Mine...

Hier ein Beispiel fr den Aufbau der Eingabedatei :

Erste Zerlegung
Die Aufgabenstellung lsst sich auf der obersten Ebene in drei Funktionseinheiten zerlegen : T Lesen des Minenfeldes, T Ermitteln des Mogelzettels, T 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 Aufzhlung von Zeichenketten. Dabei wird die erste Zeile der Datei einfach ignoriert. Sie enthlt die Anzahl der Zeilen und Spalten. Diese Information wird jedoch nicht bentigt, da sie aus dem Inhalt der Datei ebenso hervorgeht. Das Ergebnis von LeseMinenfeld wird an die Methode BerechneMogelzettel weitergeleitet. Ergebnis ist wieder eine Aufzhlung 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.
[Abb. 3] ... oder Feld fr Feld vorgehen.

Fr das obige Beispiel soll folgende Ausgabedatei erzeugt werden :

In der Aufgabenstellung zum Minesweeper-Mogelzettel habe ich erwhnt, 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 gelst habe und in zahlreichen Seminaren beobachten durfte, wie andere Entwickler an die Lsung herange-

genden Zhler jeweils um eins erhhen. Abbildung 2 zeigt dieses Verfahren, das aus zwei Schritten besteht: T Minenpositionen ermitteln, T Nachbarfelder inkrementieren. Oder man geht Feld fr Feld vor und sucht in der Umgebung nach Minen. Man erhht den Zhler fr das in Bearbeitung befindliche Feld fr jede gefundene Mine. Diese Variante zeigt Abbildung 3. Ich habe nach Variante 1 implementiert, suche also alle Minen und erhhe dann bei den Zellen, die um die Mine herum liegen, den Zhler jeweils um eins. Die algorithmische Vorgehensweise lsst sich wieder sehr schn in einem Flow implementieren, er ist in Abbildung 4 zu sehen. Aus der StringReprsentation 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 Aufzhlung von Strings bersetzt. Das Ermitteln der Minenkoordinaten habe ich zunchst fr eine einzelne Zeile implementiert. Listing 1 zeigt einen Test dazu.

Mogelzettel berechnen
Natrlich muss die Methode BerechneMogelzettel weiter zerlegt werden. Dies war in der Planung mein nchster 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 Mglichkeiten, den Mogelzettel zu erstellen. Man kann entweder die Felder suchen, auf denen eine Mine liegt und dann die darum herum lie-

[Abb. 1] Der Flow der Funktionen.

www.dotnetpro.de dotnetpro.dojos.2011

23

LSUNG
Listing 1 Das Einlesen einer Zeile testen.
[Test] public void Eine_Zeile_mit_einigen_Minen() { Assert.That(Mogelzettel.MinenErmitteln("..*.*..*"), Is.EqualTo(new[] {2, 4, 7})); }

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.

Fummelei beim Index


Eine Besonderheit gibt es bei den Indizes zu beachten: Bercksichtigen Sie an den Rndern, dass es nicht in allen Richtungen benachbarte Felder gibt. Das bedeutet, dass Sie bei jedem Zugriff auf die Nachbarfelder prfen mssen, ob die Zelle an einem der Rnder liegt. Diese Indexprfungen machen den Code recht unbersichtlich.
if ((point.Y - 1 >= 0) && (point.X - 1 >= 0)) { result[point.Y - 1, point.X - 1]++; }

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 Aufzhlung 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 Rckgabewert beider Methoden ist eine Aufzhlung, technisch gesprochen vom Typ IEnumerable<T>. Wenn der Rckgabewert einer Methode von diesem Typ ist, steht innerhalb der Methode das Schlsselwort yield zur Verfgung. Wie in [2] erlutert, erstellt der C#-Compiler einen Automaten fr

die Methode. So entfllt die Notwendigkeit, innerhalb der Methode eine Liste fr 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 Lsung zum INotifyPropertyChanged-Tester in [3]. Ich verwende dieses Muster immer dann, wenn sich im Architekturentwurf Methoden abzeichnen, die weiter zerlegt werden knnen. Erfolgt diese Zerlegung bereits im Rahmen der Planung, teste ich diese Methoden isoliert, so wie hier gezeigt. Manchmal entstehen

Um die Indizes nicht prfen zu mssen, knnen Sie einen Trick anwenden: Wenn Sie das Array mit einem zustzlichen Rand anlegen und dann nur Indizes von 1 bis Length 2 statt von 0 bis Length 1 verwenden, knnen Sie sich die Indexprfungen sparen. Da der zustzliche Randbereich mit Nullen initialisiert ist, macht es nichts, dort auch die Minen aufzuaddieren. Nach der Berechnung wird der Rand einfach wieder entfernt. Ob diese Lsung besser lesbar ist als bei der Variante, bei der die Indizes vor jedem Zugriff geprft werden, sei dahingestellt. Letztlich gewinnt man durch den RandTrick etwas Lesbarkeit beim Zugriff auf das Array, muss aber zustzlich das Umkopieren zum Entfernen des Randes implementieren, siehe Listing 4. Da ich mit dieser Lsung 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 Gltigkeit der Indizes. Die Grundidee ist hier also: Erst mal alle acht mglichen Indizes bestimmen, dann prfen, welche davon gltig sind. Listing 5 zeigt das Ergebnis. Am Ende waren die beiden Lsungen vom Umfang des Codes her miteinander vergleichbar. Die Lesbarkeit und Verstndlichkeit der Lsung ohne Rand-Trick scheint mir etwas besser, da man den Trick mit dem Rand eben nicht bentigt.

Struktur
Auch dieses Mal verwende ich bei der Lsung die Projekt- und Verzeichnisstruktur, die ich durchgngig immer in allen Projekten verwende. Das hat den Vorteil, dass mir diese Schritte so zur Gewohnheit werden, dass ich nicht mehr darber nachdenken

24

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
[Abb. 4] Der Flow fr die Vorgehensweise nach Mine.

und die einzelnen Komponenten wrden je eine eigene Solution erhalten.

Fazit
Der Minesweeper-Mogelzettel ist eine zeitlich berschaubare bung. Der Umgang mit den Indizes bietet ein reichhaltiges Bettigungsfeld. Falls Sie es also bislang noch nicht selbst versucht haben: Mogeln Sie mal wieder beim Minesweeper, aber nicht beim bungspensum ! [ml]

[Abb. 5] Die bewhrte Projektstruktur.

auf das Problem der Indizes konzentrieren wollte. Dennoch sind in der Solution mehrere Projekte mit klar definierten Aufgaben vorhanden. Fr eine komponentenorientierte Lsung kmen 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

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) verstt, 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 zunchst in der Praxis bewhren. Denn wenn das nicht der Fall ist, ntzt die schnste Konvention nichts.Wenn ich regelmig Projektstrukturen anlege und verwende, zeigt sich, ob im Detail noch Verbesserungen mglich sind. Die in Abbildung 5 gezeigte Struktur hat sich in vielen Projekten, bungen und Seminaren bewhrt. Die Pfeile zeigen, welche Projekte referenziert werden. Beim Mogelzettel habe ich mich fr zwei Implementierungsprojekte sowie ein Testprojekt entschieden. Bei der Implementierung unterscheide ich zwischen der Logik und dem Host. Im Host wird lediglich die Konsolenschnittstelle zur Verfgung gestellt. Die beiden Parameter der Kommandozeile werden als Dateinamen interpretiert. Durch die Abtrennung des Hosts lsst sich die Logik des Mogelzettels auch einmal in einem GUI-Host verwenden. Diesmal habe ich keine komponentenorientierte Architektur gewhlt, da ich mich

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 prfen.


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; }

www.dotnetpro.de dotnetpro.dojos.2011

25

AUFGABE
Zahlenreihen visualisieren mit Boxplots

Papa, was ist ein Boxplot?


Wie lange dauert und was kostet dies und jenes im Durchschnitt, hchstens, 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?

dnpCode: A1007DojoAufgabe

Wer bt, gewinnt

In jeder dotnetpro finden Sie eine bungsaufgabe von Stefan Lieser, die in maximal drei Stunden zu lsen sein sollte. Wer die Zeit investiert, gewinnt in jedem Fall wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen. Es gilt : T Falsche Lsungen gibt es nicht. Es gibt mglicherweise elegantere, krzere oder schnellere Lsungen, aber keine falschen. T Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das knnen Sie, indem Sie Ihre Lsung mit der vergleichen, die Sie eine Ausgabe spter in dotnetpro finden. bung macht den Meister. Also los gehts. Aber Sie wollten doch nicht etwa sofort Visual Studio starten

aben Sie sich auch schon mal gefragt, wie Sie eine Zahlenreihe anschaulich darstellen knnen? 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 grten 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 gleichmig 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 groe Ausreier gibt, knnen Sie die Werte mit einem Boxplot schn visualisieren. Ich habe nicht gemessen, aber die Werte knnten so aussehen :

[Abb. 1] Die Ausgangs-Zahlenreihe.

[Abb. 2] Grafische Darstellung mit einem Boxplot.

18, 24, 19, 19, 20, 25, 24, 18, 24, 17

Aus den reinen Zahlen wird man nicht sofort etwas erkennen knnen. Helfen wrde 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 mssen die Werte zunchst 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

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 lsen, ist egal. Selbst eine Ausgabe auf der Konsole wre reizvoll.

Die Logik von der Umsetzung trennen


Bedenken Sie, dass eine saubere Trennung der Belange wichtig ist. Die Logik des Boxplots sollte unabhngig sein von den technischen Details des Controls. Das hilft in jedem Fall beim automatisierten Testen. Ferner schaffen Sie durch diese Trennung die Grundlage dafr, dass Sie das Control spter auch einmal in einer anderen Technologie realisieren knnen. Denken Sie auch darber nach, wie die Schnittstellen der beteiligten Funktionseinheiten am besten aussehen. Sollte das Control die komplette Zahlenreihe erhalten? Oder direkt die Quartile und die anderen bentigten Werte? Viele Fragen, nchsten Monat gibt es hier wieder Antworten. Bis dahin, viel Spa mit Boxplots! [ml]

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

[1] http://de.wikipedia.org/wiki/Boxplot

26

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
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 untersttzt.

er sich an der Aufgabenstellung versucht hat und auf die Schnelle keine Definition fr Quartile im Kopf hatte, wird die Suchmaschine seines Vertrauens bemht haben. Das Ergebnis drfte berraschen: Man findet unterschiedliche Vorschlge, wie das untere und obere Quartil zu bestimmen seien. Unter [1] ist eine Erklrung 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, mchte ich auf ein Problem hinweisen, welches mich whrend der Fahrt erwischt hat. Ich habe die Aufgabe als Silverlight-Anwendung begonnen. Ein Grund dafr 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 fr die testgetriebene Entwicklung nicht geeignet, da man nicht die Mglichkeit hat, aus Visual Studio heraus einen einzelnen Test zu starten. Mir wurde es nach kurzer Zeit zu lstig, 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 drftig aus. Zwar gibt es einige Werkzeuge, mit denen Silverlight-Controls und Anderes getestet werden knnen. Und natrlich mssen diese Tests im Browser laufen, um eine vollstndige Silverlight-Umgebung abzubilden. Was fehlt, ist Untersttzung fr das Testen von Nicht-UI-Klassen. Ich bin schlielich doch noch fndig geworden. Roy Osherove hat eine Ergnzung zu TypeMock Isolator [5] entwickelt, mit der Silverlight-Tests innerhalb von Visual

Studio laufen knnen. Das SilverUnit genannte Open-Source-Projekt ist unter [6] zu finden. Es setzt allerdings eine kostenpflichtige Lizenz von TypeMock Isolator voraus. Doch zurck zum Boxplot. Die Aufgabenstellung lsst sich grob in zwei Bereiche unterteilen : T Benutzerschnittstelle (UI, Control), T Berechnung. Ausgangspunkt eines Boxplots ist eine Aufzhlung von Werten. Fr diese Werte mssen Sie fr die Visualisierung folgende Gren ermitteln : Minimum, Unteres Quartil, Median, Oberes Quartil, Maximum. Zur Ermittlung dieser Gren 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 Gren sind unabhngig voneinander und knnen daher auch unabhngig implementiert werden. Widerstehen Sie dem Reflex, von Anfang an eine Implementierung vorzusehen, in der die Sortierung herausgezogen wird. Sollte sich spter herausstellen, dass das mehrfache Sortieren zu Problemen bei der Geschwindigkeit fhrt, knnen Sie immer noch nach Abhilfe suchen.

Fest steht: Bei der Berechnung bentigen Sie einige Methoden, die aus den Grunddaten die zur Visualisierung bentigten Gren ermitteln. Diese Methoden lassen sich testgetrieben recht gut entwickeln.

Control-API
Die Schnittstelle des Controls sollte fr den Verwender mglichst komfortabel sein: Ich mchte das Control auf ein Formular ziehen, die Gre 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 Gren aus den Werten liegt nicht im Verantwortungsbereich des Controls. Die Ermittlung des Medians hat nichts mit der Funktionalitt eines Controls zu tun. Damit sich das Control um die Skalierung kmmern kann, mssen die Gren wie Minimum, Maximum, Median und so weiter nichtskaliert, das heit als Originalwert, angegeben werden. Aufgabe des Controls ist es, den Bereich zwischen Minimum und Maximum in der zur Verfgung stehenden Breite darzustellen. Folglich mssen smtliche Gren auf die zur Verfgung 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 fr die Dependency-Property.


public double LowScaled { get { return (double)GetValue(LowScaledProperty); } }

www.dotnetpro.de dotnetpro.dojos.2011

27

LSUNG
habe mich daher entschlossen, fr die darzustellenden Werte jeweils zwei Eigenschaften im Control bereitzustellen: eine Eigenschaft fr den nichtskalierten Originalwert sowie eine fr den skalierten Wert. Das Control bernimmt das Skalieren. Immer wenn einer der nichtskalierten Werte verndert wird, muss das Control den skalierten Wert anpassen. Realisiert man die Eigenschaften als sogenannte Dependency-Properties, knnen sie per Data-Binding im Control verwendet werden. Mit dieser Idee zur grundstzlichen Vorgehensweise habe ich mich allerdings etwas unsicher gefhlt. Mir war nmlich nicht ganz klar, ob dies tatschlich 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 mgliche Lsungswege entwickeln knnen. Am Ende bleiben jedoch manchmal Unsicherheiten ber den konkreten Lsungsweg. Diese knnen 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 groes Aber: Beim Spike ist zwar alles erlaubt, aber das Ergebnis wird nicht in der Produktion verwendet. Nachdem der Spike zum Erkenntnisgewinn gefhrt hat, muss die betreffende Funktionalitt anschlieend nach allen Regeln der Kunst testgetrieben implementiert werden. Nachdem ich also per Spike geklrt 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 Eigenschaften fr Minimum, Quartile, Median und Maximum. Auf der anderen Seite gibt es einige statische Methoden, um diese Werte zu berechnen. Damit sind Control und Berechnungslogik unabhngig voneinander. Beim Control habe ich eine einfache Lsung gewhlt: Minimum und Maximum liegen jeweils am Rand des Controls. Damit fllt der Boxplot den fr das Control zur Verfgung stehenden Platz vollstndig 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.

Listing 3 set veranlasst die Skalierung.


public double Low { get { return (double)GetValue(LowProperty); } set { SetValue(LowProperty, value); SetValue(LowScaledProperty, Scaled(value)); } }

Ich habe dazu als Primitive nur Linien verwendet. Bei einem waagerecht liegenden Boxplot sind die y-Koordinaten nicht von den darzustellenden Gren abhngig, sondern nur vom zur Verfgung 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 : T XAML-Datei zur Definition der Linien und der Data-Bindings, T Dependency-Properties fr die darzustellenden Gren, T Skalierungslogik. Die XAML-Datei enthlt 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 abhngen. Damit das Data-Binding sich auf eigene Eigenschaften des Controls bezieht, mssen Sie im UserControl den DataContext wie folgt setzen :
<UserControl ... DataContext="{Binding RelativeSource={RelativeSource Self}}">

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})); }

Dadurch knnen 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" />

28

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Im Beispiel ist LowScaled die Dependency-Property, welche den skalierten Wert fr das untere Quartil enthlt. Fr die Dependency-Properties wird jeweils ein statisches Feld definiert, welches die Dependency-Property beschreibt, siehe Listing 1. Zustzlich sind normale C#-Eigenschaften definiert, um in der blichen Art und Weise auf die Eigenschaften zugreifen zu knnen, 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 Gren erfolgt in den Settern der jeweiligen zugehrigen nichtskalierten Gre. 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 prfen zu knnen, 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 wre. Der

Testrahmen kann immerhin dazu verwendet werden, Beispieldaten automatisiert zum Control zu bertragen. So entfllt die manuelle Interaktion mit dem Control zum

codekicker.de Die deutschsprachige Q&A-Plattform fr Software-Entwickler

codekicker.de Antworten fr Entwickler

LSUNG
Testzeitpunkt. Zudem sind dadurch die verwendeten Testdaten dokumentiert. und aus diesen der Mittelwert berechnet. Die Ergnzung um das Sortieren war keine groe Sache, wie Listing 6 zeigt. Nach dem Median kamen Minimum und Maximum an die Reihe auch kein groes Problem. Nach dem Sortieren den ersten beziehungsweise letzten Wert zu verwenden ist einfach. Aber halt: Gibt es diese Funktionalitt nicht in LINQ? Listing 7 zeigt es. Siehe da, ganz einfach. Bleiben noch die beiden Quartile. Hier war, wie eingangs schon erwhnt, eher die Frage, welcher Algorithmus verwendet werden sollte. Die Tests sind wieder keine groe 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 zugehrige 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-

Berechnungen
Nachdem das Control fertiggestellt ist, geht es an die Berechnung der bentigten Gren. Dabei kann man dank SilverUnit und TypeMock Isolator wieder testgetrieben vorgehen. Ich habe zunchst einige Beispiele zusammengestellt und diese dann nach und nach in automatisierte Tests berfhrt, siehe Listing 5. Da mir der Algorithmus zur Berechnung des Medians vor der Implementierung vertraut war, habe ich die Tests so gewhlt, dass ich den Algorithmus schrittweise implementieren konnte. Zunchst habe ich daher den Median aus einer bereits sortierten Aufzhlung ermittelt. Dabei sind zwei Flle zu unterscheiden: Die Anzahl der Werte kann ungerade oder gerade sein. Bei einer geraden Anzahl von Werten werden die beiden mittleren Werte herangezogen

[Abb. 2] Geschafft: Ein Boxplot im Browser.

tierung des Sortierens damit in einer Methode zusammengefasst. Sollte sich spter zeigen, dass das Sortieren ber Arrays zu Performance- oder Speicherproblemen fhrt, kann dies an einer einzigen Stelle behoben werden. Abbildung 2 zeigt das fertige Control im Browser.

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})); }

Fazit
Die Herausforderung lag diesmal im Tooling. Automatisiertes Testen von SilverlightAnwendungen ist immer noch ein schwieriges Unterfangen. Bei dem kostenlos verfgbaren Tool aus dem Silverlight Toolkit strt mich persnlich vor allem, dass es auf MSTest basiert. Als NUnit-Anwender fllt es mir schwer, Assert.AreEqual zu schreiben statt Assert.That. Ferner ist die ausschlieliche Ausfhrung im Browser nicht zu tolerieren. Hier sollte Microsoft schnell nachbessern und einen in Visual Studio integrierten Unit Test Runner liefern. Dass dies mglich ist, zeigt Testdriven.NET [7]. Leider kann damit aber immer nur ein einziger Test ausgefhrt werden. Die Alternative lautet zurzeit SilverUnit. Dazu ist zwar eine kostenpflichtige Lizenz von TypeMock Isolator erforderlich, das drfte aber fr ernsthafte kommerzielle Entwicklungen im Silverlight-Umfeld kein Problem [ml] darstellen.

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; } }

[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

30

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 gefgter Phalanx. Aber geht Persistenz nicht auch anders? Da gibt es doch diese NoSQL-Dokumentendatenbanken. Stefan, fllt dir dazu eine bung ein?

In jeder dotnetpro finden Sie eine bungsaufgabe von Stefan Lieser, die in maximal drei Stunden zu lsen sein sollte. Wer die Zeit investiert, gewinnt in jedem Fall wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen. Es gilt : T Falsche Lsungen gibt es nicht. Es gibt mglicherweise elegantere, krzere oder schnellere Lsungen, aber keine falschen. T Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das knnen Sie, indem Sie Ihre Lsung mit der vergleichen, die Sie eine Ausgabe spter in dotnetpro finden. bung macht den Meister. Also los gehts. Aber Sie wollten doch nicht etwa sofort Visual Studio starten

[Abb. 1] Ungefhr so knnte die Seite fr Produktbewertungen aussehen.

sehen. berlegen Sie sich also ein kleines Datenmodell, bestehend aus Produkten, Kategorien, in die ein Produkt fllt, sowie Bewertungen und Kommentaren zu einem Produkt. Da Raven DB eben gerade nicht relational ist, besteht die Herausforderung mglicherweise darin, sich von der in uns schlummernden relationalen Denkweise ganz bewusst zu lsen. Um die Fhigkeiten von Raven DB zu erkunden, sollten Sie in der Anwendung ein Feature vorsehen, das Daten aggregiert. Sie knnen 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 Beschftigung mit Raven DB. Die zweite besteht darin, beispielhafte Anforderungen mit Raven DB umzusetzen. Viel Spa bei der Arbeit als Forscher auf unbekanntem Terrain. [ml]

[1] Raven DB, http://ravendb.net/

www.dotnetpro.de dotnetpro.dojos.2011

31

Wer bt, gewinnt

ersistenz ist ein wichtiger Aspekt in vielen Anwendungen. Seit Jahrzehnten bewhrt sich die Technologie der relationalen Datenbanken. Sie ist allerdings nicht in allen Fllen 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. Schlielich bedeutet regelmiges ben fr Softwareentwickler auch, sich ab und zu mal mit vllig neuen Dingen zu beschftigen. Da Ayende Rahien gerade sein neuestes Projekt Raven DB [1] verffentlicht hat, bietet sich die Chance, zu den Early Adoptern zu gehren. 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 Produktionsqualitt erreicht, sondern Ziel ist Erkenntnisgewinn. Doch wenn sich automatisierte Tests fr ein flssiges Entwickeln von Produktionscode eignen, mgen sie auch in einem Spike ntzlich sein, um schnell voranzukommen. Denn nach dem Speichern und Laden eines Objektes mit Raven DB wird schnell der Wunsch entstehen, auch die anderen Fhigkeiten des APIs auszuloten. Da kommt man mit einer Reihe von Tests, die im Unit Test Runner einzeln gestartet werden knnen, zgig 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-

dnpCode: A1008DojoAufgabe

LSUNG
Die NoSQL-Dokumentendatenbank Raven DB ausprobieren

So sammeln Raben Daten


Zum Entwickleralltag gehrt es, sich in neue Technologien einzuarbeiten, beispielsweise in eine NoSQL-Datenbank. Der Code, der dabei entsteht, muss nicht die Qualitt von Produktionscode haben. Ein testgetriebener Ansatz ist dafr aber dennoch ntzlich, denn die Tests dokumentieren die gewonnenen Erkenntnisse in leicht nachvollziehbarer Form.

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 zurckdrngen, die man bei Produktionscode in jedem Fall anwenden wrde. Das bedeutet jedoch nicht, dass Spikes ein Freifahrtschein fr schlechte Angewohnheiten wren. 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 fr 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 Stck Automatisierung, viele der Schritte erledige ich in Handarbeit. Aber da ich sie so oft anwende, gehen sie flssig von der Hand. So landen bei mir auch in Spikes die Tests in einem eigenen Projekt. Und auch die bentigten 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. Wrde ich nicht so verfahren, ht-

ten Sie als Leser spter das Nachsehen. Denn dann wrden 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 Verffentlichung in einem Artikel schreibe. Auch in Ihren tglichen Projekten werden andere Entwickler den Code aus der Quellcodeverwaltung entnehmen und bersetzen wollen. Wenn Projekte dann nicht self-contained, also in sich abgeschlossen, sind, fngt 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 Selbstverstndlichkeiten wie NUnit zu der Umgebung gehren wrden, die Sie bei jedem Entwickler voraussetzen knnen. Je weniger Abhngigkeiten das Projekt zu seiner Umgebung hat, desto besser. Sie werden mglicherweise 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 knnen. 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 gengt, um zu verstehen, was da passiert. Es ist nicht notwendig, eine Anwendung laufen zu lassen, um zustzlich noch die Konsolenausgabe zu sehen. Der zweite Vorteil von Tests ist, dass ich sie zur Dokumentation der Funktionalitt verwenden kann. Wenn ich mir nicht sicher bin, ob eine bestimmte Funktionalitt sich nun so oder anders verhlt, erstelle ich einen Test, der das Verhalten dokumentiert. Im weiteren Verlauf des Artikels wird ein solcher Test beispielsweise zeigen, zu welchem Zeitpunkt RavenDB Schlsselwerte 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, binre Version verwenden oder auf den Quellcode setzen sollte. Ich entschied mich fr eine binre Version, die jeweils aktualisiert unter [3] zum Download erhltlich ist. Wenn schon tglich aktualisierte Binrversionen zur Verfgung stehen, muss ich mir nicht die Mhe machen, lokal jeweils eine aktuelle Version zu bersetzen. Leider ist bei Weitem nicht fr alle OpenSource-Projekte ein tglich 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(); }

32

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Build verfgbar, daher bietet sich in anderen Fllen die Arbeit mit den Quellen an. Nach dem Download stand mir RavenDB nun zur Verfgung. Ich habe es komplett in das lib-Verzeichnis innerhalb der Projektstruktur abgelegt. Damit unterliegt es der Versionierung, und das Projekt ist in sich abgeschlossen. Konvention ist einzuhalten: RavenDB bentigt eine Eigenschaft namens Id vom Typ string. In dieser Eigenschaft wird der eindeutige Schlssel des Objekts von RavenDB erwartet. Natrlich kann diese Konvention gendert werden, wenn sie nicht passt.
public class Produkt { public string Id { get; set; } public string Name { get; set; } public string Kategorie { get; set; }

Client und Server


RavenDB kann auf verschiedenen Wegen verwendet werden: eingebettet in die Anwendung oder getrennt in Client und Server. Ich entschied mich dafr, 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 selbstverstndlich. Oft befinden sich alle binren Artefakte eines Frameworks gemeinsam in einem bin-Verzeichnis, aus dem man sich selbst heraussuchen muss, was man bentigt. Da gefllt mir diese Aufteilung bei RavenDB doch sehr gut. Sie vereinfacht die ersten Schritte. Doch zurck 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 ausfhren : T RavenDb.exe /install T 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, kmmert sich RavenDB bei Bedarf um die Elevation, also das Beschaffen der ntigen Rechte. Das ist vorbildlich! Statt eine kryptische Fehlermeldung auszugeben, eventuell mit dem Hinweis, man mge das Programm als Administrator starten, wirds mir hier sehr einfach gemacht.

Um eine Instanz der Klasse mit RavenDB zu persistieren, bentigt 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 enthlt keinen Zustand, dafr ist die DocumentSession zustndig. Whrend der Lebenszeit der Session sorgt diese fr die Objektidentitt: 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 Objektidentitt wre die Gefahr sehr gro, dass an unterschiedlichen Objekten nderungen vorgenommen werden, die

Listing 2 RavenDB sichert die Objektidentitt.


[Test] public void Session_stellt_Objektidentitt_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 erhlt 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); }

CRUDe Methoden
Nun mchte ich als Erstes ein Objekt in der RavenDB-Datenbank abspeichern. Dazu habe ich die Klasse Produkt angelegt. Die Klasse bercksichtigt 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

33

LSUNG
sich tatschlich aber auf dasselbe Dokument beziehen. Um zu dokumentieren, wie sich RavenDB in diesem Punkt verhlt, 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 dafr, dass das Dokument eine Id erhlt, ber die es spter 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 abhngt, wie viele Zugriffe auf den Server erforderlich sind. Ferner hngt davon ab, zu welchem Zeitpunkt man die Id verwenden kann, um Referenzen zwischen Dokumenten herzustellen (auch wenn man dies vermeiden sollte, es ist schlielich 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. Natrlich kann die Id auch vorgegeben werden. Ist dies der Fall, erzeugt RavenDB keine Id, sondern bernimmt die vorhandene. Natrlich 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 htten wir Create und Retrieve aus CRUD gelst. Wie sieht es mit Update aus? Ganz einfach: Wenn Sie session.Store mit einem Objekt aufrufen, das bereits eine Id hat, dann sorgt RavenDB dafr, dass das Dokument entweder neu angelegt oder aktualisiert wird. Dies soll in einem greren 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 zurckliegenden Artikeln der Dojo-Serie bereits thematisiert. Fr 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 erffnet werden. Listing 4 zeigt den Test fr Updates, und Listing 5 zeigt die Implementation des Repositorys. Als letzte CRUD-Operation steht das Lschen an. Dazu verfgt DocumentSession ber die Methode Delete, der das zu l-

LSUNG
Listing 4 Ein Update berprfen.
[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")); } }

beiten zu knnen, habe ich die Sessions ineinandergeschachtelt. Das heit, whrend die erste Session noch aktiv ist, wird innerhalb des using-Blocks eine zweite Session geffnet, um dort ein Update vorzunehmen. Dann wird aus der ersten Session ebenfalls ein Update abgesetzt.

Das Verhalten von RavenDB hngt an dieser Stelle davon ab, ob die Session Optimistic Concurrency untersttzen soll. Dies kann man per Session einstellen, standardmig 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; } } }

schende Objekt bergeben wird. Dabei stellte sich mir die Frage, wie man ein Dokument aus der Datenbank lscht, dessen Id man kennt, das aber nicht als Objekt geladen wurde. Natrlich 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 Lschen ber die Id ist im ClientAPI nicht vorgesehen. Das bedeutet aber nicht, dass es unmglich ist. Das Client-API ist nur ein Wrapper, der ber das HTTPProtokoll gelegt ist, und in diesem API ist Lschen per Id nicht vorgesehen. Wie man das API erweitert, habe ich mir allerdings in diesem Spike nicht weiter angesehen.

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 ldt das Dokument als 'referenz1' using (var session1 = store.OpenSession()) { session1.UseOptimisticConcurrency = true; var referenz1 = session1.Load<Produkt>(produkt.Id); // Zweite Session ldt 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); } }

Konkurrierende Zugriffe
Beim Erforschen des APIs ist mir an dieser Stelle die Frage gekommen, wie RavenDB sich bei konkurrierenden Zugriffen verhlt. 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 verndert wird. Merkt RavenDB das beim Update? Um die Frage zu klren, habe ich einen Test geschrieben. Um dabei mit zwei Sessions ar-

www.dotnetpro.de dotnetpro.dojos.2011

35

LSUNG
fen nicht erkannt und Updates einfach der Reihe nach ausgefhrt. Um diese Erkennung zu aktivieren, mssen Sie in der Session die Option UseOptimisticConcurrency auf true setzen. Listing 6 zeigt den Test fr die Erkennung konkurrierender Zugriffe. Um die ConcurrencyException zu vermeiden, knnen Sie das Objekt vor der nderung mit session.Refresh(referenz1) auf den aktuellen Stand bringen. Allerdings gehen damit natrlich alle nderungen verloren, die am Objekt zuvor bereits vorgenommen wurden. Eine Strategie knnte dann sein, das Refresh nur dann auszufhren, wenn die ConcurrencyException tatschlich aufgetreten ist. Dann knnten die nderungen, die in den beiden konkurrierenden Sessions vorgenommen wurden, zusammengefhrt werden. Wie das im Einzelnen geschieht, hngt von der Business Domain ab.

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 = "Gemse"}); 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"})); }

Query
Der nchste Schritt in meinem Spike sollte die Frage klren, wie man Dokumente aus der Datenbank lesen kann, die bestimmte Anforderungen erfllen. 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 zunchst 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 wrde bedeuten, dass bei jeder Suche die JSON-Strings aller Dokumente interpretiert werden mssten. Bei einer relationalen Datenbank ist die Suche nur mglich, weil es dort von vornherein ein Schema gibt, welches dafr 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-

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 fr den Zugriff auf alle Produkte einer Kategorie erstellen. Das Erstellen dieses Index muss einmalig erfolgen. RavenDB sorgt dafr, dass der Index jeweils aktualisiert wird, wenn sich die zugehrigen Daten ndern. Soll die Definition des Index gendert werden, muss man ihn zunchst lschen. Das geht ganz einfach:
store.DatabaseCommands.DeleteIndex( "ProdukteNachKategorie");

natrlich keinesfalls tun. Um anonyme Zugriffe fr smtliche 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"/>

Listing 7 Einen Index erstellen.


store.DatabaseCommands.PutIndex( "ProdukteNachKategorie", new IndexDefinition<Produkt> { Map = produkte => from produkt in produkte select new {produkt.Kategorie} });

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 zunchst gewundert. Die Lsung ist auf zwei Wegen mglich: Entweder man bergibt beim ffnen einer Session Anmeldedaten, sogenannte Credentials, oder man erlaubt auch anonymen Nutzern den vollstndigen Zugriff. In einer lokalen Testumgebung ist es einfacher, den Server zu ffnen. Im Produktivbetrieb sollte man das

Anschlieend mssen Sie den RavenDBServer-Dienst neu starten. Doch zurck zum Index. Der wichtigste Teil beim Erstellen des Index ist die LINQQuery, die fr den Map-Vorgang zustndig ist. Diese Query wird von RavenDB fr jedes Dokument ausgefhrt. 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 Schlssel. Im Ergebnis sind zu einer gegebenen Kategorie im Index Referenzen auf die betreffenden Produkte abgelegt. Damit wird eine Suche nach smtlichen Produkten einer Kategorie mglich, siehe dazu Listing 8. Wichtig ist hier der Aufruf von ToArray(). Da die Query innerhalb einer Session aufgerufen wird, die mit Verlassen des using-

36

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Blocks geschlossen wird, muss das tatschliche Lesen der Daten innerhalb der Session passieren. Lsst 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 fr 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 knnen. Auch hier gilt es, noch einen weiteren Stolperstein zu beachten. RavenDB fhrt 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 Performanceeinbuen 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 zurck. der jeweiligen Kategorie enthalten sind. Das Erstellen dieses Index gleicht dem vorhergehenden Beispiel. Der wesentliche Unterschied besteht in der zustzlichen 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 fehleranfllig, siehe Listing 10. Die Map-Query liefert fr jedes Produkt ein Objekt zurck. Dieses Objekt hat zwei Eigenschaften: Kategorie und Anzahl. Die Anzahl ist immer 1, dies ist der Startwert fr die sptere 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, erhlt man eine Fehlermeldung vom RavenDB-Server, die besagt, man msse anonyme Typen verwenden. Dennoch mssen diese anonymen Typen den gleichen Aufbau haben wie der definierte Typ. Wenn man diese Hrde genommen hat, steht einem Test des Index nichts mehr im Weg, wie Listing 11 zeigt. Das Schne an diesem Index ist, dass RavenDB ihn jeweils auf dem aktuellen Stand hlt. Jede nderung an Dokumenten, die im Index verwendet werden, fhrt zu einem entsprechenden Update des Index.

Map/Reduce
Als Nchstes habe ich mir ein Feature angeschaut, das bei Dokumentdatenbanken sehr hufig zum Einsatz kommt und groen Einfluss auf die Skalierbarkeit hat: Map/Reduce. Eine Map-Funktion wurde bereits fr den Index bentigt, mit dem alle Produkte einer Kategorie ermittelt werden knnen. Fgt man einem solchen Index noch eine Reduce-Methode hinzu, knnen Daten aus den Dokumenten aggregiert werden. Damit ist es mglich, beispielsweise einen Index zu erstellen, der aus den Produkten alle Kategorien ermittelt. Zustzlich ist es durch die Aggregation mglich zu zhlen, 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) } });

Fazit
Ich habe mir in diesem Spike die Funktionalitt von RavenDB nur ausschnittweise angesehen. RavenDB hat darber hinaus noch mehr zu bieten wie etwa die Verteilung einer Datenbank auf mehrere Server. Die ersten Schritte gingen zgig voran. Aber beim Map/Reduce hat es doch etwas lnger gedauert, bis ich die zu bercksichtigenden Konventionen alle zusammenhatte. Hier wre ein Beispiel in der Dokumentation sicherlich hilfreich. Gelernt habe ich wieder einiges, und das war schlie[ml] lich Zweck der bung.

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 = "Gemse"}); 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 = "Gemse", Anzahl = 1}, })); }

[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/

www.dotnetpro.de dotnetpro.dojos.2011

37

AUFGABE
Algorithmen und Datenstrukturen

Was ist im Stapel?


In den Zeiten der groen 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 zurckfhrt?
dnpCode: A1009DojoAufgabe

Wer bt, gewinnt

In jeder dotnetpro finden Sie eine bungsaufgabe von Stefan Lieser, die in maximal drei Stunden zu lsen sein sollte. Wer die Zeit investiert, gewinnt in jedem Fall wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen. Es gilt : T Falsche Lsungen gibt es nicht. Es gibt mglicherweise elegantere, krzere oder schnellere Lsungen, aber keine falschen. T Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das knnen Sie, indem Sie Ihre Lsung mit der vergleichen, die Sie eine Ausgabe spter in dotnetpro finden. bung macht den Meister. Also los gehts. Aber Sie wollten doch nicht etwa sofort Visual Studio starten

atrlich 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 Funktionalitt so gut bekannt ist, bietet sich ein Stack als bung an. Hier knnen Sie sich voll auf den Entwurf einer Lsung konzentrieren und anschlieend testgetrieben implementieren. Die Aufgabe soll gelst werden, ohne dass vorhandene Datenstrukturen aus dem .NET Framework verwendet werden. Aus einer Liste einen Stack zu machen scheidet also aus. Und natrlich soll der Stack generisch sein. Das bedeutet, dass man den Typ der Elemente als generischen Typparameter angeben kann. Ein Stack fr IntegerElemente wird also folgendermaen instanziert:
var stack = new Stack<int>();

Stack zur Laufzeit abzubilden. Die Verwendung von Collections aus dem .NET Framework scheidet aus. Auch ein Array scheidet aus, da der Stack keine Grenbeschrnkung haben soll. Die in einem Stack angewandte Strategie beim Entnehmen eines Elementes lautet: Last In/First Out, abgekrzt 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 folgendermaen aus:
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 erklrt: T Mit Push(element) kann ein Element oben auf den Stack gelegt werden. Jede weitere PushOperation legt ein weiteres Element obendrauf. T 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(); }

Auch hier gilt: berlegen Sie sich eine Datenstruktur, mit welcher die Aufgabenstellung gelst werden kann. Wie immer hilft es, sich dazu ein Blatt Papier zu nehmen. Oder lsen Sie die bung mit Kollegen gemeinsam im Team, und planen Sie am Whiteboard. Die testgetriebene Entwicklung wird hufig so verstanden, dass man einfach mit einem ersten Test loslegt und sich von da an alles schon irgendwie ergeben wird. Ich halte das fr falsch. Ein bisschen Planung vor dem Codieren schadet nicht, ganz im Gegenteil. Wer sich mit der bung unterfordert fhlt, kann brigens noch eine weitere Methode auf Queue<T> ergnzen:
void Reverse();

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

Diese Methode soll die Reihenfolge der Elemente in der Warteschlange umkehren. Dazu sollen die Verweise zwischen den Elementen so verndert werden, dass die Queue in place verndert wird. Wie immer gilt: test first! Viel Spa. [ml]

38

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Stack und Queue implementieren

Der Nchste bitte!


Immer hbsch 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.

ls Entwickler nehmen wir Stack und Queue, wie viele andere Datenstrukturen, als selbstverstndlich 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 heit, 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 schlieen und zu einem Stck Papier greifen. Denn die zentrale Frage vor dem ersten Test lautet, wie denn die interne Reprsentation 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 nchsten Mal das nchste 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 nchste Element ist. So ergibt sich eine ganz einfache interne Datenstruktur fr 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 Zustnde zu betrachten: T einen leeren Stack, T einen nicht leeren Stack. In solchen Fllen erstelle ich fr die unterschiedlichen Szenarien gerne getrennte

[Abb. 1] top- und next-Zeiger im Stack.

Listing 1 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); } }

Testklassen. Dann kann nmlich das Setup des Tests dafr 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 befllt. So knnen 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 fr den ersten Test bereits sehr viel Funktionalitt implementiert werden muss. Ich mchte lieber in kleinen Schritten vorgehen, um sicher zu sein, dass ich wirklich nur gerade so viel Code schreibe, dass ein weiterer Test erfolgreich verluft. Fr den ersten Test zu einem leeren Stack berlege ich mir, wie die interne Reprsentation 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 fr int-Elemente instanziert. Der erste Test prft, 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 Reprsentation nach auen getragen wird und zweitens Interna getestet werden. Zur Sichtbarkeit sei gesagt, dass

ich das Feld top auf internal setze. Damit ist es zunchst 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 fr vernachlssigbar. Sicher ist es erstrebenswert, Tests so zu schreiben, dass sie mglichst wenige Abhngigkeiten zu den Interna der Klasse haben. Denn wenn nur ber die ffentliche Schnittstelle getestet wird, sind die Tests

Listing 2 Datenstruktur fr die StackElemente.


internal class Element<TData> { public TData Data { get; set; } public Element<TData> Next { get; set; } }

www.dotnetpro.de dotnetpro.dojos.2011

39

LSUNG
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 hufig weniger fokussiert. Im Fall eines Stacks stellt sich nmlich die Frage, wie man Push ausschlielich ber die ffentliche Schnittstelle testen kann, ohne dabei andere Methoden des Stacks zu verwenden. Natrlich 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 mglich. Um mit der Implementierung weiterzukommen, mssen 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 nchste Element hat. Folglich ist der topZeiger einfach das erste Element im Stack. Listing 2 zeigt die Datenstruktur fr die Elemente. Da diese Datenstruktur auerhalb des Stacks nicht in Erscheinung tritt, wird sie durch die Sichtbarkeit internal verborgen. Wie weiter oben erwhnt, kann dennoch in Tests darauf zugegriffen werden. Listing 3 zeigt die Implementierung des Stacks fr den ersten Test. Die Methoden Push und Pop werden eigentlich noch nicht bentigt, sind aber syntaktisch erforderlich aufgrund des Interfaces IStack<T>. Nun stand ich vor der Wahl, ob ich als Nchstes mit Push oder Pop weitermachen wollte. Ich halte Push fr naheliegender, denn bei einem leeren Stack wird Pop ohnehin nur zu einer Ausnahme fhren. Gerade zu Beginn der Implementierung

Listing 4 Erster Test fr 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 Enthlt_nach_einem_Push_nur_dieses_eine_Element() { sut.Push(5); Assert.That(sut.top.Next, Is.Null); }

mchte ich weiterkommen und mich nicht mit Rand- und Fehlerfllen befassen. Aber das ist sicherlich Geschmackssache. Listing 4 zeigt den ersten Test fr 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 tatschlich in einem eigenstndigen Test berprft oder ein zweites Assert im vorhergehenden Test zulsst, sei dahingestellt. Ich habe mich fr zwei getrennte Tests entschieden, weil ich keine treffende Bezeichnung fr einen Test finden konnte, der beides prft. Die Regel, nur ein Assert pro Test zuzulassen, halte ich jedenfalls fr dogmatisch. Ein Test sollte sich mit einer Sache befassen. Wenn diese eine Sache mit mehr als einem Assert berprft werden muss, finde ich das vllig in Ordnung. An dieser Stelle kann man mit einem leeren Stack nicht viel mehr anstellen, auer nun doch Push und Pop in Beziehung zu setzen. Also sieht mein nchster Test so aus wie in Listing 6. Bei diesem Test liegt die Versuchung nahe, auch noch zu prfen, 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 gefllt 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")); } }

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

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Listing 8 Auf einen leeren Stack testen.
[Test] public void Enthaelt_nach_der_Entnahme_des_top_Elementes_keine_weiteren_Elemente() { sut.Pop(); Assert.That(sut.top, Is.Null); }

Listing 10 Die fertige Implementierung des Stacks.


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; } }

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 geprft werden, ob dieses Element bei Aufruf der Pop-Methode zurckgegeben wird, siehe Listing 7. Nun kann berprft werden, ob der Stack denn nach dem Pop auch wieder leer ist, siehe Listing 8. Und jetzt hilft alles nichts, wir mssen uns mit mehr als einem Push befassen. Dabei kommt es darauf an, das neue Element vor das bisherige topElement einzuordnen. Dazu muss der topZeiger gendert 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.

mehrere Methoden auf einem internen Zustand arbeiten, kann es sich lohnen, den Zustand fr die Tests sichtbar zu machen, um bei der Implementierung Methode fr Methode vorgehen zu knnen.

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 eingefgte Element, um dort bei der Enqueue-Methode ein weiteres Element ergnzen zu knnen, sowie einen Zeiger auf das nchste zu entnehmende Element fr 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.

Im Anschluss habe ich berlegt, wie die Elemente untereinander sinnvoll zu verbinden sind. Dabei gibt es mehrere Mglichkeiten : T Jedes Element zeigt mit Next auf das ihm folgende. T Jedes Element zeigt mit Prev auf das hinter ihm liegende. T Jedes Element enthlt sowohl Next- als auch Prev-Zeiger. Die doppelte Verkettung habe ich nicht weiter bercksichtigt, 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 Einfgen und Entfernen von Elementen zwei Zeiger korrigieren muss. Das erschien mir in jedem Fall mhsamer, als nur einen Zeiger korrigieren zu mssen. Es siegte sozusagen die pure Faulheit. Es blieb noch die Frage zu klren, ob Next- oder Prev-Zeiger sinnvoller sind. Das habe ich mir wieder anhand meiner Skizze berlegt. Wenn ein neues Element in die Warteschlange eingefgt wird, muss enqueue anschlieend 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 zurckverweisen. Beides ist kein Problem, es ergibt sich also hier noch keine Prferenz fr eines der bei-

Reflexion
Die testgetriebene Vorgehensweise hat mir keine groen Probleme bereitet. Das lag zum Groteil daran, dass ich meine Skizze zur Hand hatte. So konnte ich bei Fragen sofort nachsehen, wie die top- und NextZeiger jeweils aussehen mssen. Und dadurch, dass die Tests auf die Interna zugreifen knnen, musste ich nicht schon fr den ersten Test gleich zwei Methoden implementieren. Das ist ein groer Vorteil, der bei einem so kleinen Beispiel wie einem Stack mglicherweise nicht so deutlich wird. Ich habe diesen Effekt jedoch schon in einigen Fllen als vorteilhaft empfunden, bei denen es um den internen Zustand von Klassen ging. Immer da, wo

[Abb. 2] enqueueund dequeueZeiger.

www.dotnetpro.de dotnetpro.dojos.2011

41

LSUNG
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 anschlieend auf das vorhergehende Element verndert werden. Wenn die Elemente mit Next jeweils auf das nchste verweisen, wre diese Korrektur nur mglich, indem die gesamte Warteschlange durchlaufen wird, bis das vorletzte Element erreicht ist. Bei Verwendung von Prev-Zeigern enthlt das letzte Element den bentigten Verweis auf seinen Vorgnger. Damit war klar: Prev-Zeiger sind hier eindeutig einfacher, siehe Abbildung 3. Das bedeutete aber auch, dass es fr die Queue<T> eine eigene Klasse Element<T> geben muss, da beim Stack auf Next gezeigt wird. Hier bemerkte ich einen weiteren Reflex, nmlich den Versuch, die Klasse Element<T> wiederzuverwenden. Das lag irgendwie nahe, htte jedoch dazu gefhrt, dass Stack<T> und Queue<T> durch eine gemeinsam verwendete Klasse nicht vllig entkoppelt wren. Glcklicherweise 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 nchste Test sollte ausdrcken, was beim Hinzufgen des ersten Elements in die Warteschlange passiert. Beide Zeiger verweisen dann nmlich auf das neue Element, siehe Listing 12. Als Nchstes kam wieder ein Test der ffentlichen Schnittstelle, der prft, was bei der Entnahme eines Elements aus der Warteschlange passiert. Zum einen wird das einzige Element der Warteschlange zurckgegeben, zum anderen ist die Warte-

Listing 12 Ein erstes Element hinzufgen.


[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 hinzufgen.


[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")); }

Listing 11 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); } }

schlange dann wieder leer, siehe Listing 13. Die Implementierung war bis hierher trivial. Als Nchstes ging es wieder um eine Warteschlange, die bereits ein Element enthlt. Wenn nmlich 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 ergnzt, welche nur die ffentliche Schnittstelle verwen-

den und demonstrieren, wie sich eineWarteschlange verhlt, siehe Listing 15. Listing 16 zeigt zu guter Letzt die Implementierung. Auch hier war, hnlich wie beim Stack, die Implementierung keine groe Sache. Die Skizze sowie die Vorberlegungen 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 groe Strke

42

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Listing 15 Die ffentliche Schnittstelle verwenden.
[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)); }

Listing 16 Die Queue implementieren.


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; } }

Listing 17 Die Umkehr der Reihenfolge testen.


[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)); } }

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 glcklicherweise schon lange her. Listing 17 zeigt den Test fr 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 nchste Element (next) verweisen. Da die Elemente jeweils mit Prev auf ihren Vorgnger verweisen, wird die Warteschlange von hinten nach vorne abgearbeitet. Daher ist jeweils das Element, welches im zurckliegenden Schleifendurch-

Listing 18 Die Reihenfolge umkehren.

lauf bearbeitet wurde, das nchste Element im Sinne der normalen Vorwrts-Reihenfolge. Am Ende sind noch enqueue und dequeue zu vertauschen, siehe Listing 18. Die Methode habe ich ber die ffentliche Schnittstelle getestet. Man htte sicherlich auch hier die interne Reprsentation heranziehen knnen, ich glaube aber, dass die Tests dadurch schlecht lesbar geworden wren. Daher wird die Beispielwarteschlange mit enqueue aufgebaut, anschlieend

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 berprft, ob die Elemente in der richtigen, umgekehrten Reihenfolge geliefert werden.

Fazit
Bei der testgetriebenen Entwicklung hat sich fr mich besttigt, wie ntzlich 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 Testflle sammeln und diese dann in eine sinnvolle Reihenfolge bringen, bevor [ml] man mit dem ersten Test beginnt.

[Abb. 4] Die Reihenfolge der Elemente umkehren.

www.dotnetpro.de dotnetpro.dojos.2011

43

AUFGABE
Infrastruktur

Wie zhmt man den Dmon?


In der Unix-Welt heien sie Dmonen: die Dienste, die im Hintergrund ihre Arbeit verrichten. Stefan, stell doch mal eine Aufgabe, die in die Unterwelt der Windows-Dienste fhrt.

dnpCode: A1010DojoAufgabe

Wer bt, gewinnt

In jeder dotnetpro finden Sie eine bungsaufgabe von Stefan Lieser, die in maximal drei Stunden zu lsen sein sollte. Wer die Zeit investiert, gewinnt in jedem Fall wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen. Es gilt : T Falsche Lsungen gibt es nicht. Es gibt mglicherweise elegantere, krzere oder schnellere Lsungen, aber keine falschen. T Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das knnen Sie, indem Sie Ihre Lsung mit der vergleichen, die Sie eine Ausgabe spter in dotnetpro finden. bung macht den Meister. Also los gehts. Aber Sie wollten doch nicht etwa sofort Visual Studio starten

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 verfgen. Kein Benutzer, keine Interaktion, so einfach ist das. Damit der Dienst vom Betriebssystem ohne Zutun eines Benutzers gestartet werden kann, mssen 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 Verfgung stehen. Weitere Einstellungen betreffen das Startverhalten: Soll der Dienst beim Systemstart automatisch mit gestartet werden? Ist der Dienst von anderen Diensten abhngig, die dann zuerst gestartet werden mssen? 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 lstig, fr jeden Dienst erneut den Installationsvorgang entwickeln zu mssen. Daher geht es in diesem Monat darum, eine wiederverwendbare Infrastruktur zu entwickeln, mit der Windows-Dienste erstellt werden knnen. 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 fr die bung lauten: Erstellen Sie eine Komponente, mit der ein Windows-Dienst realisiert werden kann. Als Anwender mchte ich den Dienst an der Konsole folgendermaen bedienen knnen : T Mit myService.exe /install und myService.exe /uninstall kann der Dienst installiert beziehungsweise aus dem System entfernt werden. T myService.exe /run fhrt den Dienst als normale Konsolenanwendung aus, ohne dass er zuvor als Windows-Dienst registriert werden muss. Dies ist fr Test und Fehlersuche sehr hilfreich. Das Starten und Stoppen des Dienstes kann zunchst mit den Windows-Bordmitteln bewerkstelligt werden : T net start myService T net start myService Eine denkbare Erweiterung wre, den Dienst auch mit myService.exe /start starten zu knnen. 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 mchte ich mglichst wenig mit der Windows-Infrastruktur konfrontiert werden. Ich mchte 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 drngeln. Die Klasse, welche die Logik des Dienstes implementiert, sollte nicht von einer vorgegebenen Basisklasse ableiten mssen. 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 Lsung. Hier geht es darum, die Balance zu finden zwischen zu viel und zu wenig. Zu viel wre beispielsweise, wenn im ersten Entwurf schon berlegt wird, wie man den Dienst zur Laufzeit kontrollieren kann. Zu wenig wre, wenn die Anforderungen an die Bedienung der Kommandozeile nicht umgesetzt [ml] wren oder alles in einer Klasse landet.

44

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
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 unabhngig von der Infrastruktur halten, ist er dennoch fr automatisierte Tests zugnglich.

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 bercksichtigen. Zum anderen ist die Versuchung gro, von unten nach oben zu entwickeln. Beides fhrt in der Tendenz dazu, dass man zu viel tut. Nun mgen Sie sich vielleicht fragen, was denn so schlimm daran ist, mehr zu tun als gefordert. Sicher, es wre schlimmer, weniger zu tun als gefordert. Jedoch wird beim mehr tun Aufwand getrieben, den am Ende niemand bezahlen mchte. Mglicherweise wird sogar der geplante Termin nicht gehalten, weil unterwegs hier und da noch zustzliche Schmankerl eingebaut wurden. Aus diesem Grund sollten Anforderungen mglichst 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 ntig nachfragen, um unklare Anforderungen zu przisieren. Nun stand ich Ihnen whrend der bung nicht als Kunde unmittelbar zur Verfgung, aber in der Aufgabenstellung war ein Feature explizit als denkbare Erweiterung aufgefhrt, nmlich 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 nachtrglich zu ergnzen, statt es von vornherein vorzusehen. Hilfreich ist es deshalb, die mglichen Features zunchst zu sammeln. Dann knnen 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 schlielich der Kunde das Feature als fertig akzeptieren und ab-

nehmen. Das ist nur mglich, wenn das Feature fr den Kunden tatschlich 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 groen Datenmengen umgehen, liegt der Fokus auf dem Kundennutzen statt auf einem technischen Detail.

matisierten Tests sehen werden, aber es werden wenige sein.

Entwurf
Fr Feature F1 kann nun eine Architektur entworfen werden. Dabei ist einerseits zu bercksichtigen, dass mglicherweise nicht alle Features sofort implementiert werden. Ein Feature wird nach dem anderen implementiert, niedrig priorisierte mglicherweise gar nicht. Wird ein Feature nach dem anderen implementiert, bietet das fr den Kunden sehr viel Flexibilitt: Er kann die Priorisierung von Features jederzeit ndern. Und er kann Features streichen oder auch neue hinzunehmen. Logischerweise gilt dies nicht fr Features, die bereits in Arbeit sind. Aber alle noch nicht begonnenen Features stehen zur Disposition. Folglich sollte ein Architekturentwurf nur die Features im Detail bercksichtigen, die konkret zur Implementierung anstehen. Da weitere mgliche 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 bentigt 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. Fr den Kunden ist es weitaus angenehmer, das wichtigste Feature einsatzbereit geliefert zu bekommen, als viele unvollendete Features, die nicht einsatzbereit sind. Um einen mglichst flexiblen Architekturentwurf zu erreichen, mssen die beteiligten Funktionseinheiten lose gekoppelt sein. So ist die Wahrscheinlichkeit gro, dass zustzliche Features leicht integriert werden knnen. Es liegt also auf der Hand, hier Event-Based Components (EBC) zu verwenden. Doch bevor es so weit ist, mssen die Anforderungen weiter przisiert werden, denn noch ist nicht klar, wie die geforderte Dienstinfrastruktur eingesetzt werden soll.

Featureliste
Fr die AufgabenstellungWindows-Dienst knnten 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 przisieren, sollten Abnahmekriterien definiert werden. Dadurch wei der Entwickler, wann er mit der Arbeit fertig ist eben dann, wenn alle Abnahmekriterien erfllt sind. Die Abnahmekriterien fr 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 natrlich auch gestoppt werden). z Wenn der Dienst mit myservice.exe/uninstall deinstalliert wird, taucht er unter Systemsteuerung/Services nicht mehr auf. Sptestens 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 lsst sich nur durch einen Integrationstest wirklich sicherstellen. Das bedeutet nun nicht, dass wir gar keine auto-

www.dotnetpro.de dotnetpro.dojos.2011

45

LSUNG
Listing 1 Ein Interface fr 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, mchte ich es so einfach wie mglich haben. Das bedeutet fr mich unter anderem: geringe Abhngigkeiten. Denkbar wre die Realisierung ber ein Interface, das wie in Listing 1 zu implementieren ist. Durch dieses Interface wrden alle Informationen bereitgestellt, die relevant sind, damit man einen Windows-Dienst installieren und starten kann. Das knnte 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 fr jeden Dienst eine Klasse implementieren muss. Stattdessen knnte ich eine generische Klasse verwenden, von der Instanzen angelegt werden, siehe Listing 3. Ich habe die Klasse EasyService genannt, weil es damit so schn einfach ist, die erforderlichen Angaben fr einen WindowsDienst zusammenzustellen. Es bleibt allerdings die Frage, wozu eine Instanz von EasyService angelegt werden muss. Das Objekt wrde nur dazu dienen, die Dienstbeschreibung zu transportieren. Eigentlich wird aber kein Zustand bentigt, also gengt auch eine statische Methode Run innerhalb der Klasse, siehe Listing 4. Nachdem klar ist, wie der Kunde seine Software bedienen will, knnen Sie einen Architekturentwurf fr Feature F1 angehen. Von ferne betrachtet, ist das Feature eine Funktionseinheit, die als Parameter die Kommandozeilenargumente sowie eine Dienstbeschreibung erhlt. Die Dienstbeschreibung besteht vor allem aus dem Namen des Dienstes. Ferner sind zwei Lambda-Ausdrcke ntig, die beim Starten und Stoppen des Dienstes ausgefhrt werden sollen. Abbildung 1 zeigt den Entwurf.

[Abb. 1] Erster Entwurf.

Der nchste 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 mssen, fllt es mir leicht, vier Funktionseinheiten zu identifizieren: z Argumente auswerten, z Dienst installieren, z Dienst deinstallieren, z Dienst ausfhren. 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 whrend der Implementierung herausstellen, dass dem nicht so ist, muss der Entwurf mglicherweise weiter verfeinert werden. Aus dem Entwurf ergeben sich folgende Kontrakte : z Ein Datenmodell fr die Dienstbeschreibung. z Ein Kontrakt fr das Auswerten der Kommandozeilenargumente. z Je ein Kontrakt fr die Installation, die Deinstallation und die Ausfhrung des Dienstes. Das Datenmodell wird anstelle von einfachen Typen wie string oder hnlichem verwendet, weil dadurch ein hheres Abstraktionsniveau erreicht wird. Im Entwurf werden bei den Input- und Outputpins ebenfalls Bezeichner verwendet, die aus der sogenannten allgegenwrtigen Sprache (Ubiquitous Language) stammen. Da es sich bei Begriffen wie Dienstbeschreibung um einen Begriff aus der Problemdomne handelt, ist es gut, diese auch im Code wiederzufinden. Das erleichtert das Verstndnis, da keine gedankliche bersetzung erforderlich ist. Wrde eine Me-

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

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
thode mehrere Parameter von einfachen Typen erwarten, msste jemand, der den Code liest, daraus gedanklich die Dienstbeschreibung erst wieder zusammensetzen. Das Datenmodell fr die Dienstbeschreibung sieht aus wie in Listing 5. Die Kontrakte fr die Funktionseinheiten sind in EBC-Manier erstellt. Das bedeutet, dass sie ber sogenannte Inputund Outputpins verfgen. 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 fr das Auswerten der Argumente sieht aus wie in Listing 6. Auf dem Inputpin In_Process der Funktionseinheit werden die Kommando[Abb. 2] Verfeinerung des Entwurfs.

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 fr die Dienstbeschreibung.


public class ServiceBeschreibung { public string Name { get; set; } public string DisplayName { get; set; } public string Description { get; set; } }

zeilenparameter in Form eines string-Arrays bergeben. Je nachdem, welcher Parameter bergeben wurde, wird daraufhin der korrespondierende Outputpin ausgelst. Die beiden Kontrakte fr die Installation und Deinstallation sind noch einfacher, da sie nicht ber Outputpins verfgen, siehe Listing 7. Und zu guter Letzt ist da noch der Kontrakt fr die Ausfhrung des Service, siehe Listing 8. Damit haben Sie nun alle Kontrakte zusammen und knnen 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 IServiceAusfhren mit der abstrakten Klasse ServiceBase aus dem .NET Framework kombiniert werden. Diese stellt die erforderliche Infrastruktur fr die Dienstausfhrung zur Verfgung. Damit die Dienste nicht von ServiceBase ableiten mssen und tatschlich keine Abhngigkeiten 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 ausgefhrt werden. Dadurch kann ein Lambda-Ausdruck verwendet werden, und der Dienst ist infra-

strukturunabhngig. Der Dienstproxy sieht aus, wie in Listing 9 gezeigt wird. Diese Klasse ist so simpel, dass ich auf Tests verzichtet habe. Sie zu ergnzen wrde im brigen auch erfordern, die protected Methoden OnStart und OnStop im Test aufzurufen. Aufgrund der Vererbung kann die Sichtbarkeit nicht zu internal gendert werden. Aufwand und Nutzen stnden daher in einem sehr ungnstigen Verhltnis. 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 prft, ob beim Aufruf ohne Parameter der Outputpin Out_RunAsService ausgelst wird. Dies ist wichtig, da Windows den Dienst so startet. Der zweite Test prft, ob bei Aufruf mit /install der Event Out_Install ausgelst wird. Die zugehrige Implementierung ist einfach gehalten. Zunchst wird geprft, ob kein Argument bergeben wurde. In dem Fall wird der Event Out_RunAsService ausgefhrt. Andernfalls wird ber ein switchStatement in die jeweiligen Events verzweigt. Diese Form der Argumentauswertung ist zwar zu einfach, um damit etwa zustzliche 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; }

Installation und Deinstallation.


public interface IServiceInstallieren { void In_Installieren(ServiceBeschreibung beschreibung); } public interface IServiceDeinstallieren { void In_Deinstallieren(ServiceBeschreibung beschreibung); }

www.dotnetpro.de dotnetpro.dojos.2011

47

LSUNG
Listing 8 Den Dienst ausfhren und stoppen.
public interface IServiceAusfhren { void In_Start(); void In_Stop(); }

zu knnen. Kommandos mit Parametern wie etwa /install myService2 erfordern etwas mehr Aufwand beim Parsen. Fr Feature F1 gengt ein solch einfacher Parser aber vllig, warum also mehr tun. Tatschlich ist sogar die Gro-/Kleinschreibung signifikant, das heit /INSTALL wrde zu einem Fehler fhren. Aber auch diese Einschrnkung ist in Ordnung, es sei denn, der Kunde wrde explizit fordern, dass Gro-/ Kleinschreibung zu ignorieren sei. Als Nchstes 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 knnen, 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 lsst sich nun mal nicht anders berprfen. Neben den Details der Dienstinstallation ging es auch um andere Details. Denn natrlich 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 bentigt. Das geht ganz einfach: Man fgt 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. Fr die Installation eines Dienstes stehen im .NET Framework die Klassen TransactedInstaller, ServiceProcessInstaller und ServiceInstaller zur Verfgung. Der TransactedInstaller ist dafr zustndig, eine Installation transaktional auszufhren. Das bedeutet, die angeforderte Installation wird entweder vollstndig ausgefhrt oder bei einem Fehler komplett wieder rckgngig gemacht. ServiceProcessInstaller und ServiceInstaller werden konfiguriert und zum TransactedInstaller hinzugefgt; die-

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)); } }

Listing 9 Ein Proxy fr den Dienst.


public class ServiceProxy : ServiceBase, IServiceAusfhren { 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(); } }

ser wird anschlieend angewiesen, die Installation durchzufhren. Da die Schritte fr 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, mssen 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 wre das natrlich mglich. Dazu mssten die einzelnen Bausteine durch ein Mock-Framework instanziert und in die Klasse EasyService injiziert werden. Anschlieend knnte automatisiert geprft 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 tatschlich feststellen zu knnen, ob Feature F1 fertiggestellt ist, mssen die Abnahmekriterien berprft werden. Dazu muss ein exemplarischer Dienst implementiert werden, um so zu prfen, ob dieser tatschlich in der Systemsteuerung sichtbar ist und gestartet werden kann.

48

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Listing 11 Die Administratorberechtigung anfordern.
<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>

[Abb. 3] DebugView zeigt die Trace-Ausgaben.

Hier stellte sich mir die Frage, wie man am einfachsten berprft, ob der Dienst tatschlich gestartet und gestoppt werden kann. Eine Ausgabe auf der Konsole scheidet aus, denn schlielich handelt es sich um einen Dienst ohne Benutzerinteraktion. Ich entschied mich fr eine Ausgabe mittels System.Diagnostics.Trace aus dem .NET Framework. Diese kann nmlich 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 dafr ist wiederum, dass das Programm mit Administratorrechten gestartet wird. Abbildung 3 zeigt die Ausgabe von DebugView. Die Features F2 und F3 zu ergnzen ist dank der EBCs ganz leicht. Dazu musste ich lediglich die Auswertung der Argumente so ergnzen, dass weitere Kommandozeilenparameter erkannt werden. Das Ausfhren des Dienstes von der Konsole aus, also ohne Installation im Betriebssystem, war ganz leicht und bentigte keine weitere Funktionseinheit. Fr das Starten und Stoppen des Dienstes habe ich die Klasse ServiceStarter ergnzt. Anschlieend konnte ich in der Klasse EasyService die Verdrahtung ergnzen, und das neue Feature war fertig.

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 natrlich nicht kaltgelassen. Allerdings gibt es zwei Dinge zu bercksichtigen: Zum einen wren 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 dafr, dass der eigentliche Kern des zu implementierenden Dienstes vllig befreit ist von Infrastrukturabhngigkeiten. Dadurch ist der Kern des Dienstes besser zu testen. Insofern kann ich akzeptieren, dass der Infrastrukturanteil mittels manueller Integra-

tionstests berprft wird. Die Vorgehensweise, ausgehend von einer Featureliste ber die EBC-Architektur hin zur Implementierung, hat sich bewhrt. 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 auen schn, dotnetpro 8/2010, S. 126ff., www.dotnetpro.de/A1008ArchitekturKolumne [4] DebugView for Windows v4.76, www.dotnetpro.de/SL1011dojoLoesung1

www.dotnetpro.de dotnetpro.dojos.2011

49

AUFGABE
Event-Based Components

Wie baue ich einen Legostein?


Softwarekomponenten so einfach wie Legosteine zusammenstecken zu knnen mit diesem Versprechen tritt das Konzept der Event-Based Components an. Stefan, kannst du dazu eine bung stellen?

dnpCode: A1011DojoAufgabe

Wer bt, gewinnt

In jeder dotnetpro finden Sie eine bungsaufgabe von Stefan Lieser, die in maximal drei Stunden zu lsen sein sollte. Wer die Zeit investiert, gewinnt in jedem Fall wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen. Es gilt : z Falsche Lsungen gibt es nicht. Es gibt mglicherweise elegantere, krzere oder schnellere Lsungen, aber keine falschen. z Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das knnen Sie, indem Sie Ihre Lsung mit der vergleichen, die Sie eine Ausgabe spter in dotnetpro finden. bung macht den Meister. Also los gehts. Aber Sie wollten doch nicht etwa sofort Visual Studio starten

n Ergnzung zur Artikelserie von Ralf Westphal [1] [2] [3] [4] ber Event-Based Components (EBC) lautet die Aufgabe in diesem Monat: Entwickeln Sie eine Textumbruchkomponente. Fr die Silbentrennung knnen Sie die Komponente NHunspell [5] verwenden. Abbildung 1 zeigt, wie eine kleine Testanwendung aussehen knnte, die den Textumbruch als Komponente verwendet. Die Komponente soll ber folgenden Kontrakt verfgen :
public interface ITextumbruch { string Umbrechen(string text, int breiteInZeichen); }

Der Text sowie die gewnschte Breite werden in die Methode gegeben, diese liefert den umbrochenen Text zurck. Die Breite des Textes wird der Einfachheit halber als Anzahl der Zeichen angegeben. Eine Angabe in Millimetern wrde es erfordern, dass man die Laufweiten der jeweiligen Zeichen bercksichtigt. Das wre fr die bung dann doch zu viel des Guten. So weit zum gewnschten API der Komponente. Diese ist damit eine Komponente im klassischen Sinne, also eine binre Funktionseinheit mit separatem Kontrakt. Intern soll sie jedoch durch EBCs realisiert werden. berlegen Sie sich dazu, welche Bearbeitungsschritte ntig 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. Fr die Silbentrennung mag es auf der Hand liegen, dass NHunspell die Verantwortlichkeit fr diesen Prozessschritt bernimmt. Fr alle anderen Schritte drfte es nicht so offenkundig sein. Die Anforderungen an die Komponente sollten Sie sich vorher notieren. Auch mgliche Testflle sollten Sie sammeln. In einem realen Projekt wrden Sie solche Testflle mit dem Kunden diskutieren. Bei dieser bung treffen Sie selber sinnvolle Annahmen. So knnten 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 fr den Textumbruch.

interessante Szenarien auffallen. Dieses Mal sollen automatisierte Tests eine grere Rolle spielen als bei der vorherigen bung zum WindowsDienst. Die einzelnen Funktionseinheiten sollen mglichst isoliert getestet werden. Und natrlich drfen ein paar Integrationstests nicht fehlen. Fr das explorative Testen ist die Testanwendung gedacht. Damit knnen Sie ausprobieren, wie sich die Komponente bei bestimmten Konstellationen verhlt. Ich wnsche viel Spa und groen 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 auen schn, 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/

50

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Event-Based Components

So trennt man Feu-er-wehr


Das Konzept der Event-Based Components einzuben das war das Ziel dieses dojos. Die konkrete Aufgabe bestand darin, eine Komponente fr den Textumbruch mit Silbentrennung zu entwickeln. Zum Glck hat Stefan Lieser ein eigenes Test-GUI entwickelt, denn damit konnte er viele Fehler entdecken und beseitigen.

inen Textumbruch mit Silbentrennung zu implementieren, hrt 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 fr das Problem der Silbentrennung. Als Erstes sind die Anforderungen zu klren. Im Falle des Textumbruchs knnen 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.

Dazu ein Beispiel, in dem die Leerzeichen durch einen Punkt ersetzt sind, damit sie besser zu erkennen sind :
BauerKlauserntetKartoffeln.

Wenn dieser Satz auf eine Breite von zehn Zeichen unter Beibehaltung der Leerzeichen umbrochen wird, ergibt sich zunchst folgendes Ergebnis:
Bauer Klaus erntet Kartoffeln.

Dann soll das Ergebnis folgendermaen aussehen: Bauer Klaus erntet Kartoffeln. Das Beispiel enthlt keine besonderen Schwierigkeiten oder Spezialflle. Aber genau die gilt es natrlich ebenfalls in den Blick zu nehmen. So stellt sich beispielsweise die Frage, wie mit Zeilenumbrchen verfahren werden soll, die im Eingangstext schon vorhanden sein knnen:
Bauer Klaus erntet Kartoffeln.

Dabei sind die Leerzeichen in der zweiten Zeile vor dem Wort Klaus jedoch strend. Folglich sollen sie entfernt werden, mit folgendem Ergebnis:
Bauer KlauserntetKartoffeln.

Leerzeichen am Zeilenanfang werden also entfernt, innerhalb des Satzes oder auch am Ende bleiben sie erhalten. Um die Anforderungen fr die bung mglichst einfach zu halten, lassen wir es dabei zunchst bewenden.

Denn zwischen den Wrtern steht Leerraum, der erhalten bleiben muss. Als Oberbegriff fr Wort und Leerraum habe ich Zeichenfolge gewhlt. Der Text wird also zunchst in Zeichenfolgen zerlegt. Das lsst auch Spielraum fr mgliche Erweiterungen. Schlielich knnen im Text auch Zahlen als Zeichenfolgen auftreten, die mglicherweise besonders behandelt werden mssen. Ein weiteres Beispiel sind Interpunktionszeichen, auch diese kann man unter den berbegriff Zeichenfolgen stellen. Nachdem Zeichenfolgen in Silben zerlegt sind, mssen die Silben so zu Zeilen zusammengefasst werden, dass die einzelnen Zeilen hchstens die maximale Lnge haben. Dabei stehen die einzelnen Silben natrlich nicht fr sich. Denn Silben knnen nur innerhalb der Zeile einfach so aneinandergereiht werden. Am Zeilenende muss ein Trennstrich ergnzt werden, wenn die letzte Silbe der Zeile zum selben Wort gehrt wie die erste Silbe der Folgezeile. Daher muss der Zusammenhang zwischen Silben und Wrtern erhalten bleiben. Offensichtlich gengt es daher nicht, das Zusammenfassen zu Zeilen auf Basis eines Stroms von Silben zu implementieren.

Algorithmus
Nachdem die Anforderungen przisiert sind, mssen Sie eine Idee fr einen Algorithmus entwickeln. Dabei steht die Frage im Vordergrund, wie das Problem algorithmisch gelst werden kann. Es geht noch nicht darum, wie der Algorithmus konkret zu implementieren ist und welche Funktionseinheiten dabei eine Rolle spielen. Die erste Idee fr den Textumbruch sah bei mir folgendermaen aus : T Zerlege den Text in Wrter. T Zerlege die Wrter in Silben. T Fasse die Silben neu zu Zeilen zusammen. Dabei bemerkte ich schnell, dass die Zerlegung des Textes in Wrter nicht wirklich przise beschreibt, was zu tun ist.

Entwurf
Aus diesen Vorberlegungen entstand mein Entwurf fr eine EBC-Architektur (EventBased Components). Abbildung 1 zeigt die folgenden vier Aktionen: T Zerlegen in Zeichenfolgen, T Zeichenfolgen in Silben trennen, T Zusammenfassen zu Zeilen, T Zusammenfassen zu Text. Diese vier Aktionen sind zu einer EBCAktivitt zusammengefasst, welche die Aktionen umschliet. Aufgabe der Aktivitt ist es, die Input- und Outputpins der beteiligten Aktionen zu verbinden. Die Aktivitt selbst verfgt ber je einen Input- und Outputpin und verbirgt die internen Details der Realisierung. Sollten spter Aktionen hin-

Das Ergebnis soll das gleiche sein wie oben. Bereits vorhandene Zeilenumbrche werden also ignoriert. Das soll auch fr mehrere hintereinander stehende Zeilenumbrche gelten. Auch Abstze werden damit ignoriert. Diese Vereinfachung ist der Tatsache geschuldet, dass es hier nur um eine bung geht. Als Nchstes ist Leerraum zu betrachten. Wenn im Satz zustzliche 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 fragwrdig aussieht.

www.dotnetpro.de dotnetpro.dojos.2011

51

LSUNG
zukommen, knnen diese nderungen lokal innerhalb der Aktivitt 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 Kardinalitt hlt. Wenn also beispielsweise die Aktion Zusammenfassen zu Zeilen im Entwurf mehrere Zeilen liefert, darf nicht in der Implementation ein einzelner string zurckkommen. Dies ergibt sich aus dem Prinzip Implementation spiegelt Entwurf [2], welches dafr sorgt, dass die Implementation besser verstndlich ist. Wrde man in der Implementation vom Entwurf abweichen, wre fr einen Entwickler, der spter in den Code einsteigt, ein bersetzungsaufwand erforderlich. Ein Blick in den Entwurf wrde 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 Aktivitt verwendet wird. So ist die Realisierung als EBC auen nicht mehr sichtbar. einheiten viel rigoroser unter die Lupe genommen, als dies am Whiteboard mglich ist. So werden Ungereimtheiten frhzeitig aufgedeckt. Im konkreten Fall des Textumbruchs habe ich also zunchst die API-Klasse Textumbruch sowie die Aktivitt TextumbruchAktivitt erstellt. Anschlieend habe ich die einzelnen Aktionen mit ihren Input- und Outputpins erstellt und innerhalb der Aktivitt verdrahtet. Um die Aktionen nicht einzeln ausimplementieren zu mssen, habe ich lediglich die Daten der Inputpins auf die Outputpins bertragen. Dabei mssen natrlich nach Bedarf entsprechende Datenobjekte erzeugt werden, um den Signaturen von Input- und Outputpins gerecht zu werden. Nach der Verdrahtung der Aktionen in der Aktivitt konnte ich dann sehen, dass ein Text, der ber den Inputpin in die Aktivitt hineingegeben wird, tatschlich am Outputpin wieder herauskommt. Wunderbar! Commit nicht vergessen! Zustzlich kann man bei der Implementation des Tracer Bullet Features auch Trace-Ausgaben ergnzen. Dadurch lsst sich mit Tools wie DebugView [3] verfolgen, ob der Ablauf der einzelnen Aktionen korrekt erfolgt. Listing 1 zeigt die Verdrahtung der Aktionen in der Aktivitt. Zunchst werden von den bentigten Aktionen Instanzen erstellt. Anschlieend werden Input- und Outputpins gem dem Entwurf verbunden. Die letzte Zeile zeigt die Verbindung zum Outputpin der Aktivitt. Doch wie erfolgt dieVerbindung zum Inputpin der Aktivitt? Dazu wird im Konstruktor der Aktivitt eine Action<string, int> erstellt, die zur Signatur des Inputpins passt:
process = (text, breiteInZeichen) => { zusammenfassenZuZeilen. In_SetzeBreite(breiteInZeichen); zerlegenInZeichenfolgen.In_Process(text); };

[Abb. 1] Der Entwurf in EBC-Architektur.

Wo beginnen?
Bei vier Aktionen, einer Aktivitt 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

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 mgliche Anforderungen aus den Fingern zu saugen. Bei einer Bottomup-Vorgehensweise ist diese Gefahr nicht zu unterschtzen. Sie fhrt hufig dazu, dass Funktionalitt implementiert wird, von der niemand wei, ob sie bentigt wird. Um zu berprfen, 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 Funktionalitt 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 dafr, 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();

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); }

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);

Dank dieses Kniffs mssen in der Klasse keine anderen Felder definiert werden, um auf die Aktionen zugreifen zu knnen.

Und Action!
Nach der Aktivitt ging es an die Implementation der einzelnen Aktionen. Auch dabei

52

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
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 abzuwgen, welche Teilfunktionalitt dem Kunden den jeweils grten Nutzen bringt. Das kann bedeuten, eine Funktion komplett zu realisieren und andere nur rudimentr. Genauso gut kann es aber ntzlich sein, an allen Stellen einen Teil der geforderten Leistung zu erbringen. Im Zweifel gilt: Reden hilft! Eine Rckfrage beim Kunden oder Product Owner sollte den Sachverhalt klren. Doch zurck 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 fr 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 fr den Test zeigt Listing 2. Die Rckgabe des Ergebnisses erfolgt bei EBCs ber einen Outputpin. Outputpins werden ber Events realisiert. Daher muss im Test geprft werden, ob der Event das richtige Ergebnis liefert. Dazu erstelle ich im Setup der Testklasse eine Instanz des Prflings 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 enthlt lediglich eine Eigenschaft fr den string, siehe Listing 3. Ferner ist eine Equals-Methode implementiert, damit Zeichenfolgen im Test verglichen werden knnen. Das Erzeugen von Equals und GetHashCode bernimmt fr mich das ReSharper-Add-in. Im Prinzip wre eine Implementation ohne die Datenklasse Zeichenfolge denkbar. Schlielich kapselt diese lediglich eine string-Eigenschaft. Durch die Einfhrung dieser Datenklasse ist die Typisierung der Input- und Outputpins jedoch strenger, da so nur Input- und Outputpins verbunden werden knnen, die eine Zeichenfolge als Argument erwarten. Das frdert nicht nur die Verstndlichkeit, sondern vereinfacht auch ein automatisiertesVerdrahten der Aktionen, auch wenn das hier nicht verwendet wird. Ferner verwendet die Implementation so auch die Ubiquitous Language, die allgegenwrtige Sprache des Projektes, was ebenfalls zur Verstndlichkeit beitrgt. Die Implementation der Textzerlegung sieht am Ende so aus wie in Listing 4. Was mich daran strt, ist der Umgang mit den Zeilenumbrchen. Diese werden nmlich hier ebenfalls behandelt, obwohl die Aufgabe der Klasse das Zerlegen des Textes in Zeichenfolgen ist. Damit kmmert sich die Klasse um zwei Dinge und verstt so gegen das Single Responsibility Principle [4]. In der nchsten Iteration wrde ich das ndern und die Behandlung der Zeilenumbrche herausziehen. Als Nchstes kam die Silbentrennung an die Reihe. Durch den Einsatz von NHunspell steht die Funktionalitt bereits zur Verfgung. Es geht also lediglich darum, das NHunspell-API an unsere Bedrfnisse anzupassen. Die Implementation ist einfach, bedarf aufgrund der Verwendung von LINQ aber einer kurzen Erklrung, siehe Listing 5. Der Inputpin erhlt eine Aufzhlung von Zeichenfolgen. Fr jede Zeichenfolge muss die Silbentrennung aufgerufen werden. Anschlieend 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 Kernfunktionalitt 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 mglichst wenig Rauschen enthalten und dadurch gut verstndlich sind, arbeitet die Methode mit strings statt mit Zeichenfolge und GetrennteZeichenfolge. Listing 6 zeigt einen der Tests. Diese Vorgehensweise bietet den groen 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 Aufzhlung der Zeichenfolgen iteriert und jeweils eine Instanz von GetrennteZeichenfolge erzeugt.

Listing 2 Ein erster Test.


private ZerlegenInZeichenfolgen sut; private IEnumerable<Zeichenfolge> result; [SetUp] public void Setup() { sut = new ZerlegenInZeichenfolgen(); sut.Out_Result += wrter => result = wrter; } [Test] public void Einzelnes_Wort() { sut.In_Process("A"); Assert.That(result, Is.EqualTo(new[] { new Zeichenfolge("A") })); }

Listing 3 Klasse fr die Zeichenfolge.


public class Zeichenfolge { public Zeichenfolge(string text) { Text = text; } public string Text { get; private set; } }

Zusammensetzen
Die Silben mssen nun wieder zu Zeilen zusammengefasst werden. Dabei muss zum einen die gewnschte Breite der Zeilen bercksichtigt werden. Zum anderen mssen gegebenenfalls Trennstriche am Zeilenende

ergnzt werden. Dazu muss der Zusammenhang von Silben und Wrtern bekannt sein. Am Ende stellte sich heraus, dass das Zusammenfassen der Silben zu Zeilen die schwierigste Funktionseinheit darstellt. Hier hatte ich mal wieder das Gefhl, dass ich ohne automatisierte Tests vllig aufgeschmissen wre. Whrend 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 tatschlich 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 verstndlich ist. Die Details knnen 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 fr die zahlreichen if-Statements sind konsequent als Methoden herausgezogen. Das bietet den Vorteil, einen sprechenden Namen verwen-

www.dotnetpro.de dotnetpro.dojos.2011

53

LSUNG
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 knnen. So muss man beim Lesen nicht interpretieren, was die Bedingung eigentlich testet, sondern kann die Bedeutung aus dem Namen ableiten. Bei dieser Implementation wre es interessant auszuprobieren, wie schwierig es ist, die Zeilenbreite nicht in Zeichen, sondern in Millimetern zu definieren. Dazu msste die Schriftart des Textes herangezogen werden, um die tatschliche Breite ermitteln zu knnen. 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.

Die Entscheidung, ob eine Silbe noch in die Zeile passt oder die nchste Zeile begonnen wird, steht an genau einer Stelle in der Methode SilbePasstNochInDieZeile. Hier msste man also mit einer entsprechenden Erweiterung ansetzen. Richtig spannend wird es bei solchen Erweiterungen ja immer dann, wenn nicht der ursprngliche 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 ergnzt, welche ganz oben auf dem ffentlichen API aufsetzen. So ist sichergestellt, dass die Integration von Aktivitt 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 hufig mehrere automatisierte Integrationstests erforderlich. Aber selbst Unit-Tests plus Integrationstests gengen nicht. Man kommt nicht umhin, auch den Bereich der explorativen Tests abzudecken. Je leichter es fllt, die Funktionalitt mal eben auf die Schnelle auszuprobieren, desto grer ist die Wahrscheinlichkeit, Fehler zu finden. Aus gutem Grund sind Softwaretester nicht pltzlich berflssig 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 frderte 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 unschn, aber so ist es nun mal. Was tun? Klar ist: Man muss null abfangen und eine leere Silbenliste zurckliefern. Aber vorher sollte das Problem durch einen automatisierten Test reproduziert werden. Dieser sollte so nah wie mglich an der fr 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, Funktionalitt zu kapseln. Im Entwurf ist eine Schnittstelle fr die Silbentrennung entstanden. Dabei habe ich keine Rcksicht auf NHunspell genommen (mir war dieses fragwrdige Verhalten vorher gar nicht bekannt). Htte ich NHunspell direkt verwendet, ohne eine eigene Klasse drumherum zu legen, wre es mglicherweise schwieriger, dieses unschne Verhalten an einer Stelle zu beseitigen. Dies gilt brigens auch fr den Rckgabewert. NHunspell liefert als Ergebnis nicht etwa eine Liste der Silben, sondern einen string, in dem die Silben durch ein Gleichheitszeichen abgetrennt sind. Fr Feuerwehrauto wird Feu=er=wehr=au=to geliefert. Dies muss dem Entwurf gem umgesetzt werden, in eine Liste von Silben.

54

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Die zweite Erkenntnis aus den Versuchen mit dem Test-GUI: Zeilenumbrche wurden nicht bercksichtigt. In den Anforderungen habe ich diese zwar aufgefhrt, aber nicht sofort implementiert. Das war schnell nachgeholt, allerdings mit den bereits weiter oben erwhnten Einschrnkungen. Dritte Erkenntnis: Nicht trennbare Wrter knnen lnger als die maximale Zeilenlnge sein. Ferner knnen in trennbaren Wrtern Silben auftreten, die lnger als die maximale Zeilenlnge sind. Dadurch drehte sich eine Schleife beim Zusammenfassen von Silben zu Zeilen im Kreis. Auch hier habe ich erst zwei Tests ergnzt, um den Fehler automatisiert reproduzieren zu knnen. Erst danach habe ich das Problem behoben. Die Vorgehensweise ist hierbei pragmatisch: Zu lange Wrter oder Silben werden nicht umbrochen. Das nchste Problem, das ich durch Ausprobieren mit dem Test-GUI identifiziert habe, hngt damit zusammen, wie NHunspell auf Interpunktionszeichen reagiert. Beim Zerlegen des Textes in Zeichenfolgen werden Interpunktionszeichen nicht gesondert betrachtet, sondern einfach an die Wrter mit angehngt. Dadurch entstanden aber merkwrdige Trennungen. So wurde Welt in Wel-t getrennt. Auch hier konnten automatisierte Tests das Verhalten von NHunspell reproduzieren. Hngt nmlich 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 gendert. Die Bercksichtigung von Interpunktionszeichen wrde eine ganze Reihe von nderungen nach sich ziehen, die den Umfang dieser bung sprengen wrden.

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 Abstze natrlich ebenfalls unter den Tisch. Hier mssten also zwei hintereinander stehende Zeilenumbrche anders behandelt werden. Auch das wre mit einem endlichen Automaten realisierbar.

Fazit und Nachtrag Erweiterungen


Einige Erweiterungsmglichkeiten 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 msste also erweitert werden, da string.Split doch zu simpel fr die Aufgabe ist. Vermutlich wre hier ein endlicher Automat besser geeignet. Eine weitere Vereinfachung betrifft Abstze. Zeilenumbrche werden derzeit komplett entfernt, bevor mit der Trennung in Zeichenfolgen begonnen wird. Dadurch Einerseits bin ich berrascht, dass sich mit vergleichsweise wenig Aufwand doch eine recht leistungsfhige Textumbruchkomponente realisieren lsst. Andererseits zeigt sich, dass man fr ein reales Projekt weitaus mehr Aufwand in die Analyse der Anforderungen und den Entwurf stecken msste. Das wird beispielsweise beim Umgang mit Leerraum und Zeilenumbrchen deutlich. Ich bin, nachdem dieser Artikel fertig war, noch der Frage nachgegangen, ob die Lsung tatschlich so evolvierbar ist, dass eine Umstellung von Breite in Zeichen auf Breite in Millimetern leicht zu bewerkstelligen ist. Um ein aussagekrftigeres Ergebnis zu erreichen, habe ich Ralf Westphal gebe-

ten, diese nderung ebenfalls vorzunehmen. Dazu habe ich ihm lediglich den Code und eine Entwurfskizze zur Verfgung 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 schnes 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

www.dotnetpro.de dotnetpro.dojos.2011

55

AUFGABE
Algorithmen und Datenstrukturen

Wie viele Bltter hat der Baum?


Baumstrukturen sind in der Informatik allgegenwrtig. Wer selbst Bume implementiert, lernt dabei viel ber ihre Arbeitsweise. Stefan, kannst du dazu eine bung stellen?

dnpCode: A1012DojoAufgabe In jeder dotnetpro finden Sie eine bungsaufgabe von Stefan Lieser, die in maximal drei Stunden zu lsen sein sollte. Wer die Zeit investiert, gewinnt in jedem Fall wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen. Es gilt : T Falsche Lsungen gibt es nicht. Es gibt mglicherweise elegantere, krzere oder schnellere Lsungen, aber keine falschen. T Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das knnen Sie, indem Sie Ihre Lsung mit der vergleichen, die Sie eine Ausgabe spter in dotnetpro finden. bung macht den Meister. Also los gehts. Aber Sie wollten doch nicht etwa sofort Visual Studio starten
enn man den sprichwrtlichen Wald vor lauter Bumen nicht mehr sehen kann, mag es helfen, einmal ber die Implementierung von Bumen nachzudenken. Im .NET Framework gibt es dazu zwar keine generische Implementation, dennoch werden Bume auch dort verwendet, nmlich an so prominenter Stelle wie LINQ. Genauer gesagt bersetzt der Compiler Lambda-Ausdrcke in sogenannte Expression Trees. Auf diese Weise ist es mglich, aus LINQ-Ausdrcken SQL-Code zu erzeugen. Mit den WCF-RIA-Services knnen LINQ-Ausdrcke 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 fr diesen Monat. Ein Baum hat immer genau einen Wurzelknoten. Dieser und alle anderen Knoten knnen beliebig viele untergeordnete Knoten, sogenannte Kinder, haben. Das API fr Bume besteht aus zwei Interfaces, einem fr den Baum, genannt ITree<T>, sowie einem fr die Knoten, genannt INode<T>, siehe Listing 1. Das Interface fr den Baum ist simpel, es enthlt lediglich den Wurzelknoten des Baums. Beim Knoten sieht das schon anders aus. Jeder

Wer bt, gewinnt

[Abb. 1] Eine einfache Baumstruktur.

Listing 1 Interfaces fr 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(); }

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 hinzuzufgen, rufen Sie die Add-Methode auf und bergeben den Wert des neuen Knotens. Fr den Wert mssen Sie intern einen Knoten anlegen und in die Children-Liste aufnehmen. Mit der Eigenschaft ChildValues knnen 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 Bumen auf unterschiedliche Weise mglich. Bei Listen wird ein Element nach dem anderen geliefert. Bei Bumen 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. Fr 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. Fr 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 zugehrigen Traversierungsalgorithmen, natrlich inklusive automatisierter Unit-Tests. Happy Learning! [ml]

56

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Algorithmen und Datenstrukturen

So bauen Sie Bume


Im .NET Framework gibt es keine vordefinierte Datenstruktur fr Bume. Wer seine Daten in einer Baumstruktur ablegen will, muss sich diese Struktur selbst implementieren. Eine ideale Aufgabe fr das dotnetpro.dojo!
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 fr einen Entwickler wichtig, eine Vorstellung davon zu erlangen, was hinter den Kulissen geschieht. Nicht zuletzt eignen sich Datenstrukturen sehr gut dafr, die testgetriebene Entwicklung einzuben. Und da die Datenstruktur Baum im .NET Framework nicht zur Verfgung steht, lohnt es sich tatschlich, eine solche Implementation vorzunehmen. Die Aufgabenstellung hat das API fr Bume vorgegeben, siehe Listing 1. Sie besteht aus zwei Interfaces: einem fr den Baum, genannt ITree<T>, und einem fr 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 fgt eigentlich keine Funktionalitt 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 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); }

Listing 1 Das API fr die Bume.


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(); }

schaft fr den Wert des Knotens. Da das Interface lediglich einen Getter erwartet, mssen Sie sich berlegen, wie ein Knoten zu seinem Wert kommt. Eine Mglichkeit wre, die Eigenschaft zustzlich mit einem Setter auszustatten. Allerdings wren die Knoten damit vernderbar. Solange Sie dies nicht bentigen, gengt ein privater Setter, um den Wert einmalig setzen zu knnen. Der Wert muss dann im Konstruktor zugewiesen werden, da private Setter nur innerhalb der Klasse verwendet werden knnen. 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 prft 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 fr diesen ersten Test die Klasse Node<T> erzeugt werden. Es entsteht also das grobe Codegerst der Klasse, welches erforderlich ist, um das Interface zu implementieren. Gerade Anfngern 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 gebt werden. Der nchste 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-Prfung vornehmen msste. Die Tatsache, dass der Knoten keine Nachfolger hat, wird perfekt reprsentiert durch eine leere Liste. Diese kann beispielsweise auch ohne vorherige Prfung mit foreach durchlaufen werden. Wenn die Liste leer ist, wird die Schleife halt nicht ausgefhrt. Eine zustzliche null-Prfung wrde den Code nur unntig aufblhen. Listing 3 zeigt den Test fr 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 darber klarzuwerden ist nicht ganz unwichtig.

www.dotnetpro.de dotnetpro.dojos.2011

57

LSUNG
Listing 4 ChildValues testen.
[Test] public void Node_hat_nach_dem_Instanzieren_keine_Nachkommenwerte() { var node = new Node<int>(1); Assert.That(node.ChildValues, Is.Empty); }

Listing 6 Einen neuen Knoten hinzufgen.


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 Fr_einen_hinzugefgten_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 fr einen Knoten ohne Nachkommen trivial. Immerhin haben Sie jetzt die Initialwerte aller Eigenschaften definiert und knnen sich einer neuen Aufgabe zuwenden. Die einzige ndernde Operation, die ein Knoten anbietet, ist das Hinzufgen eines weiteren Knotens. Dabei habe ich mich in der API-Definition dafr entschieden, dem Knoten einen weiteren Wert hinzuzufgen. Nehmen wir an, die Knoten htten Zeichenketten als Werte. Dann gbe es fr das API die beiden folgenden Mglichkeiten : T Hinzufgen eines Knotens vom Typ Node<string>. T Hinzufgen einesWertes vom Typ string. Das Hinzufgen eines Knotens she in der Anwendung wie folgt aus :
node.Add(new Node("a");

Ferner muss der Wert des Knotens in der Liste der Nachkommenswerte (ChildValues) auftauchen. Dass der neu erzeugte Knoten von Add als Ergebnis zurckgeliefert wird, ist brigens nicht der Testbarkeit geschuldet. Beim Aufbauen eines Baumes ist es handlich, auf die jeweils erzeugten Knoten zugreifen zu knnen, weil auf diesen dann weitere Methoden aufgerufen werden knnen. Damit wird das API fr 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 Hinzufgen 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 hinzugefgt, siehe Listing 6. Der nchste Test prft, ob der hinzugefgte 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 Aufzhlungen zur Verfgung. Ergebnis der Select-Methode ist wieder eine Aufzhlung. Der bergebene Lambda-Ausdruck gibt an, wie die Werte fr die Ergebnisaufzhlung gebildet werden sollen. Im vorliegenden Fall sollen aus der Aufzhlung von Knoten alle Werte extrahiert werden. Folglich gibt der Lambda-Ausdruck an, dass node.Value geliefert werden soll. Wenn die Knoten implementiert sind, knnen Sie sich dem Baum zuwenden. Da

Listing 7 Das erfolgreiche Hinzufgen eines Knotens testen.


[Test] public void Der_hinzugefgte_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 Mglichkeit bietet, direkt einen Wert hinzuzufgen :
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 eingefgten Knoten als Rckgabewert zurck. So knnen Sie nach dem Hinzufgen beobachten, dass der neue Knoten in der Liste der Nachkommen vorhanden ist.

Listing 8 LINQ nutzen.


public IEnumerable<T> ChildValues { get { return Children.Select(node => node.Value); } }

58

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Listing 9 Einen neu angelegten Baum prfen.
[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 fr 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 zugehrigen Test. Auch hier bietet es sich an, den Test in eine eigene Testklasse zu schreiben. Schlielich geht es nicht um Knoten, sondern um Bume. Ich habe die Testklasse Tree_Tests genannt.

Traversieren
Nun geht es an das Traversieren der Bume. 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})); }

derValues und PostOrderValues zu implementieren. Die beiden Methoden mssen jeweils den Baum durchlaufen und alle Knotenwerte in Form einer Aufzhlung IEnumerable<T> liefern. Auch hier stand die Entscheidung an, ob die Knoten oder deren jeweilige Werte geliefert werden sollen. Alternativ wre das Ergebnis also vom Typ IEnumerable<INode<T>>. Sollte sich spter zeigen, dass eine solche Aufzhlung bentigt wird, knnen die zugehrigen Methoden leicht ergnzt werden. Zunchst steht wieder die Frage an, wie der erste Test aussehen soll. Einen leeren Baum zu traversieren ist nicht wirklich spannend. Vor allem wrde durch diesen Test die Implementation nicht wirklich vorangetrieben. Schlielich besteht ein neu initialisierter Baum lediglich aus einem Knoten, der keine Nachkommen hat. Am anderen Ende stehen Tests, fr die gleich die komplette Traversierung implementiert werden muss. Ein Test dazwischen wre gut: mehr als nur ein einzelner Knoten, aber weniger als gleich ein ganzer Baum. Ein einzelner Knoten mit Nachkommen drfte das Kriterium erfllen. Damit ergeben sich folgende Testszenarien : T Traversieren eines Knotens, der keine Nachkommen hat; T Traversieren eines Knotens, der Nachkommen hat; T 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. Sprt man aber Unsicherheit bei der Implementation, hilft es mglicherweise, 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 anschlieend in quivalenzklassen zu-

sammenzufassen. So liegt beispielsweise das Traversieren eines Knotens mit drei Nachkommen in der gleichen quivalenzklasse wie das Traversieren von fnf Nachkommen. Haben die Nachkommen jedoch selbst wieder Nachkommen, so ergibt sich eine andere quivalenzklasse, denn hier muss pltzlich rekursiv vorgegangen werden. Wrde man gleich mit einem ersten Test beginnen, ohne vorher ber die Testdaten und eine sinnvolle Reihenfolge der Implementation nachzudenken, wre der anstehende Implementationsaufwand fr den nchsten Schritt mglicherweise zu gro. Nachdenken hilft! Die Tests zur Traversierung habe ich wieder in eine eigene Testklasse abgelegt. Sie heit Node_Pre_Order_Traversieren_Tests. Fr die Implementation dieses Tests von Listing 10 ist es lediglich notwendig, erst den eigenen Knotenwert und anschlieend die Werte der Nachkommen zu liefern. Unter tatkrftiger Mithilfe von yield return sieht die Implementation dazu wie in Listing 11 aus. Ergnzt man nun einen Test fr das erste Szenario, bei dem ein Knoten keine Nachkommen hat, wird man feststellen, dass dieser sofort erfolgreich verluft, ohne dass an der Implementation etwas gendert 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 knnte folgendermaen aussehen: Gib den eigenen Knotenwert aus, und rufe anschlieend die Traversierung fr jeden Nachkommen auf. Dazu mssen Sie allerdings die bisherige Implementation zunchst in eine Methode auslagern, die einen Knoten als Parameter erhlt. Die Methode ist dann dafr 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 })); }

Listing 11 Pre-Order-Traversierung durchfhren.


public IEnumerable<T> PreOrderValues(){ yield return Value; foreach (var child in Children) { yield return child.Value; } }

www.dotnetpro.de dotnetpro.dojos.2011

59

LSUNG
Listing 13 PreOrderValues refaktorisieren.
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; } }

Listing 14 Rekursion einfhren.


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 eingefhrt wird, siehe Listing 13. Nach dieser Refaktorisierung knnen Sie die Rekursion in der Methode TraversePreOrder einfhren, siehe Listing 14. Durch Verwendung von yield return kann hier das Ergebnis des rekursiven Aufrufs nicht direkt als Resultat zurckgegeben werden. Schlielich 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 fr Element an yield return bergeben werden.
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; }

Und jetzt andersrum


Um die Post-Order-Traversierung zu implementieren, ist nur eine kleine nderung notwendig. Sie mssen 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 kreisfr-

migen Pfeil. Die Tests fr 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 berprft. Alternativ htte ich sie auch Akzeptanztests nennen knnen, da es sich um die Beispiele handelt, die quasi zwischen Kunde und Auftragnehmer besprochen wurden. Wenn man als Entwickler mit dem Kunden Beispielflle 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 Gesprch 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 Schne an der Implementation von Datenstrukturen ist, dass sich diese meist gut automa-

[Abb. 1] Rekursion mit ReSharper visualisieren.

tisiert testen lassen. Dadurch kann man sich bei solchen bungen darauf konzentrieren, eine geeignete Reihenfolge fr die Tests zu finden. Voraussetzung ist, dass man vor dem ersten Test Testflle sammelt. Beim Zusammenstellen der Testflle sollte man mglichst darauf achten, ob diese in dieselbe quivalenzklasse fallen. Schlielich gengt es, jeweils einen Reprsentanten der quivalenzklasse fr einen Test herauszugreifen. Bezglich der Addition sind etwa die beiden zu addierenden Zahlen 2 und 3 sowie 4 und 5 in derselben quivalenzklasse. Es gengt daher, fr eines der beiden Zahlenpaare die Addition zu testen. Ein zustzlicher Test mit einem weiteren Reprsentanten derselben quivalenzklasse wrde keinen weiteren Erkenntnisgewinn bringen. Beim Traversieren gengt es, einen Knoten zu testen, dessen Nachkommen ebenfalls einen Nachkommen haben. Es ist nicht ntig, einen Baum mit vielen Ebenen zu testen. Durch die rekursive Implementation ist dies auch ganz offensichtlich. In anderen Fllen mag es nicht so offensichtlich sein, wie die quivalenzklassen der Testdaten aussehen. Dann muss man mglicherweise lnger darber nachdenken und mehr Testdaten sammeln. Einfach drauflos mit den Tests zu beginnen ist aber so oder so wenig hilfreich, da man dann Gefahr luft, Testflle zu bersehen. [ml]

60

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?

dnpCode: A1101DojoAufgabe In jeder dotnetpro finden Sie eine bungsaufgabe von Stefan Lieser, die in maximal drei Stunden zu lsen sein sollte. Wer die Zeit investiert, gewinnt in jedem Fall wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen. Es gilt : T Falsche Lsungen gibt es nicht. Es gibt mglicherweise elegantere, krzere oder schnellere Lsungen, aber keine falschen. T Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das knnen Sie, indem Sie Ihre Lsung mit der vergleichen, die Sie eine Ausgabe spter in dotnetpro finden. bung macht den Meister. Also los gehts. Aber Sie wollten doch nicht etwa sofort Visual Studio starten

static IEnumerable<T> Where<T>(this IEnumerable<T> values, Predicate<T> predicate);

Die Query Comprehension Syntax sorgt dafr, dass Sie im Quellcode Abfragen schreiben knnen wie beispielsweise diese:
var query = from kunde in kunden where kunde.Ort == "Kln" select kunde;

Kurz und knackig und fr 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 nmlich in quivalente Aufrufe der Extension Methods aus dem Namespace System.Linq. Das sieht dann etwa so aus:
var query = kunden .Where(kunde => kunde.Ort == "Kln") .Select(kunde => kunde);

Where arbeitet also auf einer Aufzhlung und liefert eine solche zurck. Damit stellt die WhereMethode eine Selektion oder Filterung dar. Fr jedes Element der Aufzhlung wird nmlich mithilfe des Prdikats geprft, ob die Bedingung fr das Element zutrifft. Und ausschlielich Elemente, fr die das Prdikat true zurckgibt, 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 knnen. Anschlieend 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 drfte so schnell nicht aufkommen. Viel Spa! [ml]

Diese Compilermagie ist dem C#- sowie dem VB.NET-Compiler spendiert worden. Die CLR hat also von LINQ keine Ahnung und musste dazu nicht verndert werden. Die eigentliche Funktionalitt von LINQ steckt folglich in den Extension Methods. Diese haben nmlich die Aufgabe, den jeweiligen Teil der Query auszufhren. So wird die Where-Klausel einer Query in einen Auf-

[1] Mirko Matytschak, Was steckt hinter LINQ? Language Integrated Queries: Neue Sprachmerkmale fr 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 fr den eigenen Datenspeicher, dotnetpro 8/2008, Seite 72ff., www.dotnetpro.de/A0808LINQ2X

www.dotnetpro.de dotnetpro.dojos.2011

61

Wer bt, gewinnt

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 aufzurumen. 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 aufzhlbar ist, sprich IEnumerable<T> implementiert. LINQ besteht aus zwei Teilen: T der sogenannten Query Comprehension Syntax, die so sehr an SQL erinnert; T einer Reihe von Extension Methods im .NET Framework aus dem Namespace System.Linq.

ruf der Where-Methode bersetzt. Gleiches geschieht fr Select, Order, Group by etc. Um dabei mglichst flexibel zu bleiben, ist LINQ auf dem Interface IEnumerable<T> definiert. Die Signatur der Where-Methode sieht wie folgt aus:

LSUNG
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.

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 Funktionalitt. Unterschtzen Sie nicht, wie viel Sie lernen knnen, wenn Sie vorhandene Funktionalitt 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 : T Die Methode muss in einer statischen Klasse deklariert sein. Dadurch ist sie selbst ebenfalls statisch. T Der erste Parameter der Methode muss zustzlich mit dem Schlsselwort this gekennzeichnet werden. Fr die Where-Methode kommt hinzu, dass sie generisch sein muss. Das bedeutet, dass der Typ der Aufzhlung nicht fix ist, sondern als generischer Typparameter angegeben werden kann. Lsst man die Be-

dingung der Where-Methode frs Erste einmal weg, ergibt sich folgende Signatur :
IEnumerable<T> Where<T>(this IEnumerable<T> values);

Damit haben Sie alle Zutaten fr die Signatur der Where-Methode zusammen :
public static IEnumerable<T> Where<T>(this IEnumerable<T> values, Predicate<T> predicate);

Die Methode arbeitet also auf einer Aufzhlung vom Typ T und liefert eine ebensolche zurck. Der generische Typparameter T muss syntaktisch beim Methodennamen deklariert werden. Doch es fehlt noch das Prdikat. In der Logik liefert ein Prdikat fr ein Element einen booleschen Wert. Den erforderlichen Typ gibt es natrlich im .NET Framework, aber das gilt ja auch fr 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 Rckgabewert der Methode vom Typ bool ist und dass der Methode ein Parameter vom generischen Typ T bergeben werden muss.

Mein erster Test fr die Implementation prft, ob alle Elemente geliefert werden, wenn das Prdikat immer true liefert, siehe Listing 1. Die zugehrige Implementation ist leicht: Die Eingabe wird einfach zurckgeliefert und das Prdikat ignoriert. Der nchste Test erfordert dann bereits das Iterieren und elementweise Auswerten des Prdikats. Der in Listing 2 gezeigte Test filtert die Aufzhlung 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; } } }

Listing 1 Seid ihr alle da?


[Test] public void Prdikat_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 Prdikat_liefert_nur_fr_gerade_Werte_true() { var values = new[] {1, 2, 3, 4}.Where(x => x % 2 == 0); Assert.That(values, Is.EqualTo(new[] {2, 4})); }

Wer yield return bislang nicht kannte, wird die Werte, fr die das Prdikat true liefert, vermutlich in einer List<T> gesammelt haben. Doch Vorsicht, neben dem eleganteren, weil krzeren ueren unterscheiden sich die beiden Lsungen 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 mssen. Bei kleinen Datenmengen ist das sicher kein Problem aber berlegen Sie, was passiert, wenn Sie auf diesem Weg eine etwas gre-

62

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Listing 4 Berechnen mit Select.
[Test] public void String_Lnge_wird_ermittelt() { var values = new[] {"abc", "a", "ab"}.Select(s => s.Length); Assert.That(values, Is.EqualTo(new[]{3, 1, 2})); }

Listing 5 Select anwenden.


public delegate TOutput Func<TInput, TOutput>(TInput input); public static IEnumerable<TOutput> Select<TInput, TOutput>(this IEnumerable<TInput> values, Func<TInput, TOutput> selector) { foreach (var value in values) { yield return selector(value); } }

re Datei einlesen und dann filtern. Bei Verwendung von yield return sorgt der Compiler fr etwas Magie. Denn er erzeugt einen endlichen Automaten fr das Zusammenstellen der Aufzhlung. Dieser sorgt dafr, dass die Methode immer nur dann aufgerufen wird, wenn wieder ein Element bentigt wird. Es muss quasi erst jemand an der Aufzhlung ziehen, damit ein Element durch das Prdikat berprft wird. Die Auswertung des Prdikats erfolgt somit Element fr Element statt fr die gesamte Eingabe auf einmal. Somit knnen Sie mit yield return potenziell unendlich groe Datenmengen bearbeiten.

Listing 6 IGrouping implementiert IEnumerable.


public interface IGrouping<TKey, TElement> : IEnumerable<TElement> { TKey Key { get; } }

Listing 7 Gruppierung ermglichen.


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; } } }

Select
Danach steht die Select-Methode an. Sie dient dazu, den Elementtyp der Aufzhlung zu transformieren. Enthlt die ursprngliche Aufzhlung beispielsweise Adressen, knnen Sie mit Select eine einzelne Eigenschaft selektieren. Dabei knnen natrlich auch Berechnungen erfolgen, wie der Test in Listing 4 zeigt. Der Test liefert zu jedem Eingangsstring dessen Lnge zurck. Dazu mssen Sie die Aufzhlung durchlaufen und die SelectFunktion auf jedes Element anwenden. Mit yield return ist das ebenfalls keine Hexerei, siehe Listing 5. Fr die Select-Methode bentigen Sie wiederum eine delegate-Deklaration. Diesmal definieren Sie einen Methodentyp mit einem Eingangsparameter und einem Rckgabewert. 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 Aufzhlung angewandt. Das Ergebnis wird mit yield return an den Aufrufer geliefert.

piert die Elemente einer Aufzhlung nach einem Schlsselwert und liefert eine neue Aufzhlung zurck. Die Ergebnisaufzhlung enthlt fr jeden Schlsselwert der Eingabe ein Element. Ein Beispiel: Sie mchten die Zahlen von 1 bis 10 danach gruppieren, ob sie gerade oder ungerade sind. Das Ergebnis von GroupBy(x => x % 2 == 0) wrde dann folgendermaen aussehen :
new[]{ new[] { 1, 3, 5, 7, 9 }, new[] { 2, 4, 6, 8, 10} }

GroupBy
Kommen wir nun zu den etwas kniffligeren Methoden. Die GroupBy-Methode grup-

Das Ergebnis ist also eine Aufzhlung, die wiederum zwei Aufzhlungen enthlt. Der Elementtyp dieser Aufzhlung lautet

IGrouping<bool, int>. Der Trick an der Stelle ist: IGrouping<TKey, TElement> implementiert IEnumerable<TElement>. Dadurch sind die einzelnen Elemente der Aufzhlung ebenfalls aufzhlbar. Aber IGrouping hat noch eine weitere Eigenschaft, wie das Interface in Listing 6 zeigt. ber die Eigenschaft Key kann der Schlsselwert ermittelt werden, der zu diesem Element der Gruppierung gehrt. Listing 7 zeigt das Interface. Im Konstruktor werden Schlssel und zugehrige Werte bergeben und in Feldern abgelegt. Doch wie erfolgt nun die Gruppierung der Eingangsdaten? Schauen Sie sich dazu zunchst die Signatur der GroupBy-Methode an:

www.dotnetpro.de dotnetpro.dojos.2011

63

LSUNG
Listing 8 Zeichenketten gruppieren.
[Test] public void GroupBy_Lnge_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"}})); }

Listing 10 Wer ist der Erste?


[Test] public void Drei_Elemente() { Assert.That(new[] {1, 2, 3}.First(), Is.EqualTo(1)); }

Listing 11 Listing 9 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 T First<T> (this IEnumerable<T> values) { var enumerator = values.GetEnumerator(); enumerator.MoveNext(); return enumerator.Current; }

Mit MoveNext auf den 1.Platz.

existiert. Immer wenn ein Schlssel zum ersten Mal auftritt, wird das Element im Dictionary angelegt. Im zweiten Teil wird das fertige Dictionary durchlaufen und fr jedes Element ein Grouping zurckgegeben.

public static IEnumerable<IGrouping<TKey, TElement>> GroupBy<TKey, TElement>(this IEnumerable<TElement> values, Func<TElement, TKey> keyFunction)

Die Methode erhlt neben den Eingangsdaten eine Funktion, die zu einem Element den zugehrigen Schlsselwert liefert. Aufgabe der GroupBy-Methode ist es nun, die Eingangsdaten Element fr Element zu durchlaufen und jeweils den Schlssel des Elements zu ermitteln. Anschlieend muss das Element in die zu seinem Schlssel gehrige Aufzhlung eingereiht werden. Gruppiert man beispielsweise Zeichenketten nach ihrer Lnge, muss das Element a in die Aufzhlung zum Schlsselwert 1 eingereiht werden. Listing 8 zeigt einen Test, der Zeichenketten nach ihrer Lnge gruppiert. Wenn man nun berlegt, wie man diese Funktionalitt implementieren kann, wird klar, dass die Eingangsdaten innerhalb der GroupBy-Methode vollstndig behandelt werden mssen, ehe das Ergebnis geliefert werden kann. Das Ergebnis kann nicht Element fr Element gebildet werden, weil die Elemente des Ergebnisses selbst wieder

Aufzhlungen sind. Um die erste gruppierte Liste herausgeben zu knnen, mssen die Schlssel aller Eingangselemente geprft worden sein. Also ist es fr diese Methode angemessen, eine Variable zu verwenden, in der das Ergebnis erst vollstndig gebildet wird. Die GroupBy-Methode ist brigens nicht die einzige, bei der das Ergebnis vollstndig gebildet werden muss, ehe es als Rckgabewert herausgegeben werden kann. Das Sortieren der Elemente ist ein weiteres Beispiel. Fr die Gruppierung bietet es sich an, mit einem Dictionary zu arbeiten. Darin knnen Sie die Schlsselwerte der Elemente als Schlssel im Dictionary verwenden. Der zugehrige Wert der Dictionary-Eintrge ist dann jeweils eine Liste von Elementen. Das Dictionary ist daher von folgendem Typ :
var dictionary = new Dictionary<TKey, IList<TElement>>();

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 Aufzhlung, siehe Listing 10. Fr die Implementation ist es wichtig zu wissen, wie ein Enumerator funktioniert. Er muss nmlich 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 fr den Enumerator. Die Methode GetEnumerator liefert nmlich bei jedem Aufruf einen neuen Enumerator. Da MoveNext und Current jedoch auf demselben Enumerator aufgerufen werden mssen, ist das Zwischenspeichern in einer Variablen notwendig.

Distinct
Weiter gehts mit der Methode Distinct. Sie liefert jedes Element einer Aufzhlung nur genau einmal. Enthlt die Aufzhlung ein Element mehrfach, wird es nur einmal weitergeleitet. Dazu ist es erforderlich, dass sich die Methode merkt, welche Elemente

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 Schlssel gehrige Liste eingereiht. Dabei ist jeweils zu prfen, ob die Liste bereits

64

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
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})); }

kein Problem. Doch um das kleinste Element einer Aufzhlung zu ermitteln, mssen die Elemente verglichen werden knnen. Listing 14 zeigt dazu einen Test. Fr den Vergleich zweier Elemente haben Sie mehrere Mglichkeiten : T Sie knnen mehrere berladungen der Min-Methode anbieten. Die dabei verwendeten Elementtypen mssen einen Vergleichsoperator definieren. T Sie knnen den generischen Elementtyp T mit einem Constraint versehen, welches dafr sorgt, dass der Typ T das Interface IComparable implementieren muss. T Sie knnen 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 zunchst nach Variante zwei implementiert und den Elementtyp mit einem Constraint versehen, siehe Listing 15. Das Constraint where T : IComparable sorgt dafr, dass der Compiler berprft, ob der Typ T das Interface IComparable implementiert. Damit wird zur Kompilierzeit sichergestellt, dass CompareTo aufgerufen werden kann. Etwas unschn 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 hlt, nicht mit null initialisiert werden, um anzuzeigen, dass es noch kein Minimum gibt. Den Typ T mit einem Constraint auf Referenztypen einzuschrnken wre auch nicht sinnvoll, denn dann knnte kein Minimum einer Integer-Aufzhlung ermittelt werden. Daher habe ich eine boolescheVariable minimumGefunden eingefhrt, in der festgehalten wird, ob bereits ein Minimum gefunden wurde. Nur dann darf das aktuelle Element gegen das bislang gefundene kleinste Ele-

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; }

sie bereits geliefert hat. Mithilfe dieses Merkzettels ist es mglich, jedes Element einzeln zu behandeln. Im Gegensatz zu GroupBy oder Sort muss also nicht die gesamte Aufzhlung 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 spter 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? Fr die bislang gezeigten Methoden gengte es, dass zwei Elemente auf Gleichheit berprft werden konnten. Da die Equals-Methode zum Bestandteil der Basisklasse object gehrt, war dies bislang

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!

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; } } }

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

www.dotnetpro.de dotnetpro.dojos.2011

65

AUFGABE
Einen Twitterticker realisieren

Was pfeifen die Spatzen?


Gute bungsaufgaben mssen cool sein. Sonst macht das Herumtfteln keinen Spa. Also, Stefan: Kannst du eine Aufgabe stellen, bei der ein cooles Programm entsteht, das zugleich technisch herausfordernd ist?

dnpCode: A1102DojoAufgabe

Wer bt, gewinnt

In jeder dotnetpro finden Sie eine bungsaufgabe von Stefan Lieser, die in maximal drei Stunden zu lsen sein sollte. Wer die Zeit investiert, gewinnt in jedem Fall wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen. Es gilt : T Falsche Lsungen gibt es nicht. Es gibt mglicherweise elegantere, krzere oder schnellere Lsungen, aber keine falschen. T Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das knnen Sie, indem Sie Ihre Lsung mit der vergleichen, die Sie eine Ausgabe spter in dotnetpro finden. bung macht den Meister. Also los gehts. Aber Sie wollten doch nicht etwa sofort Visual Studio starten

witterwalls erfreuen sich auf Veranstaltungen wachsender Beliebtheit. Eine Twitterwall zeigt regelmig aktualisiert Tweets, die ein vordefiniertes Hashtag enthalten. Die gefundenen Tweets werden an eine Wand projiziert. Auf diese Weise knnen 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 Brsentickern bei einschlgigen Nachrichtensendern. Der Suchbegriff soll dem Programm ber die Kommandozeile bergeben werden. Anschlieend soll das Programm die Tweets abrufen und darstellen. Dabei sollen die blichen Angaben visualisiert werden: T Text des Tweets, T Benutzername, T Profilfoto, T Zeitstempel, T Client, mit dem der Tweet abgesetzt wurde. Zu Anfang knnen Sie natrlich den Funktionsumfang reduzieren und zunchst nur den Text des Tweets anzeigen. In regelmigen Abstnden muss das Programm die Tweets aktualisieren. Dazu muss erneut eine Anfrage an Twitter abgesetzt werden. Die Benutzerschnittstelle soll whrend der Abfrage nicht einfrieren. Hier kommt also Multithreading ins Spiel. Ein Timer, der in regelmigen Abstnden ein Ereignis auslst, kann hier zum Einsatz kommen. Doch Obacht! Die beiden

Threads mssen dann synchronisiert werden, damit die Aktualisierung der Benutzerschnittstelle auf dem UI-Thread erfolgt. Den Zugriff auf das Twitter-API knnten Sie natrlich selbst entwickeln. Hier empfehle ich jedoch, eines der vorhandenen Open-SourceFrameworks einzusetzen. Andernfalls wird Sie diese bung lngere Zeit beschftigen. Ich habe gute Erfahrungen gemacht mit Twitterizer [1]. Die Benutzerschnittstelle knnen Sie mit Windows Forms oder WPF angehen. Auch eine Silverlight-Anwendung wre 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 fr 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 zurckliegenden Heften einiges verffentlicht [2][4]. Probieren Sie es doch mal aus! Im nchsten Heft gibts meine Lsung. [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 auen schn, Event-Based Components, dotnetpro 8/2010, S. 126 ff., www.dotnetpro.de/A1008ArchitekturKolumne

[Abb. 1] Ein Twitterticker.

66

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
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 Schlsselwort und prsentiert das Ergebnis. brigens lsst sich auch ein Zwitscherfinder vorteilhaft ber Event-Based Components realisieren.
m Anfang dieser kleinen Anwendung stand fr mich ein Spike. Ich hatte nmlich 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 wrde, 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 zwangslufig wegwerfen. Hufig dient eine Spike-Solution auch spter noch mal dazu, wieder in ein Thema reinzukommen. Oder ein Kollege mchte sich mit der Technik vertraut machen. Legen Sie daher Ihre Spikes ruhig im Versionskontrollsystem ab, natrlich gut gekennzeichnet in einem separaten Verzeichnis. Nachdem ich durch den Spike herausgefunden hatte, wie das Twitter-API zu bedienen ist, habe ich begonnen, die Lsung 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 groen Nutzen fr 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 Verfgung. Ich komme auf diesen Aspekt spter zurck.

[Abb. 1] Erster Schritt mit zwei Funktionseinheiten.

Die Modellierung habe ich in Form von Datenflssen vorgenommen. Pfeile bedeuten hier den Fluss von Daten, Kreise stehen fr 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 heien Platinen (engl. Boards) [2]. Ein Bauteil enthlt Logik, whrend eine Platine dafr zustndig ist, Funktionseinheiten zu verdrahten. Das knnen 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 ermglicht die sptere Verfeinerung durch Hierarchisierung. Eine ursprnglich als Bauteil modellierte Funktionseinheit kann spter zu einer Platine verfeinert werden. Dadurch ist am ursprnglichen 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 zunchst mit zwei Funktionseinheiten begonnen, wie Abbildung 1 zeigt: T Die Funktionseinheit UI steht fr die Benutzerschnittstelle, also alle Elemente, ber die der Benutzer mit der Anwendung interagiert. Ob dabei alles in einer Form realisiert wird oder zustzlich User Controls eingesetzt werden, spielt bei der Modellierung noch keine Rolle, denn es ist ein Implementationsdetail. T Die Funktionseinheit Tweets suchen enthlt 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 spter als Array, List oder IEnumerable realisiert wird, spielt im Modell keine Rolle. Wichtig ist hier lediglich auszudrcken, 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. Wrde die Funktionseinheit beispielsweise Twitter heien, wre es notwendig, den Datenfluss zu benennen. So knnte die Eingabe dann beispielsweise Tweets suchen (Suchbegriff) heien, whrend die Ausgabe Ergebnis liefern (Tweet*) heien knnte. Hier wird der Unterschied deutlich zwischen einer Modellierung mit Aktivitten wie Tweets suchen und Akteuren wie Twitter. Bei der Modellierung mit Aktivitten kann die Benennung der Nachricht meist entfallen, whrend sie bei Modellierung mit Akteuren fr das Verstndnis notwendig ist. In der Praxis hat sich gezeigt, dass die konsequente Modellierung in Aktivitten natrlicher ist und nach einer kurzen Gewhnungsphase leichter fllt. Die Gewhnungsphase ist vor allem fr Entwickler erforderlich, die sehr in der Objektorientierung verhaftet sind. Sie sind eher gewohnt, die Substantive zu suchen, um daraus Akteure zu machen. Fr die Tweets, die von der Funktionseinheit Tweets suchen zum UI geliefert wer-

www.dotnetpro.de dotnetpro.dojos.2011

67

LSUNG
bewusst weggelassen. Das hat mehrere Vorteile. Zum einen wird dadurch die Modellierung vereinfacht. Das gilt natrlich nur unter der Prmisse, dass sptere Ergnzungen einfach mglich sind. Durch die Modellierung mit Datenflssen ist das gegeben. Wre eine sptere Ergnzung des Modells mit hohem nderungsaufwand verbunden, wre die vorlufige 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 Funktionalitt umgesetzt, aber auch hier gilt, dass man diese spter ergnzen kann, ohne dabei die bereits implementierten Funktionseinheiten ndern zu mssen. Das liegt mageblich 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 frh Feedback geben. Und so wie das gesamte Modell iterativ entwickelt werden kann, kann man auch bei der Implementation iterativ vorgehen: T Die bergabe des Suchbegriffs als Kommandozeilenparameter kann zunchst weggelassen werden. Stattdessen wird ein fester Suchbegriff verwendet, der in der Anwendung hart codiert ist. T Statt direkt auf das Twitter-API zuzugreifen und die eingehenden Tweets zu mappen, kann zunchst eine hart codierte Liste von Tweets zurckgegeben werden. Dadurch lsst sich die Entwicklung des Controls zur Anzeige der Tweets vorantreiben. T Umgekehrt kann auch zunchst auf das Control verzichtet werden. Stattdessen

[Abb. 2] Die Funktionseinheit Tweets suchen weiter zerlegen.

den, habe ich einen eigenen Datentyp Tweet vorgesehen. Alternativ htte ich den Datentyp Tweet aus dem Twitterizer-Framework verwenden knnen. Dann wre das UI jedoch von dieser Infrastruktur abhngig. Im UI wre dann eine Referenz auf die Twitterizer-Assembly erforderlich gewesen. Das wollte ich in jedem Fall vermeiden, schlielich sind Benutzerschnittstelle und Ressourcenzugriffe vllig unterschiedliche Concerns und sollten daher getrennt werden. Ferner bietet ein eigener Datentyp die Mglichkeit, die Daten bereits so aufzubereiten, dass sie vom UI direkt verwendet werden knnen. Das fhrt dazu, dass das UI die Daten nicht deuten muss. Damit bleibt das UI extrem dnn und enthlt 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 kmmert: Zum einen findet hier der Zugriff auf Twitter ber das Twitterizer-API statt. Zum anderen ist die Funktionseinheit dafr zustndig, die Daten aus dem Twitterizer-Datentyp in meinen eigenen Datentyp zu mappen und dabei gegebenenfalls aufzubereiten. Da diese Erkenntnis bereits whrend 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 verwssern, die auf der Ebene nicht relevant sind, habe ich die Verfeinerung in einem weiteren Diagramm modelliert. Die Funktionseinheit Tweets suchen zerfllt dadurch intern in weitere Funktionseinheiten, ist demnach also eine Platine und kein Bauteil. Diese hierarchische Zerlegung ist durch die Modellierung in Datenflssen und die Umsetzung mit Event-Based Components auf einfache Weise mglich. Vor allem kann die Schachtelung in beliebiger Tiefe erfolgen, ohne dass dadurch bei der spteren Implementation Probleme auftreten. Abbildung 2 zeigt die Zerlegung von Tweets suchen. Die uere Schnittstelle ist

logischerweise gleich geblieben, andernfalls wrde 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 : T Bauteile, die nicht weiter verfeinert sind und Logik enthalten. T Platinen, die weitere Funktionseinheiten enthalten und fr deren Verbindungen zustndig 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 dafr zustndig, die beiden enthaltenen Bauteile zu verdrahten, und enthlt selbst keine Logik. In dieser ersten Modellierung habe ich das regelmige Aktualisieren der Tweets

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); } }

68

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
werden die Tweets ganz simpel in einem Label als Text angezeigt. Es bieten sich also zahlreiche Mglichkeiten, das Feature Tweets suchen und anzeigen in kleinen Schritten umzusetzen. Ganz wichtig dabei: Es handelt sich trotzdem immer um Lngsschnitte durch alle Funktionseinheiten. Dadurch kann man die Anwendung bereits sehr frh an den Kunden bergeben, um Feedback einzuholen. Fr die Implementation von Tweets suchen mssen 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 fr die Verdrahtung der Bauteile zustndig ist. Dazu muss der eingehende Methodenaufruf an das erste Bauteil der Platine weitergereicht werden. Das Ergebnis der Suche muss zum Mapper weitergeleitet werden. Und schlielich muss das Ergebnis des Mappers als Endergebnis der Platine zurckgegeben werden. Diese Verdrahtung zeigt Listing 1. Hier werden die Bauteile im Konstruktor unmittelbar instanziert, statt sie per Parameter zu injizieren. Da das Board lediglich fr die Verdrahtung zustndig ist, verzichte ich auf einen automatisierten Test dieser Verdrahtung. Dieser wre relativ aufwendig, weil dazu mittels Attrappen berprft werden msste, 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 ergnzt, der explizit gestartet werden muss, um damit zu berprfen, ob das Twitter-API prinzipiell korrekt verwendet wird und irgendein Ergebnis zurckgeliefert wird. Einen vergleichbaren Test habe ich fr die Platine ebenfalls erstellt. So kann berprft 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 abhngig sind. Da jedoch Platine und Twitter-Bauteil praktisch keine Logik enthalten, halte ich das Vorgehen fr angemessen. Das Explicit-Attribut an den Testmethoden sorgt dafr, dass diese Tests nur ausgefhrt werden, wenn dies explizit angefordert wird, siehe Listing 2. So kann man weiterhin alle Tests der Assembly aus-

Listing 2 Die Anwendung testen.


[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)); } }

fhren, ohne dabei jedesmal lange auf die Antwort von Twitter warten zu mssen. Fr das Mappen der Daten vom Twitterizer-Datentyp in den eigenen Datentyp knnen natrlich Tests geschrieben werden, da diese Operation ja lediglich auf Daten arbeitet. Fr das Laufband-Control habe ich die Suchmaschine meiner Wahl befragt. Dies fhrte 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, whrend Treffer mit nur einem Tweet sehr langsam angezeigt wurden. Die gewhlte Lsung der Animation scheint mir

relativ viel Prozessorzeit zu verbraten. Ich gestehe erneut, dass ich kein WPF-Spezialist bin. Wenn einem Leser eine bessere Lsung fr das Marquee-Control einfllt, mge er sich bitte melden. Nachdem das Feature Tweets suchen und anzeigen umgesetzt ist, muss das nchste Feature modelliert werden: Periodisches Aktualisieren der Suche. Dabei will ich natrlich auf dem schon vorhandenen Modell aufsetzen und dieses erweitern. Durch die Modellierung mit Datenflssen ist das einfach mglich, da zustzliche Funktionseinheiten leicht in einen schon vorhandenen Datenfluss eingesetzt werden knnen. 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 Schne dabei: An den vorhandenen Funktionseinheiten mssen keine nderungen vorgenommen werden. Einzige Ausnahme stellt die Platine dar, die fr die Verdrahtung der Funktionseinheiten zustndig ist. Diese erhlt zustzliche Funktionseinheiten, und die Verdrahtung muss abgendert werden. Ein weiterer schner Effekt: Der Timer hat nichts mit einer Twittersuche zu tun. Das Bauteil ist also vllig 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 ergnzen.

www.dotnetpro.de dotnetpro.dojos.2011

69

LSUNG
Listing 3 Periodische Aktualisierung ermglichen.
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 durchfhren.


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; }

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 Verstndnis der Implementation. Wre die Synchronisation im UI auf die bliche Art und Weise realisiert worden, wrde die Implementation den Entwurf nicht widerspiegeln. Einziger Ausweg: den Aspekt im Modell weglassen. Damit wre 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. Fr einen automatisierten Test habe ich einen weiteren Konstruktor ergnzt, 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 Verfgung, 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 spter die eingehenden Ereignisse synchronisiert werden sollen. Auch dieses Bauteil ist generisch, um den Typ des ein- und ausgehenden Datenflusses angeben zu knnen.

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 Mglichkeiten, iterativ vorzugehen. Das gibt beim Entwickeln ein gutes Gefhl, weil man den Fortschritt sieht und immer wieder etwas wirklich fertig wird. Durch den Einsatz von Funktionseinheiten fr die periodische Wiederholung und die Synchronisation der Threads sind diese beiden wichtigen Aspekte im Modell sichtbar. Das frdert das Verstndnis und erhht den Nutzen der Modellierung als Dokumentation der Implementation. [ml]

in der Anwendung nur einen einzigen Thread, nun sind es zwei. Damit das UI nicht meckert, mssen die Aktualisierungen der UI-Controls auf dem UI-Thread vorgenommen werden. Nun knnte man dazu Anpassungen im UI vornehmen und dort das Wechseln des Threads implementieren. Einfacher ist es allerdings, auch hier wieder eine zustzliche Funktionseinheit in den Datenfluss einzusetzen, die den Threadwechsel vornimmt. Diese Synchronization-Funktionseinheit lsst sich mit einem SynchronizationContext aus dem .NET Framework einfach realisieren. Diese Vorgehensweise fhrt dazu, dass die unterschiedlichen Belange sowohl im

Modell als auch in der Implementation sauber getrennt bleiben. Ferner taucht der Aspekt der Synchronisation im Modell explizit auf und sorgt damit fr bessere Verstndlichkeit. Wrde man die Synchronisation im UI implementieren, indem dort das bliche Muster von InvokeRequired-Abfrage und Invoke-Aufruf angewandt wrde, wre der Aspekt in der Implementation verborgen. Dadurch wrde das Verstndnis der Implementation erschwert. Hinzu kommt, dass nun die Funktionseinheiten Periodic Dispenser und Synchronization zu Standardbauteilen werden, die auch in anderen Anwendungen zum Einsatz kommen knnen.

[1] www.twitterizer.net [2] Ralf Westphal, Zusammenstecken funktioniert, Event-Based Components, dotnetpro 6/2010, S. 132 ff., www.dotnetpro.de/A1006ArchitekturKolumne

70

dotnetpro.dojos.2011 www.dotnetpro.de

AUFGABE

Algorithmen und Datenstrukturen zu Graphen

Wie hngt alles zusammen?


Mit einem Graphen kann man darstellen, wie die Dinge miteinander zusammenhngen. Weil aber alles mit allem irgendwie zusammenhngt, kann man mit Graphen eigentlich alles darstellen. Das ist interessant, und deswegen gibt es hier dazu eine bung.

dnpCode: A1103DojoAufgabe
raphen sind eine Datenstruktur, mit der sich viele Probleme auf einfache und elegante Art lsen 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 mssen. Die Fragestellung ist leicht zu lsen, wenn man von einer Baumstruktur ausgeht. Solange also zirkulre Referenzen unterbunden werden, gengt es, alle referenzierten Projekte in einer Baumstruktur abzulegen und anschlieend den Baum zu traversieren. Allerdings muss die Projektstruktur nicht zwingend einen einzigen Baum ergeben, sondern es knnen mehrere Bume 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 mglich. Enthlt ein Graph nur ungerichtete Kanten, spricht man von einem ungerichteten Graphen. Enthlt er gerichtete Kanten, spricht man von einem gerichteten Graphen. Das Beispiel der Projektreferenzen lsst 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 mglich sein, so spricht man von einem Multigraphen. Das Schne 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 wrde das eine Abhngigkeit der betroffenen Projekte bedeuten. Durch eine topologische Sortierung lsst sich die Reihenfolge der bersetzung von abhngigen Projekten herausfinden. Und auch fr 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 fr den Umgang mit gerichteten Graphen. Anschlieend implementieren Sie die Datenstruktur. Dazu gibt es in der Literatur gengend Vorschlge. 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 Ergnzung zur Implementation der Datenstruktur ist natrlich 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. Gengend 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 lsen sein sollte. Wer die Zeit investiert, gewinnt in jedem Fall wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen. Es gilt : T Falsche Lsungen gibt es nicht. Es gibt mglicherweise elegantere, krzere oder schnellere Lsungen, aber keine falschen. T Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das knnen Sie, indem Sie Ihre Lsung mit der vergleichen, die Sie eine Ausgabe spter in dotnetpro finden. bung macht den Meister. Also los gehts. Aber Sie wollten doch nicht etwa sofort Visual Studio starten

[1] www.graphviz.org/ [2] http://research.microsoft.com/en-us/projects/msagl/

www.dotnetpro.de dotnetpro.dojos.2011

71

Wer bt, gewinnt

LSUNG
Algorithmen und Datenstrukturen zu Graphen

Wie die Welt zusammenhlt


Adjazenz bezeichnet keinen geistlichen Wrdentrger und ist auch kein militrischer Dienstgrad, sondern steht fr die Beziehung zwischen Knoten und Kanten. ber adjazente, also miteinander verbundene Knoten kann man Zusammenhnge modellieren und erforschen. dotnetpro macht einen Ausflug in die Graphentheorie, fr 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, hlt zahlreiche Vortrge und hat gemeinsam mit Ralf Westphal die Clean Code Developer Initiative ins Leben gerufen. Sie erreichen ihn unter stefan@lieser-online.de oder lieser-online.de/blog.

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 knnte. Bevor es an die Algorithmen geht, muss man sich natrlich Gedanken ber die zugrunde liegende Datenstruktur machen. Auch hierbei hilft die Literatur. Sie enthlt Vorschlge, wie Graphen implementiert werden knnen. Ich habe mit der Reprsentation in Form einer sogenannten Adjazenzmatrix begonnen. Der Begriff Adjazenz steht fr 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 enthlt also die Information ber die Kanten. Reprsentiert wird sie durch ein zweidimensionales Array von booleschen Werten:
private readonly bool[,] adjacency = new bool[VertexMaxCount,VertexMaxCount];

Kante vom Knoten v zum Knoten w existiert, gengt es, in der Adjazenzmatrix nachzuschauen:
if(adjacency[v, w]) { ... }

[Abb. 1] Ein Graph mit zugehriger Adjazenzmatrix.

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 reprsentiert und knnen daher als Index in der Adjazenzmatrix verwendet werden. Ein Beispiel fr einen Graphen und die zugehrige 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 enthlt und dadurch die meisten Eintrge 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 vernachlssigbar. Im Vordergrund steht die Verstndlichkeit der Implementation, und die ist hier definitiv gegeben. Um beispielsweise zu ermitteln, ob eine

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 Gre von willkrlich 50 Knoten angelegt. Es ist ein Leichtes, dies durch einen zustzlichen Konstruktor konfigurierbar zu machen. Und natrlich knnte man das Array bei Bedarf auch in der Gre anpassen. Dazu muss lediglich geprft werden, ob noch Platz fr den zu ergnzenden Knoten ist. Da dann allerdings ein Umkopieren der Werte erforderlich ist, sollte man gut berlegen, ob es nicht besser ist, gleich die richtige Gre zu verwenden. API und Implementation des Graphen habe ich aus dem erwhnten Buch bernommen. Allerdings habe ich dabei einige der Bezeichner gendert, weil mir diese im Original zu stark abgekrzt waren. Das erschwert fr mich die Lesbarkeit, daher bevorzuge ich ausgeschriebene Begriffe. Lediglich bei den Eigenschaften fr die Anzahl der Knoten und Kanten habe ich es bei V und E in Grobuchstaben belassen, weil dies die in der Literatur allgemein verwendeten Symbole sind (abgeleitet von Vertex und Edge). Aber auch ber diese Bezeichner liee sich natrlich reden. Listing 1 zeigt die Implementation der Datenstruktur. Die beiden Properties V und E liefern jeweils die Anzahl von Knoten und Kanten zurck. Zu beachten ist dabei, dass V jeweils die maximale Anzahl mglicher Knoten im Graph liefert. Das liegt daran, dass in der Adjazenzmatrix lediglich die Kanten verwaltet werden. Diese werden beim Einfgen und Lschen zustzlich noch gezhlt, damit fr die Ermittlung ihrer Anzahl E kein Zhlen in der Matrix erforderlich ist. Fr das Hinzufgen und Lschen von Kanten wird die Datenklasse Edge verwendet. Diese hat

72

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Listing 1 Die Datenstruktur fr einen Graphen.
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; } }

Listing 2 Graphen verndern.


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)); } }

Listing 3 Benachbarte Knoten traversieren.


[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})); }

lediglich zwei Properties fr Start- und Zielknoten. Auch hier habe ich es bei den Kleinbuchstaben v und w belassen, da diese in der Literatur sehr oft als Symbole fr Knoten verwendet werden. Neben den Methoden zum Verndern des Graphen stehen die beiden Methoden Edge und AdjacentVertices zur Verfgung. Die Methode Edge liefert fr 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-

ten O-Notation auch Landau-Symbol genannt finden Sie unter [3]. Mit AdjacentVertices knnen zu einem gegebenen Knoten v smtliche 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 fr diese Methode betrgt O(n), ist also linear. Das bedeutet, dass die Laufzeit linear mit der Gre der Adjazenzmatrix ansteigt. Natrlich habe ich zu dieser Datenstruktur einige automatisierte Tests erstellt. Diese knnen gleichzeitig auch als Beispiele fr die Verwendung des APIs dienen. Listing 2 zeigt ein Beispiel fr die Vernderung eines Graphen. Listing 3 zeigt einen Test zur Traversierung der benachbarten Knoten. Der erste Test zeigt, dass die Aufzhlung der adjazenten Knoten leer ist, wenn keine Kanten hinzugefgt wurden. Im zweiten Test sieht man, dass die ber Kanten unmittelbar erreichbaren Zielknoten aufgelistet werden.

Im Rausch der Tiefe


Dieses Beispiel fhrt uns wieder zur Frage aus der Aufgabenstellung zurck: Wie kann man ermitteln, ob es eine Verbindung zwischen zwei Knoten im Graph gibt, die ber mehrere Kanten verluft? Da hilft die Tiefensuche: Man besucht, ausgehend vom Startknoten, so lange alle erreichbaren

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 whrend 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 natrlich immer. Danach wird der Startknoten dieser Suche markiert, damit er spter nicht erneut bercksichtigt 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 ntzlich, dass die Knoten als Zahlen reprsentiert werden. Dadurch kann nmlich 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 ntigen 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-

www.dotnetpro.de dotnetpro.dojos.2011

73

LSUNG
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; } } }

Visualisierung
Ich wollte dann noch prfen, 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. Natrlich drfen dabei mehrere andere Knoten traversiert werden. Der Test dazu war schnell erstellt. Doch kam der Wunsch auf, den zu testenden Graphen visualisieren zu knnen: So ist sichergestellt, dass die Testdaten tatschlich den gewnschten Graphen reprsentieren. Dies visuell zu prfen 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 zunchst, meine Reprsentation eines Graphen in die Reprsentation aus dem MSAGL-Framework zu berfhren Dies habe ich als Extension-Methode implementiert. Dadurch bleibt meine GraphKlasse weiterhin frei von unntigen Abhngigkeiten, siehe Listing 6. In einer Schleife werden alle Knoten des Graphen durchlaufen. Fr jeden Knoten werden dann die adjazenten Knoten ermittelt. Fr je zwei adjazente Knoten wird anschlieend eine Kante im MSAGL-Graphen angelegt. Ein Knoten wird durch MSAGL standardmig als Kstchen 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 eingefgt und eine Methode ergnzt, mit welcher der zu visualisierende Graph an die Form bergeben

[Abb. 2] Zyklische Bezge visualisieren.

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); }

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. Natrlich 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 mchte, 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 knnen wir beispielsweise ermitteln, ob eine Assembly von einer anderen Assembly abhngig ist, auch wenn sich die-

fen. Die Klasse kann dazu einfach mit einer weiteren Eigenschaft versehen werden, die whrend der Suche die traversierten Knoten aufsammelt. Natrlich wre es auch denkbar, den Algorithmus nicht direkt von der Klasse Graph abhngig zu machen. Dazu msste Graph nur mit einem Interface versehen werden. So knnten 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 groe Tat.

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; } }

74

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Listing 7 Einen Graphen visualisieren.
public static void ShowGraph(this Graph g) { var viewer = new Viewer(); viewer.SetGraph(g); viewer.ShowDialog(); }

Listing 9 Umgekehrte topologische Sortierung.


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]; } }

Listing 10 Graphen testen.


[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)); }

Listing 8 Zyklische Bezge 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); }

Sich im Kreis drehen


Beim Ermitteln der Build-Reihenfolge bleibt abschlieend noch die Frage, ob zyklische Abhngigkeiten existieren. Wenn A von B abhngt 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 fhren wrden. Die Vorgehensweise ist demnach wie folgt: Zunchst wird ein Graph mit den bereits vorhandenen Projekten und Referenzen erzeugt. Danach wird die neu hinzuzufgende Referenz in den Graphen eingefgt. Bevor die Referenz tatschlich in das Visual-Studio-Projekt aufgenommen wird, muss der Graph auf Kreisfreiheit berprft werden. Wrde durch die zustzliche Referenz ein Kreis entstehen, muss diese Referenz logischerweise abgelehnt werden. Die Lsung des Problems kann man zurckfhren auf die Frage, ob zwischen zwei Knoten eine sogenannte starke Verbindung existiert.Wenn fr 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 zusammenhngenden Knoten in einem Graph, die ber starke Verbindungen erreichbar sind, werden starke Komponenten genannt. Und das fhrt uns zu einem Algorithmus fr 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 heit, es liegen keine Kreise vor. Um herauszufinden, ob ein Graph Kreise enthlt, ermittelt man also die Anzahl der starken Komponenten. Ist diese gleich der Anzahl der Knoten, ist der Graph kreisfrei.

se Abhngigkeit ber mehrere Assemblies hinweg ergibt. Doch wie knnen wir die Build-Reihenfolge mehrerer Projekte ermitteln, die untereinander Abhngigkeiten haben? Die Lsung 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. Fr das Bestimmen der BuildReihenfolge bentigen wir die umgekehrte topologische Sortierung: Projekte, die von anderen bentigt werden, mssen 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 eigenstndige 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, fr die der Initialwert noch im Array steht, werden besucht. Die beiden Arrays order und relabel nehmen das Ergebnis der Sortierung auf. T Order liefert fr einen gegebenen Index die Knotennummer.

T Relabel liefert zu einer gegebenen Knotennummer den Index des Knotens. Listing 10 zeigt einen Test des Algorithmus fr einen Graphen, bei dem drei Knoten hintereinander angeordnet sind. Man sieht bereits, dass die beiden Methoden Order und Relabel invers zueinander sind. Natrlich gibt es fr manche Graphen mehrere mgliche Ergebnisse. Schon bei einem Knoten mit zwei Nachfolgern stellt sich die Frage, welcher Nachfolger zuerst besucht werden soll. Hier gibt es also zwei mgliche Ergebnisse bei der topologischen Sortierung. Fr die Reihenfolge beim bersetzen von Visual-Studio-Projekten spielt das natrlich keine Rolle.

www.dotnetpro.de dotnetpro.dojos.2011

75

LSUNG
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 enthlt 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 Gefhlen verbindet, sei getrstet: Die Algorithmen konnte ich unverndert bei Sedgewick abschreiben. Die bersetzung von Java nach C# war leicht. Das hat fr mich wieder besttigt, 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 fr 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 regelmig im Rahmen der persnlichen Weiterbildung, ist man fr zuknftige Herausforderungen gerstet. [ml]

Fazit
Ich habe mich whrend dieser bung an mein Studium erinnert. Die Vorlesungen zu Graphenalgorithmen waren immer sehr lehrreich, weil grundlegende Dinge wie Rekursion, Tiefensuche etc. erklrt wurden. Wer solche gedanklichen Zeitreisen eher

[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/

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 fr den Stellenmarkt
Angelika Hochmuth Anzeigenleitung Tel: 089 / 74 11 7 - 1 25 angelika.hochmuth@nmg.de

76

dotnetpro.dojos.2011 www.dotnetpro.de

AUFGABE

Eine interaktive Anwendung mit Datenflssen modellieren

Wie flieen die Daten?


Software modellieren: Ja, dem gehrt die Zukunft. Aber was soll man eigentlich genau modellieren? Datenflsse oder Abhngigkeiten von Funktionseinheiten? Stefan, kannst du dazu eine bung stellen?
dnpCode: A1104DojoAufgabe
ei einer algorithmischen Fragestellung liegt es nahe, Datenflsse zu modellieren.Wer Datenflsse modelliert, verdeutlicht den Ablauf der Anwendung. Bei der Modellierung von Abhngigkeiten ist das hingegen meistens nicht der Fall. Zwar sieht man bei einem Abhngigkeitsdiagramm, dass eine Funktionseinheit die Dienste anderer Funktionseinheiten in Anspruch nimmt und damit von diesen Funktionseinheiten abhngig wird. Wie die Interaktion der Funktionseinheiten aber genau aussieht, lsst sich meist nur erahnen. Fr Aufgabenstellungen ohne Benutzerinteraktion sind Datenflussdiagramme naheliegend. Das Zerlegen einer Zeichenkette in Konfigurationswerte, die in ein Dictionary bernommen werden, wre 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 mglichst frei von Logik zu halten. Somit mssen alle Entscheidungen, die sich auf die GUI auswirken, auerhalb der GUI getroffen werden. Diese Form der Interaktion lsst sich mit Datenflssen sehr gut modellieren. Doch diese Denkweise fllt vor allem eingefleischten OOP-Verfechtern schwer. Wie sehen die Anforderungen an die Anwendung aus? Ich habe dazu eine Featureliste erstellt. Die Features knnen bei einer konsequenten Datenflussmodellierung einzeln modelliert werden. Das Zusammenfgen der dabei entstandenen Modelle ist bei Datenflussdesigns kein Problem. Das liegt daran, dass die beteiligten Funktionseinheiten keine Abhngigkeiten 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 gendert 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.
Da die einzelnen Features mglicherweise 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 Lngsschnitten zu bleiben, also vertikal zu zerlegen. Wrde man Feature F1 beispielsweise in den GUI-Anteil und den Rest zerlegen, so wre der GUI-Anteil kein eigenstndiger Lngsschnitt. Besser ist es, die Zerlegung so zu whlen, dass sie durch alle Funktionseinheiten hindurch verluft. Dann wird ein Teil des GUIs implementiert, aber auch ein Teil des Rests, sodass eine voll funktionsfhige Teilfunktionalitt zur Verfgung steht. Weitere Informationen zum Entwicklungsprozess siehe [1]. Abbildung 1 zeigt eine mgliche Benutzerschnittstelle. Die Aufgabe fr diesen Monat besteht darin, die Features zu modellieren und anschlieend zu implementieren. Bei der Implementation mag eine Beschrnkung 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 mglichst frh Feedback vom Kunden zu erhalten. Happy modeling! [ml]

In jeder dotnetpro finden Sie eine bungsaufgabe von Stefan Lieser, die in maximal drei Stunden zu lsen sein sollte. Wer die Zeit investiert, gewinnt in jedem Fall wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen. Es gilt : T Falsche Lsungen gibt es nicht. Es gibt mglicherweise elegantere, krzere oder schnellere Lsungen, aber keine falschen. T Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das knnen Sie, indem Sie Ihre Lsung mit der vergleichen, die Sie eine Ausgabe spter in dotnetpro finden. bung macht den Meister. Also los gehts. Aber Sie wollten doch nicht etwa sofort Visual Studio starten

Featureliste
F1: Ein neues To-do hinzufgen. 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

[Abb. 1] So knnte die To-do-Liste aussehen.

www.dotnetpro.de dotnetpro.dojos.2011

77

Wer bt, gewinnt

LSUNG
Eine interaktive Anwendung mit Datenflssen modellieren

Wissen, was zu tun ist


MVVM-Pattern? Kennt man. Flow Design? Schon mal gehrt. Event-Based Components? Klar, das ist die Spezialitt 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, hlt zahlreiche Vortrge und hat gemeinsam mit Ralf Westphal die Initiative Clean Code Developer ins Leben gerufen. Sie erreichen ihn unter stefan@lieser-online.de oder lieser-online.de/blog.

as Modellieren der ToDo-Listenanwendung in Form eines Flow Designs ist eine ntzliche 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 Aufklrung not. Das Krzel MVVM steht fr 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 enthlt alle Daten, die von der View anzuzeigen sind. Das Ziel dabei ist es, die View so dnn wie mglich zu halten. Sie soll die Daten des ViewModels nicht deuten mssen. Der Grund liegt zum einen in der Testbarkeit. Views sind typischerweise schwierig automatisiert zu testen, daher sollte dort mglichst kein Code untergebracht werden. Zum anderen darf die View keine Domnenlogik enthalten. Es ist Aufgabe der Domnenlogik, die Daten im ViewModel so aufzubereiten, wie die View sie bentigt. Das ViewModel wird an die View bergeben. Es gibt also eine Abhngigkeit der View vom ViewModel. Fr WPF- und Silverlight-Anwendungen bedeutet dies vor allem, dass das ViewModel optimal fr 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 durchgefhrt 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 fr die eigentliche Geschftslogik. Die Geschftslogik soll frei sein von Abhngigkeiten zur View und ihren technischen Details. Als Mittler zwischen Model undView steht das ViewModel. Das Model kann das ViewModel erzeugen bzw. verndern, 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 fr die Abarbeitung der zugehrigen Logik in der Regel Informationen aus dem ViewModel erforderlich. Umgekehrt fhrt die Abarbeitung der Geschftslogik meist zu nderungen, die in der View visualisiert werden mssen. Eine Mglichkeit wre, das ViewModel als Ergebnis einer nderung als Datenfluss zur View zu bertragen, so wie es Abbildung 1 zeigt. Damit wrde die View allerdings jedes Mal eine neue Instanz des ViewModels erhalten, was das Data Binding ad absurdum fhren wrde. 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 gendertes ViewModel, in das ein zustzliches ToDo eingefgt wurde. Daraus ergeben sich nun gleich zwei Fragen: T Woher kennt die Aktion Neues ToDo erzeugen den vorhergehenden Zustand desViewModels? T Wie stellt dieView sicher, dass das Data Binding funktioniert?

[Abb. 1] ViewModel im Datenfluss.

[Abb. 2] Neues ToDo einfgen.

78

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
Die Frage nach dem Zustand fhrt zur Lsung: Das ViewModel ist ein Zustand, der von der View und der Aktion gemeinsam verwendet wird. Beide sind von diesem Zustand abhngig. Wenn dem so ist, stellt sich erneut die Frage, ob es sinnvoll ist, das ViewModel als Datenfluss zur View zu bertragen. Wenn nmlich 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. Schlielich erfolgt die Aktualisierung der View durch das Data Binding. Fr solche Abhngigkeiten haben Ralf Westphal und ich die Notation des Flow Designs um eine weitere Pfeilart ergnzt. Ein Pfeil mit einem Punkt am Ende steht fr eine Abhngigkeit. Abbildung 3 zeigt, wie man das ViewModel als gemeinsame Abhngigkeit zwischen die View und eine Aktion stellen kann. Abbildung 4 zeigt erneut das Feature Neues ToDo erzeugen, diesmal jedoch mit einer Abhngigkeit anstelle eines Datenflusses. Auf diese Weise ist es nun bequem mglich, die Abhngigkeit von einem Zustand zu modellieren. Durch die Modellierung des gemeinsamen Zustands als Abhngigkeit gehen die Datenflsse 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.
[Abb. 3] View und Aktion sind vom ViewModel abhngig.

[Abb. 4] Neues ToDo erzeugen.

[Abb. 5] Geffnetes und geschlossenes ToDo.

Features realisieren
Beginnen wir mit dem ersten Feature : F1: Ein neues ToDo hinzufgen. 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 ergnzt, fertig. Durch die gemeinsame Abhngigkeit von View und Aktion vom ViewModel wird die nderung im ViewModel per Data Binding in der View visualisiert. Bleibt noch die Frage zu klren, 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 abhngig sind, geht aus dem Flow-Design-Modell hervor. Wie diese Abhngigkeit initialisiert

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 zunchst konsequent mit einer Minimalimplementation des GUIs zufriedengegeben und die restlichen Funktionseinheiten realisiert, damit am Ende ein Lngsschnitt fertig wird statt nur ein schickes GUI. Nach den Minimalimplementationen standen App-Projekt und Buildskript an. Das App-Projekt ist dafr zustndig, die bentigten 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 hrt 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. Ausfhren 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 Komponentenwerkbnke aufgesammelt werden. Da die Buildreihenfolge der Komponenten egal ist, muss dieser Teil beim Hinzufgen 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 binr referenzieren muss, um sie instanzieren zu knnen. Aus diesem Grund erstelle ich von allen Komponenten zunchst eine sogenannte Tracer-BulletImplementation. Die so implementierten Funktionseinheiten machen noch nicht wirklich etwas, auer 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 mchte erreichen, dass ein ToDo in der Liste in zwei verschiedenen Zustnden angezeigt werden kann : T geffnet zur Bearbeitung, T geschlossen zum Anzeigen. Abbildung 5 zeigt die Form mit je einem geffneten und einem geschlossenen ToDo. Im geffneten Zustand soll der Text des Eintrags editiert werden knnen. Ferner soll man das ToDo mit einer Checkbox als erledigt kennzeichnen knnen. In einem spteren Schritt soll auch die Eingabe von Tags mglich sein. Somit stand schnell die

www.dotnetpro.de dotnetpro.dojos.2011

79

LSUNG
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>

do selektiert wird. Daraufhin muss die InBearbeitung-Eigenschaft des selektierten Elements auf false gendert werden. Durch Data Binding sorgt WPF dann dafr, dass das Listenelement mit dem anderen DataTemplate im geschlossenen Zustand visualisiert wird. Umgekehrt muss es mglich 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 auszulsen, wenn ein Eintrag der Liste per Doppelklick ausgewhlt wird. Die Umsetzung der Logik ist trivial: Es muss lediglich die InBearbeitung-Eigenschaft angepasst werden.

Idee im Raum, dazu eine ListView mit zwei unterschiedlichen DataTemplates zu verwenden. Die Frage war nur, wie man es erreicht, dass das DataTemplate in Abhngigkeit von einer Eigenschaft im ViewModel ausgewhlt wird. Denn meine Idee war, dass jedes einzelne ToDo eine Eigenschaft InBearbeitung erhalten soll. Abhngig davon, ob diese Eigenschaft auf true oder false gesetzt ist, soll das passende DataTemplate ausgewhlt werden. Die Lsung liegt in der Verwendung eines Data Triggers, der an die Eigenschaft im ViewModel gebunden wird. Listing 1 zeigt den relevanten XAML-Ausschnitt. Um das Tfteln an der Form zu beschleunigen, braucht man einen Testrahmen. Msste man erst die gesamte Anwendung vollstndig bersetzen, um die Form in Aktion sehen zu knnen, wre 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(); }

ckelt werden knnen. Das soll auch fr das GUI mglich 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 flle sie mit entsprechenden Testdaten. So ist ein zgiges Arbeiten mglich, 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 weiterluft, wenn die Form wieder geschlossen ist, habe ich das NUnit-Attribut Explicit ergnzt. Es sorgt dafr, dass der Test nur ausgefhrt wird, wenn er explizit gestartet wird. Beim Ausfhren aller Tests der Assembly, beispielsweise auf dem Continuous-Integration-Server, werden die expliziten Tests ignoriert. Des Weiteren habe ich das Attribut RequiresSTA ergnzt, damit den Anforderungen von Windows Forms bzw. WPF entsprochen wird. Das Austfteln 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, wre es jedoch leicht mglich gewesen, diese Details von einem erfahrenen WPF-Entwickler vornehmen zu lassen. Dazu htte dieser nicht die gesamte Anwendung bentigt, sondern lediglich die Visual-Studio-Solution mit der WPF-Form und dem zugehrigen Testrahmen.

Persistenz
Das nchste Feature hat es in sich: Die ToDo-Eintrge sollen von einem zum anderen Programmstart erhalten bleiben. Dazu habe ich zunchst identifiziert, bei welchen bereits umgesetzten Features die ToDo-Liste so gendert wird, dass die Daten erneut persistiert werden mssen. Das ist bei folgenden Interaktionen der Fall: T Ein neues ToDo wird angelegt. T Der Text eines vorhandenen ToDos wird gendert. T Die Erledigt-Eigenschaft eines vorhandenen ToDos wird gendert. Anschlieend 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 vollstndig mittels Data Binding gelst ist. Daher habe ich im GUI einen ausgehenden Datenfluss ergnzt, der mitteilt, dass ein ToDo gendert wurde. Ausgehend von den Aktivitten ToDo gendert, ToDo-Bearbeitung beenden und Neues ToDo ergnzen wird die Aktivitt ViewModel in ToDo-Liste bersetzen gestartet. Sie mappt das ViewModel auf ein Datenmodell fr die Persistenz. Beide Modelle sind strikt zu trennen, damit es nicht zu Abhngigkeiten 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,

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 geffnet dargestellt wird, muss die View einen Event auslsen, sobald ein anderes To-

80

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
[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 gehrt 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 lsst 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. Spaeshalber 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. Zustzlich sollten mehrere Speicherauftrge, die in kurzer zeitlicher Folge eintreffen, zu einem einzigen zusammengefasst werden. Das Speichern soll so lange verzgert werden, bis fr eine gewisse Zeit keine nderungen mehr eintreffen. Die Umsetzung dieser Anforderung ist mit Flow Design und EBC ein Leichtes. Zunchst habe ich die Funktionseinheit ToDoListe speichern von einem Bauteil in eine Platine gendert. Anschlieend habe ich in der Platine vor das eigentliche Speichern einen Standardbaustein Throttle eingesetzt. Dieser verzgert einen eingehenden Datenfluss fr fnf Sekunden. Trifft das Ereignis in diesem Zeitraum mehrfach ein, wird der interne Timer dadurch wieder zurckgesetzt. Diesen Standardbaustein sowie einige andere finden Sie im OpenSource-Projekt ebclang [4].

Um die Benutzerschnittstelle ber das Speichern zu informieren, liefert die Platine drei Datenflsse, die anzeigen, dass mit dem Speichern begonnen wurde, dass es beendet ist bzw. dass ein Fehler aufgetreten ist. Damit diese Datenflsse im GUI verwendet werden knnen, um eine Meldung in der Statusleiste auszugeben, mssen 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 knnen. Dadurch ist eine visuelle Kontrolle mglich, 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.

in dem dasWPF-Problem beschrieben wird. Hier steht auch gleich der Link zu einer Lsung [5]. Allerdings funktionierte die dort beschriebene Lsung 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 dafr, 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 lnger gedauert als geplant. Das lag an drei Bereichen: T Das Modellieren mit MVVM und Abhngigkeiten brauchte noch etwas Feinschliff. T Der XAML-Code des GUIs ist recht aufwendig geraten. T Es gab Probleme mit ILmerge. Trotz der Herausforderungen hatte ich zwischendurch immer wieder Versionen, die ich htte liefern knnen. Konsequentes Fokussieren auf Lngsschnitte sowie schrittweises Verfeinern sind Schlssel zum entspannten Arbeiten. [ml]

ILmerge
Als die Anwendung in Grundzgen 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 gestoen. 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 Lsung findet sich im Hinweistext zu ILmerge,

[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

www.dotnetpro.de dotnetpro.dojos.2011

81

GRUNDLAGEN
MVVM und Flow-Design kombinieren

Alles unter einem Hut


Das Data Binding von WPF bietet beeindruckende Mglichkeiten. MVVM ist fr WPF-Anwendungen das geeignete Konzept. Flow-Design erlaubt eine sehr natrliche Art der Modellierung. Und Event-Based Components stellen ein universales Konzept fr Modellierung und Implementierung. dotnetpro zeigt, wie Sie diese Konzepte gemeinsam nutzen knnen.

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 stefan@lieser-online.de oder unter lieser-online.de/blog.

Inhalt
Flow-Design fr den Entwurf einer grafischen Oberflche verwenden. Model und View in Abhngigkeit von einer einzelnen Instanz des ViewModels modellieren. Die Phasen des Konzepts der Event-Based Components bei der Implementierung umsetzen.

dnpCode
A1105MVVM

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 Datenflsse die Daten in die View gelangen. zentrale Rolle spielen. Die DaDie gleiche Frage stellt sich auf ten flieen hier zwischen dem Rckweg: 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 Einfhrung Kundenadresse vorgenommen von WPF deutlich an Leiswerden soll, flieen die gentungsfhigkeit gewonnen hat, derten Adressdaten von der liegt es auf der Hand, diese Benutzerschnittstelle zu einer Leistungsfhigkeit auch auszuFunktionseinheit Kundenadresnutzen. Zwar verfgte auch se ndern. Von dort flieen die schon Windows Forms ber Daten weiter zu einer Aktion Data-Binding-Funktionalitt, [Abb. 1] Ein Datenfluss mit GUI. Genderte 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 Mglichkeiten 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 natrliche 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 mssrung wie das skizzierte ndern einer Kundente, wre lngere Zeit beschftigt. adresse anschauen, analysieren wir meist geAkzeptieren wir also fr 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 spter folgt [1] [2]. Trotz MVVM Implementation ist mithilfe von Event-Based bleibt aber die Frage offen, wie der Rest der AnComponents (EBC) mglich. Dabei liegt der growendung strukturiert wird. Schlielich muss ire Vorteil in der Tatsache, dass das Modell 1:1 in gendwer das ViewModel mit Daten befllen 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 wre 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 drfte den Hauptteil jeder nicht trischlich die vornehmste Aufgabe eines Modells vialen Anwendung ausmachen. MVVM ist nur bernehmen, die darin besteht, zu abstrahieren. ein Pattern fr bestimmte Teile der Anwendung Das Modell ist eine abstrakte Darstellung der Imund gibt noch nicht das Modell fr den wesentplementation. Damit dient das Modell einerseits lich umfangreicheren Rest der Anwendung vor.

82

dotnetpro.dojos.2011 www.dotnetpro.de

GRUNDLAGEN
als Vorlage fr die Implementation, andererseits auch als Dokumentation derselben. Ohne den Einsatz von MVVM ist das Einbeziehen der Benutzerschnittstelle in das Flow-Design ganz leicht: Daten flieen von der Benutzerschnittstelle zur Logik und werden dort verarbeitet. Und in vielen Fllen fliet aus der Logik ein Resultat zurck zur Benutzerschnittstelle, um dort visualisiert zu werden. Doch wie bezieht man nun Data Binding und ViewModels in das Flow-Design ein?

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 { }; }

ViewModels
Dazu muss zunchst geklrt werden, was es mit dem ViewModel auf sich hat. Ein ViewModel enthlt alle Daten, die eine View darstellen soll. Das ViewModel hat keine Abhngigkeit 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 hinausfliet, sind diese Tests sehr leicht zu automatisieren. Daraus folgt eine wichtige Erkenntnis: Views sollen die Daten nicht deuten mssen. Dann wre nmlich 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 mssen, ist das Testen einfach. Dazu ein Beispiel: Hufig sind Controls in einer View nur unter bestimmten Umstnden 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 dafr verantwortlich ist, Controls abhngig von einem Zustand auszublenden. Wrde dies durch die View bewerkstelligt, indem dort eine entsprechende Prfung des Zustands mit anschlieendem Deaktivieren der Controls programmiert ist, wre 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 fr dieses Szenario fr jedes Control eine Eigenschaft vorhanden sein, die anzeigt, ob das

[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 lsst 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 lsst sich das Ergebnis im ViewModel ablesen und damit leicht automatisiert testen. Auch die View lsst sich auf diese Weise leicht testen. Im Test knnen einfach ViewModels mit unterschiedlichen Daten instanziert und an die View bergeben werden. Anschlieend wird die View mit ShowDialog angezeigt und visuell berprft. So ist sichergestellt, dass auch die in XAML formulierten Data Bindings schnell berprft werden knnen. Es ist nicht mehr notwendig, die komplette Anwendung zu starten und mit Testdaten zu fttern, um die unterschiedlichen visuellen Effekte der Views zu berprfen.

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 irrefhrend, weil im Rahmen des Entwurfs das Ergebnis der Modellierung

ebenfalls Modell heit. Daher spreche ich lieber von Logik. Eigentlich msste es dann also Logik-View-ViewModel heien. View und Logik sind beide vom ViewModel abhngig. Dadurch wird eine saubere Trennung zwischen View und Logik erreicht, denn zwischen ihnen gibt es keine direkte Abhngigkeit. Allerdings bleibt noch die Frage zu klren, wie beim FlowDesign das ViewModel konkret von View und Logik zu verwenden ist. Eine Mglichkeit wre, 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 hlt. In beiden Fllen 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 Abhngigkeit dar. SowohlView als auch Logik sind vom ViewModel abhngig. Und wenn sich beide auf dieselbe Instanz beziehen wrden, wre es nicht mehr ntig, 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

www.dotnetpro.de dotnetpro.dojos.2011

83

GRUNDLAGEN
[Abb. 3] View und Logik sind vom ViewModel abhngig.

[Abb. 4] MVVM mit Flow verheiratet.

Funktionalitt ausgefhrt wird. Der Datenfluss trgt allerdings keine Daten mehr, was durch das leere Klammerpaar ausgedrckt wird. Stattdessen sind nun View und Logik vom ViewModel abhngig, 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. Fliet 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 abhngigen 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 flieen knnen: T Build, T Bind, T Inject, T Config, T Run. Die Build-Phase ist dafr verantwortlich, Funktionseinheiten zu instanzieren. Fr Bauteile ist dies ganz einfach, da die Konstruktoren von Bauteilen keine Parameter haben. Das liegt daran, dass Bauteile keine Abhngigkeiten 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.

Aufgabe der Platinen ist es, Funktionseinheiten zu verbinden, indem Input- und Output-Pins zusammengesteckt werden. Dazu mssen 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 Abhngigkeiten in die betroffenen Funktionseinheiten zu injizieren. Hier ist es wichtig, zu differenzieren. Es geht nicht um die Abhngigkeiten von Platinen zu den in ihnen enthaltenen Funktionseinheiten. Diese sind ja bereits durch die Build-Phase abgehandelt und wurden durch Konstruktorinjektion aufgelst. In der Inject-Phase geht es um Abhngigkeiten, die explizit als solche modelliert wurden. Diese werden nun durch Aufruf einer Methode in die abhngigen Funktionseinheiten hineingereicht. In der vorletzten Phase, der Config-Phase, sind alle Funktionseinheiten bereits betriebsbereit. Sie sind verdrahtet und ihre Abhngigkeiten sind erfllt. Bevor die Anwendung am sogenannten Entry Point startet, mgen manche Funktionseinheiten noch eine Konfiguration oder Initialisierung bentigen. So knnte es beispielsweise notwendig sein, die Daten der Anwendung durch einen Persistenzmechanismus zu laden. Zum Schluss ist eine Funktionseinheit dafr zustndig, die Ausfhrung der Anwendung zu beginnen. Meist ist dies das Hauptformular der Anwendung. Diese Funktionseinheit stellt eine Run-Methode bereit. Fr die Phasen Inject, Config und Run sollten Interfaces verwendet werden, um diese Aspekte bei beliebigen Funktionseinheiten definieren zu knnen. Listing 2 zeigt das Interface fr die Inject-Phase. In der Inject-Phase mssen alle Funktionseinheiten aufgesucht werden, die IDependsOn<T> implementieren. Diesen muss dann die er-

forderliche Abhngigkeit durch Aufruf der Inject-Methode bergeben werden. Nun haben wir fr die gemeinsame Verwendung eines ViewModels alles zusammen. Beim Programmstart werden alle Funktionseinheiten instanziert und die Datenflsse verbunden. Anschlieend 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 fr die Inject-Phase.


public interface IDependsOn<T> { void Inject(T independent); }

Listing 3 Interface fr 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); }

84

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 zugehrigen Logik eine Interaktion? Wie sind Buttons, Toolbars und Mens an die Logik anzubinden?

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); } }

Kommandos
WPF verwendet das Konzept der Commands. Dahinter steht das Interface ICommand, siehe Listing 3. Ein Kommando muss zunchst eine Execute-Methode bereitstellen, die von WPF aufgerufen wird, um das Kommando auszufhren. Natrlich sollte der Code, der fr das Kommando auszufhren ist, auf keinen Fall im Kommando abgelegt werden. Das wre nicht viel besser, als wrde die Logik direkt in der View untergebracht. Um den IsEnabledZustand eines Buttons oder Menpunkts passend setzen zu knnen, ruft WPF die CanExecute-Funktion auf. Diese muss true liefern, wenn das Kommando ausgefhrt 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. Fr einen Button sieht das beispielsweise so aus:
<Button Command="{Binding Path=berweisungCmd}">berweisung </Button>

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")); } } }

Doch wie bringt man ein Kommando mit einem Flow zusammen? Dazu muss das Kommando einen Ausgang haben, an dem ein Datenfluss beginnen kann. Ausgehende Flsse werden bei EBC mit Events implementiert. Folglich muss das Kommando bei Aufruf der Execute-Methode einen Event auslsen, der dann einen Datenfluss startet. Umgekehrt muss es mglich sein, durch einen eingehenden Datenfluss zu bestimmen, ob ein Kommando weiterhin eingeschaltet ist. Eingehende Datenflsse 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

Listing 7 Datenflsse verdrahten.


public class Mainboard { public Mainboard(MainWindow mainWindow, Logik logik, FlowCommand berweisungCmd) { berweisungCmd.ExecuteAction += logik.berweisungAktivieren; logik.berweisungAktiviert += berweisungCmd.SetCanExecute; } }

www.dotnetpro.de dotnetpro.dojos.2011

85

GRUNDLAGEN
in einem Datenfluss stehen. Fr die WPFSeite ist ICommand zustndig, fr die EBCSeite IFlowCommand. Fr das Data Binding ist es sinnvoll, die Kommandos mit im ViewModel abzulegen. So kann der DataContext der Form fr alle Bindings verwendet werden. Das ViewModel fr ein einfaches Beispiel zeigt Listing 6. Auf diese Weise knnen in der XAMLDatei die Eigenschaften berweisungCmd, Kontonummer und KontonummerEnabled gebunden werden. Auf der EBC-Seite werden View und Logik sowie das Kommando in eine Platine injiziert. Die Platine kann dann die Datenflsse 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 Funktionalitt ausfhren. Durch die Verbindung von berweisungAktiviert zu SetCanExecute wird nach Ausfhrung 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 abhngig vom ViewModel. Dies ist notwendig, damit die View per Data Binding auf das ViewModel zugreifen kann. Auch die Logik ist vom ViewModel abhngig, damit sie Daten zur Visualisierung im ViewModel ablegen und Benutzereingaben von dort entnehmen kann. Zustzlich zu den Abhngigkeiten steht ein Teil des ViewModels, nmlich die darin enthaltenen Kommandos, im Flow. Dabei knnen Datenflsse zum einen von Kommandos ausgehen, um eine Benutzerinteraktion zu signalisieren. Zum anderen knnen 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 Datenflssen basieren, nichts mehr im Weg. So knnen Sie die tollen Mglichkeiten des Data Bindings voll ausschpfen. Und natrlich knnen so auch die tollen Mglichkeiten des Flow-Designs und der Umsetzung mit Event-Based Components zum Einsatz kommen. [ml]

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);

[1] Torsten Zimmermann, Ein Rahmen fr Feinheiten, Die Funktionsweise von MVVM-Frameworks fr WPF, dotnetpro 1/2011, Seite 14 ff., www.dotnetpro.de/A1101FrameworkTest [2] Torsten Zimmermann, Geschickt verbunden, Microsofts Framework fr 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 gengend spekuliert. Es wird Zeit, sie konkret anzuwenden. Stefan, kannst du zu diesem wolkigen Thema eine mglichst handfeste bung stellen?

dnpCode: A1105DojoAufgabe
ie Idee zu dieser bung hngt mit der Aufgabe im vorhergehenden Heft zusammen, deren Lsung auf den folgenden Seiten prsentiert 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 groen Vorteil, dass sie auch dann zur Verfgung stehen, wenn das entsprechende Gert mal gerade nicht mit dem Internet verbunden ist. Dennoch besteht oft der Wunsch, die Daten ber die Cloud mit einem anderen Gert zu synchronisieren. Diese Mglichkeit fehlt mir beispielsweise bei Things [1], einer To-do-Listenanwendung fr 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 wre aber eine Synchronisation ber einen Service in der Cloud: Dann knnte die Software im Hintergrund selbststndig synchronisieren, und zwar unabhngig davon, ob die betreffenden Gerte gerade eingeschaltet sind oder nicht. berschreiben. Findet ein Client in der Cloud neue oder genderte Daten, mssen diese in den lokalen Speicher eingepflegt werden. Ferner muss der Client seine lokalen nderungen an den Cloudspeicher melden. Natrlich knnen beim Synchronisieren Konflikte entstehen. Das passiert, wenn beide Clients dieselben Daten ndern und dann synchronisieren. Solche Konflikte sollten mindestens erkannt werden. Die Lsung des Konflikts knnte darin bestehen, den Anwender entscheiden zu lassen, welchen Stand er als aktuell akzeptieren mchte. Treten solche Konflikte hufig auf, knnte die Lsung darin bestehen, die nderungen zusammenzufassen. Dies ist allerdings relativ aufwendig und vor allem nicht generisch lsbar. Es wird hier nicht weiter betrachtet. Als Cloudspeicher kann beispielsweise Amazon SimpleDB [2] zum Einsatz kommen. Dieser Service bietet ausreichend kostenlose Kapazitten, um damit zu experimentieren. Mit dem Open-Source-Framework Simple Savant [3] steht ferner ein einfach zu bedienendes API zur Verfgung. Wer mag, kann auch eine Lsung mit Windows Azure versuchen [4]. Bleibt am Ende die Frage, wie man die Synchronisation algorithmisch lst. Klar ist, dass jeder Datensatz ber eine eindeutige ID verfgen 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. Fr die Synchronisation der nderungen sind Flle 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, bentigt man eine Versionsnummer der Daten. Viel Spa beim Tfteln! [ml]

In jeder dotnetpro finden Sie eine bungsaufgabe von Stefan Lieser, die in maximal drei Stunden zu lsen sein sollte. Wer die Zeit investiert, gewinnt in jedem Fall wenn auch keine materiellen Dinge, so doch Erfahrung und Wissen. Es gilt : T Falsche Lsungen gibt es nicht. Es gibt mglicherweise elegantere, krzere oder schnellere Lsungen, aber keine falschen. T Wichtig ist, dass Sie reflektieren, was Sie gemacht haben. Das knnen Sie, indem Sie Ihre Lsung mit der vergleichen, die Sie eine Ausgabe spter in dotnetpro finden. bung macht den Meister. Also los gehts. Aber Sie wollten doch nicht etwa sofort Visual Studio starten

Durch die Wolke stechen


Doch wie schwierig ist die Realisierung einer solchen Synchronisation? Das herauszufinden ist die bung fr 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 wre. Um Daten ber die Cloud zu synchronisieren, bentigt man einen Datenspeicher in der Cloud. Dieser ist von allen Clients aus erreichbar und kann somit verwendet werden, um darber 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

[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/

www.dotnetpro.de dotnetpro.dojos.2011

87

Wer bt, gewinnt

LSUNG
Synchronisation ber die Cloud

Nicht ohne meine Wolke


Seine Daten will man am liebsten berall von verschiedenen Gerten aus verfgbar 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, hlt zahlreiche Vortrge und hat gemeinsam mit Ralf Westphal die Initiative Clean Code Developer ins Leben gerufen. Sie erreichen ihn unter stefan@lieser-online.de oder lieser-online.de/blog.

ie Synchronisierung von Daten wird immer wichtiger. Immer mehr Anwender nutzen Smartphones und Tablets, die ihrerseits immer leistungsfhiger werden. Damit wchst der Wunsch, alle relevanten Daten auch offline, also ohne Verbindung zum Internet, auf dem Gert zur Verfgung zu haben. Aber natrlich mssen die Daten mit anderen Gerten synchronisiert werden, da die meisten Nutzer von Smartphones wohl zustzlich einen Arbeitsplatzrechner und/oder ein Notebook verwenden. Ein zweiter Trend verstrkt die Nachfrage nach Synchronisationslsungen: die Cloud. Dienste in der Cloud werden ebenfalls immer leistungsfhiger, gleichzeitig sinken die Preise. Was liegt also nher, als die Synchronisation der Daten ber die Cloud auszufhren? Die Idee dabei: Alle beteiligten Gerte synchronisieren sich mit einem Dienst, der in der Cloud luft, siehe Abbildung 1. Dadurch muss man nicht mehr zwei Gerte miteinander verbinden, um Daten zu synchronisieren. Denn das ist lstig. Wenn ich mit dem Notebook arbeite, mchte ich alle nderungen von dort in die Cloud synchronisieren. Anschlieend 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 Gert auch mal ein lngerer Zeitraum vergehen. Das ist komfortabler, als wenn man zum Synchronisieren beide Gerte gleichzeitig verfgbar haben muss. Damit sich die Gerte synchronisieren knnen, mssen die Daten ber die Cloud erreich-

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 erfllen. Damit die Lsung hier nicht zu umfangreich ausfllt, 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 zustzlicher Webservice entfllt. Die Clients greifen zum Synchronisieren direkt auf den Speicherservice in der Cloud zu.

Amazon SimpleDB
Wie in der Aufgabenstellung angedeutet, setzte ich bei meiner Lsung auf Amazon SimpleDB. Amazon bietet kostenlos ein ausreichend groes monatliches Kontingent an, sodass bei den Experimenten keine Kosten anfallen. Man muss sich lediglich fr 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 Datenstze einen eindeutigen Identifier bentigen. Und natrlich ist

[Abb. 1] Daten ber einen Cloud-Service synchronisieren.

[Abb. 2] Synchronisieren als Flow.

88

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
es naheliegend, dazu den .NET-Datentyp Guid zu verwenden. Der Vorteil: Das Erzeugen einer ID fr einen neuen Datensatz kann dezentral erfolgen, da der Guid-Algorithmus sicherstellt, dass die generierten IDs eindeutig sind. Fr den Synchronisationsvorgang habe ich die einzelnen zu synchronisierenden Datenstze zustzlich 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 erhht wird. Nur beim Speichern in der Cloud darf die Versionsnummer verndert werden! So knnen Clients leicht feststellen, ob in der Cloud eine neuere Version eines Datensatzes existiert. Allerdings gengt die Versionsnummer noch nicht, um alle mglichen Flle abzudecken. Zustzlich muss jeder Client an den Datenstzen festhalten, ob sie lokal gendert wurden. Sind die Versionsnummern nmlich gleich und liegen lokale nderungen vor, mssen diese zur Cloud bertragen werden. Alle Szenarien sind in der Tabelle 1 abgebildet. Die beiden ersten Flle sind einfach: Wenn Daten lokal, aber nicht entfernt vorhanden sind, muss der Datensatz in die Cloud bertragen und dort eingefgt werden. Sind die Daten umgekehrt nur in der Cloud, aber nicht lokal vorhanden, mssen sie lokal eingefgt werden. Der dritte Fall liegt vor, wenn Daten lokal gendert wurden. Beide Versionsnummern sind gleich, das heit, lokal lag vor den nderungen der Stand vor, der derzeit in der Cloud liegt. Daher mssen die lokalen nderungen in die Cloud bertragen werden. Wurden Daten auf einem anderen Client gendert und in die Cloud bertragen, liegt der vierte Fall vor: Die entfernte Versionsnummer ist hher als die lokale, und es gibt lokal keine nderungen. In diesem Fall werden die nderungen aus der Cloud lokal bernommen. Im fnften 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,
[Tabelle 1] Mgliche Flle bei der Synchronisation.

die entfernten daran, dass entfernt eine hhere Versionsnummer als lokal vorliegt. In diesem Konfliktfall mssen die nderungen zusammengefhrt werden, sofern dies mglich ist. Im einfachsten Fall wird der Anwender informiert und um eine Entscheidung gebeten, welche Daten herangezogen werden sollen. Zuletzt bleiben noch zwei Flle in der Tabelle brig, die nicht eintreten knnen, wenn alles richtig implementiert ist: Die lokale Versionsnummer ist grer als die entfernte. Dieser Fall kann nicht eintreten, sofern die Versionsnummer nur beim Spei-

chern von Daten auf dem entfernten System erhht wird.

Lschen
Die Tabelle sieht einfach und bersichtlich aus, die Implementation drfte eigentlich keine Schwierigkeiten bereiten. Doch der Teufel steckt im Detail: Das Verfahren eignet sich nicht fr das Synchronisieren von Lschungen. berhaupt ist das Lschen von Daten die grte Herausforderung beim Synchronisieren. Dazu ein Beispiel: Zunchst 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>

www.dotnetpro.de dotnetpro.dojos.2011

89

LSUNG
Listing 2 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); } }

chronisieren entfernt werden, wenn sichergestellt ist, dass diese Daten beim Synchronisieren von der Cloud zum Client nicht wieder auf dem Client angelegt werden.

Show me your Code!


Doch nun zur Implementation. Zunchst 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 anschlieend 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 reprsentieren Datenflsse, Abhngigkeiten sind durch eine Verbindungslinie mit einem Punkt am Ende dargestellt. Die Abbildung ist aus einer XMLDatei generiert. Gleichzeitig werden aus dieser Datei die Interfaces fr 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 Ladevorgnge werden durch einen Join-Baustein zu einem Tuple zusammengefasst. Ganz wichtig bei diesem Join: Erst wenn beide Eingnge ber Daten verfgen, wird der Datenstrom am Ausgang in Gang gesetzt. Bei einem erneuten Durchlaufen der Synchronisation mssen wieder beide Eingnge des Joins anliegen. Daher wird hier ein ResetJoin-Baustein aus dem ebclangProjekt verwendet. Bei diesem mssen jedes Mal beide Eingnge 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 Abhngigkeit des Bauteils ToDo_Listen_synchronisieren von SharedState<ToDoListe>. Diese Abhngigkeit ist im Kontext der ToDo-Listenanwendung erforderlich, damit das Modell der ToDo-Liste im Speicher aktualisiert wird. Dieses Modell reprsentiert den gesamten Zustand der Anwendung.

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 Verfgung. Wird er nun lokal gelscht und anschlieend synchronisiert, landen wir bei Fall zwei der Tabelle: Der Datensatz wird aus der Cloud wieder zum Client bertragen, das Lschen damit rckgngig gemacht. Ich habe die Synchronisation in die ToDo-Listenanwendung integriert, die ich fr die bung aus dem vorhergehenden Heft erstellt habe. Fr das Synchronisieren der Lschungen habe ich die Anwendung zunchst so umgestellt, dass das Lschen nicht hart auf den Daten ausgefhrt wird, sondern nur soft, indem ein Marker in den Daten gesetzt wird. BeimVisualisieren ignoriert der Client alle als gelscht markierten Daten. Allerdings werden die gelschten Daten weiterhin in der Datendatei abge-

legt. Damit ist das Problem der Lschsynchronisation auf das normale Synchronisieren von nderungen zurckgefhrt. Auf dem Client fhrt beim Synchronisieren von Lschungen ohnehin kein Weg daran vorbei, ber die Lschungen bis zum nchsten Synchronisieren Buch zu fhren. Insofern ist das softe Lschen eine gute Vorbereitung fr eine leistungsfhigere Implementation. Schlielich wrden sich im Laufe der Zeit einige Daten ansammeln, wenn die als gelscht markierten Daten nie entfernt wrden. In der vorliegenden Implementation werden einfach alle als gelscht markierten Daten dauerhaft gespeichert und synchronisiert. In einem spteren Schritt kann man dies so erweitern, dass die als gelscht markierten Daten nur in der Cloud gehalten werden. Auf den Clients knnten die Lschungen nach dem Syn-

Bauteile implementieren
Nachdem das Flow-Design der Synchronisation erstellt war, habe ich begonnen, die

90

6. 2011 www.dotnetpro.de

LSUNG
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 Lsung der ToDo-Listenanwendung im vorhergehenden Heft studiert hat, sollte sich die aktuelle Version der Anwendung noch mal anschauen es hat sich einiges gendert. Zur Implementation standen also an: T Laden und Speichern der entfernten ToDo-Liste in einer SimpleDB-Datenbank in der Cloud. T Das eigentliche Synchronisieren zweier Listen.

den sollen, siehe Listing 4. Anschlieend knnen 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 fr 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); }

ber den Wolken


Beginnen wir beim Laden und Speichern in der Cloud. Um Amazons SimpleDB nutzen zu knnen, 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. Natrlich haben solch vertrauliche Daten nichts im Quellcode zu suchen, sondern mssen in eine Konfigurationsdatei ausgelagert werden. Und tun Sie sich gleich den Gefallen, den Dateinamen in die IgnoreListe der Versionsverwaltung aufzunehmen. Sonst erhht sich das Risiko, dass Ihre Zugangsdaten pltzlich nicht mehr so geheim sind, wie sie es verdient haben. Fr 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-Datenstzen abgelegt wer-

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(); }

www.dotnetpro.de 6. 2011

91

LSUNG
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() { 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} )); } }

Synchronisieren
Das Synchronisieren arbeitet auf zwei Listen. Eine enthlt den lokalen Stand der Daten, die andere den Stand der Daten in der Cloud. Um die Listen zu synchronisieren, mssen immer je zwei zusammengehrige Eintrge verglichen werden. Dabei sind zunchst drei Flle zu unterscheiden: T ein Datensatz ist nur lokal vorhanden, T ein Datensatz ist nur entfernt vorhanden, T 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.

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 Datenstzen zu bilden. Dabei werden jeweils die Datenstze aus der lokalen und entfernten Liste mit gleicher ID zusammengestellt. Der dritte Schritt (GetSyncActions) ermittelt zu jedem der Paare die Aktion, die auszufhren ist. Dabei werden die in der Tabelle gezeigten Regeln umgesetzt. Zuletzt (GetRe-

sult) werden die ermittelten Aktionen auf das zugehrige 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 gelst, 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 gengen dann wenige Integrationstests, die den Datenfluss vollstndig durchlaufen.

[Abb. 3] Zwei Listen synchronisieren.

92

dotnetpro.dojos.2011 www.dotnetpro.de

LSUNG
In den einzelnen Methoden habe ich LINQ wieder einmal schtzen 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 spter 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 fr das Ermitteln der Synchronisationsaktion. Die Umsetzung besteht dann nur aus ein paar Bedingungen, die in der richtigen Reihenfolge berprft werden mssen, siehe Listing 9. Fr das Zusammenstellen der Ergebnisliste mssen nun die Aktionen jeweils pro Datenpaar ausgefhrt werden, siehe Listing 10. Je nach ermittelter Aktion wird der lokale oder der entfernte Datensatz in die Ergebnisliste bernommen. Synchronisationskonflikte habe ich hier nicht bercksichtigt, da dazu eine Interaktion mit dem Benutzer erforderlich ist. Die Konflikte mssten also vor dem Bilden der Ergebnisliste behandelt werden. Doch die bung ist ohnehin wieder etwas lnglich geraten, daher habe ich die Konfliktbehandlung weggelassen.

Listing 9 Datenstze 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; } } }

Fazit
Das Synchronisieren von Datenbestnden mehrerer Clients mittels eines Cloud-Speichers ist kein Hexenwerk. Die Lsung 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. Fr den Einsatz einer Synchronisation in einem Produkt will man dem Benutzer sicher auch nicht zumuten, seine AmazonSimpleDB-Credentials zurVerfgung zu stellen. Das heit, dass man einen Webservice ergnzen msste, ber den die Synchronisation erfolgt. Auch das ist nicht mit Zauberei verbunden. Ich frage mich also am Ende weiterhin, wieso einzelne Produkte trotz lange zurckliegender Ankndigung immer noch keine Cloud-Synchronisation anbieten. An der technischen Herausforderung kann es jedenfalls nicht liegen.

Ein weiterer Aspekt ist mir bei der bung erneut aufgefallen: Durch die Modellierung und Umsetzung mit Flows lsst sich die Anwendung einfach erweitern. Die Wiederverwendung einzelner Bausteine war problemlos mglich, und die Integra-

tion der zustzlichen Funktionalitt war leicht. [ml]

[1] http://aws.amazon.com/de/simpledb [2] http://simol.codeplex.com [3] http://ebclang.codeplex.com

www.dotnetpro.de dotnetpro.dojos.2011

93

Impressum

Imprint Dojos fr Entwickler Stefan Lieser, Tilman Brner published by: epubli GmbH, Berlin, www.epubli.de Copyright: 2012 Stefan Lieser, Tilman Brner

GRUNDLAGEN
Coding Dojo : Mit Spa lernen

Lass uns einen lernen gehen


Auch nach der Ausbildung gehrt Lernen zum Berufsbild des Softwareentwicklers. Aber Lernen kann auch richtig Spa machen: In lockerer Runde eine Programmieraufgabe lsen macht Laune.
as fllt 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 fr unsere Kunden Software schreibe, mag Ihr Kommentar lauten. Das ist aber nicht richtig. Wer lernen will, muss spielen und ausprobieren. Spielen heit im Fall von Programmieren schlicht, sich auch mal an etwas anderem versuchen, was nicht zur tglichen Arbeit gehrt. Bentigen Sie in Ihrer tglichen Arbeit je Asynchronizitt? Oder wie viele Male haben Sie schon die Rx [1] benutzt? Sind Sie fit mit Event-Based Components, oder wissen Sie, wie Sie Code tatschlich 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 fr das Spielen zu geben. Eine Lernbung hingegen sollte vom Umfang berschaubar sein und nicht mehr als ein, zwei oder drei Stunden in Anspruch nehmen.

Kampfsportarten lernt, luft 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 lsen, sondern um das Eruieren von Neuem. Auch der Begriff Dojo kommt aus Fernost. Er bezeichnet den Platz, an dem Kampfsportarten gebt werden. In der Softwareentwicklung ist mit Coding Dojo nicht der Raum an sich gemeint, sondern das Event des Zusammenkommens und Lsens der Aufgabe.

Auf einen Blick

Tilman Brner ist Diplomphysiker und Chefredakteur der dotnetpro. Programmieren ist fr ihn ein kreativer Akt, fr den er leider viel zu wenig Zeit hat.

Es geht los fast


Am Anfang eines Coding Dojos bestimmt die Gruppe, welche Aufgabe gelst werden soll. Sowohl Moderator als auch Teilnehmer knnen Vorschlge unterbreiten. Die Anwesenden einigen sich auf eine Kata, und los gehts. Na ja, noch nicht ganz. Was gern vergessen wird, ist, zu prfen, ob die Kata auch komplett von allen verstanden wurde. Was sind die Anforderungen? Wie sieht beispielsweise ein Use Case aus? Welche Nebenbedingungen sind zu erfllen? Was ist eigentlich zu erzeugen? Ist das Ergebnis eine Klasse oder eine Methode? Erst wenn die Aufgabenstellung klar ist, sollte es losgehen. Und das heit: programmieren nach Test-Driven Development. Das wiederum bedeutet, dass es eine Schnittstelle gibt, deren Funktionalitt ber Unit-Tests getestet wird. Es muss somit keine Kompilierung laufen, nur ein Testframework und ein Testrunner sind ntig. Auch das ist wieder so eine Randbedingung der Aufgabenstellung: Die Kata darf keine Abhngigkeiten haben. Sie muss losgelst getestet werden knnen.

Inhalt
In Coding Dojos lst man gemeinsam eine Programmieraufgabe per TDD. Die populrsten bungsaufgaben vorgestellt. Spa und Spiel gehren unbedingt mit zum Lernen.

dnpCode
A11DOJOKatas

Aufgaben stellen ist nicht einfach


Der gravierendste und am hufigsten von Softwareentwicklern begangene Fehler liegt in folgenden Stzen: Das sind nur ein paar Zeilen Code oder Das haben wir gleich. Meist werden Aufwnde komplett unterschtzt. Mit dem Ausdenken von bungsaufgaben verhlt es sich da nicht anders. Aufgaben zu finden ist doch nicht schwer. Dieser Satz ist falsch, denn es mssen so viele Randbedingungen eingehalten werden: Idee, Machbarkeit, Dauer und so weiter. Finden Sie eine Aufgabe, die in etwa zwei bis drei Stunden lsbar ist, haben Sie eine sogenannte Kata erfunden. Wollen Sie sich die Mhe aber nicht machen, eigene Katas zu erfinden, knnen Sie auch welche aus dem Internet verwenden [2], [3]. Weiter unten listen wir die bekanntesten Katas auf.

Die Spielarten
Es gibt verschiedene Arten von Coding Dojos, ber deren Sinn oder Unsinn schon trefflich gestritten wurde. Oft luft ein Dojo aber so ab: Die Aufgabe wird an einem Rechner gelst, 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 wnscht. In einer anderen Art des Coding Dojos kann jeder, der mchte, fr eine gewisse Zeit am Dojo-

Kata und Dojo


Der Begriff Kata ist der fernstlichen Kampfkunst entlehnt und bedeutet so viel wie bung. Wer

www.dotnetpro.de dotnetpro.dojos.2011

95

GRUNDLAGEN _Coding Dojo : Mit Spa programmieren lernen


[Abb. 1] Coding Dojo auf der .NET DevCon 2011 in Nrnberg mit Ilker Cetinkaya (stehend).

Rechner sitzen. Damit ist er in der Lage, die Aufgabe so zu lsen, wie er mchte. Meist sorgt eine Zeitbegrenzung dafr, dass mglichst viele mal ihre Ideen zeigen knnen. Der Nachteil: Der Nachfolgende kann den Code des Vorgngers lschen 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 berprft. Eine erste Implementierung des System Under Test (SUT) muss dann den ersten Test grn machen. Weitere Anforderungen werden in Form von Unit-Tests beschrieben und das SUT so implementiert, dass die Tests alle grn werden. Ein mglicher Streitpunkt ist, wann eine Refaktorisierung erfolgen soll und ob das berhaupt zu den Aufgaben des Coding Dojos gehrt. Wenn Code nach TDD wchst, empfiehlt es sich nach gewissen Schritten, den Code so umzustrukturieren, dass er wieder lesbarer wird. Aber ist das tatschlich eine Anforderung, die an die Software gestellt wird, oder soll der Code

nur die funktionale Ebene erfllen, und es ist egal, wie er aussieht?

Coding Dojo = Spa + Lernen


Dialog und Diskussion gehren zum Coding Dojo. Aber Vorsicht vor zu langen Diskussionen. Meistens entznden 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 gebt werden muss.

Schlielich ist die vorgegebene Zeit um, und die Aufgabe ist nicht gelst. War das Coding Dojo deshalb ein Misserfolg? Diese Frage kann nur jeder fr sich beantworten. Schlielich 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 verluft.

Die Klassiker : Ausgewhlte 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 wrden dann so aussehen:
_ _ _ _ _ _ _ _ | | | _| _||_||_ |_ ||_||_| |_| ||_ _| | _||_| ||_| _|

Diese Zahl lsst 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 nchsten 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 rechtsbndig ausgerichtet. Das bedeutet, die 1, die ja nur ein Zeichen breit ist, erhlt links noch zwei Leerzeichen. Jede Zahl ist 9 Ziffern breit. Damit ist jede Zahl 27 Zeichen breit und drei Zeilen hoch.

96

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 ntig. Diese Kata lsst sich noch erweitern, etwa indem ber eine Checksumme geprft wird, ob die Zahl in Ordnung ist. Meist reicht aber fr den ersten Teil die Zeit gerade so.

Kata FizzBuzz
Diese Kata geht auf ein Spiel zurck, bei dem die Konzentration eine wichtige Rolle spielt. Eine Gruppe Menschen steht zusammen und zhlt der Reihe nach oben. 1, 2, 3 und so weiter. So weit ist das noch einfach. Jetzt kommt aber die Verschrfung: Ist eine Zahl durch drei teilbar, ruft der Mensch statt des Zahlenwerts Fizz, ist sie durch fnf teilbar, dann Buzz, und ist sie durch drei und fnf teilbar, dann muss derjenige FizzBuzz rufen. Sie drfen sich selbst ausmalen, was derjenige machen muss, der nicht richtig Zahl, Fizz, Buzz oder FizzBuzz ruft.

[Abb. 2] Das Code Kata Manifesto fasst alle wichtigen Regeln fr eine Kata zusammen.

Aufgabe : Schreiben Sie einen Algorithmus, der jedes Mal, wenn er aufgerufen wird, entweder eine Zahl, Fizz, Buzz oder FizzBuzz zurckgibt. Startpunkt soll bei 1 liegen. Eine Folge von Aufrufen gibt also das Folgende zurck: 1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz

nur um zwei verschiedene handelt (also ein Buch kauft er zweimal), erhlt er nur auf die zwei verschiedenen 5 Prozent Rabatt. Das doppelte Buch kostet weiterhin 8 Euro.

Aufgabe : Schreiben Sie einen Algorithmus, der die Zhlweise des Tennis nachahmt. Dabei bekommt Spieler A oder Spieler B den Gewinn des Ballwechsels zugesprochen. Der Algorithmus soll den aktuellen Spielstand zurckgeben.

Kata Potter
In dieser Kata geht es um den Verkauf von Bchern. Genauer gesagt: von Harry-Potter-Bchern. Eine Buchhandlung mchte 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 wre die Kata zwar sehr einfach, die Werbeaktion aber ein Fiasko. Also kommt Rabatt ins Spiel. Wer zwei verschiedene Bcher aus der Reihe erwirbt, erhlt 5 Prozent Rabatt auf die beiden Bcher. Wer drei verschiedene Bcher kauft, erhlt 10 Prozent Rabatt. Bei vier verschiedenen Bchern sind es schon 20 Prozent Rabatt. Kauft ein Kunde fnf verschiedene Bcher aus der Reihe, bekommt er 25 Prozent Rabatt. Knifflig wird es, wenn die Bcher nicht unterschiedlich sind. Wer beispielsweise drei Exemplare kauft, wobei es sich aber

Aufgabe : Schreiben Sie einen Algorithmus, der fr beliebig viele Exemplare den Preis berechnet, wobei der den maximalen Rabatt geben soll. Bei dieser Aufgabe ist es extrem wichtig, sich durch einige Flle mit der Problematik vertraut zu machen. Die Optimierung bezglich geringstem Preis ist nicht so einfach, wie das auf den ersten Blick scheint.

Kata Rmische Zahlen


Das darf nicht fehlen: Wenn es seltsame Zahlensysteme gibt, dann gehrt das rmische mit dazu. Gesehen haben Sie solche Zahlen sicher schon, vor allem als Jahreszahlangabe auf Grbern oder Husern. MCMLI steht etwa fr 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 fr 1 II steht fr 2 III steht fr 3 IV steht fr 4 V steht fr 5 VI steht fr 6 VII steht fr 7 VIII steht fr 8 IX steht fr 9 X fr 10.

Kata Tennis
Tennisspieler sind anders. Sie spielen auf Sand oder Rasen, zu zweit oder zu viert und hauen sich mit Schlgern den Ball gegenseitig um die Ohren. Am seltsamsten ist aber die Zhlweise, die den Punktestand in einem Tennismatch festhlt. 0, 15, 30, 40, Spiel lautet die Zhlweise. Erreichen die Gegner beide den Punktestand 40, entsteht ein sogenannter Einstand. Wer nach dem Einstand einen Ballwechsel fr sich entscheiden kann, erhlt 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 schne Aufgabe fr einen Coding Dojo.

Aufgabe : Schreiben Sie einen Algorithmus, der eine dezimale Zahl als String einer rmischen Zahl zurckgibt. Achten Sie darauf, dass die rmische Zahl auch valide

www.dotnetpro.de dotnetpro.dojos.2011

97

GRUNDLAGEN _Coding Dojo : Mit Spa programmieren lernen


ist. IM beispielsweise fr 999 ist falsch. Richtig wre CMXCIX.

Kata Taschenrechner Rmische Zahlen


Wer konvertieren kann, kann auch rechnen. Somit kann man die Kata Rmische Zahlen so erweitern, dass daraus ein Taschenrechner wird. Aber Vorsicht : Ist das Konvertieren berhaupt ntig ? Oder lsst sich das ganze auch ohne Konversion erledigen. Es gibt nmlich die folgende Erleichterung : Der Taschenrechner soll nur addieren. Subtraktion, Division und Multiplikation sind nicht zu implementieren.

Aufgabe : Schreiben Sie einen Algorithmus, der zwei rmische 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 nchste Generation an Zellen berechnet. Welche Zelle in der nchsten 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 nchste Generation: 1. Jede Zelle, die weniger als zwei Nachbarn hat, stirbt wegen Vereinsamung. 2. Jede Zelle, die mehr als drei Nachbarn hat, stirbt wegen berbevlkerung. 3. Jede Zelle, die zwei oder drei Nachbarn hat, lebt auch in der nchsten Generation weiter. 4. Jede tote Zelle mit genau drei Nachbarn wird reanimiert und lebt in der nchsten Generaton wieder. Besonders zu beachten ist die Bedingung 4 : Wir werden zu Frankensteins, die Leben erschaffen knnen. Hier ein Beispiel fr drei aufeinanderfolgende Generationen. Drei Zellen in einer Reihe werden zu einem Oszillator, der von einer Generation zur nchsten die Ausrichtung von vertikal zu horizontal und wieder zurck bildet. Generation 1 ........ ....*... ....*... ....*...

[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 ........ ....*... ....*... ....*...

so lange probieren, bis Sie die richtige Kombination gefunden haben. Wre das Wort etwa ei, dann ist das schnell erledigt, denn es gibt nur noch ie als Permutation. Bei ein sind es schon sechs mgliche : ein eni ine ien nei nie

Aufgabe : Schreiben Sie einen Algorithmus, der aus einem zweidimensionalen Array mit lebenden und toten Zellen auf Basis der vier Regeln die nchste Generation berechnet. Der Algorithmus soll ein zweidimensionales Array zurckgeben, das dann wieder zur Berechnung der nchsten Generation verwendet werden kann. Hinweis : Teilen Sie die Randbereiche ab. Diese bedrfen einer eigenen Behandlung.

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 wchst mit der Fakultt der Zahl der Buchstaben. Also sind es bei einem Wort mit vier Buchstaben schon 24 mgliche Wrter. [tib] [1] http://msdn.microsoft.com/enus/data/gg577609 [2] http://codingkata.org/ [3] http://codingdojo.org/ [4] ilker.de/code-kata-pickakin

Kata Anagram
Gegeben sei ein Wort, das der Schlssel zum Schloss eines sagenhaften Schatzes ist. Nur leider sind die Buchstaben dieses Wortes durcheinandergeraten. Sie mssen

98

dotnetpro.dojos.2011 www.dotnetpro.de

#1

dotnetpro.de facebook.de/dotnetpro twitter.com/dotnetpro_mag gplus.to/dotnetpro