15.02.2013 Aufrufe

b2aat6n

b2aat6n

b2aat6n

MEHR ANZEIGEN
WENIGER ANZEIGEN

Sie wollen auch ein ePaper? Erhöhen Sie die Reichweite Ihrer Titel.

YUMPU macht aus Druck-PDFs automatisch weboptimierte ePaper, die Google liebt.

Stefan Lieser, Tilman Börner<br />

Dojos<br />

für EntwicklEr<br />

15 Aufgaben und Lösungen in .NET<br />

#1


E<br />

in Profimusiker übt täglich mehrere<br />

Stunden. Er übt Fingerfertigkeit,<br />

Phrasierung, Ansatz beziehungsweise<br />

Haltung, Intonation und Vom-<br />

Blatt-Spielen. Als Hilfsmittel verwendet er Tonleitern,<br />

Etüden, Ausschnitte von Stücken und<br />

Unbekanntes. Ohne Üben könnte er die Qualität<br />

seines Spiels nicht halten, geschweige denn verbessern.<br />

Üben gehört für ihn dazu.<br />

Wie sieht das bei Ihnen und der Programmiererei<br />

aus? Sie sind doch auch Profi. Nicht in der<br />

Musik, aber doch beim Codieren an der Computertastatur.<br />

Üben Sie auch? Gemeint ist nicht die<br />

Aufführung, sprich das Program-mieren, mit dem<br />

Sie sich Ihr Einkommen verdienen. Gemeint sind<br />

die Etüden, das Üben von Fingerfertigkeit, Intonation,<br />

Ansatz und Vom-Blatt-Spielen.<br />

Wie sehen diese Aufgaben denn bei einem<br />

Programmierer aus? Freilich ließe sich die Analogie<br />

bis zum Abwinken auslegen. Hier mag ein<br />

kleiner Ausschnitt genügen: Sie könnten als Etüde<br />

zum Beispiel trainieren, dass Sie immer erst<br />

den Test schreiben und dann die Implementation<br />

der Methode, die den Test erfüllt. Damit verwenden<br />

Sie künftig nicht immer wieder den falschen<br />

Fingersatz, sondern immer gleich die richtige<br />

Reihenfolge: Test – Implementation.<br />

Klar, Üben ist zeitraubend und manchmal<br />

nervtötend – vor allem für die, die zuhören.<br />

Aber Üben kann auch Spaß machen. Kniffeln,<br />

eine Aufgabe lösen und dann die eigene Lösung<br />

mit einer anderen Lösung vergleichen. Das ist der<br />

Grundgedanke beim dotnetpro.dojo. In jeder<br />

Ausgabe stellt dotnetpro eine Aufgabe, die in maximal<br />

drei Stunden zu lösen sein sollte. Sie investieren<br />

einmal pro Monat wenige Stunden und ge-<br />

EINLEITUNG<br />

Wer übt, gewinnt<br />

Wer übt, gewinnt<br />

winnen dabei jede Menge Wissen und Erfahrung.<br />

Den Begriff Dojo hat die dotnetpro nicht erfunden.<br />

Dojo nennen die Anhänger fernöstlicher<br />

Kampfsportarten ihren Übungsraum. Aber auch<br />

in der Programmierung hat sich der Begriff eines<br />

Code Dojo für eine Übung eingebürgert.<br />

Das können Sie gewinnen<br />

Der Gewinn lässt sich in ein Wort fassen: Lernen.<br />

Das ist Sinn und Zweck eines Dojo. Sie können/<br />

dürfen/sollen lernen. Einen materiellen Preis loben<br />

wir nicht aus.<br />

Ein dot-netpro.dojo ist kein Contest. Dafür gilt<br />

aber:<br />

❚ Falsche Lösungen gibt es nicht. Es gibt möglicherweise<br />

elegantere, kürzere oder schnellere,<br />

aber keine falschen.<br />

❚ Wichtig ist, dass Sie reflektieren, was Sie gemacht<br />

haben. Das können Sie, indem Sie Ihre<br />

Lösung mit der vergleichen, die Sie eine Ausgabe<br />

später in der dotnetpro finden.<br />

Wer stellt die Aufgabe? Wer liefert die<br />

Lösung?<br />

Die kurze Antwort lautet: Stefan Lieser. Die lange<br />

Antwort lautet: Stefan Lieser, seines Zeichens<br />

Mitinitiator der Clean Code Deve-loper Initiative.<br />

Stefan ist freiberuflicher Trainer und Berater<br />

und Fan von intelligenten Entwicklungsmethoden,<br />

die für Qualität der resultierenden Software<br />

sorgen. Er denkt sich die Aufgaben aus und gibt<br />

dann auch seine Lösung zum Besten. Er wird<br />

auch mitteilen, wie lange er gebraucht und wie<br />

viele Tests er geschrieben hat. Das dient – wie<br />

oben schon gesagt – nur als Anhaltspunkt. Falsche<br />

Lösungen gibt es nicht.<br />

Der Spruch „Übung<br />

macht den Meister“<br />

ist abgedroschen,<br />

weil oft bemüht,<br />

weil einfach richtig.<br />

Deshalb finden<br />

Sie in diesem<br />

Sonderheft 15<br />

dotnetpro.dojos, also<br />

Übungsaufgaben<br />

inklusive einer<br />

Musterlösung und<br />

Grundlagen.<br />

www.dotnetpro.de dotnetpro.dojos.2011 3


INHALT<br />

15 Aufgaben und Lösungen<br />

5 Aufgabe 1:Vier gewinnt<br />

Ein Spielfeld, zwei Spieler und jede Menge Spaß beim<br />

Programmieren: Das kleine Brettspiel ist genau das Richtige<br />

zum Warmwerden.<br />

9 Aufgabe 2:Data Binding<br />

Knüpfe Kontrollelement an Eigenschaft, und schon wirkt<br />

der Zauber: Veränderungen der Eigenschaft spiegeln sich im<br />

Control wider und auch andersherum.<br />

14 Aufgabe 3:Testdatengenerator<br />

Meier, Müller, Schulze – ganze 250000 Mal: Für einen<br />

Testdatengenerator ist das eine Sache von Sekunden. Aber<br />

wie baut man einen solchen?<br />

22 Aufgabe 4:Mogeln mit EVA<br />

Statt Rein-Raus-Kaninchentechnik die Eingabe,<br />

Verarbeitung, Ausgabe: modernste Technik im Dienst des<br />

Mogelns beim Minesweeper-Spiel. Na super.<br />

26 Aufgabe 5:Boxplot<br />

Packen Sie den Sandsack wieder weg: nicht Box, platt,<br />

sondern Boxplot: Diese spezielle Grafikform zeigt kleinsten<br />

und größten Wert, Mittelwert und die Quartile.<br />

31 Aufgabe 6:RavenDB<br />

Computer aus, Daten weg? Von wegen: Eine Persistenzschicht<br />

sorgt für deren Überleben. Mit RavenDB braucht<br />

man dafür auch keinen SQL-Server.<br />

38 Aufgabe 7:Stack und Queue<br />

Wie bitte? Stack und Queue bietet doch das .NET<br />

Framework. Stimmt. Aber die Selbstimplementierung bringt<br />

viel Selbsterkenntnis. Sie werden es sehen.<br />

44 Aufgabe 8:Windows-Dienst<br />

Er arbeitet im Verborgenen, im Untergrund. Ist aber so<br />

wichtig, dass auf ihn nicht verzichtet werden kann. Bauen<br />

Sie doch mal einen.<br />

50 Aufgabe 9:Event-Based Components<br />

Was, bitte schön, hat Silbentrennung mit EBC zu tun?<br />

Erst einmal gar nichts. Es sei denn, die Aufgabe lautet:<br />

Baue Silbentrennservice mit EBCs.<br />

56 Aufgabe 10:ITree<br />

Ich bau ’nen Baum für dich. Aus Wurzel, Zweig und Blatt<br />

und den Interfaces ITree und INode. Und Sie dürfen<br />

ihn erklettern.<br />

61 Aufgabe 11:LINQ<br />

Frage: Wie heißt die bekannteste Abfragesprache? Richtig:<br />

SQL. Aber in dieser Aufgabe geht es um eine andere:<br />

Language Integrated Query.<br />

66 Aufgabe 12: Twitter<br />

Es treten auf: mehrere Threads, eine Synchronisation, ein<br />

Timer, ein Control – wahlweise in WPF-, Windows-Forms- oder<br />

Silverlight-Qualität – und ein API. Fertig ist das Twitter-Band.<br />

71 Aufgabe 13:Graphen<br />

Entwerfen Sie ein API für den Umgang mit gerichteten<br />

Graphen, implementieren Sie die Datenstruktur und einen<br />

beliebigen Algorithmus dazu, wie etwa topologische<br />

Sortierung. Und los.<br />

77 Aufgabe 14:ToDo, MVVM und Datenfluss<br />

Am Ende haben Sie eine nützliche ToDo-Listen-Anwendung.<br />

Am Anfang haben Sie ein Problem: Wie modellieren Sie die<br />

Softwarearchitektur? Aber nur Mut: Auch das klappt.<br />

87 Aufgabe 15:ToDo und die Cloud<br />

Die ToDo-Listen-Anwendung soll jetzt noch richtig cool<br />

werden: durch eine Synchronisation über die Cloud. Ein<br />

bisschen Hirnschmalz ...<br />

Grundlagen<br />

82 MVVM und EBC<br />

Model View ViewModel und Event-Based Components: Das<br />

sind zwei aktuelle Technologien, die sich aber gut miteinander<br />

kombinieren lassen. Stefan Lieser zeigt, wie das geht.<br />

95 Klassische Katas<br />

Sie heißen Kata Potter, Kata BankOCR oder Kata FizzBuzz:<br />

An klassischen Programmieraufgaben gibt es inzwischen<br />

schon ganze Kataloge. Tilman Börner stellt die wichtigsten vor.<br />

Impressum<br />

94 Impressum<br />

4 dotnetpro.dojos.2011 www.dotnetpro.de


K<br />

lar, können wir machen. Wie<br />

wäre es beispielsweise mit dem<br />

Spiel 4 gewinnt? Bei dieser Aufgabe<br />

geht es vor allem um eine<br />

geeignete Architektur und die Implementierung<br />

der Logik und nicht so sehr um eine<br />

schicke Benutzeroberfläche.<br />

4 gewinnt wird mit einem aufrecht stehenden<br />

Spielfeld von sieben Spalten gespielt. In<br />

jede Spalte können von oben maximal sechs<br />

Spielsteine geworfen werden. Ein Spielstein<br />

fällt nach unten, bis er entweder auf den Boden<br />

trifft, wenn es der erste Stein in der Spalte<br />

ist, oder auf den schon in der Spalte liegenden<br />

Steinen zu liegen kommt. Die beiden<br />

Spieler legen ihre gelben beziehungsweise<br />

roten Spielsteine abwechselnd in das<br />

Spielfeld. Gewonnen hat der Spieler, der zuerst<br />

vier Steine direkt übereinander, nebeneinander<br />

oder diagonal im Spielfeld platzieren<br />

konnte.<br />

Implementieren Sie ein Spiel …<br />

Ein Spiel, das zwei Spieler gegeneinander<br />

spielen. Die Implementierung soll die Spielregeln<br />

überwachen. So soll angezeigt werden,<br />

welcher Spieler am Zug ist (Rot oder Gelb).<br />

Ferner soll angezeigt werden, ob ein Spieler<br />

gewonnen hat. Diese Auswertung erfolgt<br />

nach jedem Zug, sodass nach jedem Zug angezeigt<br />

wird, entweder welcher Spieler an<br />

der Reihe ist oder wer gewonnen hat. Hat ein<br />

Spieler gewonnen, ist das Spiel zu Ende und<br />

kann neu gestartet werden.<br />

Damit es unter den Spielern keinen Streit<br />

gibt, werden die Steine, die zum Gewinn führten,<br />

ermittelt. Bei einer grafischen Benutzeroberfläche<br />

könnten die vier Steine dazu farblich<br />

markiert oder eingerahmt werden. Bei<br />

einer Konsolenoberfläche können die Koordinaten<br />

der Steine ausgegeben werden.<br />

Die Bedienung der Anwendung erfolgt so,<br />

dass der Spieler, der am Zug ist, die Spalte angibt,<br />

in die er einen Stein werfen will. Dazu<br />

sind die Spalten von eins bis sieben nummeriert.<br />

Bei einer grafischen Benutzeroberfläche<br />

können die Spalten je durch einen Button<br />

gewählt werden. Wird das Spiel als Konsolenanwendung<br />

implementiert, genügt die<br />

Eingabe der jeweiligen Spaltennummer per<br />

Tastatur.<br />

AUFGABE<br />

„Stefan, vielleicht sollten wir erst einmal mit etwas Einfacherem<br />

anfangen. Vielleicht wäre ein kleines Spiel zum Warmwerden genau<br />

das Richtige. Fällt dir dazu eine Aufgabe ein?“<br />

Die Abbildungen 1 und 2 zeigen, wie eine<br />

Oberfläche aussehen könnte. Ist die Spalte,<br />

in die der Spieler seinen Stein legen möchte,<br />

bereits ganz mit Steinen gefüllt, erfolgt eine<br />

Fehlermeldung, und der Spieler muss erneut<br />

einen Spielstein platzieren.<br />

Programmieraufgabe<br />

Die Programmieraufgabe lautet, ein Spiel<br />

4 gewinnt zu implementieren. Dabei liegt der<br />

Schwerpunkt auf dem Entwurf einer angemessenen<br />

Architektur, der Implementierung<br />

der Spiellogik und zugehörigen automatisierten<br />

Tests.<br />

Die Benutzerschnittstelle des Spiels steht<br />

eher im Hintergrund. Ob animierte WPF-<br />

Oberfläche, WinForms, ASP.NET oder Konsolenanwendung,<br />

das ist nicht wichtig. Im Vordergrund<br />

soll eine Lösung stehen, die leicht<br />

in eine beliebige Oberflächentechnologie integriert<br />

werden kann. Evolvierbarkeit und<br />

Korrektheit sollen hier also stärker bewertet<br />

werden als eine superschicke Oberfläche.<br />

Im nächsten Heft zeigen wir eine exemplarische<br />

Musterlösung.„Die“ Lösung kann es in<br />

einem solchen Fall bekanntlich eh nicht geben.<br />

Damit möchte ich Sie, lieber Leser, noch<br />

mal ermutigen, sich der Aufgabe anzunehmen.<br />

Investieren Sie etwas Zeit, und erarbeiten<br />

Sie eine eigene Lösung. Die können Sie<br />

dann später mit der hier vorgestellten vergleichen.<br />

Viel Spaß!<br />

[Abb. 1 und 2]<br />

Eine mögliche<br />

Oberfläche<br />

(links) und die<br />

Anzeige der<br />

siegreichen vier<br />

Steine (rechts).<br />

Aber auf die<br />

Oberfläche<br />

kommt es bei<br />

dieser Übung<br />

nicht an.<br />

Wer übt, gewinnt<br />

In jeder dotnetpro finden Sie<br />

eine Übungsaufgabe von<br />

Stefan Lieser, die in maximal<br />

drei Stunden zu lösen sein<br />

sollte. Wer die Zeit investiert,<br />

gewinnt in jedem Fall – wenn<br />

auch keine materiellen Dinge,<br />

so doch Erfahrung und Wissen.<br />

Es gilt:<br />

❚ Falsche Lösungen gibt es<br />

nicht. Es gibt möglicherweise<br />

elegantere, kürzere<br />

oder schnellere Lösungen,<br />

aber keine falschen.<br />

❚ Wichtig ist, dass Sie reflektieren,<br />

was Sie gemacht<br />

haben. Das können Sie,<br />

indem Sie Ihre Lösung mit<br />

der vergleichen, die Sie eine<br />

Ausgabe später in der dotnetpro<br />

finden.<br />

Übung macht den Meister.<br />

Also − los geht’s. Aber Sie<br />

wollten doch nicht etwa<br />

sofort Visual Studio starten …<br />

www.dotnetpro.de dotnetpro.dojos.2011 5


LÖSUNG<br />

Eine Übung, bei der Sie nur gewinnen konnten<br />

Vier gewinnt. Eine Lösung.<br />

Die Aufgabe war, das Spiel „Vier gewinnt“ zu implementieren. Auf den ersten Blick ist das eine eher leichte Übung.<br />

Erst bei genauerem Hinsehen erkennt man die Schwierigkeiten. Wie zerlegt man beispielsweise die Aufgabenstellung,<br />

um überschaubare Codeeinheiten zu erhalten?<br />

Leser, die sich der Aufgabe an -<br />

genommen haben, ein Vier-gewinnt-Spiel<br />

zu implementieren<br />

[1], werden es gemerkt haben:<br />

Der Teufel steckt im Detail. Der Umgang mit<br />

dem Spielfeld, das Erkennen von Vierergruppen,<br />

wo soll man nur anfangen? Wer zu<br />

früh gezuckt hat und sofort mit der Codeeingabe<br />

begonnen hat, wird es vielleicht<br />

gemerkt haben: Die Aufgabe läuft aus dem<br />

Ruder, wächst einem über den Kopf.<br />

Das ging mir nicht anders. Früher. Heute<br />

setze ich mich erst mit einem Blatt Papier<br />

hin, bevor ich beginne, Code zu<br />

schreiben. Denn die erste Herausforderung<br />

besteht nicht darin, das Problem zu<br />

lösen, sondern es zu verstehen.<br />

Beim Vier-gewinnt-Spiel war eine Anforderung<br />

bewusst ausgeklammert: die Benutzerschnittstelle.<br />

In der Aufgabe geht es<br />

um die Logik des Spiels. Am Ende soll demnach<br />

eine Assembly entstehen, in der die<br />

Spiellogik enthalten ist. Diese kann dann in<br />

einer beliebigen Benutzerschnittstelle verwendet<br />

werden.<br />

Beim Spiel selbst hilft es, sich die Regeln<br />

vor Augen zu führen. Zwei Spieler legen abwechselnd<br />

gelbe und rote Spielsteine in ein<br />

7 x 6 Felder großes Spielfeld. Derjenige, der<br />

als Erster vier Steine seiner Farbe nebenein -<br />

ander liegen hat, hat das Spiel gewonnen.<br />

Hier hilft es, sich mögliche Vierergruppen<br />

aufzumalen, um zu erkennen, welche Konstellationen<br />

im Spielfeld auftreten können.<br />

Nachdem ich das Problem durchdrungen<br />

habe, zeichnet sich eine algorithmische<br />

Lösung ab. Erst jetzt beginne ich, die<br />

gesamte Aufgabenstellung in Funktionseinheiten<br />

zu zerlegen. Ich lasse zu diesem<br />

Zeitpunkt ganz bewusst offen, ob eine<br />

Funktionseinheit am Ende eine Methode,<br />

Klasse oder Komponente ist. Wichtig ist<br />

erst einmal, dass jede Funktionseinheit eine<br />

klar definierte Aufgabe hat.<br />

Hat sie mehr als eine Aufgabe, zerlege ich<br />

sie in mehrere Funktionseinheiten. Stellt<br />

man sich die Funktionseinheiten als Baum<br />

vor, in dem die Abhängigkeiten die ver-<br />

schiedenen Einheiten verbinden, dann<br />

steht auf oberster Ebene das gesamte Spiel.<br />

Es zerfällt in weitere Funktionseinheiten,<br />

die eine Ebene tiefer angesiedelt sind. Diese<br />

können wiederum zerlegt werden. Bei<br />

der Zerlegung können zwei unterschiedliche<br />

Fälle betrachtet werden:<br />

❚ vertikale Zerlegung,<br />

❚ horizontale Zerlegung.<br />

Der Wurzelknoten des Baums ist das<br />

gesamte Spiel. Diese Funktionseinheit ist jedoch<br />

zu komplex, um sie „in einem Rutsch“<br />

zu implementieren. Also wird sie zerlegt.<br />

Durch die Zerlegung entsteht eine weitere<br />

Ebene im Baum. Dieses Vorgehen bezeichne<br />

ich daher als vertikale Zerlegung.<br />

Kümmert sich eine Funktionseinheit um<br />

mehr als eine Sache, wird sie horizontal<br />

zerlegt. Wäre es beispielsweise möglich, einen<br />

Spielzustand in eine Datei zu speichern,<br />

könnte das Speichern im ersten<br />

Schritt in der Funktionseinheit Spiellogik<br />

angesiedelt sein. Dann stellt man jedoch<br />

fest, dass diese Funktionseinheit für mehr<br />

als eine Verantwortlichkeit zuständig wäre,<br />

und zieht das Speichern heraus in eine eigene<br />

Funktionseinheit. Dies bezeichne ich<br />

als horizontale Zerlegung.<br />

Erst wenn die Funktionseinheiten hinreichend<br />

klein sind, kann ich mir Gedanken<br />

darum machen, wie ich sie implementiere.<br />

Im Falle des Vier-gewinnt-Spiels zerfällt<br />

das Problem in die eigentliche Spiellogik<br />

und die Benutzerschnittstelle. Die Benutzerschnittstelle<br />

muss in diesem Fall nicht<br />

weiter zerlegt werden. Das mag in komplexen<br />

Anwendungen auch mal anders sein.<br />

Diese erste Zerlegung der Gesamtaufgabe<br />

zeigt Abbildung 1.<br />

Die Spiellogik ist mir als Problem noch zu<br />

groß, daher zerlege ich diese Funktions -<br />

einheit weiter. Dies ist eine vertikale Zerlegung,<br />

es entsteht eine weitere Ebene im<br />

Baum. Die Spiellogik zerfällt in die Spielregeln<br />

und den aktuellen Zustand des Spiels.<br />

Die Zerlegung ist in Abbildung 2 dargestellt.<br />

Die Spielregeln sagen zum Beispiel aus, wer<br />

das Spiel beginnt, wer den nächsten Zug<br />

machen darf et cetera.<br />

Der Zustand des Spiels wird beim echten<br />

Spiel durch das Spielfeld abgebildet. Darin<br />

liegen die schon gespielten Steine. Aus<br />

dem Spielfeld geht jedoch nicht hervor,<br />

wer als Nächster am Zug ist. Für die Einhaltung<br />

der Spielregeln sind beim echten Spiel<br />

die beiden Spieler verantwortlich, in meiner<br />

Implementierung ist es die Funktionseinheit<br />

Spielregeln.<br />

Ein weiterer Aspekt des Spielzustands ist<br />

die Frage, ob bereits vier Steine den Regeln<br />

entsprechend zusammen liegen, sodass<br />

ein Spieler gewonnen hat. Ferner birgt der<br />

Spielzustand das Problem, wohin der<br />

nächste gelegte Stein fällt. Dabei bestimmt<br />

der Spieler die Spalte und der Zustand des<br />

Spielbretts die Zeile: Liegen bereits Steine<br />

in der Spalte, wird der neue Spielstein zuoberst<br />

auf die schon vorhandenen gelegt.<br />

Damit unterteilt sich die Problematik<br />

des Spielzustands in die drei Teilaspekte<br />

❚ Steine legen,<br />

❚ nachhalten, wo bereits Steine liegen,<br />

❚ erkennen, ob vier Steine zusammen liegen.<br />

Vom Problem zur Lösung<br />

Nun wollen Sie sicher so langsam auch mal<br />

Code sehen. Doch vorher muss noch geklärt<br />

werden, was aus den einzelnen Funktionseinheiten<br />

werden soll. Werden sie jeweils<br />

eine Klasse? Eher nicht, denn dann<br />

wären Spiellogik und Benutzerschnittstelle<br />

nicht ausreichend getrennt. Somit werden<br />

Benutzerschnittstelle und Spiellogik mindestens<br />

eigenständige Komponenten. Die<br />

Funktionseinheiten innerhalb der Spiel -<br />

logik hängen sehr eng zusammen. Alle leisten<br />

einen Beitrag zur Logik. Ferner scheint<br />

mir die Spiellogik auch nicht komplex<br />

genug, um sie weiter aufzuteilen. Es bleibt<br />

also bei den beiden Komponenten Benutzerschnittstelle<br />

und Spiellogik.<br />

Um beide zu einem lauffähigen Programm<br />

zusammenzusetzen, brauchen wir noch ein<br />

weiteres Projekt. Seine Aufgabe ist es, eine<br />

EXE-Datei zu erstellen, in der die beiden<br />

6 dotnetpro.dojos.2011 www.dotnetpro.de


Komponenten zusammengeführt werden.<br />

So entstehen am Ende drei Komponenten.<br />

Abbildung 3 zeigt die Solution für die<br />

Spiellogik. Sie enthält zwei Projekte: eines für<br />

die Tests, ein weiteres für die Implemen -<br />

tierung.<br />

Die Funktionseinheit Spielzustand zerfällt<br />

in drei Teile. Beginnen wir mit dem Legen<br />

von Steinen. Beim Legen eines Steins in das<br />

Spielfeld wird die Spalte angegeben, in die<br />

der Stein gelegt werden soll. Dabei sind drei<br />

Fälle zu unterscheiden: Die Spalte ist leer,<br />

enthält schon Steine oder ist bereits voll.<br />

Es ist naheliegend, das Spielfeld als zweidimensionales<br />

Array zu modellieren. Jede<br />

Zelle des Arrays gibt an, ob dort ein gelber,<br />

ein roter oder gar kein Stein liegt. Der erste<br />

Index des Arrays bezeichnet dabei die Spalte,<br />

der zweite die Zeile. Beim Platzieren eines<br />

Steins muss also der höchste Zeilenindex<br />

innerhalb der Spalte ermittelt werden.<br />

Ist dabei das Maximum noch nicht erreicht,<br />

kann der Stein platziert werden.<br />

Bleibt noch eine Frage: Wie ist damit<br />

umzugehen, wenn ein Spieler versucht, einen<br />

Stein in eine bereits gefüllte Spalte zu<br />

legen? Eine Möglichkeit wäre: Sie stellen<br />

eine Methode bereit, die vor dem Platzieren<br />

eines Steins aufgerufen werden kann,<br />

um zu ermitteln, ob dies in der betreffenden<br />

Spalte möglich ist. Der Code sähe<br />

dann ungefähr so aus:<br />

if(spiel.KannPlatzieren(3)) {<br />

spiel.LegeSteinInSpalte(3);<br />

}<br />

Dabei gibt der Parameter den Index der<br />

Spalte an, in die der Stein platziert werden<br />

soll. Das Problem mit diesem Code ist, dass<br />

er gegen das Prinzip „Tell don’t ask“ verstößt.<br />

Als Verwender der Funktionseinheit,<br />

die das Spielbrett realisiert, bin ich gezwungen,<br />

das API korrekt zu bedienen. Bevor<br />

ein Spielstein mit LegeSteinInSpalte() in<br />

das Spielbrett gelegt wird, müsste mit<br />

KannPlatzieren() geprüft werden, ob dies<br />

überhaupt möglich ist. Nach dem „Tell<br />

don’t ask“-Prinzip sollte man Klassen so erstellen,<br />

dass man den Objekten der Klasse<br />

mitteilt, was zu tun ist – statt vorher nachfragen<br />

zu müssen, ob man eine bestimmte<br />

Methode aufrufen darf. Im Übrigen bleibt<br />

bei der Methode LegeSteinInSpalte() das<br />

Problem bestehen: Was soll passieren,<br />

wenn die Spalte bereits voll ist?<br />

Eine andere Variante könnte sein, die<br />

Methode LegeSteinInSpalte() mit einem<br />

Rückgabewert auszustatten. War das Platzieren<br />

erfolgreich, wird true geliefert, ist die<br />

Spalte bereits voll, wird false geliefert. In<br />

[Abb. 1] Die Aufgabe in Teile zerlegen: erster Schritt ...<br />

[Abb. 2] ... und zweiter Schritt.<br />

dem Fall müsste sich der Verwender der<br />

Methode mit dem Rückgabewert befassen.<br />

Am Ende soll der Versuch, einen Stein in eine<br />

bereits gefüllte Spalte zu platzieren, dem<br />

Benutzer gemeldet werden. Also müsste der<br />

Rückgabewert bis in die Benutzerschnittstelle<br />

transportiert werden, um dort beispielsweise<br />

eine Messagebox anzuzeigen.<br />

Die Idee, die Methode mit einem Rückgabewert<br />

auszustatten, verstößt jedoch<br />

ebenfalls gegen ein Prinzip, nämlich die<br />

„Command/Query Separation“. Dieses<br />

Prinzip besagt, dass eine Methode entweder<br />

ein Command oder eine Query sein<br />

sollte, aber nicht beides. Dabei ist ein Command<br />

eine Methode, die den Zustand des<br />

Objekts verändert. Für die Methode Lege-<br />

Stein InSpalte() trifft dies zu: Der Zustand<br />

des Spielbretts ändert sich dadurch. Eine<br />

Query ist dagegen eine Methode, die eine<br />

Abfrage über den Zustand des Objekts enthält<br />

und dabei den Zustand nicht verändert.<br />

Würde die Methode LegeSteinInSpalte()<br />

einen Rückgabewert haben, wäre sie<br />

dadurch gleichzeitig eine Query.<br />

Nach diesen Überlegungen bleibt nur<br />

eine Variante übrig: Die Methode LegeStein -<br />

InSpalte() sollte eine Ausnahme auslösen,<br />

wenn das Platzieren nicht möglich ist. Die<br />

Ausnahme kann in der Benutzerschnittstelle<br />

abgefangen und dort in einer entsprechenden<br />

Meldung angezeigt werden. Damit<br />

entfällt die Notwendigkeit, einen Rückgabewert<br />

aus der Spiellogik bis in die Benutzerschnittstelle<br />

zu transportieren. Ferner sind<br />

die Prinzipien „Tell don’t ask“ und „Command/Query<br />

Separation“ eingehalten.<br />

Vier Steine finden<br />

Nun sind mit dem zweidimensionalen Array<br />

und der Methode LegeSteinInSpalte()<br />

bereits zwei Teilprobleme des Spielzu-<br />

LÖSUNG<br />

[Abb. 3] Aufbau der Solution.<br />

stands gelöst: Im zweidimensionalen Array<br />

ist der Zustand des Spielbretts hinterlegt,<br />

und die Methode LegeSteinInSpalte() realisiert<br />

die Platzierungslogik. Das dritte Problem<br />

ist die Erkennung von Vierergruppen,<br />

also eines Gewinners.<br />

Vier zusammenhängende Steine können<br />

beim Vier-gewinnt-Spiel in vier Varianten<br />

auftreten: horizontal, vertikal, diagonal<br />

nach oben, diagonal nach unten.<br />

Diese vier Varianten gilt es zu implementieren.<br />

Dabei ist wichtig zu beachten, dass<br />

die vier Steine unmittelbar zusammen liegen<br />

müssen, es darf sich also kein gegnerischer<br />

Stein dazwischen befinden.<br />

Ich habe zuerst versucht, diese Vierergruppenerkennung<br />

direkt auf dem zwei -<br />

dimensionalen Array zu lösen. Dabei habe<br />

ich festgestellt, dass das Problem in zwei<br />

Teilprobleme zerlegt werden kann:<br />

❚ Ermitteln der Indizes benachbarter Felder.<br />

❚ Prüfung, ob vier benachbarte Felder mit<br />

Steinen gleicher Farbe besetzt sind.<br />

Für das Ermitteln der Indizes habe ich<br />

daher jeweils eigene Klassen implementiert,<br />

welche die Logik der benachbarten Indizes<br />

enthalten. Eine solche Vierergruppe<br />

wird mit einem Startindex instanziert und<br />

liefert dann die Indizes der vier benachbarten<br />

Felder. Diese Vierergruppen werden anschließend<br />

verwendet, um im Spielfeld zu<br />

ermitteln, ob die betreffenden Felder alle<br />

www.dotnetpro.de dotnetpro.dojos.2011 7


LÖSUNG<br />

Listing 1<br />

Vierergruppe ermitteln.<br />

internal struct HorizontalerVierer : IVierer<br />

{<br />

private readonly int x;<br />

private readonly int y;<br />

public HorizontalerVierer(int x, int y) {<br />

this.x = x;<br />

this.y = y;<br />

}<br />

public Koordinate Eins {<br />

get { return new Koordinate(x, y); }<br />

}<br />

public Koordinate Zwei {<br />

get { return new Koordinate(x + 1, y); }<br />

}<br />

public Koordinate Drei {<br />

get { return new Koordinate(x + 2, y); }<br />

}<br />

public Koordinate Vier {<br />

get { return new Koordinate(x + 3, y); }<br />

}<br />

public override string ToString() {<br />

return string.Format("Horizontal X: {0},<br />

Y: {1}", x, y);<br />

}<br />

}<br />

Steine derselben Farbe enthalten. Die betreffenden<br />

Klassen heißen HorizontalerVierer,<br />

VertikalerVierer, DiagonalHochVierer<br />

und DiagonalRunterVierer. Listing 1 zeigt<br />

exemplarisch die Klasse HorizontalerVierer.<br />

Zunächst fällt auf, dass die Klasse internal<br />

ist. Sie wird im Rahmen der Spiellogik<br />

nur intern benötigt, daher soll sie nicht außerhalb<br />

der Komponente sichtbar sein.<br />

Damit Unit-Tests für die Klasse möglich<br />

sind, habe ich auf der Assembly das Attribut<br />

InternalsVisibleTo gesetzt. Dadurch<br />

kann die Assembly, welche die Tests enthält,<br />

auf die internen Details zugreifen.<br />

Aufgabe der Klasse HorizontalerVierer ist<br />

es, vier Koordinaten zu horizontal neben -<br />

einander liegenden Spielfeldern zu liefern.<br />

Dies erfolgt in den Properties Eins, Zwei,<br />

Drei und Vier. Dort werden jeweils die Indizes<br />

ermittelt.<br />

Das Ermitteln eines Gewinners geschieht<br />

anschließend in einem Flow aus zwei<br />

Schritten. Im ersten Schritt wird aus einem<br />

Spielfeld die Liste der möglichen Vierergruppen<br />

bestimmt. Im zweiten Schritt wird<br />

aus dem Spielfeld und den möglichen Vierergruppen<br />

ermittelt, ob eine der Vierergruppen<br />

Steine derselben Farbe enthält.<br />

Die beiden Schritte des Flows sind als<br />

Extension Methods realisiert. Dadurch<br />

sind sie leicht isoliert zu testen. Anschließend<br />

können sie hintereinander ausge-<br />

führt, also als Flow zusammengeschaltet<br />

werden:<br />

var gewinnerVierer = spielfeld<br />

.AlleVierer()<br />

.SelbeFarbe(spielfeld);<br />

Der Flow wird an zwei Stellen verwendet:<br />

zum einen beim Ermitteln des Gewinners,<br />

zum anderen, um zu bestimmen,<br />

welche Steine zum Sieg geführt haben. Da<br />

die Methode AlleVierer() ein IEnumerable<br />

liefert und SelbeFarbe() dies als ersten Parameter<br />

erwartet, können die beiden Extension<br />

Methods hintereinander geschrieben<br />

werden. Da das Spielfeld in beiden<br />

Methoden benötigt wird, verfügt Selbe -<br />

Farbe() über zwei Parameter.<br />

Das Ermitteln von vier jeweils neben -<br />

einander liegenden Feldern übernimmt<br />

die Methode AlleVierer(). Ein kurzer Ausschnitt<br />

zeigt die Arbeitsweise:<br />

internal static IEnumerable<br />

AlleVierer(this int[,] feld) {<br />

for (var x = 0; x


INotifyPropertyChanged-Logik automatisiert testen<br />

Zauberwort<br />

AUFGABE<br />

DataBinding ist eine tolle Sache: Objekt an Formular binden und wie von Zauberhand stellen die Controls die<br />

Eigenschaftswerte des Objekts dar. DataBinding ist aber auch knifflig. Stefan, kannst du dazu eine Aufgabe stellen?<br />

DataBinding ist beliebt. Lästig<br />

daran ist: Man muss die INotifyPropertyChanged-Schnittstelle<br />

implementieren. Sie fordert,<br />

dass bei Änderungen an den Eigenschaften<br />

eines Objekts das Ereignis PropertyChanged<br />

ausgelöst wird. Dabei muss dem Ereignis der<br />

Name der geänderten Eigenschaft als Parameter<br />

in Form einer Zeichenkette übergeben<br />

werden. Die Frage, die uns diesmal beim<br />

dotnetpro.dojo interessiert, ist: Wie kann<br />

man die Implementierung der INotifyPropertyChanged-Schnittstelle<br />

automatisiert testen?<br />

Die Funktionsweise des Events für eine einzelne<br />

Eigenschaft zu prüfen ist nicht schwer.<br />

Man bindet einen Delegate an den Property-<br />

Changed-Event und prüft, ob er bei Änderung<br />

der Eigenschaft aufgerufen wird. Außerdem<br />

ist zu prüfen, ob der übergebene Name der<br />

Eigenschaft korrekt ist, siehe Listing 3.<br />

Um zu prüfen, ob der Delegate aufgerufen<br />

wurde, erhöhen Sie im Delegate beispielsweise<br />

eine Variable, die außerhalb definiert<br />

ist. Durch diesen Seiteneffekt können Sie<br />

überprüfen, ob der Event beim Ändern der<br />

Eigenschaft ausgelöst und dadurch der Delegate<br />

aufgerufen wurde. Den Namen der Eigenschaft<br />

prüfen Sie innerhalb des Delegates<br />

mit einem Assert.<br />

Solche Tests für jede Eigenschaft und jede<br />

Klasse, die INotifyPropertyChanged implementiert,<br />

zu schreiben, wäre keine Lösung,<br />

weil Sie dabei Code wiederholen würden. Da<br />

die Eigenschaften einer Klasse per Reflection<br />

ermittelt werden können, ist es nicht schwer,<br />

den Testcode so zu verallgemeinern, dass<br />

damit alle Eigenschaften einer Klasse getestet<br />

werden können. Also lautet in diesem<br />

Monat die Aufgabe: Implementieren Sie eine<br />

Klasse zum automatisierten Testen der INotifyPropertyChanged-Logik.<br />

Die zu implementierende<br />

Funktionalität ist ein Werkzeug<br />

zum Testen von ViewModels. Dieses Werkzeug<br />

soll wie folgt bedient werden:<br />

NotificationTester.Verify();<br />

Die Klasse, die auf INotifyPropertyChanged-<br />

Semantik geprüft werden soll, wird als generischer<br />

Typparameter an die Methode über-<br />

geben. Die Prüfung soll so erfolgen, dass per<br />

Reflection alle Eigenschaften der Klasse gesucht<br />

werden, die über einen Setter und Getter<br />

verfügen. Für diese Eigenschaften soll geprüft<br />

werden, ob sie bei einer Zuweisung an<br />

die Eigenschaft den PropertyChanged-Event<br />

auslösen und dabei den Namen der Eigenschaft<br />

korrekt übergeben. Wird der Event<br />

nicht korrekt ausgelöst, muss eine Ausnahme<br />

ausgelöst werden. Diese führt bei der Ausführung<br />

des Tests durch das Unit-Test-Framework<br />

zum Scheitern des Tests.<br />

Damit man weiß, für welche Eigenschaft<br />

die Logik nicht korrekt implementiert ist,<br />

sollte die Ausnahme mit den notwendigen<br />

Informationen ausgestattet werden, also<br />

dem Namen der Klasse und der Eigenschaft,<br />

für die der Test fehlschlug.<br />

In einer weiteren Ausbaustufe könnte das<br />

Werkzeug dann auch auf Klassen angewandt<br />

werden, die ebenfalls per Reflection ermittelt<br />

wurden. Fasst man beispielsweise sämtliche<br />

ViewModels in einem bestimmten Namespace<br />

zusammen, kann eine Assembly nach<br />

ViewModels durchsucht werden. Damit die<br />

so gefundenen Klassen überprüft werden<br />

können, muss es möglich sein, das Testwerkzeug<br />

auch mit einem Typ als Parameter aufzurufen:<br />

NotificationTester.Verify<br />

(typeof(MyViewModel));<br />

Im nächsten Heft finden Sie eine Lösung<br />

des Problems. Aber versuchen Sie sich zunächst<br />

selbst an der Aufgabe. [ml]<br />

Listing 3<br />

Property changed?<br />

Wer übt, gewinnt<br />

In jeder dotnetpro finden Sie<br />

eine Übungsaufgabe von<br />

Stefan Lieser, die in maximal<br />

drei Stunden zu lösen sein<br />

sollte. Wer die Zeit investiert,<br />

gewinnt in jedem Fall – wenn<br />

auch keine materiellen Dinge,<br />

so doch Erfahrung und Wissen.<br />

Es gilt:<br />

❚ Falsche Lösungen gibt es<br />

nicht. Es gibt möglicherweise<br />

elegantere, kürzere<br />

oder schnellere Lösungen,<br />

aber keine falschen.<br />

❚ Wichtig ist, dass Sie reflektieren,<br />

was Sie gemacht<br />

haben. Das können Sie,<br />

indem Sie Ihre Lösung mit<br />

der vergleichen, die Sie<br />

eine Ausgabe später in<br />

dotnetpro finden.<br />

Übung macht den Meister.<br />

Also − los geht’s. Aber Sie<br />

wollten doch nicht etwa<br />

sofort Visual Studio starten …<br />

[Test]<br />

public void Name_Property_loest_PropertyChanged_Event_korrekt_aus() {<br />

var kunde = new Kunde();<br />

var count = 0;<br />

kunde.PropertyChanged += (o, e) => {<br />

count++;<br />

Assert.That(e.PropertyName, Is.EqualTo("Name"));<br />

};<br />

kunde.Name = "Stefan"; Assert.That(count,Is.EqualTo(1));<br />

}<br />

www.dotnetpro.de dotnetpro.dojos.2011 9


LÖSUNG<br />

INotifyPropertyChanged-Logik automatisiert testen<br />

Kettenreaktion<br />

Das automatisierte Testen der INotifyPropertyChanged-Logik ist nicht schwer. Man nehme einen Test, verallgemeinere<br />

ihn, streue eine Prise Reflection darüber, fertig. Doch wie zerlegt man die Aufgabenstellung so in Funktionseinheiten,<br />

dass diese jeweils genau eine definierte Verantwortlichkeit haben? Die Antwort: Suche den Flow!<br />

Wie man die INotifyPropertyChanged-Logik<br />

automa -<br />

tisiert testen kann, habe<br />

ich in der Aufgabenstellung<br />

zu dieser Übung bereits gezeigt [1].<br />

Doch wie verallgemeinert man nun diesen<br />

Test so, dass er für alle Eigenschaften einer<br />

Klasse automatisiert ausgeführt wird?<br />

Im Kern basiert die Lösung auf folgender<br />

Idee: Suche per Reflection alle Properties<br />

einer Klasse und führe den Test für die gefundenen<br />

Properties aus. Klingt einfach, ist<br />

es auch. Aber halt: Bitte greifen Sie nicht<br />

sofort zur Konsole! Auch bei vermeintlich<br />

unkomplizierten Aufgabenstellungen lohnt<br />

es sich, das Problem so zu zerlegen, dass<br />

kleine, überschaubare Funktionseinheiten<br />

mit einer klar abgegrenzten Verantwortlichkeit<br />

entstehen.<br />

Suche den Flow!<br />

Ich möchte versuchen, die Aufgabenstellung<br />

mit einem Flow zu lösen. Doch dazu<br />

sollte ich ein klein wenig ausholen und zunächst<br />

erläutern, was ein Flow ist und wo<br />

seine Vorteile liegen.<br />

Vereinfacht gesagt ist ein Flow eine An -<br />

ein anderreihung von Funktionen. Ein Argument<br />

geht in die erste Funktion hinein, diese<br />

berechnet damit etwas und liefert ein Ergebnis<br />

zurück. Dieses Ergebnis geht in die<br />

nächste Funktion, auch diese berechnet damit<br />

wieder etwas und liefert ihr Ergebnis an<br />

die nächste Funktion. Auf diesem Weg wird<br />

ein Eingangswert nach und nach zu einem<br />

Ergebnis transformiert, siehe Listing 1.<br />

Die einzelnen Funktionen innerhalb<br />

eines Flows, die sogenannten Flowstages,<br />

Listing 1<br />

Ein einfacher Flow.<br />

var input = "input";<br />

var x1 = A(input);<br />

var x2 = B(x1);<br />

var result = C(x2);<br />

sind zustandslos, das heißt, sie erledigen<br />

ihre Aufgabe ausschließlich mit den Daten<br />

aus ihren Argumenten. Das hat den Vorteil,<br />

dass mehrere Flows asynchron ausgeführt<br />

werden können, ohne dass dabei die Zugriffe<br />

auf den Zustand synchronisiert werden<br />

müssten. Ferner lassen sich zustandslose<br />

Funktionen sehr schön automatisiert<br />

testen, weil das Ergebnis eben nur von den<br />

Eingangsparametern abhängt.<br />

Einer nach dem anderen<br />

Ein Detail ist bei der Realisierung von<br />

Flows ganz wichtig: Weitergereicht werden<br />

sollten nach Möglichkeit jeweils Daten<br />

vom Typ IEnumerable. Dadurch besteht<br />

nämlich die Möglichkeit, auf diesen<br />

Daten mit LINQ zu operieren. Ferner können<br />

die einzelnen Flowstages dann beliebig<br />

große Datenmengen verarbeiten, da bei<br />

Verwendung von IEnumerable nicht alle<br />

Daten vollständig im Speicher existieren<br />

müssen, sondern Element für Element bereitgestellt<br />

werden können. Im Idealfall<br />

fließt also zwischen den einzelnen Flow -<br />

stages immer nur ein einzelnes Element.<br />

Es wird nicht etwa das gesamte Ergebnis<br />

der ersten Stage berechnet und dann vollständig<br />

weitergeleitet.<br />

Im Beispiel von Listing 2 führt die Verwendung<br />

von yield return dazu, dass der<br />

Compiler einen Enumerator erzeugt. Dieser<br />

Enumerator liefert nicht sofort die gesamte<br />

Aufzählung, sondern stellt auf Anfrage<br />

Wert für Wert bereit. Bei Ausführung der<br />

Methode Flow() werden also zunächst nur<br />

die einzelnen Aufzählungen und Funktionen<br />

miteinander verbunden. Erst wenn das<br />

erste Element aus dem Ergebnis entnommen<br />

werden soll, beginnen die Enumeratoren,<br />

Werte zu liefern. Der Flow kommt also<br />

erst dann in Gang, wenn jemand hinten das<br />

erste Element „herauszieht“.<br />

Als erste ist die Funktion C an der Reihe.<br />

Sie entnimmt aus der ihr übergebenen Aufzählung<br />

x2 das erste Element. Dadurch<br />

kommt B ins Spiel und entnimmt ihrerseits<br />

der Aufzählung x1 den ersten Wert. Dies<br />

Osetzt sich fort, bis die Methode Input den<br />

ersten Wert liefern muss. Im Flow werden<br />

die einzelnen Werte sozusagen von hinten<br />

durch den Flow gezogen. Ein Flow bietet in<br />

Verbindung mit IEnumerable und yield<br />

return die Möglichkeit, unendlich große<br />

Datenmengen zu verarbeiten, ohne dass<br />

eine einzelne Flowstage die Daten komplett<br />

im Speicher halten muss.<br />

Lesbarkeit durch Extension<br />

Methods<br />

Verwendet man bei der Implementierung<br />

der Flowstages Extension Methods, kann<br />

man die einzelnen Stages syntaktisch hintereinanderschreiben,<br />

sodass der Flow im<br />

Code deutlich in Erscheinung tritt. Dazu<br />

muss lediglich der erste Parameter der<br />

Funktion um das Schlüsselwort this ergänzt<br />

werden, siehe Listing 3. Natürlich müssen<br />

die Parameter und Return-Typen der Flow -<br />

stages zueinander passen.<br />

Lösungsansatz<br />

Der erste Schritt des INotifyProperty -<br />

Changed-Testers besteht darin, die zu testenden<br />

Properties des Typs zu ermitteln.<br />

Anschließend muss er jedem dieser Properties<br />

einen Wert zuweisen, um zu prüfen,<br />

ob der Event korrekt ausgelöst wird. Zum<br />

Zuweisen eines Wertes benötigen Sie zur<br />

Laufzeit einen Wert vom Typ der Property.<br />

Wenn Sie auf eine string-Property stoßen,<br />

müssen Sie einen string-Wert instanzieren,<br />

das ist einfach.<br />

Komplizierter wird die Sache, wenn der<br />

Typ der Property ein komplexer Typ ist.<br />

Denken Sie etwa an eine Liste von Points<br />

oder Ähnliches. Richtig knifflig wird es,<br />

wenn der Typ der Property ein Interfacetyp<br />

ist. Dann ist eine unmittelbare Instanzierung<br />

nicht möglich. Das Instanzieren der<br />

Werte scheint eine eigenständige Funk -<br />

tionseinheit zu sein, denn die Aufgabe ist<br />

recht umfangreich.<br />

Wenn Sie die Properties und ihren jeweiligen<br />

Typ gefunden haben, müssen Sie für<br />

jede Property einen Test ausführen. Jeder<br />

10 dotnetpro.dojos.2011 www.dotnetpro.de


Listing 2<br />

Rückgabedaten vom Typ IEnumerable nutzen.<br />

public void Flow() {<br />

var input = Input();<br />

var x1 = A(input);<br />

var x2 = B(x1);<br />

var result = C(x2);<br />

}<br />

foreach(var value in result) {<br />

...<br />

}<br />

public IEnumerable Input() {<br />

yield return "Äpfel";<br />

yield return "Birnen";<br />

yield return "Pflaumen";<br />

}<br />

public IEnumerable A(IEnumerable input) {<br />

foreach (var value in input) {<br />

yield return string.Format("({0})", value);<br />

}<br />

}<br />

public IEnumerable B(IEnumerable input) {<br />

foreach (var value in input) {<br />

yield return string.Format("[{0}]", value);<br />

}<br />

}<br />

public IEnumerable C(IEnumerable input) {<br />

foreach (var value in input) {<br />

yield return string.Format("-{0}-", value);<br />

}<br />

}<br />

Listing 3<br />

Die Stages syntaktisch koppeln.<br />

public static IEnumerable A(this IEnumerable input) {<br />

foreach (var value in input) {<br />

yield return string.Format("({0})", value);<br />

}<br />

}<br />

...<br />

var result = Input().A().B().C();<br />

dieser Tests ist eine Action, die auf<br />

einer Instanz der Klasse ausgeführt wird,<br />

die zu testen ist. Wenn also die Klasse KundeViewModel<br />

überprüft werden soll, wird<br />

für jede Property eine Action erzeugt. Sind die Actions erzeugt,<br />

müssen sie nur nach ein ander ausgeführt<br />

werden. Dabei soll jede Action eine neue<br />

Instanz der zu testenden Klasse erhalten.<br />

Andernfalls könnte es zu Seiteneffekten<br />

beim Testen der Properties kommen.<br />

Funktionseinheiten identifizieren<br />

Die erste Aufgabe ist also das Ermitteln der<br />

zu testenden Properties. Eingangspara -<br />

meter in diese Funktionseinheit ist der Typ,<br />

für den die INotifyPropertyChanged-Implementierung<br />

überprüft werden soll. Das Ergebnis<br />

der Flowstage ist eine Aufzählung<br />

der Property-Namen.<br />

static IEnumerable<br />

FindPropertyNames(this Type type)<br />

LÖSUNG<br />

An dieser Stelle fragen Sie sich möglicherweise,<br />

warum ich die Property-Namen<br />

als Strings zurückgebe und nicht etwa eine<br />

Liste von PropertyInfo-Objekten. Schließlich<br />

stecken in PropertyInfo mehr Informationen,<br />

insbesondere der Typ der Property,<br />

den ich später ebenfalls benötige. Ich habe<br />

mich dagegen entschieden, weil dies das<br />

Testen der nächsten Flowstage deutlich<br />

erschwert hätte. Denn diese hätte dann auf<br />

einer Liste von PropertyInfo-Objekten arbeiten<br />

müssen. Und da PropertyInfo-Instanzen<br />

nicht einfach mit new hergestellt werden<br />

können, wären die Tests recht mühsam geworden.<br />

Nachdem die Property-Namen bekannt<br />

sind, kann die nächste Flowstage dazu den<br />

jeweiligen Typ ermitteln. Die Flowstage erhält<br />

also eine Liste von Property-Namen<br />

sowie den Typ und liefert eine Aufzählung<br />

von Typen.<br />

static IEnumerable FindPropertyTypes(<br />

this IEnumerable propertyNames,<br />

Type type)<br />

Im Anschluss muss für jeden Typ ein Objekt<br />

instanziert werden. Diese Objekte werden<br />

später im Test den Properties zuge -<br />

wiesen. Die Flowstage erhält also eine Liste<br />

von Typen und liefert für jeden dieser Typen<br />

eine Instanz des entsprechenden Typs.<br />

static IEnumerable GenerateValues(<br />

this IEnumerable types)<br />

Dann wird es spannend: Die Actions<br />

müssen erzeugt werden. Dabei lässt es sich<br />

leider nicht vermeiden, die Property-Namen<br />

aus der ersten Stage nochmals zu verwenden.<br />

Die Ergebnisse der ersten Stage<br />

fließen also nicht nur in die unmittelbar<br />

nächste Stage, sondern zusätzlich auch<br />

noch in die Stage, welche die Actions erzeugt.<br />

Die Namen der Properties werden<br />

benötigt, um mittels Reflection die jeweiligen<br />

Setter aufrufen zu können.<br />

static IEnumerable<br />

GenerateTestMethods(this IEnumerable<br />

values, IEnumerable propertyNames,<br />

Type type)<br />

Der letzte Schritt besteht darin, die gelieferten<br />

Actions auszuführen. Dazu muss jeweils<br />

eine Instanz der zu testenden Klasse erzeugt<br />

und an die Action übergeben werden.<br />

Abbildung 2 zeigt den gesamten Flow.<br />

Die einzelnen Flowstages sind als Extension<br />

Method implementiert. Der Flow selbst<br />

wird in der öffentlichen Methode NotificationTester.Verify<br />

zusammengesteckt. Testen<br />

www.dotnetpro.de dotnetpro.dojos.2011 11


LÖSUNG<br />

möchte ich die einzelnen Stages aber isoliert.<br />

Denn nur so kann ich die Implementierung<br />

Schritt für Schritt vorantreiben und<br />

muss nicht gleich einen Integrationstest<br />

für den gesamten Flow schreiben. Einige<br />

Integrationstests sollten am Ende aber<br />

auch nicht fehlen.<br />

Diese Vorgehensweise hat einen weiteren<br />

Vorteil: Um den NotificationTester testen<br />

zu können, müssen Testdaten her. Da<br />

er auf Typen arbeitet, müssen also Test -<br />

daten in Form von Klassen erstellt werden.<br />

Das ist nicht nur aufwendig, sondern wird<br />

auch schnell unübersichtlich. Ganz kommt<br />

man zwar am Erstellen solcher Testklassen<br />

auch nicht vorbei, aber der Aufwand ist<br />

doch reduziert.<br />

Interna testbar machen<br />

Um die einzelnen Flowstages isoliert testen<br />

zu können, habe ich ihre Sichtbarkeit auf<br />

internal gesetzt. Damit sind die Methoden<br />

zunächst nur innerhalb der Assembly, in<br />

der sie implementiert sind, sichtbar. Um<br />

auch in der Test-Assembly darauf zugreifen<br />

zu können, muss diese zusätzliche Sichtbarkeit<br />

über das Attribut InternalsVisibleTo<br />

hergestellt werden:<br />

[assembly:InternalsVisibleTo(<br />

"INotifyTester.Tests")]<br />

Das Attribut kann prinzipiell in einer beliebigen<br />

Quellcodedatei in der Assembly<br />

untergebracht werden. Üblicherweise werden<br />

Attribute, die sich auf die Assembly be-<br />

[Abb. 2] Die zu testenden<br />

Properties ermitteln.<br />

ziehen, in der Datei AssemblyInfo.cs untergebracht.<br />

Diese finden Sie im Visual Studio<br />

Solution Explorer innerhalb des Ordners<br />

Properties.<br />

Das Sichtbarmachen der internen Methoden<br />

nur zum Zwecke des Testens halte<br />

ich auf diese Weise für vertretbar. Unit-<br />

Tests sind Whitebox-Tests, das heißt, die<br />

Art und Weise der Implementierung ist<br />

bekannt. Im Gegensatz dazu stehen Blackbox-Tests,<br />

die ganz bewusst keine Annahmen<br />

über den inneren Aufbau der zu<br />

testenden Funktionseinheiten machen.<br />

Durch Verwendung von internal ist die<br />

Sichtbarkeit nur so weit erhöht, dass die<br />

Methoden in Tests angesprochen werden<br />

können. Eine vollständige Offenlegung mit<br />

public wäre mir zu viel des Guten. Übrigens<br />

halte ich es für keine gute Idee, auf die<br />

Interna einer zu testenden Klasse mittels<br />

Reflection zuzugreifen. Dabei entziehen<br />

sich nämlich die Interna, die über Reflec -<br />

tion angesprochen werden, den Refaktorisierungswerkzeugen.<br />

Und wie man sieht,<br />

ist internal in Verbindung mit dem InternalsVisibleTo-Attribut<br />

völlig ausreichend.<br />

FindPropertyNames<br />

Die Namen der Properties werden durch<br />

die Flowstage FindPropertyNames geliefert.<br />

Dabei entscheidet diese Funktion bereits,<br />

welche Properties geprüft werden<br />

sollen. Es werden nur Properties berücksichtigt,<br />

die über öffentliche Getter und<br />

Setter verfügen.<br />

Listing 4<br />

Eine einfache Testklasse.<br />

public class ClassWithPublicGettersAndSetters<br />

{<br />

public string StringProperty { get; set;}<br />

public int IntProperty { get; set;}<br />

}<br />

Um diese Funktion testen zu können,<br />

müssen Testklassen angelegt werden. Das<br />

lässt sich leider nicht vermeiden, da die<br />

Funktion auf einem Typ als Argument arbeitet.<br />

Bei der testgetriebenen Entwicklung<br />

steht der Test vor der Implementierung,<br />

also gilt es, Testdaten zu erstellen. Ich habe<br />

mich zunächst um das „Happy Day Szenario“<br />

gekümmert, also einen Testfall, der<br />

später bei der Verwendung typisch ist, siehe<br />

Listing 4.<br />

Als Nächstes folgt eine Klasse, deren Properties<br />

private sind. Diese sollen in den<br />

Tests unberücksichtigt bleiben, ihr Name<br />

darf also nicht geliefert werden. Die Implementierung<br />

der Funktion ist mit LINQ<br />

ganz einfach, siehe Listing 5.<br />

Die beiden Where-Klauseln sorgen dafür,<br />

dass nur Properties berücksichtigt werden,<br />

die sowohl einen Getter als auch einen<br />

Setter haben. Durch die Binding Flags<br />

werden schon Properties ausgeschlossen,<br />

die nicht public sind. Durch die Select-<br />

Klausel wird festgelegt, wie die zu liefernden<br />

Ergebnisse aufgebaut sein sollen.<br />

FindPropertyTypes<br />

Die Funktion FindPropertyTypes erhält als<br />

Argumente die Liste der Property-Namen,<br />

die berücksichtigt werden sollen, sowie<br />

den Typ, zu dem die Properties gehören.<br />

Dazu liefert sie jeweils den Typ der Properties.<br />

Auch diese Tests benötigen wieder<br />

Testklassen. Ich habe einfach die schon<br />

vorhandenen Testklassen verwendet. Auch<br />

hier ist die Implementierung dank LINQ<br />

nicht schwierig.<br />

GenerateValues<br />

Um die Property-Setter später aufrufen zu<br />

können, muss jeweils ein Objekt vom Typ<br />

der Property erzeugt werden. Diese Aufgabe<br />

übernimmt die Funktion GenerateValues.<br />

Sie erhält als Argument die Liste der Typen<br />

und liefert dazu jeweils eine Instanz. Die<br />

Funktion ist derzeit recht einfach gehalten.<br />

Die Instanz wird einfach durch Verwendung<br />

von Activator.CreateInstance erzeugt. Ledig-<br />

12 dotnetpro.dojos.2011 www.dotnetpro.de


Listing 5<br />

Die zu prüfenden Properties finden.<br />

internal static IEnumerable FindPropertyNames(Type type) {<br />

return type.GetProperties(PropertyBindingFlags)<br />

.Where(propertyInfo => propertyInfo.CanRead)<br />

.Where(propertyInfo => propertyInfo.CanWrite)<br />

.Select(propertyInfo => propertyInfo.Name);<br />

}<br />

Listing 6<br />

Passende Objekte erzeugen.<br />

internal static IEnumerable GenerateValues(this IEnumerable types) {<br />

return types.Select(type => CreateInstance(type));<br />

}<br />

internal static object CreateInstance(Type type) {<br />

if (type == typeof(string)) {<br />

return "";<br />

}<br />

return Activator.CreateInstance(type);<br />

}<br />

lich Strings werden gesondert behandelt, da<br />

die Klasse über keinen parameterlosen Konstruktor<br />

verfügt, siehe Listing 6.<br />

Die Methode CreateInstance muss sicher<br />

im Laufe der Zeit angepasst werden. Sie ist<br />

in der gezeigten Implementierung nicht in<br />

der Lage, mit komplexen Typen zurechtzukommen.<br />

GenerateTestMethods<br />

Nun stehen alle Informationen zur Verfügung,<br />

um für jede Property eine Testmethode<br />

zu erzeugen. Die Funktion Generate-<br />

TestMethods erhält drei Argumente:<br />

❚ die Liste der Werte für die Zuweisung,<br />

❚ die Liste der Property-Namen,<br />

❚ den Typ, auf den sich die Tests beziehen.<br />

Das Ergebnis ist eine Liste von Actions.<br />

static IEnumerable<br />

GenerateTestMethods(this IEnumerable<br />

values, IEnumerable propertyNames,<br />

Type type)<br />

Das Testen dieser Funktion kommt leider<br />

auch wieder nicht ohne Testklassen<br />

aus, denn der Typ geht ja als Argument in<br />

die Funktion ein. Die erzeugten Testmethoden<br />

werden im Test aufgerufen, um so<br />

zu prüfen, dass sie jeweils einen bestimmten<br />

Aspekt der INotifyPropertyChanged-<br />

Semantik überprüfen. Hier wird es schon<br />

schwierig, die Vorgehensweise zu beschreiben,<br />

da es sich um Tests handelt, die testen,<br />

dass generierte Testmethoden richtig testen,<br />

sozusagen Metatests.<br />

Die Implementierung der Funktion hat es<br />

ebenfalls in sich. Zunächst müssen zwei<br />

Aufzählungen „im Gleichschritt“ durchlaufen<br />

werden. Dazu wird der Enumerator einer<br />

der beiden Aufzählungen ermittelt. Anschließend<br />

wird der andere Enumerator in<br />

einer foreach-Schleife durchlaufen. Innerhalb<br />

der Schleife wird der erste Enumerator<br />

dann „per Hand“ mit MoveNext und Current<br />

bedient. Ich hätte dies gerne in eine Methode<br />

ausgelagert, das ist jedoch durch die Verwendung<br />

von yield return nicht möglich.<br />

Damit sind wir bei der zweiten Besonderheit<br />

der Funktion. Die einzelnen Testmethoden<br />

werden jeweils mit yield return<br />

zurückgeliefert. Da das Ergebnis der Funktion<br />

eine Aufzählung von Actions ist, liefert<br />

das yield return jeweils eine Action in Form<br />

einer Lambda Expression. Dabei müssen<br />

die Werte, die aus den Enumeratoren in der<br />

Schleife entnommen werden, in lokalen<br />

Variablen abgelegt werden, damit sie als<br />

Closure in die Lambda Expression eingehen<br />

können. Andernfalls würden am Ende<br />

alle Lambda Expressions auf demselben<br />

Wert arbeiten, nämlich dem aus dem letzten<br />

Schleifendurchlauf. Auch hier macht<br />

sich übrigens wieder mal der Einsatz von<br />

Listing 7<br />

LÖSUNG<br />

Flowstages zusammenstecken.<br />

public static void Verify(Type type) {<br />

var propertyNames = type<br />

.FindPropertyNames();<br />

propertyNames<br />

.FindPropertyTypes(type)<br />

.GenerateValues()<br />

.GenerateTestMethods(<br />

propertyNames, type)<br />

.ExecuteTestMethods(type);<br />

}<br />

JetBrains ReSharper bezahlt. Der weist<br />

nämlich mit der Warnung „Access to modified<br />

closure“ auf das Problem hin.<br />

ExecuteTestMethods<br />

Der letzte Schritt im Flow ist die Ausführung<br />

der erzeugten Testmethoden. Diese<br />

Methode ist erst durch eine Refaktorisierung<br />

entstanden, daher teste ich sie nicht<br />

isoliert, sondern nur im Integrationstest.<br />

Und jetzt alle!<br />

Nun müssen nur noch alle Flowstages zusammengesteckt<br />

werden. Das ist einfach,<br />

da die Stages als Extension Methods implementiert<br />

sind. Dadurch können sie hintereinandergereiht<br />

werden, wie Listing 7 zeigt.<br />

Der Flow wird lediglich dadurch etwas<br />

unterbrochen, dass die Namen der Properties<br />

in zwei Flowstages benötigt werden.<br />

Daher werden diese nach Ausführung der<br />

ersten Stage in einer Variablen zwischengespeichert,<br />

die dann weiter unten wieder in<br />

eine andere Stage einfließt.<br />

www.dotnetpro.de dotnetpro.dojos.2011 13<br />

Fazit<br />

Die Realisierung dieses Testwerkzeugs ging<br />

mir recht leicht von der Hand. Dabei hat<br />

der Entwurf des Flows relativ viel Zeit in Anspruch<br />

genommen. Die anschließende Implementierung<br />

ging dafür rasch. Was mir an<br />

der Lösung gut gefällt, ist die Tatsache, dass<br />

Erweiterungen leicht vorzunehmen sind,<br />

weil es klar abgegrenzte Verantwortlichkeiten<br />

gibt. Bedarf für Erweiterungen erwarte<br />

ich vor allem beim Erzeugen der Testwerte,<br />

also in der Funktion CreateInstance. Diese<br />

ist bislang relativ einfach gehalten, kann<br />

aber leicht erweitert werden. [ml]<br />

[1] Stefan Lieser, Zauberwort, INotifyProperty-<br />

Changed-Logik automatisiert testen,<br />

dotnetpro 4/2010, S. 107,<br />

www.dotnetpro.de/A1004dojo


Wer übt, gewinnt<br />

AUFGABE<br />

Testdaten automatisch generieren<br />

Meier, Müller, Schulze …<br />

Nach wie vor spielt die klassische „Forms over Data“-Anwendung eine große Rolle. Daten aus einer Datenbank sollen<br />

per Formular bearbeitet werden. Wenn diese Applikationen getestet werden, spielen Testdaten eine zentrale Rolle.<br />

Möglichst viele sollten es sein und möglichst realistisch geformt noch dazu. Stefan, fällt dir dazu eine Übung ein?<br />

dnpCode: A1005dojo<br />

In jeder dotnetpro finden Sie<br />

eine Übungsaufgabe von<br />

Stefan Lieser, die in maximal<br />

drei Stunden zu lösen sein<br />

sollte. Wer die Zeit investiert,<br />

gewinnt in jedem Fall – wenn<br />

auch keine materiellen Dinge,<br />

so doch Erfahrung und Wissen.<br />

Es gilt:<br />

❚ Falsche Lösungen gibt es<br />

nicht. Es gibt möglicherweise<br />

elegantere, kürzere<br />

oder schnellere Lösungen,<br />

aber keine falschen.<br />

❚ Wichtig ist, dass Sie reflektieren,<br />

was Sie gemacht<br />

haben. Das können Sie,<br />

indem Sie Ihre Lösung mit<br />

der vergleichen, die Sie<br />

eine Ausgabe später in<br />

dotnetpro finden.<br />

Übung macht den Meister.<br />

Also − los geht’s. Aber Sie<br />

wollten doch nicht etwa<br />

sofort Visual Studio starten …<br />

[Abb. 1] So könnte das GUI für einen Testdatengenerator<br />

aussehen.<br />

Immer wieder begegnet man der Anforderung,<br />

Daten aus einer Datenbank in einem<br />

Formular zu visualisieren. Oft sind die Datenmengen<br />

dabei so groß, dass man nicht<br />

einfach alle Daten in einem Rutsch laden sollte.<br />

Stattdessen müssen die Daten seitenweise abgerufen<br />

und visualisiert werden. Suchen und Filtern<br />

kommen meistens hinzu, und schon stellt sich<br />

die Frage, ob der gewählte Ansatz auch noch<br />

funktioniert, wenn mehr als nur eine Handvoll<br />

Testdaten in der Datenbank liegen.<br />

Solche Tests auf Echtdaten Ihrer Kunden vorzunehmen<br />

wäre übrigens keine gute Idee. Diese unterliegen<br />

dem Datenschutz und sollten keinesfalls<br />

zu Testzwecken verwendet werden. Und für eine<br />

völlig neue Anwendung stehen natürlich noch gar<br />

keine Echtdaten zurVerfügung. Folglich bleibt nur<br />

die Möglichkeit, Testdaten zu generieren. Und genau<br />

darum geht es in dieser Übung: Erstellen Sie<br />

eine Bibliothek zum Erzeugen von Testdaten.<br />

Verschiedene Arten von Testdaten<br />

Die generierten Testdaten sollen eine Tabellenstruktur<br />

haben. Für jede Spalte wird definiert,<br />

von welchem Typ die Werte sind und wie sie erzeugt<br />

werden. Anschließend gibt man an, wie<br />

viele Zeilen generiert werden sollen, und die<br />

Testdaten werden generiert.<br />

Die Anforderungen an die Daten können sehr<br />

vielfältig sein. Um hier ausreichend flexibel zu sein,<br />

sollen die Daten nach verschiedenen Strategien erzeugt<br />

werden können. Reine Zufallsdaten sind ein<br />

erster Schritt, dürften aber in vielen Fällen nicht<br />

ausreichen. Zumindest eine Beschränkung<br />

innerhalb vorgegebener<br />

Minimum- und Maximumwerte<br />

erscheint sinnvoll.<br />

Eine weitere Strategie könnte<br />

darin bestehen, eine Liste<br />

von möglichen Werten vorzugeben,<br />

aus denen dann zufällig<br />

ausgewählt wird. So könnten<br />

beispielsweise Straßennamen<br />

generiert werden, die in<br />

den Formularen dann auch<br />

wie Straßennamen aussehen<br />

statt wie zufällig zusammengewürfelte Zeichenfolgen.<br />

Es müssen lediglich einige Straßennamen<br />

vorgegeben werden. Das Gleiche bietet sich für<br />

die Namen von Personen an. Auch hier kann gut<br />

mit einer Liste von Namen gearbeitet werden,<br />

aus der dann zufällig Werte ausgewählt werden.<br />

Die Strategie für die Testdatenerzeugung soll<br />

möglichst flexibel sein. Ein Entwickler sollte mit<br />

wenig Aufwand einen eigenen Generator ergänzen<br />

können. Endergebnis der Datenerzeugung<br />

soll eine Aufzählung von Zeilen sein:<br />

IEnumerable<br />

Die generierten Zeilen können dann beliebig<br />

verwendet werden. Sie können direkt in Tests<br />

einfließen oder auch zuerst als Datei gespeichert<br />

werden. Hier bietet sich beispielsweise die Speicherung<br />

als CSV-Datei an. Auch das Speichern in<br />

einer Datenbank ist natürlich ein typisches Szenario.<br />

Das konkrete Speichern der Daten sollte<br />

unabhängig sein vom Erzeugen. Es lohnt sich also<br />

wieder, sich vor der Implementierung ein paar<br />

Gedanken zur Architektur zu machen.<br />

Auch bei dieser Übung geht es wieder primär<br />

um eine Bibliothek und weniger um eine Benutzerschnittstelle.<br />

Wer mag, kann sich aber auch<br />

um eine Benutzerschnittstelle kümmern, denn<br />

die dürfte hier etwas anspruchsvoller sein.<br />

Schließlich benötigen die verschiedenen Generatoren<br />

unterschiedliche Eingabedaten. Genügen<br />

bei einem Zufallsgenerator vielleicht Minimum<br />

und Maximum, müssen bei einem anderen<br />

Generator Wertelisten eingegeben werden. Hinzu<br />

kommt, dass die Eingabedaten von unterschiedlichem<br />

Typ sein können, wofür unterschiedliche<br />

Eingabevalidierungen nötig sind. Abbildung<br />

1 zeigt eine erste Skizze einer Benutzerschnittstelle.<br />

Und denken Sie stets an die Musiker: Die verbringen<br />

die meiste Zeit mit Üben, nicht mit Auftritten!<br />

Wir Softwareentwickler sollten auch regelmäßig<br />

üben, statt immer nur zu performen.<br />

Schließlich sollte man beim Auftritt keine Fehler<br />

machen, nur beim Üben ist das zulässig und sogar<br />

erwünscht: ohne Fehler keine Weiterentwicklung.<br />

Also üben Sie und machen Sie Fehler! [ml]<br />

14 dotnetpro.dojos.2011 www.dotnetpro.de


Testdaten automatisch generieren<br />

Tückisches GUI<br />

A<br />

uch bei dieser Aufgabe zeigte<br />

sich wieder, wie wichtig es<br />

ist, sich vor der Implementierung<br />

ein paar Gedanken zur<br />

Architektur zu machen. Der erste Gedanke,<br />

das Erzeugen der Daten vom Speichern zu<br />

trennen, liegt auf der Hand und wurde in<br />

der Aufgabenstellung schon erwähnt.<br />

Doch wie geht man generell vor, wenn für<br />

eine Aufgabenstellung eine Architektur<br />

entworfen werden soll? Ganz einfach: Man<br />

malt den „kleinen König“. Den gibt es immer,<br />

denn er ist schließlich derjenige, der<br />

die Anforderungen formuliert hat. Er ist<br />

der Grund dafür, dass das System überhaupt<br />

gebaut wird. Das zu implementierende<br />

System als Ganzes kann man auch<br />

sofort hinmalen. Damit liegt man nie verkehrt.<br />

Es ergibt sich damit das in Abbildung<br />

1 gezeigte Bild.<br />

Das Diagramm nennt sich System-Umwelt-Diagramm,<br />

da es das System in seiner<br />

Umwelt zeigt. In der Umwelt des Systems<br />

gibt es immer mindestens einen Client, den<br />

kleinen König, der das System bedient. Bei<br />

manchen Systemen mag es mehrere unterschiedliche<br />

Clients geben, das spielt für den<br />

Testdatengenerator jedoch keine Rolle. Die<br />

zweite Kategorie von Elementen in der Umwelt<br />

stellen Ressourcen dar. Diese liegen<br />

außerhalb des zu erstellenden Systems und<br />

sollten daher in das System-Umwelt-Diagramm<br />

aufgenommen werden, denn unser<br />

System ist von diesen Ressourcen abhän-<br />

gig. Im Fall des Testdatengenerators sind<br />

als Ressourcen in der Umwelt CSV-Dateien<br />

und Datenbanken denkbar. Irgendwo müssen<br />

die generierten Testdaten schließlich<br />

hin. Folglich ergänze ich das System-Umwelt-Diagramm<br />

um diese Ressourcen. Das<br />

Ergebnis ist in Abbildung 2 zu sehen.<br />

Wer nun glaubt, ein solches Diagramm<br />

sei ein Taschenspielertrick, um Zeit zu<br />

schinden, ohne Nutzen für den Architekturentwurf,<br />

der irrt. Denn aus diesem Bild<br />

wird bereits deutlich, welche Komponenten<br />

mindestens entstehen müssen. Den<br />

Begriff Komponente verwende ich hier mit<br />

einer festen Bedeutung, siehe dazu die Erläuterungen<br />

im Kasten.<br />

Der Kern des Systems sollte gegenüber<br />

der Umwelt abgeschirmt werden, weil das<br />

System die Umwelt nicht kontrollieren kann.<br />

Die Umwelt kann sich verändern. Es können<br />

etwa neue Clients hinzukommen oder<br />

auch zusätzliche Ressourcen. Folglich müssen<br />

auf der Umrandung des Systems Komponenten<br />

entstehen, die den Kern des Systems<br />

über definierte Schnittstellen gegenüber<br />

der Umwelt isolieren. Andernfalls würde<br />

der Kern des Systems immer wieder von<br />

Änderungen in der Umwelt betroffen sein<br />

und wäre damit sehr anfällig. Und darin<br />

liegt die Bedeutung des System-Umwelt-<br />

Diagramms: Es zeigt, welche Komponenten<br />

das System von der Umwelt abschirmen.<br />

Für Clients, die das System verwenden,<br />

bezeichnen wir die Komponente, über wel-<br />

LÖSUNG<br />

Bei dieser Übung ging der Kern der Anwendung relativ leicht von der Hand. Die eigentliche Herausforderung lag in der<br />

dynamischen Benutzerschnittstelle. Jeder Datentyp verlangt andere Oberflächenelemente. Und der Anwender will<br />

seine Daten individuell strukturieren können.<br />

Komponente<br />

Eine Komponente ist eine binäre Funktionseinheit mit separatem Kontrakt:<br />

Binär bedeutet hier, dass die Komponente an den Verwendungsstellen binär referenziert wird. Es<br />

wird also bei der Verwendung keine Referenz auf das entsprechende Visual-Studio-Projekt gesetzt,<br />

sondern eine Referenz auf die erzeugte Assembly.<br />

Separater Kontrakt bedeutet, dass das Interface für die Komponente in einer eigenen Assembly<br />

abgelegt ist und nicht in der Assembly liegt, in welcher die Komponente implementiert ist. Daraus<br />

folgt, dass eine Komponente immer aus mindestens zwei Assemblies besteht, nämlich einer für den<br />

Kontrakt und einer für die Implementierung. Und natürlich gehören Tests dazu – also besteht jede<br />

Komponente aus mindestens drei Projekten.<br />

[Abb. 1] System-<br />

Umwelt-<br />

Diagramm,<br />

Version 1.<br />

[Abb. 2] System-Umwelt-Diagramm, Version 2.<br />

che der Client mit dem System interagiert,<br />

als Portal. In Abhängigkeitsdiagrammen<br />

werden Portale immer als Quadrate dargestellt.<br />

Die Interaktion des Systems mit Ressourcen<br />

erfolgt über Adapter. Diese werden<br />

durch Dreiecke symbolisiert. Im konkreten<br />

Fall des Testdatengenerators können wir<br />

aufgrund des System-Umwelt-Diagramms<br />

also schon vier Komponenten identifizieren,<br />

siehe Abbildung 3:<br />

❚ Portal,<br />

❚ CSV-Adapter,<br />

❚ Datenbank-Adapter,<br />

❚ Testdatengenerator.<br />

Die Komponenten sollten normalerweise<br />

allerdings nicht im System-Umwelt-Diagramm<br />

eingezeichnet werden, weil dort<br />

sonst zwei Belange vermischt werden. Es<br />

soll hier nur gezeigt werden, dass sich Portal<br />

und Adapter immer sofort aus dem System-Umwelt-Diagramm<br />

ergeben. Aus dem<br />

in Abbildung 3 gezeigten Diagramm lässt<br />

sich das in Abbildung 4 gezeigte Abhängigkeitsdiagramm<br />

ableiten.<br />

Den Kern zerlegen<br />

Nachdem ich diese Komponenten identifiziert<br />

hatte, habe ich die Aufgabenstellung<br />

www.dotnetpro.de dotnetpro.dojos.2011 15


LÖSUNG<br />

Erzeugen der Daten zerlegt. Aufgabe des<br />

Testdatengenerators ist es, Datenzeilen zu<br />

erzeugen. Dabei soll jede Datenzeile aus<br />

mehreren Spalten bestehen. Diese Aufgabe<br />

kann in folgende Funktionseinheiten zerlegt<br />

werden:<br />

❚ Erzeugen eines einzelnen Wertes,<br />

❚ Erzeugen einer Zeile,<br />

❚ Erzeugen mehrerer Zeilen.<br />

Dabei scheint die Trennung in das Erzeugen<br />

einer Zeile und das Erzeugen mehrerer<br />

Zeilen auf den ersten Blick möglicherweise<br />

etwas merkwürdig. Wenn eine Zeile erzeugt<br />

werden kann, genügt doch eine simple<br />

Schleife, und schon können mehrere Zeilen<br />

erzeugt werden. Dennoch halte ich es für<br />

wichtig, diese beiden Funktionseinheiten<br />

zu identifizieren. Denn für die testgetriebene<br />

Entwicklung ist es nützlich, im Vorfeld<br />

zu wissen, welche Funktionseinheiten auf<br />

einen zukommen. So fällt es nämlich viel<br />

leichter, ausreichende Testfälle zu finden,<br />

sprich: die Anforderungen zu klären. Und<br />

bei den Anforderungen liegt die Herausfor-<br />

[Abb. 3] System-Umwelt-<br />

Komponenten.<br />

[Abb. 4] Abhängigkeitsdiagramm.<br />

[Abb. 6] Abhängigkeits-<br />

diagramm der Kompo-<br />

nenten.<br />

derung eher darin, klar zu definieren, was<br />

die Anforderungen an das Erzeugen einer<br />

einzelnen Zeile sind. Dies dann zu übertragen<br />

auf die Erzeugung mehrerer Zeilen ist<br />

in der Tat trivial. Aber ohne die Trennung<br />

würde möglicherweise nur eine Funktionseinheit<br />

entstehen, die mehrere Datenzeilen<br />

erzeugt. Das würde die testgetriebene Entwicklung<br />

unnötig erschweren.<br />

Nachdem ich für das Erzeugen der Daten<br />

die Funktionseinheiten identifiziert hatte,<br />

habe ich überlegt, welche davon Komponenten<br />

werden sollen. Erst Komponenten<br />

erlauben eine parallele Entwicklung von<br />

Funktionseinheiten durch mehrere Entwickler<br />

oderTeams gleichzeitig. Dies ist zwar<br />

hier nicht das Ziel, doch resultiert aus der<br />

Trennung von Kontrakt und Implementierung,<br />

dass die Komponenten austauschbar<br />

sind. Dies betrachte ich beim Testdatengenerator<br />

an einer Stelle für besonders wichtig:<br />

bei den Generatoren. Die werden später<br />

sicher immer wieder ergänzt werden.<br />

Da ist es hilfreich, wenn dann nicht jeweils<br />

die gesamte Anwendung neu übersetzt<br />

[Abb. 5] Generatoren, nach Typ geordnet, in<br />

Unterverzeichnissen.<br />

werden muss, sondern neue Generatoren<br />

mit geringem Aufwand ergänzt werden<br />

können. In einer weiteren Ausbaustufe wäre<br />

es sogar denkbar, die Generatoren zur<br />

Laufzeit zu laden. Dann könnten später beliebige<br />

zusätzliche Generatoren verwendet<br />

werden, ohne dass am Testdatengenerator<br />

selbst etwas geändert werden muss.<br />

Damit sind die Generatoren zunächst<br />

einmal eine Komponente. Eine andere Aufteilung<br />

wäre ebenfalls denkbar, man könnte<br />

Generatoren zum Beispiel nach Typ in Komponenten<br />

zusammenfassen. Eine Komponente<br />

mit Stringgeneratoren, eine für int-<br />

Generatoren et cetera. Zurzeit sind es nur<br />

wenige Generatoren, daher habe ich mich<br />

dafür entschieden, sie alle in einer Komponente<br />

unterzubringen. Innerhalb der Komponente<br />

habe ich die Generatoren nach<br />

Typ in Unterverzeichnisse geordnet. Dies<br />

ist in Abbildung 5 zu sehen.<br />

Eine weitere Komponente bildet die<br />

Funktionseinheit, die dafür zuständig ist,<br />

Zeilen aus Einzelwerten zu bilden. Diese<br />

Komponente habe ich DataPump genannt.<br />

Eine dritte Komponente bildet das Speichern<br />

der Daten. Implementiert habe ich<br />

einen CsvDataAdapter. Ein DbDataAdapter<br />

zum Speichern der Testdaten in einer<br />

16 dotnetpro.dojos.2011 www.dotnetpro.de


Listing 1<br />

Einen generischen<br />

Typparameter verwenden.<br />

public interface IGenerator<br />

{<br />

T GenerateValue();<br />

}<br />

Datenbank liegt auf der Hand, auf diesen<br />

habe ich aus Zeitgründen jedoch verzichtet.<br />

Übergangsweise kann man sich damit<br />

behelfen, die CSV-Dateien mit einem ETL-<br />

Prozess (Extract, Transform, Load) in die<br />

Datenbank zu schaufeln.<br />

Die Komponenten Generators, Data-<br />

Pump, DbDataAdapter und CsvDataAdapter<br />

haben nur geringe Abhängigkeiten, wie<br />

Abbildung 6 zeigt. Der CsvDataAdapter ist<br />

nicht von den anderen Komponenten abhängig,<br />

weil er lediglich auf dem gemeinsamen<br />

Datenmodell aufsetzt.<br />

Einzelne Werte<br />

Für das Erzeugen eines einzelnen Wertes<br />

habe ich mich für die Verwendung eines<br />

Generators entschieden. Dieser hat die<br />

Aufgabe, zu einem gegebenen Typ einen<br />

Wert zu liefern. Die dabei verwendete Strategie<br />

bestimmt der Generator. So ist ein<br />

Generator denkbar, der zufällige Werte erzeugt.<br />

Genauso kann aber auch ein Generator<br />

erstellt werden, der eine Liste von Daten<br />

erhält und daraus zufällig auswählt.<br />

Die Beschreibung der zu erzeugenden<br />

Datenzeilen besteht also darin, pro Spalte<br />

einen Generator zu definieren. Ferner wird<br />

pro Spalte der Name der Spalte benötigt.<br />

Im Kontrakt der Generatoren habe ich<br />

einen generischen Typparameter verwendet,<br />

siehe Listing 1. Dadurch wird bereits<br />

zur Übersetzungszeit geprüft, ob der Rückgabewert<br />

der Methode GenerateValue zum<br />

Generatortyp passt.<br />

Die Generatoren werden in den Spaltendefinitionen<br />

verwendet. Da sie einen generischen<br />

Typparameter haben, muss dieser<br />

bei Verwendung des Generators entweder<br />

durch einen konkreten Typ oder an derVerwendungsstelle<br />

durch einen generischen<br />

Typparameter belegt werden. Für die Klasse<br />

ColumnDefinition würde das bedeuten,<br />

dass diese ebenfalls einen generischen Typparameter<br />

erhält, siehe Listing 2.<br />

So weit, so gut. Doch eine Zeile besteht<br />

aus mehreren Spalten. Daher müssen meh-<br />

Listing 2<br />

Spalten definieren.<br />

public class ColumnDefinition<br />

{<br />

public ColumnDefinition(string columnName, IGenerator generator)<br />

{<br />

ColumnName = columnName;<br />

Generator = generator;<br />

}<br />

public string ColumnName { get; private set; }<br />

public IGenerator Generator { get; private set; }<br />

}<br />

Listing 3<br />

Werte erzeugen.<br />

rere ColumnDefinition-Objekte in einer<br />

Liste zusammengefasst werden. Da natürlich<br />

jede Spalte einen anderen Typ haben<br />

kann, muss es möglich sein, beispielsweise<br />

eine ColumnDefinition sowie eine<br />

ColumnDefinition in diese Liste auf-<br />

LÖSUNG<br />

public static IEnumerable GenerateValues(this IEnumerable<br />

columnDefinitions) {<br />

return columnDefinitions<br />

.Select(x => x.Generator)<br />

.Select(x => x.GenerateValue());<br />

}<br />

Listing 4<br />

Eine Datenzeile generieren.<br />

public static Line GenerateLine(this IEnumerable values)<br />

{<br />

return new Line(values);<br />

}<br />

Listing 5<br />

Mehrere Zeilen generieren.<br />

public IEnumerable GenerateTestData(IEnumerable<br />

columnDefinitions, int rowCount)<br />

{<br />

for (var i = 0; i < rowCount; i++)<br />

{<br />

yield return<br />

columnDefinitions<br />

.GenerateValues()<br />

.GenerateLine();<br />

}<br />

}<br />

zunehmen. Dies ist jedoch mit C# 3.0 aufgrund<br />

der fehlenden Ko-/Kontravarianz<br />

noch nicht möglich. Würde man die Liste<br />

als List definieren, müsste die Liste<br />

Kovarianz unterstützen. Das tut sie jedoch<br />

nicht, Ko- und Kontravarianz stehen erst<br />

www.dotnetpro.de dotnetpro.dojos.2011 17


LÖSUNG<br />

mit C# 4.0 zur Verfügung. Ich habe daher<br />

den Generator in der ColumnDefinition als<br />

IGenerator definiert, statt Column-<br />

Definition generisch zu machen. Dies kann<br />

man dann mit Erscheinen von Visual Studio<br />

2010 ändern.<br />

Eine Zeile<br />

Durch die Generatoren können die einzelnen<br />

Werte der Spalten erzeugt werden. Um<br />

eine ganze Datenzeile zu erzeugen, muss<br />

jeder Generator einmal aufgerufen werden,<br />

um seinen jeweils nächsten Wert zu<br />

liefern. Dies ist bei Verwendung von LINQ<br />

ganz einfach, siehe Listing 3.<br />

In der Methode wird über die Aufzählung<br />

der Spaltendefinitionen iteriert und<br />

durch das erste Select jeweils der Generator<br />

aus der Spaltendefinition entnommen.<br />

Durch das zweite Select wird aus jedem Generator<br />

ein Wert abgerufen. Das Ergebnis<br />

ist eine Aufzählung der von den Generatoren<br />

gelieferten Werte. Diese Aufzählung<br />

wird später an den Konstruktor einer Zeile<br />

übergeben, siehe Listing 4.<br />

Mehrere Zeilen<br />

Die Erzeugung mehrerer Zeilen erfolgt in<br />

einer for-Schleife. Dabei wird die Schleife<br />

so oft durchlaufen, dass die Anzahl der gewünschten<br />

Datensätze erzeugt wird. Dabei<br />

kommt wieder einmal ein yield return zum<br />

Einsatz, siehe Listing 5.<br />

Flow<br />

Und schon wieder konnte ich einen Flow<br />

identifizieren. Die Aufzählung der Column-<br />

Definitions fließt in die Methode Generate-<br />

Values. Heraus kommt eine Aufzählung<br />

mit Werten. Diese wird weitergeleitet in<br />

die Methode GenerateLine, die aus den<br />

Werten eine Zeile erstellt:<br />

columnDefinitions<br />

.GenerateValues()<br />

.GenerateLine();<br />

Um den Flow so formulieren zu können,<br />

sind die beiden Methoden als Extension<br />

Methods realisiert. Dadurch wird das Aneinanderreihen<br />

der Methoden besonders<br />

einfach. Abbildung 7 zeigt den Flow.<br />

Damit ist die Komponente DataPump<br />

bereits beschrieben. Weiter geht es bei den<br />

Generatoren.<br />

Generatoren<br />

Ein Generator ist für das Erzeugen von<br />

Werten eines bestimmten Typs zuständig.<br />

Welche Strategie dabei verfolgt wird, ist Sache<br />

des Generators. Dies soll am Beispiel<br />

[Abb. 7] Flow zum<br />

Erzeugen einer Daten-<br />

zeile.<br />

eines Generators für int-Werte gezeigt werden,<br />

der zufällige Werte innerhalb vorgegebener<br />

Minimum- und Maximumwerte erzeugt.<br />

Die Implementierung des Generators ist<br />

ganz einfach. Ich verwende einen Zufallszahlengenerator<br />

System.Random aus dem<br />

.NET Framework und weise ihn an, einen<br />

Listing 6<br />

Ein Generator für int-Werte.<br />

public class RandomIntGenerator : IGenerator<br />

{<br />

private readonly int minimum;<br />

private readonly int maximum;<br />

private readonly Random random;<br />

}<br />

...<br />

Listing 7<br />

Den Zufallsgenerator testen.<br />

[TestFixture]<br />

public class RandomIntGeneratorTests<br />

{<br />

private RandomIntGenerator sut;<br />

...<br />

}<br />

}<br />

public object GenerateValue() {<br />

return random.Next(minimum, maximum + 1);<br />

}<br />

[SetUp]<br />

public void Setup() {<br />

sut = new RandomIntGenerator(1, 5, new Random(0));<br />

}<br />

Wert innerhalb der definierten Grenzen zu<br />

liefern, siehe Listing 6. Die spannende Frage<br />

ist nun: Wie kann man einen solchen<br />

Generator testen, der zufällige Werte liefern<br />

soll? Sie werden bemerkt haben, dass<br />

oben im Listing der Zufallszahlengenerator<br />

random nirgendwo instanziert und zugewiesen<br />

wird. Dies liegt in der Notwendig-<br />

[Test]<br />

public void Zufaellige_Werte_zwischen_Minimum_und_Maximum_werden_geliefert() {<br />

Assert.That(sut.GenerateValue(), Is.EqualTo(4));<br />

Assert.That(sut.GenerateValue(), Is.EqualTo(5));<br />

18 dotnetpro.dojos.2011 www.dotnetpro.de


keit begründet, den Generator automatisiert<br />

testen zu können. Würde der Generator<br />

den Zufallszahlengenerator selbst instanzieren,<br />

würde er immer zufällige Werte<br />

liefern. Dies soll er natürlich tun, aber im<br />

Test benötigen wir die Kontrolle darüber,<br />

welche Werte „zufällig“ geliefert werden,<br />

siehe Listing 7.<br />

Die Testmethode ist hier verkürzt dargestellt.<br />

Ich rufe im Test so lange Werte ab, bis<br />

alle möglichen Werte innerhalb von Minimum<br />

und Maximum mindestens einmal<br />

geliefert wurden.<br />

Der Trick, dass sich der Random-Generator<br />

immer gleich verhält, liegt darin, dass<br />

ich ihn im Test immer mit demselben<br />

Startwert (Seed) 0 instanziere.<br />

Um das zu ermöglichen, habe ich einen<br />

internal-Konstruktor ergänzt, der nur im<br />

Test verwendet wird, um den Random-Generator<br />

in die Klasse zu injizieren. Der öffentliche<br />

Konstruktor der Klasse instanziert<br />

den Random-Generator ohne Seed,<br />

sodass dieser zufällige Werte liefert, siehe<br />

Listing 8.<br />

Bei Konstruktoren sollte man übrigens<br />

generell das Highlander-Prinzip beachten:<br />

Es kann nur einen geben (eine Anspielung<br />

auf den Film Highlander – Es kann nur einen<br />

geben). Der interne Konstruktor ist derjenige,<br />

der die eigentliche Arbeit verrichtet.<br />

Der öffentliche Konstruktor verfügt nur<br />

über die beiden Parameter für Minimum<br />

und Maximum. Er bezieht sich auf den internen<br />

Konstruktor und übergibt diesem,<br />

neben den beiden Grenzwerten, auch einen<br />

mit new Random() erzeugten Random-Generator.<br />

Das Highlander-Prinzip<br />

sollte beachtet werden, damit es in den Konstruktoren<br />

nicht zur Verletzung des Prinzips<br />

Don’t Repeat Yourself (DRY) kommt.<br />

Der öffentliche Konstruktor könnte ja die<br />

Grenzwerte selbst an die Felder zuweisen,<br />

dann würden diese Zuweisungen jedoch<br />

an zwei Stellen auftreten.<br />

Eine weitere interessante Implementierung<br />

bietet der RollingIntGenerator. Er liefert,<br />

ausgehend von einem Minimumwert,<br />

[Abb. 8] Das fertige Portal.<br />

Listing 8<br />

Zufallsgenerator für int-Werte.<br />

public RandomIntGenerator(int minimum, int maximum)<br />

: this(minimum, maximum, new Random()) {<br />

}<br />

internal RandomIntGenerator(int minimum, int maximum, Random random) {<br />

this.minimum = minimum;<br />

this.maximum = maximum;<br />

this.random = random;<br />

}<br />

Listing 9<br />

Zufällig einen Stringwert auswählen.<br />

immer den nächsten Wert, bis er beim Maximumwert<br />

angekommen ist. Dann wird<br />

wieder von vorn begonnen. Bei diesem Generator<br />

lag die Herausforderung darin,<br />

korrekt mit dem größtmöglichen int-Wert<br />

(int.MaxValue) umzugehen. Ohne Unit-<br />

Tests wäre das ein elendiges Rumprobieren<br />

geworden. So war es ganz leicht.<br />

Für Stringwerte habe ich einen Generator<br />

implementiert, der eine Liste von<br />

Strings erhält und daraus zufällig einen<br />

auswählt. Die zur Verfügung stehenden<br />

Strings habe ich im Konstruktor als Parameter-Array<br />

definiert, siehe Listing 9.<br />

Das ist für die Unit-Tests ganz angenehm,<br />

weil man einfach eine beliebige Liste<br />

von Stringwerten übergeben kann:<br />

sut = new RandomSelectedStringsGenerator(<br />

new Random(0), "Apfel", "Birne",<br />

"Pflaume");<br />

Bei der Verwendung des Generators aus<br />

Sicht einer Benutzerschnittstelle ist es<br />

LÖSUNG<br />

internal RandomSelectedStringsGenerator(Random random, params string[] values) {<br />

this.random = random;<br />

this.values = values;<br />

}<br />

wünschenswert, einen String zu übergeben,<br />

der eine Liste von Werten enthält, die<br />

mit Semikolon getrennt sind:<br />

"Apfel; Birne; Pflaume"<br />

Um das zu ermöglichen, habe ich eine<br />

separate Extension Method ToValues() implementiert,<br />

die einen String entsprechend<br />

zerlegt. Diese Methode kann bei Bedarf in<br />

den Konstruktoraufruf eingesetzt werden:<br />

"Apfel; Birne; Pflaume".ToValues().ToArray()<br />

Natürlich hätte ich das Zerlegen des<br />

Strings in die Einzelwerte auch im entsprechenden<br />

Generator implementieren können.<br />

Dann hätte der sich aber um mehr als<br />

eine Verantwortlichkeit gekümmert. Ferner<br />

war die Implementierung so etwas einfacher,<br />

da ich mich jeweils auf eine einzelne<br />

Aufgabenstellung konzentrieren konnte.<br />

Portal<br />

Das Portal hatte es in sich. Obwohl ich mit<br />

WPF schon einiges gemacht habe, fühlte<br />

ich mich etwas unsicher, diese sehr dynamische<br />

Aufgabenstellung mit WPF anzugehen,<br />

und entschied mich daher, das Problem<br />

mit Windows Forms zu lösen, weil<br />

mir das schneller von der Hand geht. Doch<br />

der Reihe nach. Wie die Benutzerschnittstelle<br />

des Testdatengenerators ungefähr<br />

aussehen könnte, habe ich in der Aufgabenstellung<br />

bereits durch ein Mockup angedeutet.<br />

Abbildung 8 zeigt, wie mein Ergebnis<br />

aussieht.<br />

www.dotnetpro.de dotnetpro.dojos.2011 19


LÖSUNG<br />

Listing 10<br />

Eine Spalte definieren.<br />

public class SpaltenDefinition {<br />

public string Bezeichnung { get; set; }<br />

public Type ControlType { get; set; }<br />

public Func Columndefinition { get; set; }<br />

}<br />

Listing 11<br />

Spalten definieren.<br />

new SpaltenDefinition {<br />

Bezeichnung = "Random DateTime",<br />

ControlType = typeof(MinimumMaximum),<br />

Columndefinition = (columnName, control) => new ColumnDefinition(columnName,<br />

new RandomDateTimeGenerator(<br />

DateTime.Parse(((MinimumMaximum)control).Minimum),<br />

DateTime.Parse(((MinimumMaximum)control).Maximum)))<br />

}<br />

Ich sehe beim Portal zwei Herausforderungen:<br />

Die Anzahl der Spalten in den zu<br />

generierenden Daten ist variabel. Daraus<br />

ergibt sich, dass die Anzahl der Controls für<br />

Spaltendefinitionen variabel sein muss. Im<br />

Mockup habe ich daher Schaltflächen vorgesehen,<br />

mit denen eine Spaltendefinition<br />

entfernt bzw. hinzugefügt werden kann.<br />

Die zweite Herausforderung sehe ich im<br />

Aufbau der Spaltendefinitionen. Je nach<br />

ausgewähltem Generatortyp sind unterschiedliche<br />

Eingaben notwendig. Mal sind<br />

zwei Textfelder für Minimum und Maximum<br />

erforderlich, mal nur eine für die Elemente<br />

einer Liste. Das heißt, dass sich der<br />

Aufbau der Benutzeroberfläche mit der<br />

Wahl des Generatortyps ändert.<br />

Um diese beiden Herausforderungen<br />

möglichst isoliert angehen zu können, habe<br />

ich für die variablen Anteile einer Spaltendefinition<br />

mit UserControls gearbeitet.<br />

So habe ich für einen Generator, der Minimum-<br />

und Maximumwerte benötigt, ein<br />

UserControl erstellt, in dem zwei Textboxen<br />

mit zugehörigen Labels zusammengefasst<br />

sind.<br />

Wird aus der Dropdownliste ein Generator<br />

ausgewählt, muss das zum Generator<br />

passende Control angezeigt werden. Ferner<br />

muss zum ausgewählten Generator<br />

später die zugehörige ColumnDefinition<br />

erzeugt werden, um damit dann die Daten<br />

zu generieren. Diese Informationen habe<br />

ich im Portal in einer Datenklasse Spalten-<br />

Definition zusammengefasst. Objekte dieser<br />

Klasse werden direkt in der Dropdownliste<br />

verwendet. Daher enthält die Spalten-<br />

Definition auch eine Beschreibung. Diese<br />

wird als DisplayMember in der Dropdownliste<br />

angezeigt, siehe Listing 10.<br />

Die Eigenschaft ControlType enthält den<br />

Typ des zu verwendenden Controls. Genügt<br />

ein Textfeld, kann hier typeof(TextBox) gesetzt<br />

werden. In komplizierteren Fällen<br />

wird der Typ eines dafür implementierten<br />

UserControls gesetzt.<br />

Um für den ausgewählten Generatortyp<br />

eine ColumnDefinition erzeugen zu können,<br />

habe ich eine Eigenschaft ergänzt, die<br />

eine Funktion erhält, die genau dies bewerkstelligt:<br />

Sie erzeugt eine ColumnDefinition.<br />

Dazu erhält sie als Eingangsparameter<br />

zum einen den Namen der Spalte,<br />

zum anderen das Control mit allen weiteren<br />

Angaben. Da der Typ des Controls variabel<br />

ist, wird es vom Typ object übergeben.<br />

Die Funktion muss dieses Objekt<br />

dann auf den erwarteten Typ casten.<br />

Bei der Initialisierung des Portals wird<br />

für die verfügbaren Generatoren jeweils<br />

eine Spaltendefinition erzeugt und in die<br />

Item-Liste des Dropdown-Controls gestellt,<br />

siehe Listing 11.<br />

Interessant hierbei ist die Lambda Expression.<br />

Diese erhält die beiden Parameter<br />

columnName und control und erzeugt<br />

daraus eine ColumnDefinition mit dem<br />

ausgewählten Generator. Da diese Lambda<br />

[Abb. 9] Document Outline.<br />

Expression im Kontext einer SpaltenDefinition<br />

steht, kann das übergebene Control<br />

gefahrlos auf den Typ gecastet werden, der<br />

auch in der Eigenschaft ControlType verwendet<br />

wird. Auch hier sähe eine Lösung<br />

mit Generics sicher eleganter aus, ist aber<br />

ohne Ko-/Kontravarianz nicht möglich.<br />

Wird nun in der Combobox ein anderer<br />

Generatortyp ausgewählt, muss das in der<br />

Spaltendefinition angegebene Control angezeigt<br />

werden. Um dynamisch die zugehörigen<br />

Controls zu finden, füge ich alle<br />

Controls, die zu einer Spalte gehören (Plusund<br />

Minus-Button, Textfeld für den Spaltennamen,<br />

Combobox, Platzhalter für<br />

UserControl) in ein Panel ein. Um in diesem<br />

Panel später dynamisch das UserControl<br />

austauschen zu können, füge ich dieses<br />

zusätzlich in ein weiteres Panel. Dieses<br />

dient jeweils als Platzhalter für das auszutauschende<br />

Control.<br />

In der Form sind nur wenige statische<br />

Elemente vorhanden. Den Aufbau der Form<br />

zeigt die Document Outline in Abbildung 9.<br />

Darin ist dargestellt, wie die einzelnen Controls<br />

ineinandergeschachtelt sind.<br />

Abbildung 10 zeigt, wie die Controls für<br />

eine Spaltendefinition dynamisch zur Laufzeit<br />

zusammengesetzt werden. Dabei zeigen<br />

die Pfeile an, auf welches Control gegebenenfalls<br />

die Tag-Eigenschaft verweist.<br />

Nun zur zweiten Herausforderung, dem<br />

dynamischen Ergänzen und Löschen von<br />

Spaltendefinitionen. Jede Spaltendefinition<br />

verfügt über die beiden Schaltflächen zum<br />

Hinzufügen und Löschen von Spaltendefinitionen.<br />

Zurzeit füge ich eine neue Spaltendefinition<br />

jeweils ans Ende an, künftig<br />

könnte diese aber auch an der betreffenden<br />

Position eingefügt werden. Daher habe<br />

ich bereits an jeder Spaltendefinition einen<br />

Plus-Button vorgesehen. Für die Aufnahme<br />

aller Spaltenbeschreibungen ist im statischen<br />

Teil der Form ein Panel zuständig.<br />

Wird mit der Minus-Schaltfläche versucht,<br />

eine Spaltenbeschreibung zu entfernen,<br />

müssen die zugehörigen Controls aus die-<br />

20 dotnetpro.dojos.2011 www.dotnetpro.de


[Abb. 10] Controls<br />

im Panel.<br />

sem Panel entfernt werden. Um dies zu<br />

vereinfachen, ist das zugehörige Panel an<br />

der Tag-Eigenschaft des Buttons gesetzt. So<br />

„weiß“ der Button, zu welchem Panel er<br />

gehört und kann dieses aus dem umschließenden<br />

Panel entfernen.<br />

Wird im Portal die Schaltfläche Generieren<br />

angeklickt, muss für jede Spaltenbeschreibung<br />

eine ColumnDefinition erzeugt<br />

werden, um dann die Testdaten zu generieren.<br />

Dazu wird die Liste der Spaltenbeschreibungen<br />

im statischen Panel durchlaufen.<br />

Darin befindet sich jeweils ein Textfeld,<br />

das den Namen der Spalte enthält. Ferner<br />

befindet sich im Platzhalterpanel ein<br />

Control, in dem die Parameter für den Generator<br />

enthalten sind. In der Dropdownliste<br />

enthält das SelectedItem eine Spalten-<br />

Definition, aus der sich die ColumnDefinition<br />

erstellen lässt. Dazu wird aus der SpaltenDefinition<br />

die Funktion zum Erzeugen<br />

der ColumnDefinition aufgerufen.<br />

Insgesamt hat das Erstellen des Portals<br />

knapp zwei Stunden in Anspruch genommen.<br />

Automatisierte Tests habe ich dazu<br />

fast keine erstellt. Diese würde ich allerdings<br />

in einem „echten“ Projekt im Nachhinein<br />

ergänzen, da die Logik für den dynamischen<br />

Aufbau des Portals doch recht<br />

umfangreich geworden ist. Um hier bei<br />

späteren Erweiterungen Fehler auszuschließen,<br />

würde ich die typischen Bedienungsschritte<br />

eines Anwenders automatisiert<br />

testen.<br />

Host<br />

Am Ende benötigen wir für die gesamte<br />

Anwendung noch eine EXE-Datei, mit der<br />

die Anwendung gestartet werden kann.<br />

Aufgabe dieses Hosts ist es, die benötigten<br />

Komponenten zu beschaffen und sie den<br />

Abhängigkeiten gemäß zu verbinden. Die<br />

Abhängigkeiten sind hier in Form von Konstruktorparametern<br />

modelliert. Folglich<br />

muss der Host die Komponenten in der<br />

richtigen Reihenfolge instanzieren, im Abhängigkeitsbaum<br />

von unten nach oben,<br />

von den Blattknoten zur Wurzel. Anschließend<br />

übergibt er die Kontrolle an das Portal.<br />

Für die vorliegende Anwendung, bestehend<br />

aus einer Handvoll Komponenten, ist<br />

diese Aufgabe trivial. Bei größeren Anwendungen<br />

kostet diese Handarbeit Zeit und<br />

sollte automatisiert werden. Die Grund-<br />

www.dotnetpro.de dotnetpro.dojos.2011<br />

LÖSUNG<br />

idee dabei ist: Man überlässt das Instanzieren<br />

der Komponenten einem DI-Container<br />

wie StructureMap [2] oder Castle Windsor<br />

[3]. Über ein eigenes Interface identifiziert<br />

man den Startpunkt der Anwendung, und<br />

los geht’s. Ein solcher Host kann dann sogar<br />

generisch sein und in allen Anwendungen<br />

verwendet werden.<br />

Denkbare Erweiterungen<br />

Für wiederkehrende Aufgaben wäre es<br />

sinnvoll, das Schema der Datengenerierung<br />

speichern und laden zu können. Dies<br />

kann beispielsweise mit dem Lounge Repository<br />

[4] erfolgen. In der Architektur<br />

würde dafür ein weiterer Adapter ergänzt,<br />

mit dem ein Schema gespeichert und geladen<br />

werden kann. Natürlich müssten im<br />

Portal entsprechende Anpassungen vorgenommen<br />

werden, um entsprechende Menüfunktionen<br />

zu ergänzen.<br />

Des Weiteren wäre es denkbar, die Generatoren<br />

zur Laufzeit dynamisch zu laden.<br />

Damit könnten Entwickler ihre eigenen<br />

Generatoren implementieren und verwenden,<br />

ohne dazu die gesamte Anwendung<br />

übersetzen zu müssen. Mithilfe eines DI-<br />

Containers wie StructureMap oder des<br />

Managed Extensibility Framework MEF [5]<br />

sollte auch diese Erweiterung keine große<br />

Hürde darstellen.<br />

Fazit<br />

Bei dieser Aufgabe stellte sich heraus, dass<br />

die Benutzerschnittstelle einer Anwendung<br />

durchaus einige Zeit in Anspruch<br />

nehmen kann. Die eigentliche Funktionalität<br />

war dagegen schnell entworfen und implementiert.<br />

Das lag maßgeblich daran,<br />

dass ich mir im Vorfeld einige Gedanken<br />

über die Architektur gemacht hatte. Danach<br />

ging die testgetriebene Entwicklung<br />

flüssig von der Hand. [ml]<br />

[1] Stefan Lieser, Meier, Müller, Schulze…,<br />

Testdaten automatisch generieren,<br />

dotnetpro 5/2010, Seite 108ff.,<br />

www.dotnetpro.de/A1005dojo<br />

[2] http://structuremap.sourceforge.net/<br />

[3] http://www.castleproject.org/container/<br />

[4] http://loungerepo.codeplex.com/ und Ralf<br />

Westphal, Verflixte Sucht, dotnetpro 11/2009,<br />

Seite 52f. www.dotnetpro.de/A0911Sandbox<br />

[5] http://mef.codeplex.com/<br />

Wir liefern passgenaue<br />

Strategien und Lösungen<br />

für Ihre Inhalte auf<br />

iPhone/iPad<br />

Android<br />

BlackBerry<br />

Windows Phone 7<br />

dem mobilen<br />

Browser<br />

Besuchen Sie<br />

uns unter<br />

www.digitalmobil.com


Wer übt, gewinnt<br />

AUFGABE<br />

Daten umformen<br />

Mogeln mit EVA<br />

Eingabe,Verarbeitung,Ausgabe: Das EVA-Prinzip durchdringt die gesamte Softwareentwicklung. Eine Analyse<br />

der Datenstrukturen und die Verwendung der passenden Algorithmen spielen dabei eine herausragende Rolle.<br />

Stefan, fällt dir dazu eine Übung ein?<br />

dnpCode: A1006dojo<br />

In jeder dotnetpro finden Sie<br />

eine Übungsaufgabe von<br />

Stefan Lieser, die in maximal<br />

drei Stunden zu lösen sein<br />

sollte. Wer die Zeit investiert,<br />

gewinnt in jedem Fall – wenn<br />

auch keine materiellen Dinge,<br />

so doch Erfahrung und Wissen.<br />

Es gilt:<br />

❚ Falsche Lösungen gibt es<br />

nicht. Es gibt möglicherweise<br />

elegantere, kürzere<br />

oder schnellere Lösungen,<br />

aber keine falschen.<br />

❚ Wichtig ist, dass Sie reflektieren,<br />

was Sie gemacht<br />

haben. Das können Sie,<br />

indem Sie Ihre Lösung mit<br />

der vergleichen, die Sie<br />

eine Ausgabe später in<br />

dotnetpro finden.<br />

Übung macht den Meister.<br />

Also − los geht’s. Aber Sie<br />

wollten doch nicht etwa<br />

sofort Visual Studio starten …<br />

Wer kennt nicht Minesweeper,<br />

das beliebte Spiel, welches<br />

zum Lieferumfang von Windows<br />

gehört? Doch keine Sorge,<br />

es geht dieses Mal nicht wieder um eine aufwendige<br />

Benutzerschnittstelle, sondern um eine<br />

kleine Kommandozeilenanwendung. Die soll aus<br />

einer Eingabedatei eine Ausgabedatei erzeugen.<br />

Die Eingabedatei enthält die Beschreibung eines<br />

Minesweeper-Spielfeldes. Das Programm erzeugt<br />

als Ausgabedatei einen dazu passenden „Mogelzettel“.<br />

Der Aufruf erfolgt folgendermaßen:<br />

mogelzettel spiel1.txt mogelzettel1.txt<br />

Der Aufbau der Eingabedatei ist wie folgt: Die<br />

erste Zeile enthält die Anzahl der Zeilen und<br />

Spalten des Spielfeldes. Beide Zahlen sind durch<br />

ein Leerzeichen getrennt. Die nachfolgenden<br />

Zeilen enthalten dann jeweils die Konfiguration<br />

einer Zeile des Spielfeldes. Dabei sind freie Felder<br />

durch einen Punkt dargestellt, Felder mit einer<br />

Mine durch einen Stern. Hier ein Beispiel:<br />

5 6<br />

....*.<br />

...*..<br />

......<br />

*....*<br />

.*....<br />

Für diese Eingabedatei soll eine Ausgabedatei<br />

erzeugt werden. Die Ausgabedatei gibt für jedes<br />

Feld die Anzahl der Minen in der unmittelbaren<br />

Nachbarschaft an. Jedes Feld hat maximal acht<br />

Nachbarn, folglich können maximal acht Minen<br />

in der Nachbarschaft eines Feldes vorkommen,<br />

am Rand des Spielfeldes sind es natürlich weniger.<br />

Felder, die selbst eine Mine enthalten, sollen<br />

mit der Ziffer 0 belegt sein, es sei denn, in der<br />

Nachbarschaft befinden sich Minen. Dann sollen<br />

diese ebenfalls gezählt werden.<br />

Der Mogelzettel gibt also keine direkte Auskunft<br />

darüber, wo die Minen liegen, sondern nur<br />

über die Anzahl der Minen in den jeweils benachbarten<br />

Feldern.<br />

Für das obige Beispiel soll folgende Ausgabedatei<br />

erzeugt werden:<br />

001211<br />

001121<br />

111121<br />

121010<br />

211011<br />

Sie dürfen davon ausgehen, dass die Eingabedatei<br />

im korrekten Format vorliegt. Geht in der<br />

Folge etwas schief, ist gegebenenfalls das Ergebnis<br />

inkorrekt, oder das Programm bricht sogar<br />

ab. Dies soll hier keine Rolle spielen.<br />

Die Vorgehensweise bei der Lösung dieser<br />

Übung dürfte Lesern der vorhergehenden Übungen<br />

inzwischen geläufig sein. Zunächst sollten<br />

die Anforderungen geklärt werden. Dazu ist es<br />

sinnvoll, Beispiele aufzuschreiben. Eines habe ich<br />

Ihnen gegeben, vielleicht definieren Sie weitere,<br />

um beispielsweise Randfälle zu definieren. Da<br />

ich Ihnen als „Kunde“ nicht so ohne Weiteres für<br />

Rückfragen zur Verfügung stehe, treffen Sie gegebenenfalls<br />

selber sinnvolle Annahmen. Nach der<br />

Klärung der Anforderungen beginnt die Planung.<br />

Dieser Phase sollten Sie viel Aufmerksamkeit und<br />

Zeit schenken. Je intensiver Sie sich mit dem Problem<br />

und seiner Lösung auseinandersetzen, desto<br />

besser sind Sie vorbereitet für die Implementierung.<br />

Zerlegen Sie das Gesamtproblem in kleinere<br />

Teilprobleme, suchen Sie mögliche Flows.<br />

Wenn Sie sich bei der Herangehensweise nicht<br />

sicher sind, ob eine Idee tatsächlich zur Lösung<br />

des Problems führen wird, lohnt es sich, einen<br />

Spike zu erstellen. Ein Spike dient dazu, eine Idee<br />

zu überprüfen oder eine Technik oder Technologie<br />

zu explorieren. Code, der bei einem Spike entsteht,<br />

wird nicht produktiv verwendet, daher sind<br />

keine Tests erforderlich, und auch gegen Prinzipien<br />

darf gern verstoßen werden. Es geht um den<br />

Erkenntnisgewinn. Es könnte sich dabei schließlich<br />

auch herausstellen, dass eine Idee nicht zum<br />

Ziel führt. Im Anschluss an den Spike beginnen<br />

Sie dann testgetrieben mit der Entwicklung des<br />

Produktionscodes. Dabei beachten Sie selbstverständlich<br />

alle Prinzipien und Praktiken.<br />

Und nun frisch ans Werk! Mogeln Sie aber bitte<br />

nur beim Minesweeper-Spielen, nicht bei der<br />

Softwareentwicklung. [ml]<br />

22 dotnetpro.dojos.2011 www.dotnetpro.de


Daten verarbeiten und auswerten<br />

So mogeln Sie mit EVA!<br />

Die Aufgabenstellung lautete<br />

vorigen Monat: Entwickle ein<br />

Programm, das einen Mogelzettel<br />

für Minesweeper erstellt<br />

[1]. Es soll so aufgerufen werden:<br />

mogelzettel spiel1.txt mogelzettel1.txt<br />

Hier ein Beispiel für den Aufbau der Eingabedatei:<br />

5 6<br />

....*.<br />

...*..<br />

......<br />

*....*<br />

.*....<br />

Für das obige Beispiel soll folgende Ausgabedatei<br />

erzeugt werden:<br />

001211<br />

001121<br />

111121<br />

121010<br />

211011<br />

In der Aufgabenstellung zum Minesweeper-Mogelzettel<br />

habe ich erwähnt, dass vor<br />

der Implementierung die Planung stehen<br />

sollte. Und so habe ich dieses Mal wieder<br />

mit einem Blatt Papier begonnen. Obwohl<br />

ich die Aufgabenstellung schon mehr als<br />

einmal selbst gelöst habe und in zahlreichen<br />

Seminaren beobachten durfte, wie<br />

andere Entwickler an die Lösung herange-<br />

hen, habe ich dennoch nicht sofort begonnen,<br />

Code zu schreiben. Die Planung auf<br />

Papier hilft mir, mich zu fokussieren. Und<br />

sie hilft, auch mal auf andere Lösungen zu<br />

kommen. Schließlich soll Üben dem Gewinnen<br />

von Erkenntnissen dienen.<br />

Erste Zerlegung<br />

Die Aufgabenstellung lässt sich auf der<br />

obersten Ebene in drei Funktionseinheiten<br />

zerlegen:<br />

❚ Lesen des Minenfeldes,<br />

❚ Ermitteln des Mogelzettels,<br />

❚ Schreiben des Mogelzettels.<br />

Diese drei Funktionseinheiten bilden<br />

einen Flow, wie Abbildung 1 zeigt. In die<br />

Methode LeseMinenfeld geht der Dateiname<br />

als Parameter ein, die Methode liefert<br />

eine Aufzählung von Zeichenketten. Dabei<br />

wird die erste Zeile der Datei einfach ignoriert.<br />

Sie enthält die Anzahl der Zeilen und<br />

Spalten. Diese Information wird jedoch<br />

nicht benötigt, da sie aus dem Inhalt der<br />

Datei ebenso hervorgeht.<br />

Das Ergebnis von LeseMinenfeld wird an<br />

die Methode BerechneMogelzettel weitergeleitet.<br />

Ergebnis ist wieder eine Aufzählung<br />

von Zeichenketten, diesmal ist es aber<br />

nicht das Minenfeld, sondern der dazu generierte<br />

Mogelzettel. Die einzelnen Zeilen<br />

des Mogelzettels werden zuletzt, gemeinsam<br />

mit dem Dateinamen, an die Methode<br />

SchreibeMogelzettel übergeben und von<br />

dieser als Datei geschrieben.<br />

Mogelzettel berechnen<br />

Natürlich muss die Methode BerechneMogelzettel<br />

weiter zerlegt werden. Dies war in<br />

der Planung mein nächster Schritt. Die beiden<br />

Methoden zum Laden und Speichern<br />

von Minenfeld und Mogelzettel erscheinen<br />

mir recht einfach, daher habe ich auf eine<br />

weitere Zerlegung verzichtet.<br />

Prinzipiell sehe ich zwei Möglichkeiten,<br />

den Mogelzettel zu erstellen. Man kann entweder<br />

die Felder suchen, auf denen eine<br />

Mine liegt und dann die darum herum lie-<br />

LÖSUNG<br />

Den Mogelzettel für ein Minesweeper-Minenfeld erstellen Sie nach dem EVA-Prinzip: Eingabe,Verarbeitung, Ausgabe.<br />

Die Lösung gestaltet sich nach gründlicher Planung recht einfach. Da im Detail aber Variationen möglich sind, können<br />

Sie diese Übung mit Gewinn auch mehrmals lösen.<br />

[Abb. 1] Der Flow der Funktionen.<br />

[Abb. 3] ...oder<br />

Feld für Feld<br />

vorgehen.<br />

[Abb. 2] Ent-<br />

weder Mine<br />

für Mine...<br />

genden Zähler jeweils um eins erhöhen.<br />

Abbildung 2 zeigt dieses Verfahren, das aus<br />

zwei Schritten besteht:<br />

❚ Minenpositionen ermitteln,<br />

❚ Nachbarfelder inkrementieren.<br />

Oder man geht Feld für Feld vor und<br />

sucht in der Umgebung nach Minen. Man<br />

erhöht den Zähler für das in Bearbeitung<br />

befindliche Feld für jede gefundene Mine.<br />

Diese Variante zeigt Abbildung 3.<br />

Ich habe nach Variante 1 implementiert,<br />

suche also alle Minen und erhöhe dann bei<br />

den Zellen, die um die Mine herum liegen,<br />

den Zähler jeweils um eins. Die algorithmische<br />

Vorgehensweise lässt sich wieder sehr<br />

schön in einem Flow implementieren, er<br />

ist in Abbildung 4 zu sehen. Aus der String-<br />

Repräsentation werden zuerst die Koordinaten<br />

der Minen und die Dimensionen des<br />

Minenfeldes extrahiert. Beides wird dann<br />

verwendet, um daraus den Mogelzettel in<br />

Form eines zweidimensionalen Arrays zu<br />

berechnen. Das Array wird danach wieder<br />

in eine Aufzählung von Strings übersetzt.<br />

Das Ermitteln der Minenkoordinaten habe<br />

ich zunächst für eine einzelne Zeile implementiert.<br />

Listing 1 zeigt einen Test dazu.<br />

www.dotnetpro.de dotnetpro.dojos.2011 23


LÖSUNG<br />

Listing 1<br />

Das Einlesen einer Zeile testen.<br />

[Test]<br />

public void Eine_Zeile_mit_einigen_Minen() {<br />

Assert.That(Mogelzettel.MinenErmitteln("..*.*..*"), Is.EqualTo(new[] {2, 4, 7}));<br />

}<br />

Listing 2<br />

Das Einlesen mehrerer Zeilen testen.<br />

[Test]<br />

public void Mehrere_Zeilen_voller_Minen() {<br />

Assert.That(Mogelzettel.MinenErmitteln(new[] {"***", "***", "***"}),<br />

Is.EqualTo(new[] {<br />

new Point(0, 0), new Point(1, 0), new Point(2, 0),<br />

new Point(0, 1), new Point(1, 1), new Point(2, 1),<br />

new Point(0, 2), new Point(1, 2), new Point(2, 2)<br />

}));<br />

}<br />

Listing 3<br />

Minen ermitteln.<br />

internal static IEnumerable MinenErmitteln(string zeile) {<br />

var x = 0;<br />

foreach (var zeichen in zeile) {<br />

if (zeichen == '*') {<br />

yield return x;<br />

}<br />

x++;<br />

} }<br />

internal static IEnumerable MinenErmitteln(IEnumerable zeilen) {<br />

var y = 0;<br />

foreach (var zeile in zeilen) {<br />

foreach (var x in MinenErmitteln(zeile)) {<br />

yield return new Point(x, y);<br />

}<br />

y++;<br />

} }<br />

Die Methode liefert eine Aufzählung der<br />

Indizes, an denen sich in der Zeile eine Mine<br />

befindet. Wenn man eine Schleife um<br />

die Methode macht, lassen sich die Koordinaten<br />

ganzer Minenfelder ermitteln. Listing<br />

2 zeigt einen Test.<br />

Die Implementierung der beiden Methoden<br />

ist einfach. Interessant ist die Verwendung<br />

von yield return und internal. Der<br />

Rückgabewert beider Methoden ist eine<br />

Aufzählung, technisch gesprochen vom Typ<br />

IEnumerable. Wenn der Rückgabewert<br />

einer Methode von diesem Typ ist, steht innerhalb<br />

der Methode das Schlüsselwort<br />

yield zur Verfügung. Wie in [2] erläutert, erstellt<br />

der C#-Compiler einen Automaten für<br />

die Methode. So entfällt die Notwendigkeit,<br />

innerhalb der Methode eine Liste für das<br />

Sammeln der Ergebniswerte zu erstellen.<br />

Der Code wird kompakter und besser lesbar.<br />

Ein zweiter Aspekt ist die Verwendung<br />

von Methoden, die mit internal sozusagen<br />

halb versteckt werden, siehe Listing 3. Auch<br />

darauf wurde an anderer Stelle bereits hingewiesen,<br />

etwa bei der Lösung zum INotify-<br />

PropertyChanged-Tester in [3]. Ich verwende<br />

dieses Muster immer dann, wenn sich im<br />

Architekturentwurf Methoden abzeichnen,<br />

die weiter zerlegt werden können. Erfolgt<br />

diese Zerlegung bereits im Rahmen der<br />

Planung, teste ich diese Methoden isoliert,<br />

so wie hier gezeigt. Manchmal entstehen<br />

Methoden auch erst im Nachhinein durch<br />

Refaktorisieren. Diese belasse ich bei einer<br />

privaten Sichtbarkeit und teste sie nur in der<br />

Integration mit der verwendenden Methode.<br />

Fummelei beim Index<br />

Eine Besonderheit gibt es bei den Indizes<br />

zu beachten: Berücksichtigen Sie an den<br />

Rändern, dass es nicht in allen Richtungen<br />

benachbarte Felder gibt. Das bedeutet, dass<br />

Sie bei jedem Zugriff auf die Nachbarfelder<br />

prüfen müssen, ob die Zelle an einem der<br />

Ränder liegt. Diese Indexprüfungen machen<br />

den Code recht unübersichtlich.<br />

if ((point.Y - 1 >= 0) && (point.X - 1 >= 0)) {<br />

result[point.Y - 1, point.X - 1]++;<br />

}<br />

Um die Indizes nicht prüfen zu müssen,<br />

können Sie einen Trick anwenden: Wenn<br />

Sie das Array mit einem zusätzlichen Rand<br />

anlegen und dann nur Indizes von 1 bis<br />

Length – 2 statt von 0 bis Length – 1 verwenden,<br />

können Sie sich die Indexprüfungen<br />

sparen. Da der zusätzliche Randbereich<br />

mit Nullen initialisiert ist, macht es<br />

nichts, dort auch die Minen aufzuaddieren.<br />

Nach der Berechnung wird der Rand<br />

einfach wieder entfernt.<br />

Ob diese Lösung besser lesbar ist als bei<br />

der Variante, bei der die Indizes vor jedem<br />

Zugriff geprüft werden, sei dahingestellt.<br />

Letztlich gewinnt man durch den „Rand-<br />

Trick“ etwas Lesbarkeit beim Zugriff auf<br />

das Array, muss aber zusätzlich das Umkopieren<br />

zum Entfernen des Randes implementieren,<br />

siehe Listing 4.<br />

Da ich mit dieser Lösung nicht zufrieden<br />

war, entschied ich mich zu einer weiteren<br />

Variante. Ich wollte versuchen, das Bestimmen<br />

der Indizes der acht benachbarten<br />

Felder zu trennen vom Test auf Gültigkeit<br />

der Indizes. Die Grundidee ist hier also:<br />

Erst mal alle acht möglichen Indizes bestimmen,<br />

dann prüfen, welche davon gültig<br />

sind. Listing 5 zeigt das Ergebnis. Am<br />

Ende waren die beiden Lösungen vom<br />

Umfang des Codes her miteinander vergleichbar.<br />

Die Lesbarkeit und Verständlichkeit<br />

der Lösung ohne „Rand-Trick“ scheint<br />

mir etwas besser, da man den Trick mit<br />

dem Rand eben nicht benötigt.<br />

Struktur<br />

Auch dieses Mal verwende ich bei der Lösung<br />

die Projekt- und Verzeichnisstruktur,<br />

die ich durchgängig immer in allen Projekten<br />

verwende. Das hat den Vorteil, dass mir<br />

diese Schritte so zur Gewohnheit werden,<br />

dass ich nicht mehr darüber nachdenken<br />

24 dotnetpro.dojos.2011 www.dotnetpro.de


[Abb. 4] Der Flow<br />

für die Vorgehens-<br />

weise nach Mine.<br />

[Abb. 5] Die bewährte Projektstruktur.<br />

muss. Ich erstelle die Verzeichnisse, lege<br />

Projekte an, setze Referenzen, ändere Ausgabepfade,<br />

alles ist immer gleich. Wenn Sie<br />

jetzt denken, dass das eine Wiederholung<br />

ist, welche gegen das Prinzip Don't Repeat<br />

Yourself (DRY) verstößt, dann haben Sie<br />

mich erwischt. Eigentlich sollte ich diese<br />

immer wiederkehrenden Handgriffe automatisieren.<br />

Ein Vorteil der immer gleichen Struktur<br />

ist, dass ich mich in allen Projekten sofort<br />

zurechtfinde. Alles liegt immer am gleichen<br />

Platz. Um eine solche Konvention zu etablieren,<br />

muss sie sich allerdings zunächst in<br />

der Praxis bewähren. Denn wenn das nicht<br />

der Fall ist, nützt die schönste Konvention<br />

nichts.Wenn ich regelmäßig Projektstrukturen<br />

anlege und verwende, zeigt sich, ob im<br />

Detail noch Verbesserungen möglich sind.<br />

Die in Abbildung 5 gezeigte Struktur hat<br />

sich in vielen Projekten, Übungen und Seminaren<br />

bewährt. Die Pfeile zeigen, welche<br />

Projekte referenziert werden. Beim Mogelzettel<br />

habe ich mich für zwei Implementierungsprojekte<br />

sowie ein Testprojekt entschieden.<br />

Bei der Implementierung unterscheide<br />

ich zwischen der Logik und dem<br />

Host. Im Host wird lediglich die Konsolenschnittstelle<br />

zur Verfügung gestellt. Die beiden<br />

Parameter der Kommandozeile werden<br />

als Dateinamen interpretiert. Durch<br />

die Abtrennung des Hosts lässt sich die Logik<br />

des Mogelzettels auch einmal in einem<br />

GUI-Host verwenden.<br />

Diesmal habe ich keine komponentenorientierte<br />

Architektur gewählt, da ich mich<br />

auf das Problem der Indizes konzentrieren<br />

wollte. Dennoch sind in der Solution mehrere<br />

Projekte mit klar definierten Aufgaben<br />

vorhanden. Für eine komponentenorientierte<br />

Lösung kämen die Kontrakte hinzu,<br />

Listing 4<br />

Den Rand beim Rand-Trick entfernen.<br />

internal static int[,] RandEntfernen(int[,] array) {<br />

var result = new int[array.GetLength(0) - 2,array.GetLength(1) - 2];<br />

for (var i = 1; i < array.GetLength(0) - 1; i++) {<br />

for (var j = 1; j < array.GetLength(1) - 1; j++) {<br />

result[i - 1, j - 1] = array[i, j];<br />

}<br />

}<br />

return result;<br />

}<br />

Listing 5<br />

Erst Indizes bestimmen, dann prüfen.<br />

LÖSUNG<br />

und die einzelnen Komponenten würden je<br />

eine eigene Solution erhalten.<br />

internal static int[,] MogelzettelBerechnen(Size groesse, IEnumerable minenKoordinaten) {<br />

var result = new int[groesse.Height,groesse.Width];<br />

foreach (var mine in minenKoordinaten) {<br />

var indizes = new[] {<br />

new Point(mine.X - 1, mine.Y - 1),<br />

new Point(mine.X - 1, mine.Y),<br />

new Point(mine.X - 1, mine.Y + 1),<br />

new Point(mine.X, mine.Y - 1),<br />

new Point(mine.X, mine.Y + 1),<br />

new Point(mine.X + 1, mine.Y - 1),<br />

new Point(mine.X + 1, mine.Y),<br />

new Point(mine.X + 1, mine.Y +1),<br />

};<br />

foreach (var index in indizes) {<br />

if ((index.X >= 0) && (index.X < groesse.Width) && (index.Y >= 0) &&<br />

(index.Y < groesse.Height)) {<br />

result[index.Y, index.X]++;<br />

}<br />

}<br />

}<br />

return result;<br />

}<br />

www.dotnetpro.de dotnetpro.dojos.2011 25<br />

Fazit<br />

Der Minesweeper-Mogelzettel ist eine zeitlich<br />

überschaubare Übung. Der Umgang<br />

mit den Indizes bietet ein reichhaltiges Betätigungsfeld.<br />

Falls Sie es also bislang noch<br />

nicht selbst versucht haben: Mogeln Sie<br />

mal wieder beim Minesweeper, aber nicht<br />

beim Übungspensum! [ml]<br />

[1] Stefan Lieser: Mogeln mit EVA, Daten umformen,<br />

dotnetpro 6/2010, Seite 116ff.<br />

www.dotnetpro.de/A1006dojo<br />

[2] Golo Roden: yield return, yield break, yield...<br />

Golos scharfes C, dotnetpro 5/2010, S. 122f.<br />

www.dotnetpro.de/A1005ScharfesC<br />

[3] Stefan Lieser: Kettenreaktion, INotifyProperty-<br />

Changed-Logik automatisiert testen,<br />

dotnetpro 5/2010, S. 108ff.<br />

www.dotnetpro.de/A1005dojo


Wer übt, gewinnt<br />

AUFGABE<br />

Zahlenreihen visualisieren mit Boxplots<br />

Papa, was ist ein Boxplot?<br />

Wie lange dauert und was kostet dies und jenes im Durchschnitt, höchstens, mindestens und am wahrscheinlichsten?<br />

Statistik ist das halbe Leben, in Form von Zahlen und in Form von Grafiken. Stefan, kannst du dazu eine Aufgabe stellen?<br />

dnpCode: A1007DojoAufgabe<br />

In jeder dotnetpro finden Sie<br />

eine Übungsaufgabe von<br />

Stefan Lieser, die in maximal<br />

drei Stunden zu lösen sein<br />

sollte. Wer die Zeit investiert,<br />

gewinnt in jedem Fall – wenn<br />

auch keine materiellen Dinge,<br />

so doch Erfahrung und Wissen.<br />

Es gilt:<br />

❚ Falsche Lösungen gibt es<br />

nicht. Es gibt möglicherweise<br />

elegantere, kürzere<br />

oder schnellere Lösungen,<br />

aber keine falschen.<br />

❚ Wichtig ist, dass Sie reflektieren,<br />

was Sie gemacht<br />

haben. Das können Sie,<br />

indem Sie Ihre Lösung mit<br />

der vergleichen, die Sie<br />

eine Ausgabe später in<br />

dotnetpro finden.<br />

Übung macht den Meister.<br />

Also − los geht’s. Aber Sie<br />

wollten doch nicht etwa<br />

sofort Visual Studio starten…<br />

Haben Sie sich auch schon mal gefragt,<br />

wie Sie eine Zahlenreihe anschaulich<br />

darstellen können? Eine<br />

übersichtliche Form bieten die sogenannten<br />

Boxplots [1]. Darauf hat mich meine<br />

Tochter gebracht. Sie musste Boxplots als Hausaufgabe<br />

in Mathe zeichnen. 7. Klasse! Da werden<br />

Sie sich als gestandener Softwareentwickler doch<br />

nicht wegducken wollen, oder?<br />

Boxplots dienen dazu, die Verteilung der Werte<br />

zu visualisieren. Dazu werden neben dem kleinsten<br />

und dem größten Wert auch die sogenannten<br />

Quartile visualisiert. Die Zahlenreihe wird in vier<br />

Bereiche unterteilt. Sind diese Bereiche gleich<br />

groß, bedeutet das, dass die Werte in der Zahlenreihe<br />

gleichmäßig verteilt sind. Dazu ein Beispiel.<br />

Wenn Sie sich fragen, ob der Pizzadienst<br />

um die Ecke immer gleich lange braucht, um die<br />

Pizza anzuliefern, oder ob es auch schon mal<br />

große Ausreißer gibt, können Sie die Werte mit<br />

einem Boxplot schön visualisieren. Ich habe<br />

nicht gemessen, aber die Werte könnten so aussehen:<br />

18, 24, 19, 19, 20, 25, 24, 18, 24, 17<br />

Aus den reinen Zahlen wird man nicht sofort<br />

etwas erkennen können. Helfen würde schon<br />

mal der Mittelwert. Wie der berechnet wird, ist<br />

jedem sofort klar. Aber wie sieht es mit dem Median<br />

aus? Erinnern Sie sich noch?<br />

Zur Berechnung des Medians müssen die Werte<br />

zunächst sortiert werden. Dann nimmt man<br />

einfach den mittleren Wert. Wenn die Anzahl der<br />

Werte gerade ist, nimmt man die beiden mittleren<br />

Werte und bildet daraus den Mittelwert. Im<br />

obigen Beispiel sind es zehn Werte. Nach dem<br />

Sortieren sieht die Zahlenreihe wie folgt aus:<br />

17, 18, 18, 19, 19, 20, 24, 24, 24, 25<br />

Die beiden mittleren Werte sind 19 und 20. Der<br />

Mittelwert aus diesen ist (19 + 20) / 2 = 19,5.<br />

Beim unteren und oberen Quartil geht es sinngemäß,<br />

wie Abbildung 1 zeigt. Aus den Werten<br />

wird dann ein Boxplot erstellt, in dem Minimum<br />

und Maximum die untere bzw. obere Begrenzung<br />

bilden. Dazwischen werden die beiden<br />

Quartile sowie der Median eingezeichnet, fertig<br />

[Abb. 1] Die Ausgangs-Zahlenreihe.<br />

[Abb. 2] Grafische Darstellung mit einem Boxplot.<br />

ist der Boxplot. Abbildung 2 zeigt das Ergebnis.<br />

Aufgabe des dojos ist es, ein Control zu entwickeln,<br />

das einen Boxplot darstellt. Ob Sie das mit<br />

Windows Forms, WPF oder Silverlight lösen, ist<br />

egal. Selbst eine Ausgabe auf der Konsole wäre<br />

reizvoll.<br />

Die Logik von der Umsetzung trennen<br />

Bedenken Sie, dass eine saubere Trennung der Belange<br />

wichtig ist. Die Logik des Boxplots sollte<br />

unabhängig sein von den technischen Details<br />

des Controls. Das hilft in jedem Fall beim automatisierten<br />

Testen. Ferner schaffen Sie durch<br />

diese Trennung die Grundlage dafür, dass Sie das<br />

Control später auch einmal in einer anderen<br />

Technologie realisieren können.<br />

Denken Sie auch darüber nach, wie die Schnittstellen<br />

der beteiligten Funktionseinheiten am<br />

besten aussehen. Sollte das Control die komplette<br />

Zahlenreihe erhalten? Oder direkt die Quartile<br />

und die anderen benötigten Werte? Viele Fragen,<br />

nächsten Monat gibt es hier wieder Antworten.<br />

Bis dahin, viel Spaß mit Boxplots! [ml]<br />

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

26 dotnetpro.dojos.2011 www.dotnetpro.de


Mit Boxplots Zahlenreihen visualisieren<br />

So boxen Sie mit Silverlight!<br />

Wer sich an der Aufgabenstellung<br />

versucht hat und<br />

auf die Schnelle keine Definition<br />

für Quartile im<br />

Kopf hatte, wird die Suchmaschine seines<br />

Vertrauens bemüht haben. Das Ergebnis<br />

dürfte überraschen: Man findet unterschiedliche<br />

Vorschläge, wie das untere und<br />

obere Quartil zu bestimmen seien. Unter [1]<br />

ist eine Erklärung zu finden, aus der auch<br />

hervorgeht, wie Excel die Quartile berechnet.<br />

Unter [2] finden sich Beispielaufgaben.<br />

Dabei wird, soweit erkennbar, das Verfahren<br />

verwendet, das [3] beschreibt. Ich habe<br />

ebenfalls nach dem dort beschriebenen<br />

Verfahren implementiert.<br />

Doch bevor ich meine Implementierung<br />

beschreibe, möchte ich auf ein Problem<br />

hinweisen, welches mich „während der<br />

Fahrt“ erwischt hat. Ich habe die Aufgabe<br />

als Silverlight-Anwendung begonnen. Ein<br />

Grund dafür war, dass ich sehen wollte, ob<br />

Visual Studio 2010 im Bereich Testen von<br />

Silverlight-Anwendungen endlich etwas zu<br />

bieten hat. Doch leider – Fehlanzeige. Es<br />

gibt von Microsoft nach wie vor nur das<br />

Silverlight Unit Test Framework aus dem<br />

Toolkit [4]. Dies ist jedoch für die testgetriebene<br />

Entwicklung nicht geeignet, da man<br />

nicht die Möglichkeit hat, aus Visual Studio<br />

heraus einen einzelnen Test zu starten.<br />

Mir wurde es nach kurzer Zeit zu lästig,<br />

immer mit [Ctrl] + [F5] den Test Runner im<br />

Browser zu starten. Also habe ich nach einer<br />

Alternative gesucht. Doch selbst beim<br />

Versionsstand 4 von Silverlight sieht es im<br />

Bereich automatisiertes Testen nach wie vor<br />

dürftig aus. Zwar gibt es einige Werkzeuge,<br />

mit denen Silverlight-Controls und Anderes<br />

getestet werden können. Und natürlich<br />

müssen diese Tests im Browser laufen, um<br />

eine vollständige Silverlight-Umgebung<br />

abzubilden. Was fehlt, ist Unterstützung<br />

für das Testen von Nicht-UI-Klassen.<br />

Ich bin schließlich doch noch fündig geworden.<br />

Roy Osherove hat eine Ergänzung<br />

zu TypeMock Isolator [5] entwickelt, mit<br />

der Silverlight-Tests innerhalb von Visual<br />

Studio laufen können. Das SilverUnit genannte<br />

Open-Source-Projekt ist unter [6] zu<br />

finden. Es setzt allerdings eine kostenpflichtige<br />

Lizenz von TypeMock Isolator voraus.<br />

Doch zurück zum Boxplot. Die Aufgabenstellung<br />

lässt sich grob in zwei Bereiche<br />

unterteilen:<br />

❚ Benutzerschnittstelle (UI, Control),<br />

❚ Berechnung.<br />

Ausgangspunkt eines Boxplots ist eine<br />

Aufzählung von Werten. Für diese Werte<br />

müssen Sie für die Visualisierung folgende<br />

Größen ermitteln: Minimum, Unteres<br />

Quartil, Median, Oberes Quartil, Maximum.<br />

Zur Ermittlung dieser Größen ist es<br />

erforderlich, die Werte zu sortieren. Es ist<br />

naheliegend, die Implementierung so vorzunehmen,<br />

dass die Ausgangswerte nur<br />

einmal sortiert werden. Aber Vorsicht vor<br />

Optimierungen! Die Größen sind unabhängig<br />

voneinander und können daher<br />

auch unabhängig implementiert werden.<br />

Widerstehen Sie dem Reflex, von Anfang<br />

an eine Implementierung vorzusehen, in<br />

der die Sortierung herausgezogen wird.<br />

Sollte sich später herausstellen, dass das<br />

mehrfache Sortieren zu Problemen bei der<br />

Geschwindigkeit führt, können Sie immer<br />

noch nach Abhilfe suchen.<br />

LÖSUNG<br />

Statistik hat immer mit Zahlen zu tun. Und Zahlen kann man immer irgendwie grafisch darstellen, eine Zahlenreihe<br />

zum Beispiel in einem Boxplot.Aber wer versucht, ein entsprechendes Silverlight-Control testgetrieben zu entwickeln,<br />

muss feststellen, dass auch Silverlight 4 die testgetriebene Entwicklung nur mangelhaft unterstützt.<br />

Listing 1<br />

Fest steht: Bei der Berechnung benötigen<br />

Sie einige Methoden, die aus den Grunddaten<br />

die zur Visualisierung benötigten<br />

Größen ermitteln. Diese Methoden lassen<br />

sich testgetrieben recht gut entwickeln.<br />

Control-API<br />

Eine Dependency-Property beschreiben.<br />

public static readonly DependencyProperty LowScaledProperty =<br />

DependencyProperty.Register("LowScaled", typeof(double),<br />

typeof(BoxPlot), new PropertyMetadata(Changed));<br />

Listing 2<br />

Eine Eigenschaft für die Dependency-Property.<br />

public double LowScaled {<br />

get { return (double)GetValue(LowScaledProperty); }<br />

}<br />

Die Schnittstelle des Controls sollte für den<br />

Verwender möglichst komfortabel sein: Ich<br />

möchte das Control auf ein Formular ziehen,<br />

die Größe einstellen, fertig. Insbesondere<br />

mit der Skalierung sollte der Verwender<br />

nichts zu tun haben. Auf der anderen<br />

Seite sollte das Control allerdings auch<br />

nicht zu viel tun. Insbesondere das Berechnen<br />

der darzustellenden Größen aus den<br />

Werten liegt nicht im Verantwortungsbereich<br />

des Controls. Die Ermittlung des<br />

Medians hat nichts mit der Funktionalität<br />

eines Controls zu tun.<br />

Damit sich das Control um die Skalierung<br />

kümmern kann, müssen die Größen<br />

wie Minimum, Maximum, Median und so<br />

weiter nichtskaliert, das heißt als Originalwert,<br />

angegeben werden. Aufgabe des Controls<br />

ist es, den Bereich zwischen Minimum<br />

und Maximum in der zur Verfügung stehenden<br />

Breite darzustellen. Folglich müssen<br />

sämtliche Größen auf die zur Verfügung<br />

stehende Breite skaliert werden. Ich<br />

www.dotnetpro.de dotnetpro.dojos.2011 27


LÖSUNG<br />

habe mich daher entschlossen, für die<br />

darzustellenden Werte jeweils zwei Eigenschaften<br />

im Control bereitzustellen: eine<br />

Eigenschaft für den nichtskalierten Originalwert<br />

sowie eine für den skalierten Wert.<br />

Das Control übernimmt das Skalieren. Immer<br />

wenn einer der nichtskalierten Werte<br />

verändert wird, muss das Control den skalierten<br />

Wert anpassen. Realisiert man die<br />

Eigenschaften als sogenannte Dependency-Properties,<br />

können sie per Data-Binding<br />

im Control verwendet werden.<br />

Mit dieser Idee zur grundsätzlichen Vorgehensweise<br />

habe ich mich allerdings etwas<br />

unsicher gefühlt. Mir war nämlich nicht<br />

ganz klar, ob dies tatsächlich funktioniert.<br />

Dazu fehlt mir die Praxis mit Silverlight. Da<br />

dies ein übliches Problem in Projekten ist,<br />

will ich es hier kurz thematisieren.<br />

Entwickler stehen immer wieder vor Herausforderungen,<br />

zu denen sie zwar eine<br />

grobe Vorstellung über mögliche Lösungswege<br />

entwickeln können. Am Ende bleiben<br />

jedoch manchmal Unsicherheiten über<br />

den konkreten Lösungsweg. Diese können<br />

beispielsweise in konkreten Details der zu<br />

Listing 3<br />

set veranlasst die Skalierung.<br />

public double Low {<br />

get { return (double)GetValue(LowProperty); }<br />

set {<br />

SetValue(LowProperty, value);<br />

SetValue(LowScaledProperty, Scaled(value));<br />

}<br />

}<br />

Listing 4<br />

Die Skalierung berechnen.<br />

verwendenden Technologie oder auch in<br />

Algorithmen liegen.<br />

Um diese Unsicherheit in den Griff zu<br />

bekommen, kann man einen sogenannten<br />

Spike implementieren. Ein Spike ist eine<br />

Art „Forschungsprojekt“. Der Spike soll dazu<br />

dienen, Unsicherheiten zu beseitigen.<br />

Ziel eines Spikes ist also der Erkenntnisgewinn,<br />

nicht etwa produktionsfertige Software.<br />

Daher werden an den Spike andere<br />

Anforderungen gestellt. Er muss nicht testgetrieben<br />

entwickelt werden.<br />

Es gibt allerdings auch ein großes „Aber“:<br />

Beim Spike ist zwar alles erlaubt, aber das<br />

Ergebnis wird nicht in der Produktion verwendet.<br />

Nachdem der Spike zum Erkenntnisgewinn<br />

geführt hat, muss die betreffende<br />

Funktionalität anschließend nach allen<br />

Regeln der Kunst testgetrieben implementiert<br />

werden.<br />

Nachdem ich also per Spike geklärt hatte,<br />

dass einer Implementierung mittels Dependency-Properties<br />

nichts im Wege steht, begann<br />

ich mir über die Architektur Gedanken<br />

zu machen. Diese erwies sich als trivial: Auf<br />

der einen Seite gibt es ein Control mit Eigen-<br />

private double Scaled(double value) {<br />

return (value - Min + 1) * ((ActualWidth - StrokeThickness) / (Max - Min));<br />

}<br />

Listing 5<br />

Testbeispiele entwickeln.<br />

[TestMethod]<br />

public void Drei_sortierte_Werte() {<br />

Assert.AreEqual(2.0, Zahlenreihe.Median(new[] {1.0, 2.0, 3.0}));<br />

}<br />

[TestMethod]<br />

public void Vier_sortierte_Werte() {<br />

Assert.AreEqual(2.5, Zahlenreihe.Median(new[] {1.0, 2.0, 3.0, 4.0}));<br />

}<br />

schaften für Minimum, Quartile, Median<br />

und Maximum. Auf der anderen Seite gibt es<br />

einige statische Methoden, um diese Werte<br />

zu berechnen. Damit sind Control und Berechnungslogik<br />

unabhängig voneinander.<br />

Beim Control habe ich eine einfache Lösung<br />

gewählt: Minimum und Maximum<br />

liegen jeweils am Rand des Controls. Damit<br />

füllt der Boxplot den für das Control zur<br />

Verfügung stehenden Platz vollständig aus.<br />

Nun habe ich mir eine Skizze gemacht, die<br />

visualisiert, aus welchen Primitiven der<br />

Boxplot aufgebaut ist, siehe Abbildung 1.<br />

[Abb. 1] Den Boxplot aus einzelnen Linien aufbauen.<br />

Ich habe dazu als Primitive nur Linien<br />

verwendet. Bei einem waagerecht liegenden<br />

Boxplot sind die y-Koordinaten nicht<br />

von den darzustellenden Größen abhängig,<br />

sondern nur vom zur Verfügung stehenden<br />

Platz. Folglich sind lediglich die x-Koordinaten<br />

per Data-Binding an die Dependency-Properties<br />

gebunden.<br />

Die Implementierung des Controls besteht<br />

somit aus drei Teilen:<br />

❚ XAML-Datei zur Definition der Linien<br />

und der Data-Bindings,<br />

❚ Dependency-Properties für die darzustellenden<br />

Größen,<br />

❚ Skalierungslogik.<br />

Die XAML-Datei enthält einen Canvas<br />

als Container. Darin liegen die neun Line-<br />

Elemente, bei denen die in Abbildung 1<br />

markierten x-Koordinaten über Data-Binding<br />

von den Dependency-Properties abhängen.<br />

Damit das Data-Binding sich auf<br />

eigene Eigenschaften des Controls bezieht,<br />

müssen Sie im UserControl den DataContext<br />

wie folgt setzen:<br />

<br />

Dadurch können Sie in den Line-Elementen<br />

beim Data-Binding Eigenschaften<br />

des Controls verwenden:<br />

<br />

28 dotnetpro.dojos.2011 www.dotnetpro.de


Im Beispiel ist LowScaled die Dependency-Property,<br />

welche den skalierten Wert für<br />

das untere Quartil enthält. Für die Dependency-Properties<br />

wird jeweils ein statisches<br />

Feld definiert, welches die Dependency-Property<br />

beschreibt, siehe Listing 1.<br />

Zusätzlich sind normale C#-Eigenschaften<br />

definiert, um in der üblichen Art und<br />

Weise auf die Eigenschaften zugreifen zu<br />

können, siehe Listing 2. Der Umweg über<br />

die Dependency-Properties ist erforderlich,<br />

damit die Visualisierung jeweils aktualisiert<br />

wird, wenn sich an den zugrunde liegenden<br />

Werten etwas ändert.<br />

Die Skalierung der Größen erfolgt in den<br />

Settern der jeweiligen zugehörigen nichtskalierten<br />

Größe. Dort wird sowohl der zu<br />

setzende Originalwert als auch der berechnete<br />

skalierte Wert in die jeweiligen<br />

Dependency-Properties übertragen, wie in<br />

Listing 3 zu sehen.<br />

Auf diese Weise wird beim Setzen der<br />

Low-Eigenschaft auch die LowScaled-Eigenschaft<br />

gesetzt. Das Skalieren übernimmt<br />

die in Listing 4 gezeigte Methode. Um die<br />

korrekte Visualisierung des Controls prüfen<br />

zu können, bleibt nichts anderes übrig,<br />

codekicker.de<br />

Listing 6<br />

Gerade und ungerade Anzahl von Werten testen.<br />

[TestMethod]<br />

public void Drei_unsortierte_Werte() {<br />

Assert.AreEqual(2.0, Zahlenreihe.Median(new[] {2.0, 3.0, 1.0}));<br />

}<br />

[TestMethod]<br />

public void Vier_unsortierte_Werte() {<br />

Assert.AreEqual(2.5, Zahlenreihe.Median(new[] {3.0, 1.0, 4.0, 2.0}));<br />

}<br />

Listing 7<br />

Das Minimum über LINQ ermitteln.<br />

[TestMethod]<br />

public void Minimum() {<br />

Assert.AreEqual(1, new[]{5.0, 1.0, 2.0}.Min());<br />

}<br />

als einen kleinen Testrahmen zu erstellen,<br />

in dem das Control angezeigt wird. Das bedeutet<br />

allerdings nicht, dass in solchen<br />

Tests nichts zu automatisieren wäre. Der<br />

LÖSUNG<br />

Testrahmen kann immerhin dazu verwendet<br />

werden, Beispieldaten automatisiert<br />

zum Control zu übertragen. So entfällt die<br />

manuelle Interaktion mit dem Control zum<br />

Die deutschsprachige Q&A-Plattform<br />

für Software-Entwickler<br />

codekicker.de – Antworten für Entwickler


LÖSUNG<br />

Testzeitpunkt. Zudem sind dadurch die verwendeten<br />

Testdaten dokumentiert.<br />

Berechnungen<br />

Nachdem das Control fertiggestellt ist, geht<br />

es an die Berechnung der benötigten Größen.<br />

Dabei kann man dank SilverUnit und<br />

TypeMock Isolator wieder testgetrieben<br />

vorgehen. Ich habe zunächst einige Beispiele<br />

zusammengestellt und diese dann<br />

nach und nach in automatisierte Tests<br />

überführt, siehe Listing 5.<br />

Da mir der Algorithmus zur Berechnung<br />

des Medians vor der Implementierung vertraut<br />

war, habe ich die Tests so gewählt,<br />

dass ich den Algorithmus schrittweise implementieren<br />

konnte. Zunächst habe ich<br />

daher den Median aus einer bereits sortierten<br />

Aufzählung ermittelt. Dabei sind zwei<br />

Fälle zu unterscheiden: Die Anzahl der<br />

Werte kann ungerade oder gerade sein. Bei<br />

einer geraden Anzahl von Werten werden<br />

die beiden mittleren Werte herangezogen<br />

Listing 8<br />

Die Berechnung von Quartilen testen.<br />

und aus diesen der Mittelwert berechnet.<br />

Die Ergänzung um das Sortieren war keine<br />

große Sache, wie Listing 6 zeigt.<br />

Nach dem Median kamen Minimum und<br />

Maximum an die Reihe – auch kein großes<br />

Problem. Nach dem Sortieren den ersten beziehungsweise<br />

letztenWert zu verwenden ist<br />

einfach. Aber halt: Gibt es diese Funktionalität<br />

nicht in LINQ? Listing 7 zeigt es.<br />

Siehe da, ganz einfach. Bleiben noch die<br />

beiden Quartile. Hier war, wie eingangs<br />

schon erwähnt, eher die Frage, welcher Algorithmus<br />

verwendet werden sollte. Die<br />

Tests sind wieder keine große Sache. Das<br />

Beispiel von Listing 8 ist auf SilverUnit und<br />

NUnit ausgelegt, daher sehen die Attribute<br />

an der Testmethode etwas anders aus als in<br />

den vorigen Beispielen. Listing 9 zeigt die<br />

zugehörige Implementierung.<br />

Die Methode verwendet zum Sortieren<br />

die in Listing 10 gezeigte Extension Method.<br />

Dadurch wird die Anwendung des Sortierens<br />

besser lesbar. Zudem ist die Implemen-<br />

[Test]<br />

[SilverlightUnitTest]<br />

public void Quartil_25_bei_7_Werten() {<br />

Assert.AreEqual(2.0, Zahlenreihe.UnteresQuartil(new[] {1.0, 2.0, 3.0, 4.0,<br />

5.0, 6.0, 7.0}));<br />

}<br />

Listing 9<br />

Quartile berechnen.<br />

public static double UnteresQuartil(IEnumerable zahlenreihe) {<br />

var werte = zahlenreihe.Sort();<br />

if (werte.Count % 4 == 0) {<br />

var x1 = werte[werte.Count / 4 - 1];<br />

var x2 = werte[werte.Count / 4];<br />

return (x1 + x2) / 2;<br />

}<br />

return werte[(int)Math.Ceiling(werte.Count / 4.0) - 1];<br />

}<br />

Listing 10<br />

Die Werte sortieren.<br />

public static class ArrayExtensions {<br />

public static IList Sort(this IEnumerable enumerable) {<br />

var values = enumerable.ToArray();<br />

Array.Sort(values);<br />

return values;<br />

}<br />

}<br />

[Abb. 2] Geschafft: Ein Boxplot im Browser.<br />

tierung des Sortierens damit in einer Methode<br />

zusammengefasst. Sollte sich später<br />

zeigen, dass das Sortieren über Arrays zu Performance-<br />

oder Speicherproblemen führt,<br />

kann dies an einer einzigen Stelle behoben<br />

werden. Abbildung 2 zeigt das fertige Control<br />

im Browser.<br />

30 dotnetpro.dojos.2011 www.dotnetpro.de<br />

Fazit<br />

Die Herausforderung lag diesmal im Tooling.<br />

Automatisiertes Testen von Silverlight-<br />

Anwendungen ist immer noch ein schwieriges<br />

Unterfangen. Bei dem kostenlos verfügbaren<br />

Tool aus dem Silverlight Toolkit stört<br />

mich persönlich vor allem, dass es auf<br />

MSTest basiert. Als NUnit-Anwender fällt es<br />

mir schwer, Assert.AreEqual zu schreiben<br />

statt Assert.That.<br />

Ferner ist die ausschließliche Ausführung<br />

im Browser nicht zu tolerieren. Hier sollte<br />

Microsoft schnell nachbessern und einen in<br />

Visual Studio integrierten Unit Test Runner<br />

liefern. Dass dies möglich ist, zeigt Testdriven.NET<br />

[7]. Leider kann damit aber immer<br />

nur ein einziger Test ausgeführt werden. Die<br />

Alternative lautet zurzeit SilverUnit. Dazu<br />

ist zwar eine kostenpflichtige Lizenz von<br />

TypeMock Isolator erforderlich, das dürfte<br />

aber für ernsthafte kommerzielle Entwicklungen<br />

im Silverlight-Umfeld kein Problem<br />

darstellen. [ml]<br />

[1] Nach welchem Verfahren berechnet Excel<br />

eigentlich Quartile? Hinter die Kulissen<br />

von Excel geschaut,<br />

www.dotnetpro.de/SL1008dojo1<br />

[2] Übungen zu Boxplots,<br />

www.dotnetpro.de/SL1008dojo2<br />

[3] Zeichnen von Boxplots mithilfe von Excel,<br />

Anleitung, www.dotnetpro.de/SL1008dojo3<br />

[4] http://code.msdn.microsoft.com/silverlightut/<br />

[5] http://typemock.com<br />

[6] http://cthru.codeplex.com/<br />

[7] http://testdriven.net


Experimentieren mit Raven DB<br />

Was kann der Rabe?<br />

Kaum eine Software kommt ohne Persistenz aus.Auf diesem Gebiet stehen die relationalen<br />

Datenbanken in fest gefügter Phalanx.Aber geht Persistenz nicht auch anders? Da gibt es doch diese<br />

NoSQL-Dokumentendatenbanken. Stefan, fällt dir dazu eine Übung ein?<br />

Persistenz ist ein wichtiger Aspekt in<br />

vielen Anwendungen. Seit Jahrzehnten<br />

bewährt sich die Technologie der relationalen<br />

Datenbanken. Sie ist allerdings<br />

nicht in allen Fällen gut geeignet, die Anforderungen<br />

umzusetzen. Wenn das Schema der Daten<br />

flexibel sein muss, bieten sich Alternativen an.<br />

Mit dieser Problematik befasst sich unter dem<br />

Stichwort „NoSQL“ inzwischen eine ganze Reihe<br />

von Projekten. Sie setzen ganz bewusst nicht auf<br />

SQL. Zugleich wollen diese Projekte die relationalen<br />

Datenbanken nicht ersetzen, sondern verstehen<br />

sich als Alternative, die in bestimmten<br />

Kontexten sinnvoll ist. Daher wird NoSQL oft<br />

auch mit „not only SQL“ übersetzt.<br />

Da liegt es doch nahe, sich im Rahmen des<br />

dotnetpro.dojo einmal mit einem NoSQL-Projekt<br />

zu befassen. Schließlich bedeutet regelmäßiges<br />

Üben für Softwareentwickler auch, sich ab und<br />

zu mal mit völlig neuen Dingen zu beschäftigen.<br />

Da Ayende Rahien gerade sein neuestes Projekt<br />

Raven DB [1] veröffentlicht hat, bietet sich die<br />

Chance, zu den Early Adoptern zu gehören. Daher<br />

lautet die Aufgabe des Monats: Schreibe eine<br />

kleine Raven-DB-Anwendung.<br />

Beim Einsatz einer neuen Technologie, mit der<br />

man noch nicht vertraut ist, bietet es sich an,<br />

dies in Form eines sogenannten Spikes zu bewerkstelligen.<br />

Ziel eines Spikes ist nicht, Code zu<br />

schreiben, der Produktionsqualität erreicht, sondern<br />

Ziel ist Erkenntnisgewinn. Doch wenn sich<br />

automatisierte Tests für ein flüssiges Entwickeln<br />

von Produktionscode eignen, mögen sie auch in<br />

einem Spike nützlich sein, um schnell voranzukommen.<br />

Denn nach dem Speichern und Laden<br />

eines Objektes mit Raven DB wird schnell der<br />

Wunsch entstehen, auch die anderen Fähigkeiten<br />

des APIs auszuloten. Da kommt man mit einer<br />

Reihe von Tests, die im Unit Test Runner einzeln<br />

gestartet werden können, zügig voran.<br />

Nach den ersten Schritten, die vor allem dazu<br />

dienen, sich mit dem API vertraut zu machen,<br />

soll eine kleine Aufgabenstellung bearbeitet werden.<br />

Implementieren Sie daher eine kleine Anwendung<br />

zur Bewertung von Produkten. Die Anwender<br />

sollen damit in die Lage versetzt werden,<br />

Produktbewertungen abzugeben und sie einzu-<br />

[Abb. 1] Ungefähr so könnte die Seite für Produktbewer-<br />

tungen aussehen.<br />

sehen. Überlegen Sie sich also ein kleines Datenmodell,<br />

bestehend aus Produkten, Kategorien, in<br />

die ein Produkt fällt, sowie Bewertungen und<br />

Kommentaren zu einem Produkt. Da Raven DB<br />

eben gerade nicht relational ist, besteht die Herausforderung<br />

möglicherweise darin, sich von der<br />

in uns schlummernden relationalen Denkweise<br />

ganz bewusst zu lösen.<br />

Um die Fähigkeiten von Raven DB zu erkunden,<br />

sollten Sie in der Anwendung ein Feature<br />

vorsehen, das Daten aggregiert. Sie können beispielsweise<br />

aus allen abgegebenen Bewertungen<br />

zu einem Produkt den Mittelwert bilden. Oder<br />

die Bewertungen aller Produkte einer Kategorie<br />

aggregieren. Oder das Produkt mit der besten Bewertung<br />

innerhalb einer Kategorie ermitteln.<br />

Lassen Sie Ihrer Fantasie freien Lauf. Einige Ideen<br />

liefert das in Abbildung 1 gezeigte Mockup.<br />

Somit sind Sie diesen Monat eigentlich in<br />

zweifacher Weise herausgefordert: Die erste Herausforderung<br />

ist einfach die Beschäftigung mit<br />

Raven DB. Die zweite besteht darin, beispielhafte<br />

Anforderungen mit Raven DB umzusetzen.<br />

Viel Spaß bei der Arbeit als Forscher auf unbekanntem<br />

Terrain. [ml]<br />

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

AUFGABE<br />

dnpCode: A1008DojoAufgabe<br />

In jeder dotnetpro finden Sie<br />

eine Übungsaufgabe von<br />

Stefan Lieser, die in maximal<br />

drei Stunden zu lösen sein<br />

sollte. Wer die Zeit investiert,<br />

gewinnt in jedem Fall – wenn<br />

auch keine materiellen Dinge,<br />

so doch Erfahrung und Wissen.<br />

Es gilt:<br />

❚ Falsche Lösungen gibt es<br />

nicht. Es gibt möglicherweise<br />

elegantere, kürzere<br />

oder schnellere Lösungen,<br />

aber keine falschen.<br />

❚ Wichtig ist, dass Sie reflektieren,<br />

was Sie gemacht<br />

haben. Das können Sie,<br />

indem Sie Ihre Lösung mit<br />

der vergleichen, die Sie<br />

eine Ausgabe später in<br />

dotnetpro finden.<br />

Übung macht den Meister.<br />

Also − los geht’s. Aber Sie<br />

wollten doch nicht etwa<br />

sofort Visual Studio starten…<br />

www.dotnetpro.de dotnetpro.dojos.2011 31<br />

Wer übt, gewinnt


LÖSUNG<br />

Die NoSQL-Dokumentendatenbank Raven DB ausprobieren<br />

So sammeln Raben Daten<br />

Zum Entwickleralltag gehört es, sich in neue Technologien einzuarbeiten, beispielsweise in eine NoSQL-Datenbank.<br />

Der Code, der dabei entsteht, muss nicht die Qualität von Produktionscode haben. Ein testgetriebener Ansatz ist dafür<br />

aber dennoch nützlich, denn die Tests dokumentieren die gewonnenen Erkenntnisse in leicht nachvollziehbarer Form.<br />

Im vergangenen Monat war das dotnetpro.dojo<br />

etwas anders gelagert<br />

als sonst. Es ging nicht darum, eine<br />

konkrete Aufgabenstellung zu implementieren,<br />

sondern darum, sich mit einem<br />

bislang unbekannten Framework ausein-anderzusetzen.<br />

Auch das ist eine Form<br />

der Übung: das schnelle Sich-Einarbeiten<br />

in eine neue Technologie über einen Spike.<br />

Ein Spike dient vor allem dem Erkenntnisgewinn.<br />

Dieser steht im Vordergrund und<br />

mag im Zweifel auch schon mal Prinzipien<br />

und Praktiken zurückdrängen, die man bei<br />

Produktionscode in jedem Fall anwenden<br />

würde. Das bedeutet jedoch nicht, dass<br />

Spikes ein Freifahrtschein für schlechte<br />

Angewohnheiten wären.<br />

Bei der Überlegung, welche Prinzipien<br />

und Praktiken ich anwende, lasse ich mich<br />

auch beim Spike vom Wertesystem der<br />

Clean-Code-Developer-Initiative leiten [1].<br />

Einer dieser Werte ist die Produktionseffizienz.<br />

Daraus ergibt sich für mich beispielsweise,<br />

dass ich auch Spikes in der gewohnten<br />

Verzeichnis- und Projektstruktur<br />

anlege. Das hat zum einen den Vorteil, dass<br />

der Spike eine weitere Gelegenheit bietet,<br />

diese Struktur anzuwenden und zu hinterfragen.<br />

Zum anderen ergibt sich daraus ein<br />

Effizienzvorteil, weil ich es eben immer<br />

gleich tue. Ich gestehe, es fehlt ein Stück<br />

Automatisierung, viele der Schritte erledige<br />

ich in Handarbeit. Aber da ich sie so oft<br />

anwende, gehen sie flüssig von der Hand.<br />

So landen bei mir auch in Spikes die<br />

Tests in einem eigenen Projekt. Und auch<br />

die benötigten Frameworks wie NUnit und<br />

in diesem Fall RavenDB werden nicht aus<br />

dem GAC oder sonst woher referenziert,<br />

sondern „nach den Regeln der Kunst“ aus<br />

einem Verzeichnis innerhalb der Projektstruktur.<br />

Würde ich nicht so verfahren, hät-<br />

ten Sie als Leser später das Nachsehen.<br />

Denn dann würden sich die Beispiele, die<br />

Sie zu diesem Artikel auf der Heft-DVD finden,<br />

nicht sofort übersetzen lassen.<br />

Diese Situation ist keinesfalls speziell,<br />

nur weil ich den Code zur Veröffentlichung<br />

in einem Artikel schreibe. Auch in Ihren<br />

täglichen Projekten werden andere Entwickler<br />

den Code aus der Quellcodeverwaltung<br />

entnehmen und übersetzen wollen.<br />

Wenn Projekte dann nicht„self-contained“,<br />

also in sich abgeschlossen, sind, fängt der<br />

Ärger an: Referenzierte Assemblies werden<br />

nicht gefunden. Oder noch „gemeiner“: Sie<br />

liegen in einer anderen Version vor und<br />

verursachen dadurch Probleme. Und gehen<br />

Sie nicht davon aus, dass vermeintliche<br />

Selbstverständlichkeiten wie NUnit zu der<br />

Umgebung gehören würden, die Sie bei jedem<br />

Entwickler voraussetzen können. Je<br />

weniger Abhängigkeiten das Projekt zu seiner<br />

Umgebung hat, desto besser.<br />

Sie werden möglicherweise fragen, was<br />

denn automatisierte Tests in einem Spike<br />

zu suchen haben. Automatisierte Tests haben<br />

bei der Erkundung neuer Techniken<br />

zwei Vorteile: Zum einen verwende ich sie,<br />

um die verschiedenen Szenarien damit<br />

starten zu können. Anstatt eine Konsolenanwendung<br />

zu erstellen, welche mit Console.WriteLine<br />

versucht darzustellen, was<br />

gerade passiert, verwende ich automatisierte<br />

Tests. Das bietet den Vorteil, dass ich<br />

in einem Projekt mehrere Szenarien unterbringen<br />

kann, die sich alle einzeln starten<br />

Listing 1<br />

Ein Objekt speichern.<br />

lassen. Ferner sind diese Tests ebenfalls<br />

„self-contained“, also in sich abgeschlossen.<br />

Ein Blick auf den Test genügt, um zu<br />

verstehen, was da passiert. Es ist nicht notwendig,<br />

eine Anwendung laufen zu lassen,<br />

um zusätzlich noch die Konsolenausgabe<br />

zu sehen.<br />

Der zweite Vorteil von Tests ist, dass ich<br />

sie zur Dokumentation der Funktionalität<br />

verwenden kann. Wenn ich mir nicht sicher<br />

bin, ob eine bestimmte Funktionalität<br />

sich nun so oder anders verhält, erstelle ich<br />

einen Test, der das Verhalten dokumentiert.<br />

Im weiteren Verlauf des Artikels wird<br />

ein solcher Test beispielsweise zeigen, zu<br />

welchem Zeitpunkt RavenDB Schlüsselwerte<br />

erzeugt.<br />

Woher nehmen, den Raben?<br />

var store = new DocumentStore {Url = "http://localhost:8080"};<br />

store.Initialize();<br />

var produkt = new Produkt();<br />

using (var session = store.OpenSession()) {<br />

session.Store(produkt);<br />

session.SaveChanges();<br />

}<br />

Nachdem ich eine Solution mit zwei Projekten<br />

angelegt hatte und die Referenz auf<br />

NUnit gesetzt war, stand ich vor der Frage:<br />

Woher RavenDB nehmen? Klar, dass sich<br />

die Antwort hinter der URL [2] verbirgt, ich<br />

fragte mich aber, ob ich eine fertig übersetzte,<br />

binäre Version verwenden oder auf<br />

den Quellcode setzen sollte. Ich entschied<br />

mich für eine binäre Version, die jeweils aktualisiert<br />

unter [3] zum Download erhältlich<br />

ist. Wenn schon täglich aktualisierte<br />

Binärversionen zurVerfügung stehen, muss<br />

ich mir nicht die Mühe machen, lokal jeweils<br />

eine aktuelle Version zu übersetzen.<br />

Leider ist bei Weitem nicht für alle Open-<br />

Source-Projekte ein täglich aktualisierter<br />

32 dotnetpro.dojos.2011 www.dotnetpro.de


Build verfügbar, daher bietet sich in anderen<br />

Fällen die Arbeit mit den Quellen an.<br />

Nach dem Download stand mir RavenDB<br />

nun zur Verfügung. Ich habe es komplett in<br />

das lib-Verzeichnis innerhalb der Projektstruktur<br />

abgelegt. Damit unterliegt es der<br />

Versionierung, und das Projekt ist in sich<br />

abgeschlossen.<br />

Client und Server<br />

RavenDB kann auf verschiedenen Wegen<br />

verwendet werden: eingebettet in die Anwendung<br />

oder getrennt in Client und Server.<br />

Ich entschied mich dafür, RavenDB als<br />

Server zu starten. Im Implementierungsprojekt<br />

muss dann nur die Assembly Raven.Client.Lightweight.dll<br />

aus dem Client-<br />

Verzeichnis referenziert werden. Der Server<br />

befindet sich im Verzeichnis Server. Klingt<br />

logisch, oder? Dennoch sind solche klaren<br />

Strukturen nicht selbstverständlich. Oft befinden<br />

sich alle binären Artefakte eines<br />

Frameworks gemeinsam in einem bin-Verzeichnis,<br />

aus dem man sich selbst heraussuchen<br />

muss, was man benötigt. Da gefällt<br />

mir diese Aufteilung bei RavenDB doch<br />

sehr gut. Sie vereinfacht die ersten Schritte.<br />

Doch zurück zum Server. Der kann innerhalb<br />

eines IIS gehostet werden oder<br />

auch als Windows-Dienst laufen. Ich habe<br />

nur den Windows-Dienst ausprobiert. Dazu<br />

muss man zwei Befehle ausführen:<br />

❚ RavenDb.exe /install<br />

❚ RavenDb.exe /start<br />

Der erste Befehl installiert RavenDB als<br />

Windows-Dienst, der zweite startet den<br />

Dienst. Weil das Installieren und Starten von<br />

Diensten unterWindows nicht jedem Nutzer<br />

erlaubt sind, kümmert sich RavenDB bei<br />

Bedarf um die Elevation, also das Beschaffen<br />

der nötigen Rechte. Das ist vorbildlich!<br />

Statt eine kryptische Fehlermeldung auszugeben,<br />

eventuell mit dem Hinweis, man<br />

möge das Programm als Administrator starten,<br />

wird’s mir hier sehr einfach gemacht.<br />

CRUDe Methoden<br />

Nun möchte ich als Erstes ein Objekt in der<br />

RavenDB-Datenbank abspeichern. Dazu<br />

habe ich die Klasse Produkt angelegt. Die<br />

Klasse berücksichtigt keine Infrastruktur,<br />

Instanzen sind sogenannte POCOs: Plain<br />

Old CLR Objects. Damit wird im Allgemeinen<br />

die sogenannte Infrastrukturignoranz<br />

bezeichnet. RavenDB stellt (fast) keine Anforderungen<br />

an eine zu persistierende<br />

Klasse. Es muss nicht von einer Basisklasse<br />

abgeleitet werden, es muss kein spezielles<br />

Interface implementiert werden, und es<br />

sind keine Attribute erforderlich. Nur eine<br />

Konvention ist einzuhalten: RavenDB benötigt<br />

eine Eigenschaft namens Id vom Typ<br />

string. In dieser Eigenschaft wird der eindeutige<br />

Schlüssel des Objekts von RavenDB<br />

erwartet. Natürlich kann diese Konvention<br />

geändert werden, wenn sie nicht passt.<br />

public class Produkt {<br />

public string Id { get; set; }<br />

public string Name { get; set; }<br />

public string Kategorie { get; set;<br />

}<br />

Um eine Instanz der Klasse mit RavenDB<br />

zu persistieren, benötigt man einen sogenannten<br />

DocumentStore. Dieser sollte pro<br />

Anwendung nur einmal erzeugt werden.<br />

Mithilfe des DocumentStore wird eine Do-<br />

Listing 2<br />

RavenDB sichert die Objektidentität.<br />

LÖSUNG<br />

cumentSession erzeugt. Innerhalb einer<br />

Session werden Änderungen vorgenommen<br />

und am Ende persistiert. Listing 1<br />

zeigt das Speichern eines Objektes.<br />

Der DocumentStore enthält keinen Zustand,<br />

dafür ist die DocumentSession zuständig.<br />

Während der Lebenszeit der Session<br />

sorgt diese für die Objektidentität:<br />

Wird ein und dasselbe Dokument mehrfach<br />

aus der Datenbank geladen, liefert die<br />

Session jeweils ein und dasselbe Objekt. So<br />

ist sichergestellt, dass innerhalb einer<br />

Session nur genau eine Instanz eines Dokumentes<br />

der Datenbank existiert. Ohne<br />

diese Objektidentität wäre die Gefahr sehr<br />

groß, dass an unterschiedlichen Objekten<br />

Änderungen vorgenommen werden, die<br />

[Test]<br />

public void Session_stellt_Objektidentität_sicher() {<br />

var store = new DocumentStore {Url = "http://localhost:8080"};<br />

store.Initialize();<br />

var produkt = new Produkt();<br />

using (var session = store.OpenSession()) {<br />

session.Store(produkt);<br />

session.SaveChanges();<br />

var produkt2 = session.Load(produkt.Id);<br />

Assert.That(produkt2, Is.SameAs(produkt));<br />

var produkt3 = session.Load(produkt.Id);<br />

Assert.That(produkt3, Is.SameAs(produkt));<br />

}<br />

using (var session = store.OpenSession()) {<br />

var produkt2 = session.Load(produkt.Id);<br />

Assert.That(produkt2, Is.Not.SameAs(produkt));<br />

}<br />

}<br />

Listing 3<br />

Jedes Objekt erhält eine eigene Id.<br />

[Test]<br />

public void Id_wird_durch_Save_erzeugt_aber_Entity_noch_nicht_gespeichert() {<br />

var store = new DocumentStore {Url = "http://localhost:8080"};<br />

store.Initialize();<br />

var produkt = new Produkt();<br />

Assert.That(produkt.Id, Is.Null);<br />

using (var session = store.OpenSession()) {<br />

session.Store(produkt);<br />

}<br />

Assert.That(produkt.Id, Is.Not.Null);<br />

Produkt result;<br />

using (var session = store.OpenSession()) {<br />

result = session.Load(produkt.Id);<br />

}<br />

Assert.That(result, Is.Null);<br />

}<br />

www.dotnetpro.de dotnetpro.dojos.2011 33


LÖSUNG<br />

sich tatsächlich aber auf dasselbe Dokument<br />

beziehen. Um zu dokumentieren, wie<br />

sich RavenDB in diesem Punkt verhält,<br />

dient der in Listing 2 gezeigte Test.<br />

Der Test zeigt, was passiert, wenn man<br />

innerhalb einer Session ein Dokument<br />

mehrfach liest. Die Session liefert jeweils<br />

identische Objekte. Im zweiten Teil des<br />

Tests ist zu sehen, dass dies nur innerhalb<br />

einer Session gilt. Objekte, die in der zweiten<br />

Session geladen werden, beziehen sich<br />

zwar auf dasselbe Dokument, es wird jedoch<br />

nicht dasselbe Objekt geliefert.<br />

Dieser Test wirft gleich eine weitere Frage<br />

auf: Offensichtlich sorgt RavenDB dafür,<br />

dass das Dokument eine Id erhält, über die<br />

es später wieder aus der Datenbank geholt<br />

werden kann. Doch zu welchem Zeitpunkt<br />

passiert das? Wird die Id bereits bei session.Store<br />

gebildet oder erst bei session.SaveChanges?<br />

Die Frage ist insofern wichtig,<br />

als davon abhängt, wie viele Zugriffe auf<br />

den Server erforderlich sind. Ferner hängt<br />

davon ab, zu welchem Zeitpunkt man die<br />

Id verwenden kann, um Referenzen zwischen<br />

Dokumenten herzustellen (auch<br />

wenn man dies vermeiden sollte, es ist<br />

schließlich keine relationale Datenbank).<br />

Der in Listing 3 gezeigte Test dokumentiert<br />

das Verhalten von RavenDB: Die Id wird<br />

bereits bei session.Store gebildet.<br />

Erst mit session.SaveChanges werden die<br />

Änderungen zur Datenbank übertragen.<br />

Die Id wird bei session.Store lokal gebildet,<br />

dazu ist keine Kommunikation mit dem<br />

Server erforderlich. Natürlich kann die Id<br />

auch vorgegeben werden. Ist dies der Fall,<br />

erzeugt RavenDB keine Id, sondern übernimmt<br />

die vorhandene. Natürlich muss<br />

diese eindeutig sein, andernfalls wird das<br />

bereits vorhandene Dokument von RavenDB<br />

einfach überschrieben.<br />

Das Laden eines Dokumentes ist in den<br />

obigen Tests bereits zu sehen: Mittels<br />

session.Load laden Sie ein Dokument, dessen<br />

Id bekannt ist. Dabei ist der Typ des<br />

Dokuments in Form eines generischen<br />

Methodenparameters anzugeben, damit<br />

RavenDB weiß, welcher Typ instanziert<br />

werden soll. Damit hätten wir Create und<br />

Retrieve aus CRUD gelöst. Wie sieht es mit<br />

Update aus? Ganz einfach: Wenn Sie<br />

session.Store mit einem Objekt aufrufen,<br />

das bereits eine Id hat, dann sorgt Ra-<br />

venDB dafür, dass das Dokument entweder<br />

neu angelegt oder aktualisiert wird.<br />

Dies soll in einem größeren Kontext gezeigt<br />

werden. In einer Anwendung wird man RavenDB<br />

nicht unmittelbar verwenden, da es<br />

sich bei der Datenbank um eine externe<br />

Ressource handelt. Der Kern der Anwendung<br />

sollte generell über einen Adapter<br />

von Ressourcenzugriffen isoliert werden.<br />

Dies wurde in zurückliegenden Artikeln<br />

der Dojo-Serie bereits thematisiert. Für<br />

Datenbanken wird hier der Begriff „Repository“<br />

verwendet. Um zu sehen, wie so ein<br />

Repository, realisiert mit RavenDB, aussehen<br />

kann, habe ich ein solches bei meinen<br />

weiteren Spike-Schritten implementiert.<br />

Der Vorteil: Jetzt ist das Repository die<br />

Stelle, an der die Konfiguration des DocumentStore<br />

erfolgt. Innerhalb eines Repositorys<br />

kann über den darin vorhandenen<br />

DocumentStore jeweils bei Bedarf eine DocumentSession<br />

eröffnet werden. Listing 4<br />

zeigt den Test für Updates, und Listing 5<br />

zeigt die Implementation des Repositorys.<br />

Als letzte CRUD-Operation steht das Löschen<br />

an. Dazu verfügt DocumentSession<br />

über die Methode Delete, der das zu lö-


Listing 4<br />

Ein Update überprüfen.<br />

[TestFixture]<br />

public class ProductRepositoryTests<br />

{<br />

private ProduktRepository sut;<br />

[SetUp]<br />

public void Setup() {<br />

sut = new ProduktRepository();<br />

}<br />

}<br />

[Test]<br />

public void Ein_Produkt_ändern() {<br />

var produkt = new Produkt {<br />

Id = "#1",<br />

Name = "iPad",<br />

Kategorie = "Zeugs"<br />

};<br />

sut.Save(produkt);<br />

produkt.Kategorie = "Gadget";<br />

sut.Save(produkt);<br />

}<br />

var result = sut.Load("#1");<br />

Assert.That(result.Kategorie,<br />

Is.EqualTo("Gadget"));<br />

schende Objekt übergeben wird. Dabei<br />

stellte sich mir die Frage, wie man ein Dokument<br />

aus der Datenbank löscht, dessen<br />

Id man kennt, das aber nicht als Objekt geladen<br />

wurde. Natürlich kann man das Objekt<br />

zuerst über seine Id laden, um es dann<br />

an Delete zu übergeben. Dabei fallen aber<br />

zwei Zugriffe auf den Server an, das sollte<br />

doch auch mit einem Zugriff zu machen<br />

sein. Das Löschen über die Id ist im Client-<br />

API nicht vorgesehen. Das bedeutet aber<br />

nicht, dass es unmöglich ist. Das Client-API<br />

ist nur ein Wrapper, der über das HTTP-<br />

Protokoll gelegt ist, und in diesem API ist<br />

Löschen per Id nicht vorgesehen. Wie man<br />

das API erweitert, habe ich mir allerdings<br />

in diesem Spike nicht weiter angesehen.<br />

Konkurrierende Zugriffe<br />

Beim Erforschen des APIs ist mir an dieser<br />

Stelle die Frage gekommen, wie RavenDB<br />

sich bei konkurrierenden Zugriffen verhält.<br />

Damit meine ich Zugriffe, die in zwei unterschiedlichen<br />

Sessions stattfinden. Stellen<br />

Sie sich dazu eine Anwendung vor, von der<br />

mehrere Instanzen laufen. Was passiert,<br />

wenn ein Dokument aus der Datenbank von<br />

beiden Anwendern geladen und verändert<br />

wird. Merkt RavenDB das beim Update? Um<br />

die Frage zu klären, habe ich einen Test geschrieben.<br />

Um dabei mit zwei Sessions ar-<br />

beiten zu können, habe ich die Sessions ineinandergeschachtelt.<br />

Das heißt, während<br />

die erste Session noch aktiv ist, wird innerhalb<br />

des using-Blocks eine zweite Session<br />

geöffnet, um dort ein Update vorzunehmen.<br />

Dann wird aus der ersten Session<br />

ebenfalls ein Update abgesetzt.<br />

Listing 5<br />

Das Repository implementieren.<br />

Das Verhalten von RavenDB hängt an<br />

dieser Stelle davon ab, ob die Session Optimistic<br />

Concurrency unterstützen soll. Dies<br />

kann man per Session einstellen, standardmäßig<br />

ist es abgestellt. Das bedeutet, wenn<br />

man keine weitere Vorkehrung trifft, werden<br />

Konflikte bei konkurrierenden Zugrif-<br />

public class ProduktRepository {<br />

private readonly DocumentStore store;<br />

public ProduktRepository() {<br />

store = new DocumentStore {Url = "http://localhost:8080"};<br />

store.Initialize();<br />

}<br />

public void Save(Produkt produkt) {<br />

using (var session = store.OpenSession()) {<br />

session.Store(produkt);<br />

session.SaveChanges();<br />

}<br />

}<br />

public Produkt Load(string id) {<br />

using (var session = store.OpenSession()) {<br />

var result = session.Load(id);<br />

return result;<br />

}<br />

}<br />

}<br />

Listing 6<br />

Konkurrierende Zugriffe erkennen.<br />

[Test]<br />

public void Optimistic_Concurrency_bei_Updates_in_mehreren_Sessions() {<br />

var store = new DocumentStore {Url = "http://localhost:8080"};<br />

store.Initialize();<br />

// Initialzustand der Datenbank herstellen<br />

var produkt = new Produkt {Name = "a"};<br />

using (var session = store.OpenSession()) {<br />

session.Store(produkt);<br />

session.SaveChanges();<br />

}<br />

// Erste Session lädt das Dokument als 'referenz1'<br />

using (var session1 = store.OpenSession()) {<br />

session1.UseOptimisticConcurrency = true;<br />

var referenz1 = session1.Load(produkt.Id);<br />

// Zweite Session lädt das Dokument als 'referenz2'<br />

// und modifiziert es<br />

using (var session2 = store.OpenSession()) {<br />

var referenz2 = session2.Load(produkt.Id);<br />

Assert.That(referenz2, Is.Not.SameAs(referenz1));<br />

referenz2.Name = "b";<br />

session2.SaveChanges();<br />

}<br />

// 'referenz1' hat die Änderungen aus der zweiten<br />

// Session noch nicht gesehen, daher Bumm!<br />

referenz1.Name = "c";<br />

Assert.Throws(session1.SaveChanges);<br />

}<br />

}<br />

LÖSUNG<br />

www.dotnetpro.de dotnetpro.dojos.2011 35


LÖSUNG<br />

fen nicht erkannt und Updates einfach der<br />

Reihe nach ausgeführt. Um diese Erkennung<br />

zu aktivieren, müssen Sie in der Session<br />

die Option UseOptimisticConcurrency<br />

auf true setzen. Listing 6 zeigt den Test für<br />

die Erkennung konkurrierender Zugriffe.<br />

Um die ConcurrencyException zu vermeiden,<br />

können Sie das Objekt vor der Änderung<br />

mit session.Refresh(referenz1) auf<br />

den aktuellen Stand bringen. Allerdings gehen<br />

damit natürlich alle Änderungen verloren,<br />

die am Objekt zuvor bereits vorgenommen<br />

wurden. Eine Strategie könnte<br />

dann sein, das Refresh nur dann auszuführen,<br />

wenn die ConcurrencyException tatsächlich<br />

aufgetreten ist. Dann könnten die<br />

Änderungen, die in den beiden konkurrierenden<br />

Sessions vorgenommen wurden,<br />

zusammengeführt werden. Wie das im Einzelnen<br />

geschieht, hängt von der Business<br />

Domain ab.<br />

Query<br />

Der nächste Schritt in meinem Spike sollte<br />

die Frage klären, wie man Dokumente aus<br />

der Datenbank lesen kann, die bestimmte<br />

Anforderungen erfüllen. Ich wollte zum Beispiel<br />

alle Produkte einer Kategorie ermitteln.<br />

In einer relationalen Datenbank muss<br />

man dazu lediglich das passende SELECT-<br />

Kommando absetzen. Bei RavenDB ist es<br />

zunächst erforderlich, einen Index anzulegen.<br />

Das liegt daran, dass die RavenDB-Datenbank<br />

Dokumente als JSON-Strings [4]<br />

speichert. Damit entziehen sie sich einem<br />

effizienten suchenden Zugriff, denn das<br />

würde bedeuten, dass bei jeder Suche die<br />

JSON-Strings aller Dokumente interpretiert<br />

werden müssten. Bei einer relationalen<br />

Datenbank ist die Suche nur möglich, weil<br />

es dort von vornherein ein Schema gibt,<br />

welches dafür sorgt, dass die Daten in Spalten<br />

abgelegt werden. Dieses Schema fehlt in<br />

RavenDB ganz bewusst. Um einen Index<br />

anzulegen, muss man eine LINQ-Query<br />

definieren, die angibt, welche Eigenschaften<br />

des Objekts in den Index aufgenom-<br />

Listing 7<br />

Einen Index erstellen.<br />

store.DatabaseCommands.PutIndex(<br />

"ProdukteNachKategorie",<br />

new IndexDefinition {<br />

Map = produkte =><br />

from produkt in produkte<br />

select new {produkt.Kategorie}<br />

});<br />

Listing 8<br />

Alle Produkte einer Kategorie suchen.<br />

using (var session = store.OpenSession()) {<br />

var result = session.LuceneQuery("ProdukteNachKategorie")<br />

.Where(string.Format("Kategorie:{0}", kategorie))<br />

.ToArray();<br />

return result;<br />

}<br />

Listing 9<br />

Die Produktsuche testen.<br />

[Test]<br />

public void Alle_Produkte_einer_Kategorie_ermitteln() {<br />

sut.Save(new Produkt {Id = "#1", Name = "iPad", Kategorie = "Elektronik"});<br />

sut.Save(new Produkt {Id = "#2", Name = "iPod", Kategorie = "Elektronik"});<br />

sut.Save(new Produkt {Id = "#3", Name = "Apfel", Kategorie = "Obst"});<br />

sut.Save(new Produkt {Id = "#4", Name = "Kartoffel", Kategorie = "Gemüse"});<br />

sut.Save(new Produkt {Id = "#5", Name = "Banane", Kategorie = "Obst"});<br />

var result = sut.ProdukteDerKategorie("Elektronik");<br />

Assert.That(result.Select(x => x.Id).ToArray(), Is.EquivalentTo(new[] {"#1", "#2"}));<br />

Assert.That(result.Select(x => x.Name).ToArray(), Is.EquivalentTo(new[] {"iPad", "iPod"}));<br />

Assert.That(result.Select(x => x.Kategorie).ToArray(), Is.EquivalentTo(new[]<br />

{"Elektronik", "Elektronik"}));<br />

}<br />

men werden sollen. Ferner muss der Index<br />

einen Namen erhalten, damit man ihn bei<br />

der Suche benennen kann. Listing 7 zeigt,<br />

wie Sie einen Index für den Zugriff auf alle<br />

Produkte einer Kategorie erstellen.<br />

Das Erstellen dieses Index muss einmalig<br />

erfolgen. RavenDB sorgt dafür, dass der<br />

Index jeweils aktualisiert wird, wenn sich<br />

die zugehörigen Daten ändern. Soll die Definition<br />

des Index geändert werden, muss<br />

man ihn zunächst löschen. Das geht ganz<br />

einfach:<br />

store.DatabaseCommands.DeleteIndex(<br />

"ProdukteNachKategorie");<br />

Wenn man, wie in den bisherigen Beispielen<br />

gezeigt, anonym auf den RavenDB-<br />

Server zugreift, trifft man an dieser Stelle<br />

auf ein Problem: Der Server verweigert das<br />

Anlegen des Index. Da das Erstellen von<br />

Dokumenten gestattet ist, habe ich mich<br />

zunächst gewundert. Die Lösung ist auf<br />

zwei Wegen möglich: Entweder man übergibt<br />

beim Öffnen einer Session Anmeldedaten,<br />

sogenannte Credentials, oder man<br />

erlaubt auch anonymen Nutzern den vollständigen<br />

Zugriff. In einer lokalen Testumgebung<br />

ist es einfacher, den Server zu öffnen.<br />

Im Produktivbetrieb sollte man das<br />

natürlich keinesfalls tun. Um anonyme Zugriffe<br />

für sämtliche Operationen zu berechtigen,<br />

muss man im Server-Verzeichnis<br />

die Datei RavenDb.exe.config bearbeiten.<br />

Darin muss man in der folgenden Zeile das<br />

Get durch ein All ersetzen:<br />

<br />

Anschließend müssen Sie den RavenDB-<br />

Server-Dienst neu starten.<br />

Doch zurück zum Index. Der wichtigste<br />

Teil beim Erstellen des Index ist die LINQ-<br />

Query, die für den Map-Vorgang zuständig<br />

ist. Diese Query wird von RavenDB für jedes<br />

Dokument ausgeführt. Das Ergebnis<br />

der Query, in diesem Fall ein Objekt mit der<br />

Eigenschaft Kategorie, wird in den Index<br />

übernommen. Der durch die Map-Funktion<br />

ermittelte Wert dient im Index als<br />

Schlüssel. Im Ergebnis sind zu einer gegebenen<br />

Kategorie im Index Referenzen auf<br />

die betreffenden Produkte abgelegt. Damit<br />

wird eine Suche nach sämtlichen Produkten<br />

einer Kategorie möglich, siehe dazu<br />

Listing 8.<br />

Wichtig ist hier der Aufruf von ToArray().<br />

Da die Query innerhalb einer Session aufgerufen<br />

wird, die mit Verlassen des using-<br />

36 dotnetpro.dojos.2011 www.dotnetpro.de


Blocks geschlossen wird, muss das tatsächliche<br />

Lesen der Daten innerhalb der Session<br />

passieren. Lässt man das ToArray()<br />

weg, findet das Lesen erst beim Iterieren<br />

durch das Ergebnis statt, dann allerdings<br />

zu einem Zeitpunkt, da die Session bereits<br />

geschlossen ist. RavenDB verwendet für<br />

die Indizierung übrigens Lucene, was<br />

ebenfalls ein Open-Source-Projekt ist [5].<br />

Listing 9 zeigt, wie Sie die so erstellte Methode<br />

zum Ermitteln aller Produkte einer<br />

Kategorie gegen eine Datenbank testen<br />

können.<br />

Auch hier gilt es, noch einen weiteren<br />

Stolperstein zu beachten. RavenDB führt<br />

das Aktualisieren der Indizes im Hintergrund<br />

aus. Es kann daher sein, dass der Index<br />

zum Zeitpunkt des Lesevorgangs noch<br />

nicht aktualisiert ist. Zu Testzwecken kann<br />

man es bei Anwendung der Query mit der<br />

Methode WaitForNonStaleResults() erzwingen,<br />

dass auf die Aktualisierung des<br />

Index gewartet wird, bevor Ergebnisse geliefert<br />

werden. In einer Produktivumgebung<br />

sollten Sie diese Option allerdings<br />

nicht verwenden, da sie mit Performance-<br />

Listing 10<br />

Map und Reduce verwenden.<br />

store.DatabaseCommands.PutIndex(<br />

"ProduktKategorien",<br />

new IndexDefinition {<br />

Map = produkte => from produkt in produkte<br />

select new { produkt.Kategorie, Anzahl = 1 },<br />

Reduce = results => from result in results<br />

group result by result.Kategorie<br />

into g<br />

select new { Kategorie = g.Key, Anzahl = g.Sum(x => x.Anzahl) }<br />

});<br />

Listing 11<br />

Den Index testen.<br />

einbußen verbunden ist. An dieser Stelle<br />

zeigt es sich, dass Dokumentdatenbanken<br />

einen anderen Schwerpunkt setzen als relationale<br />

Datenbanken. Bei einer relationalen<br />

Datenbank geht die Konsistenz der Daten<br />

immer vor. Bei Dokumentdatenbanken<br />

wie RavenDB dagegen steht die Konsistenz<br />

hinter Skalierbarkeit und Ausfallsicherheit<br />

zurück.<br />

Map/Reduce<br />

Als Nächstes habe ich mir ein Feature angeschaut,<br />

das bei Dokumentdatenbanken<br />

sehr häufig zum Einsatz kommt und großen<br />

Einfluss auf die Skalierbarkeit hat:<br />

Map/Reduce. Eine Map-Funktion wurde<br />

bereits für den Index benötigt, mit dem alle<br />

Produkte einer Kategorie ermittelt werden<br />

können. Fügt man einem solchen Index<br />

noch eine Reduce-Methode hinzu,<br />

können Daten aus den Dokumenten aggregiert<br />

werden. Damit ist es möglich, beispielsweise<br />

einen Index zu erstellen, der<br />

aus den Produkten alle Kategorien ermittelt.<br />

Zusätzlich ist es durch die Aggregation<br />

möglich zu zählen, wie viele Produkte in<br />

[Test]<br />

public void Alle_Kategorien_ermitteln() {<br />

sut.Save(new Produkt {Id = "#1", Name = "iPad", Kategorie = "Elektronik"});<br />

sut.Save(new Produkt {Id = "#2", Name = "iPod", Kategorie = "Elektronik"});<br />

sut.Save(new Produkt {Id = "#3", Name = "Apfel", Kategorie = "Obst"});<br />

sut.Save(new Produkt {Id = "#4", Name = "Kartoffel", Kategorie = "Gemüse"});<br />

sut.Save(new Produkt {Id = "#5", Name = "Banane", Kategorie = "Obst"});<br />

var result = sut.Kategorien();<br />

Assert.That(result.ToArray(), Is.EquivalentTo(new[] {<br />

new KategorieMitAnzahl { Kategorie = "Elektronik", Anzahl = 2},<br />

new KategorieMitAnzahl { Kategorie = "Obst", Anzahl = 2},<br />

new KategorieMitAnzahl { Kategorie = "Gemüse", Anzahl = 1},<br />

}));<br />

}<br />

LÖSUNG<br />

der jeweiligen Kategorie enthalten sind.<br />

Das Erstellen dieses Index gleicht dem vorhergehenden<br />

Beispiel. Der wesentliche<br />

Unterschied besteht in der zusätzlichen<br />

Reduce-LINQ-Query. Dabei liegt das<br />

Hauptproblem darin, dass man darauf<br />

achten muss, dass Map- und Reduce-Query<br />

auf gleich aufgebauten Objekten arbeiten.<br />

Da hier anonyme Typen verwendet<br />

werden, ist das Unterfangen fehleranfällig,<br />

siehe Listing 10.<br />

Die Map-Query liefert für jedes Produkt<br />

ein Objekt zurück. Dieses Objekt hat zwei<br />

Eigenschaften: Kategorie und Anzahl. Die<br />

Anzahl ist immer 1, dies ist der Startwert<br />

für die spätere Aufsummierung. In der Reduce-Query<br />

wird nun auf Objekten vom<br />

Typ KategorieMitAnzahl gearbeitet. Der<br />

Typ muss als zweiter generischer Typparameter<br />

im Konstruktor von IndexDefinition<br />

angegeben werden.<br />

Die Herausforderung liegt darin, dass<br />

man in den beiden LINQ-Queries anonyme<br />

Typen verwenden muss. Wenn diese<br />

Queries Ergebnisse vom Typ KategorieMit-<br />

Anzahl liefern, erhält man eine Fehlermeldung<br />

vom RavenDB-Server, die besagt,<br />

man müsse anonyme Typen verwenden.<br />

Dennoch müssen diese anonymen Typen<br />

den gleichen Aufbau haben wie der definierte<br />

Typ. Wenn man diese Hürde genommen<br />

hat, steht einem Test des Index nichts<br />

mehr im Weg, wie Listing 11 zeigt.<br />

Das Schöne an diesem Index ist, dass RavenDB<br />

ihn jeweils auf dem aktuellen Stand<br />

hält. Jede Änderung an Dokumenten, die<br />

im Index verwendet werden, führt zu einem<br />

entsprechenden Update des Index.<br />

www.dotnetpro.de dotnetpro.dojos.2011 37<br />

Fazit<br />

Ich habe mir in diesem Spike die Funktionalität<br />

von RavenDB nur ausschnittweise<br />

angesehen. RavenDB hat darüber hinaus<br />

noch mehr zu bieten wie etwa die Verteilung<br />

einer Datenbank auf mehrere Server.<br />

Die ersten Schritte gingen zügig voran.<br />

Aber beim Map/Reduce hat es doch etwas<br />

länger gedauert, bis ich die zu berücksichtigenden<br />

Konventionen alle zusammenhatte.<br />

Hier wäre ein Beispiel in der Dokumentation<br />

sicherlich hilfreich. Gelernt habe<br />

ich wieder einiges, und das war schließlich<br />

Zweck der Übung. [ml]<br />

[1] http://clean-code-developer.de<br />

[2] http://ravendb.net/<br />

[3] http://builds.hibernatingrhinos.com/builds/<br />

ravendb<br />

[4] http://de.wikipedia.org/wiki/JSON<br />

[5] http://lucene.apache.org/lucene.net/


Wer übt, gewinnt<br />

AUFGABE<br />

Algorithmen und Datenstrukturen<br />

Was ist im Stapel?<br />

In den Zeiten der großen Programmier-Frameworks geht leicht das Wissen um die grundlegenden<br />

Algorithmen und Datenstrukturen verloren. Stefan, kannst du mal eine Aufgabe stellen, die zu den Wurzeln<br />

der Programmierung zurückführt?<br />

dnpCode: A1009DojoAufgabe<br />

In jeder dotnetpro finden Sie<br />

eine Übungsaufgabe von<br />

Stefan Lieser, die in maximal<br />

drei Stunden zu lösen sein<br />

sollte. Wer die Zeit investiert,<br />

gewinnt in jedem Fall – wenn<br />

auch keine materiellen Dinge,<br />

so doch Erfahrung und Wissen.<br />

Es gilt:<br />

❚ Falsche Lösungen gibt es<br />

nicht. Es gibt möglicherweise<br />

elegantere, kürzere<br />

oder schnellere Lösungen,<br />

aber keine falschen.<br />

❚ Wichtig ist, dass Sie reflektieren,<br />

was Sie gemacht<br />

haben. Das können Sie,<br />

indem Sie Ihre Lösung mit<br />

der vergleichen, die Sie<br />

eine Ausgabe später in<br />

dotnetpro finden.<br />

Übung macht den Meister.<br />

Also − los geht’s. Aber Sie<br />

wollten doch nicht etwa<br />

sofort Visual Studio starten…<br />

Natürlich ist mir bekannt, dass es<br />

im .NET Framework eine Klasse<br />

Stack gibt. Aus diesem Grund<br />

muss man eine solche elementare<br />

Datenstruktur nicht mehr selbst implementieren.<br />

Aber gerade weil die Funktionalität so gut<br />

bekannt ist, bietet sich ein Stack als Übung an.<br />

Hier können Sie sich voll auf den Entwurf einer<br />

Lösung konzentrieren und anschließend testgetrieben<br />

implementieren.<br />

Die Aufgabe soll gelöst werden, ohne dass vorhandene<br />

Datenstrukturen aus dem .NET Framework<br />

verwendet werden. Aus einer Liste einen<br />

Stack zu machen scheidet also aus. Und natürlich<br />

soll der Stack generisch sein. Das bedeutet, dass<br />

man den Typ der Elemente als generischen Typparameter<br />

angeben kann. Ein Stack für Integer-<br />

Elemente wird also folgendermaßen instanziert:<br />

var stack = new Stack();<br />

Dabei stellt der Typ int in spitzen Klammern<br />

den generischen Typparameter dar. Alle Elemente<br />

des Stacks sind somit vom Typ int. Die zwei<br />

Operationen auf dem Stack sind schnell erklärt:<br />

❚ Mit Push(element) kann ein Element oben auf<br />

den Stack gelegt werden. Jede weitere Push-<br />

Operation legt ein weiteres Element obendrauf.<br />

❚ Das oberste Element des Stacks kann mit der<br />

Pop()-Operation wieder vom Stack entfernt<br />

werden. Die Pop()-Operation macht also genau<br />

genommen zwei Dinge: Sie liefert das oberste<br />

Element an den Aufrufer und entfernt es vom<br />

Stack.<br />

Hier die Signaturen der beiden Methoden in<br />

Form eines Interfaces:<br />

public interface IStack {<br />

void Push(TElement element);<br />

TElement Pop();<br />

}<br />

In diesem Interface ist TElement der generische<br />

Typ. Er wird jeweils durch den konkreten<br />

Typ ersetzt. Im obigen Beispiel ist TElement mit<br />

dem Typ int belegt.<br />

Ein Tipp zur Implementierung: Überlegen Sie<br />

sich, welche Datenstruktur geeignet ist, einen<br />

Stack zur Laufzeit abzubilden. Die Verwendung<br />

von Collections aus dem .NET Framework scheidet<br />

aus. Auch ein Array scheidet aus, da der Stack<br />

keine Größenbeschränkung haben soll.<br />

Die in einem Stack angewandte Strategie beim<br />

Entnehmen eines Elementes lautet: Last In/First<br />

Out, abgekürzt LIFO. Das Element, welches als<br />

letztes in den Speicher gegeben wurde, wird als<br />

erstes entnommen. Eine andere Strategie ist die<br />

FIFO-Strategie: First In/First Out. Hier wird das<br />

Element, welches als erstes gespeichert wurde,<br />

auch wieder als erstes entnommen. Kommt Ihnen<br />

bekannt vor? Ja, so funktioniert die Schlange<br />

an der Kasse im Supermarkt.<br />

Und damit sind wir beim zweiten Teil der<br />

Übung: Implementieren Sie eine Warteschlange,<br />

engl. Queue. Das Interface der zu implementierenden<br />

Methoden sieht folgendermaßen aus:<br />

public interface IQueue<br />

{<br />

void Enqueue(TELement element);<br />

TELement Dequeue();<br />

}<br />

Auch hier gilt: Überlegen Sie sich eine Datenstruktur,<br />

mit welcher die Aufgabenstellung gelöst<br />

werden kann. Wie immer hilft es, sich dazu ein<br />

Blatt Papier zu nehmen. Oder lösen Sie die<br />

Übung mit Kollegen gemeinsam im Team, und<br />

planen Sie am Whiteboard. Die testgetriebene<br />

Entwicklung wird häufig so verstanden, dass<br />

man einfach mit einem ersten Test loslegt und<br />

sich von da an alles schon irgendwie ergeben<br />

wird. Ich halte das für falsch. Ein bisschen Planung<br />

vor dem Codieren schadet nicht, ganz im<br />

Gegenteil.<br />

Wer sich mit der Übung unterfordert fühlt,<br />

kann übrigens noch eine weitere Methode auf<br />

Queue ergänzen:<br />

void Reverse();<br />

Diese Methode soll die Reihenfolge der Elemente<br />

in der Warteschlange umkehren. Dazu<br />

sollen die Verweise zwischen den Elementen so<br />

verändert werden, dass die Queue „in place“ verändert<br />

wird. Wie immer gilt: test first! Viel Spaß.<br />

[ml]<br />

38 dotnetpro.dojos.2011 www.dotnetpro.de


Stack und Queue implementieren<br />

Der Nächste bitte!<br />

Immer hübsch der Reihe nach: Das gilt nicht nur im Wartezimmer, sondern auch im Stack und in der Queue der<br />

Informatiker. Und wer sich das Entwicklerleben vereinfachen will, sollte auch bei ihrer Implementierung<br />

die richtige Reihenfolge einhalten: Erst planen, dann Tests entwickeln, dann implementieren.<br />

A<br />

ls Entwickler nehmen wir<br />

Stack und Queue, wie viele<br />

andere Datenstrukturen, als<br />

selbstverständlich hin, sind<br />

sie doch im .NET Framework enthalten.<br />

Weil ihre Funktionsweise so einfach ist, besteht<br />

sicherlich die Versuchung, direkt mit<br />

der Implementierung zu beginnen. Doch<br />

schon kommen die ersten Fragen um die<br />

Ecke: Wie soll der erste Test aussehen? Wie<br />

testet man einen Stack überhaupt, das<br />

heißt, kann man Push isoliert testen? Oder<br />

kann man Push nur testen, indem man Pop<br />

ebenfalls testet?<br />

Bevor Sie versuchen, diese Fragen zu beantworten,<br />

sollten Sie den Konsolendeckel<br />

besser erst mal schließen und zu einem<br />

Stück Papier greifen. Denn die zentrale Frage<br />

vor dem ersten Test lautet, wie denn die<br />

interne Repräsentation des Stacks überhaupt<br />

aussieht. Dies mit Papier und Bleistift<br />

zu planen ist einfacher, als es einfach<br />

so im Kopf zu tun. Dabei übersieht man<br />

schnell mal ein Detail.<br />

Ein Stack muss in der Lage sein, jeweils<br />

das oberste Element zu liefern. Das ist die<br />

Aufgabe der Pop-Methode. Nachdem das<br />

oberste Element geliefert wurde, muss der<br />

Stack beim nächsten Mal das nächste Element<br />

liefern, das also unmittelbar auf das<br />

oberste folgt. Daraus ergibt sich die Notwendigkeit,<br />

innerhalb des Stacks jeweils zu<br />

wissen, welches das oberste Element ist.<br />

Ferner muss zu jedem Element bekannt<br />

sein, welches das nächste Element ist. So ergibt<br />

sich eine ganz einfache interne Datenstruktur<br />

für den Stack, siehe Abbildung 1.<br />

Hat man diese Datenstruktur erst einmal<br />

aufgemalt, ist es ein Leichtes, sie in Code<br />

zu übersetzen. Aber Achtung, den Test<br />

nicht vergessen! Bei einem Stack bietet es<br />

sich an, zwei unterschiedliche Zustände zu<br />

betrachten:<br />

❚ einen leeren Stack,<br />

❚ einen nicht leeren Stack.<br />

In solchen Fällen erstelle ich für die unterschiedlichen<br />

Szenarien gerne getrennte<br />

[Abb. 1] top- und<br />

next-Zeiger im<br />

Testklassen. Dann kann nämlich das Setup<br />

des Tests dafür sorgen, dass das Szenario<br />

bereitgestellt wird. Die Testklasse, welche<br />

sich mit einem leeren Stack befasst, instanziert<br />

einfach einen neuen Stack. In der Testklasse<br />

zum Szenario „nicht leerer Stack“<br />

wird der Stack im Setup gleich mit ein paar<br />

Werten befüllt. So können sich die Tests jeweils<br />

auf das konkrete Szenario beziehen.<br />

Doch wie sieht nun der erste Test aus?<br />

Ich habe mich entschieden, den ersten Test<br />

zu einem leeren Stack zu erstellen. Es erscheint<br />

mir nicht sinnvoll, bei dem Szenario<br />

eines nicht leeren Stacks zu beginnen,<br />

weil dann vermutlich für den ersten Test<br />

bereits sehr viel Funktionalität implementiert<br />

werden muss. Ich möchte lieber in<br />

kleinen Schritten vorgehen, um sicher zu<br />

sein, dass ich wirklich nur gerade so viel<br />

Code schreibe, dass ein weiterer Test erfolgreich<br />

verläuft.<br />

Für den ersten Test zu einem leeren Stack<br />

überlege ich mir, wie die interne Repräsentation<br />

eines leeren Stacks aussehen soll, und<br />

komme zu dem Schluss, dass der Zeiger, der<br />

auf das erste Element verweist, null sein soll.<br />

Listing 1 zeigt den ersten Test. In der Setup-<br />

Methode der Testklasse wird ein leerer Stack<br />

für int-Elemente instanziert. Der erste Test<br />

prüft, ob dann der top-Zeiger gleich null ist.<br />

Nun wird vielleicht dem einen oder anderen<br />

der Einwand im Kopf herumkreisen,<br />

dass damit ja erstens die Sichtbarkeit der<br />

internen Repräsentation nach außen getragen<br />

wird und zweitens Interna getestet<br />

werden. Zur Sichtbarkeit sei gesagt, dass<br />

Listing 1<br />

LÖSUNG<br />

Einen leeren Stack testen.<br />

[TestFixture]<br />

public class Ein_leerer_Stack {<br />

private Stack sut;<br />

[SetUp]<br />

public void Setup() {<br />

sut = new Stack();<br />

}<br />

[Test]<br />

public void Hat_kein_top_Element() {<br />

Assert.That(sut.top, Is.Null);<br />

}<br />

}<br />

ich das Feld top auf internal setze. Damit<br />

ist es zunächst nur innerhalb der Assembly<br />

sichtbar, in der die Stack-Implementierung<br />

liegt. Durch ein Attribut in der Datei AssemblyInfo.cs<br />

wird die Sichtbarkeit dann<br />

dosiert erweitert auf die Testassembly. Das<br />

Attribut sieht wie folgt aus:<br />

[assembly:<br />

InternalsVisibleTo("stack.tests")]<br />

Damit kann nun auch aus der Testassembly<br />

auf die Interna zugegriffen werden.<br />

Und schon sind wir beim zweiten Einwand,<br />

dass nun diese Interna im Test verwendet<br />

werden. Das halte ich für vernachlässigbar.<br />

Sicher ist es erstrebenswert, Tests<br />

so zu schreiben, dass sie möglichst wenige<br />

Abhängigkeiten zu den Interna der Klasse<br />

haben. Denn wenn nur über die öffentliche<br />

Schnittstelle getestet wird, sind die Tests<br />

www.dotnetpro.de dotnetpro.dojos.2011 39<br />

Stack.<br />

Listing 2<br />

Datenstruktur für die Stack-<br />

Elemente.<br />

internal class Element {<br />

public TData Data { get; set; }<br />

public Element Next{get; set;}<br />

}


LÖSUNG<br />

Listing 3<br />

Erste Stack-Implementierung.<br />

public class Stack :<br />

IStack {<br />

internal Element top;<br />

public void Push(TElement element) {<br />

throw new NotImplementedException();<br />

}<br />

public TElement Pop() {<br />

throw new NotImplementedException();<br />

}<br />

}<br />

weniger zerbrechlich. Allerdings sind sie<br />

dann häufig weniger fokussiert. Im Fall eines<br />

Stacks stellt sich nämlich die Frage, wie<br />

man Push ausschließlich über die öffentliche<br />

Schnittstelle testen kann, ohne dabei<br />

andere Methoden des Stacks zu verwenden.<br />

Natürlich wird man auch einen Test<br />

schreiben, der Push und Pop in Beziehung<br />

setzt, und beide Methoden in einem Test<br />

verwenden. Aber gerade bei den ersten<br />

Schritten der Implementierung ist es vorteilhaft,<br />

wenn man eine einzelne Methode<br />

Listing 4<br />

Erster Test für Push.<br />

Listing 5<br />

Der Next-Zeiger ist null.<br />

isoliert betrachten kann. Greift man dabei<br />

auf Interna zu, dann ist dies möglich. Um<br />

mit der Implementierung weiterzukommen,<br />

müssen Sie überlegen, von welchem<br />

Typ der top-Zeiger sein soll. Das ist dank<br />

der Skizze ganz einfach. Denn aus der Skizze<br />

ergibt sich, dass jedes Element im Stack<br />

neben den Daten einen Zeiger auf das<br />

nächste Element hat. Folglich ist der top-<br />

Zeiger einfach das erste Element im Stack.<br />

Listing 2 zeigt die Datenstruktur für die<br />

Elemente. Da diese Datenstruktur außerhalb<br />

des Stacks nicht in Erscheinung tritt,<br />

wird sie durch die Sichtbarkeit internal verborgen.<br />

Wie weiter oben erwähnt, kann<br />

dennoch in Tests darauf zugegriffen werden.<br />

Listing 3 zeigt die Implementierung<br />

des Stacks für den ersten Test.<br />

Die Methoden Push und Pop werden eigentlich<br />

noch nicht benötigt, sind aber<br />

syntaktisch erforderlich aufgrund des Interfaces<br />

IStack.<br />

Nun stand ich vor der Wahl, ob ich als<br />

Nächstes mit Push oder Pop weitermachen<br />

wollte. Ich halte Push für naheliegender,<br />

denn bei einem leeren Stack wird Pop<br />

ohnehin nur zu einer Ausnahme führen.<br />

Gerade zu Beginn der Implementierung<br />

[Test]<br />

public void Macht_das_mit_Push_übergebene_Element_zum_top_Element() {<br />

sut.Push(5);<br />

Assert.That(sut.top.Data, Is.EqualTo(5));<br />

}<br />

[Test]<br />

public void Enthält_nach_einem_Push_nur_dieses_eine_Element() {<br />

sut.Push(5);<br />

Assert.That(sut.top.Next, Is.Null);<br />

}<br />

Listing 6<br />

Push und Pop testen.<br />

[Test]<br />

public void Kann_ein_Element_aufnehmen_und_wieder_abliefern() {<br />

sut.Push(5);<br />

Assert.That(sut.Pop(), Is.EqualTo(5));<br />

}<br />

möchte ich weiterkommen und mich<br />

nicht mit Rand- und Fehlerfällen befassen.<br />

Aber das ist sicherlich Geschmackssache.<br />

Listing 4 zeigt den ersten Test für Push.<br />

Ferner kann man beim ersten Push feststellen,<br />

dass das übergebene Element das<br />

einzige auf dem Stack ist, der Next-Zeiger<br />

also null ist, siehe Listing 5.<br />

Ob man dies tatsächlich in einem eigenständigen<br />

Test überprüft oder ein zweites<br />

Assert im vorhergehenden Test zulässt, sei<br />

dahingestellt. Ich habe mich für zwei getrennte<br />

Tests entschieden, weil ich keine<br />

treffende Bezeichnung für einen Test finden<br />

konnte, der beides prüft. Die Regel,<br />

nur ein Assert pro Test zuzulassen, halte ich<br />

jedenfalls für dogmatisch. Ein Test sollte<br />

sich mit einer Sache befassen. Wenn diese<br />

eine Sache mit mehr als einem Assert überprüft<br />

werden muss, finde ich das völlig in<br />

Ordnung.<br />

An dieser Stelle kann man mit einem leeren<br />

Stack nicht viel mehr anstellen, außer<br />

nun doch Push und Pop in Beziehung zu<br />

setzen. Also sieht mein nächster Test so aus<br />

wie in Listing 6. Bei diesem Test liegt die<br />

Versuchung nahe, auch noch zu prüfen, ob<br />

der Stack nach dem Pop auch wieder leer<br />

ist. Doch diesen Test habe ich in den Szenarien<br />

angesiedelt, die sich mit einem<br />

nicht leeren Stack befassen. Bis hierher besteht<br />

die Implementierung nur darin, bei<br />

Push ein neues top-Element zu erzeugen<br />

und dieses bei Pop als Ergebnis zu liefern.<br />

Der wichtigste Teil der Implementierung<br />

folgt nun bei den Szenarien mit nicht leerem<br />

Stack.<br />

Das Szenario wird in der Setup-Methode<br />

dadurch hergestellt, dass der Stack direkt<br />

mit einem Element gefüllt wird. Somit kann<br />

Listing 7<br />

Pop testen.<br />

[TestFixture]<br />

public class Ein_Stack_mit_einem_Element<br />

{<br />

private Stack sut;<br />

[SetUp]<br />

public void Setup() {<br />

sut = new Stack();<br />

sut.Push("a");<br />

}<br />

[Test]<br />

public void<br />

Kann_das_top_Element_liefern() {<br />

Assert.That(sut.Pop(),<br />

Is.EqualTo("a"));<br />

}<br />

}<br />

40 dotnetpro.dojos.2011 www.dotnetpro.de


Listing 8<br />

Auf einen leeren Stack testen.<br />

[Test]<br />

public void Enthaelt_nach_der_Entnahme_des_top_Elementes_keine_weiteren_Elemente()<br />

{<br />

sut.Pop();<br />

Assert.That(sut.top, Is.Null);<br />

}<br />

Listing 9<br />

Mehrere Push-Aufrufe.<br />

[Test]<br />

public void Macht_das_naechste_uebergebene_Element_zum_top_Element() {<br />

sut.Push("b");<br />

Assert.That(sut.top.Data, Is.EqualTo("b"));<br />

}<br />

[Test]<br />

public void Legt_das_vorhandene_Element_bei_Uebergabe_eines_weiteren_unter_dieses() {<br />

sut.Push("b");<br />

Assert.That(sut.top.Next.Data, Is.EqualTo("a"));<br />

}<br />

in einem ersten Test geprüft werden, ob<br />

dieses Element bei Aufruf der Pop-Methode<br />

zurückgegeben wird, siehe Listing 7.<br />

Nun kann überprüft werden, ob der<br />

Stack denn nach dem Pop auch wieder leer<br />

ist, siehe Listing 8. Und jetzt hilft alles<br />

nichts, wir müssen uns mit mehr als einem<br />

Push befassen. Dabei kommt es darauf an,<br />

das neue Element vor das bisherige top-<br />

Element einzuordnen. Dazu muss der top-<br />

Zeiger geändert werden sowie der Next-<br />

Zeiger des top-Elements. Listing 9 zeigt die<br />

entsprechenden Tests.<br />

Daraus ergibt sich dann die fertige Implementierung<br />

des Stacks wie in Listing 10.<br />

Reflexion<br />

Die testgetriebene Vorgehensweise hat mir<br />

keine großen Probleme bereitet. Das lag<br />

zum Großteil daran, dass ich meine Skizze<br />

zur Hand hatte. So konnte ich bei Fragen<br />

sofort nachsehen, wie die top- und Next-<br />

Zeiger jeweils aussehen müssen. Und dadurch,<br />

dass die Tests auf die Interna zugreifen<br />

können, musste ich nicht schon für den<br />

ersten Test gleich zwei Methoden implementieren.<br />

Das ist ein großer Vorteil, der<br />

bei einem so kleinen Beispiel wie einem<br />

Stack möglicherweise nicht so deutlich<br />

wird. Ich habe diesen Effekt jedoch schon<br />

in einigen Fällen als vorteilhaft empfunden,<br />

bei denen es um den internen Zustand<br />

von Klassen ging. Immer da, wo<br />

mehrere Methoden auf einem internen Zustand<br />

arbeiten, kann es sich lohnen, den<br />

Zustand für die Tests sichtbar zu machen,<br />

um bei der Implementierung Methode für<br />

Methode vorgehen zu können.<br />

Queue<br />

Bei der Warteschlange bin ich so verfahren<br />

wie schon beim Stack: Ich habe mir überlegt,<br />

wie man eineWarteschlange in einer Datenstruktur<br />

darstellen kann. Meine Überlegung<br />

hier: Bei einer Warteschlange sollte es offensichtlich<br />

zwei Zeiger geben, die jeweils auf<br />

ein Element verweisen. Zum einen auf das<br />

zuletzt eingefügte Element, um dort bei der<br />

Enqueue-Methode ein weiteres Element ergänzen<br />

zu können, sowie einen Zeiger auf<br />

das nächste zu entnehmende Element für<br />

die Dequeue-Methode. In meiner ersten<br />

Skizze malte ich also das erste und letzte<br />

Element einer Warteschlange und verwies<br />

darauf jeweils mit den Zeigern enqueue und<br />

dequeue, wie es Abbildung 2 zeigt.<br />

Listing 10<br />

LÖSUNG<br />

Die fertige Implementierung<br />

des Stacks.<br />

public class Stack :<br />

IStack {<br />

internal Element top;<br />

public TElement Pop() {<br />

if (top == null) {<br />

throw new<br />

InvalidOperationException();<br />

}<br />

var result = top.Data;<br />

top = top.Next;<br />

return result;<br />

}<br />

public void Push(TElement element) {<br />

var newTop = new Element {<br />

Data = element,<br />

Next = top<br />

};<br />

top = newTop;<br />

}<br />

}<br />

Im Anschluss habe ich überlegt, wie die<br />

Elemente untereinander sinnvoll zu verbinden<br />

sind. Dabei gibt es mehrere Möglichkeiten:<br />

❚ Jedes Element zeigt mit Next auf das ihm<br />

folgende.<br />

❚ Jedes Element zeigt mit Prev auf das<br />

hinter ihm liegende.<br />

❚ Jedes Element enthält sowohl Next- als<br />

auch Prev-Zeiger.<br />

Die doppelte Verkettung habe ich nicht<br />

weiter berücksichtigt, da ich vermutete,<br />

dass es auch ohne gehen muss. Dabei stand<br />

nicht der Reflex im Vordergrund, dass eine<br />

doppelteVerkettung mehr Speicher braucht<br />

als eine einfache. Ich dachte eher daran,<br />

dass ich bei doppelter Verkettung beim Einfügen<br />

und Entfernen von Elementen zwei<br />

Zeiger korrigieren muss. Das erschien mir in<br />

jedem Fall mühsamer, als nur einen Zeiger<br />

korrigieren zu müssen. Es siegte sozusagen<br />

die pure Faulheit.<br />

Es blieb noch die Frage zu klären, ob<br />

Next- oder Prev-Zeiger sinnvoller sind. Das<br />

habe ich mir wieder anhand meiner Skizze<br />

überlegt. Wenn ein neues Element in die<br />

Warteschlange eingefügt wird, muss enqueue<br />

anschließend auf das neue Element<br />

zeigen. Bei Verwendung von Next-Zeigern<br />

muss dann nichts korrigiert werden, bei<br />

Prev-Zeigern muss das bisher erste Element<br />

auf das neue erste zurückverweisen.<br />

Beides ist kein Problem, es ergibt sich also<br />

hier noch keine Präferenz für eines der bei-<br />

www.dotnetpro.de dotnetpro.dojos.2011 41<br />

[Abb. 2]<br />

enqueue-<br />

und<br />

dequeue-<br />

Zeiger.


LÖSUNG<br />

den Verfahren. Beim Entfernen eines Elements<br />

aus der Warteschlange ist ebenfalls<br />

klar, welches Element geliefert werden<br />

muss, denn darauf verweist ja der dequeue-<br />

Zeiger. Dieser muss anschließend auf das<br />

vorhergehende Element verändert werden.<br />

Wenn die Elemente mit Next jeweils auf das<br />

nächste verweisen, wäre diese Korrektur nur<br />

möglich, indem die gesamte Warteschlange<br />

durchlaufen wird, bis das vorletzte Element<br />

erreicht ist. Bei Verwendung von Prev-Zeigern<br />

enthält das letzte Element den benötigten<br />

Verweis auf seinen Vorgänger. Damit<br />

war klar: Prev-Zeiger sind hier eindeutig einfacher,<br />

siehe Abbildung 3.<br />

Das bedeutete aber auch, dass es für die<br />

Queue eine eigene Klasse Element<br />

geben muss, da beim Stack auf Next gezeigt<br />

wird. Hier bemerkte ich einen weiteren Reflex,<br />

nämlich den Versuch, die Klasse Element<br />

wiederzuverwenden. Das lag irgendwie<br />

nahe, hätte jedoch dazu geführt,<br />

dass Stack und Queue durch eine gemeinsam<br />

verwendete Klasse nicht völlig entkoppelt<br />

wären. „Glücklicherweise“ kam es<br />

aber durch die unterschiedlichen Anforderungen<br />

erst gar nicht zurWiederverwendung.<br />

Doch nun zum ersten Test. Auch bei der<br />

Warteschlange ging es mit einer leeren<br />

Queue los. Diese zeichnet sich dadurch<br />

aus, dass enqueue- und dequeue-Zeiger<br />

beide null sind, siehe Listing 11.<br />

Der nächste Test sollte ausdrücken, was<br />

beim Hinzufügen des ersten Elements in<br />

die Warteschlange passiert. Beide Zeiger<br />

verweisen dann nämlich auf das neue Element,<br />

siehe Listing 12.<br />

Als Nächstes kam wieder ein Test der öffentlichen<br />

Schnittstelle, der prüft, was bei<br />

der Entnahme eines Elements aus der Warteschlange<br />

passiert. Zum einen wird das<br />

einzige Element der Warteschlange zurückgegeben,<br />

zum anderen ist die Warte-<br />

Listing 11<br />

Eine leere Queue testen.<br />

[TestFixture]<br />

public class Eine_leere_Queue {<br />

private Queue sut;<br />

[SetUp]<br />

public void Setup() {<br />

sut = new Queue();<br />

}<br />

[Test]<br />

public void Ist_leer() {<br />

Assert.That(sut.enqueue, Is.Null);<br />

Assert.That(sut.dequeue, Is.Null);<br />

}<br />

}<br />

Listing 12<br />

Ein erstes Element hinzufügen.<br />

[Test]<br />

public void Setzt_bei_Enqueue_enqueue_und_dequeue_auf_das_neue_Element() {<br />

sut.Enqueue(42);<br />

Assert.That(sut.enqueue.Data, Is.EqualTo(42));<br />

Assert.That(sut.dequeue.Data, Is.EqualTo(42));<br />

}<br />

Listing 13<br />

Ein Element entnehmen.<br />

[Test]<br />

public void Liefert_bei_Dequeue_das_zuvor_mit_Enqueue_übergebene_Element() {<br />

sut.Enqueue(42);<br />

Assert.That(sut.Dequeue(), Is.EqualTo(42));<br />

}<br />

[Test]<br />

public void Ist_nach_Entnahme_eines_zuvor_übergebenen_Elements_wieder_leer() {<br />

sut.Enqueue(42);<br />

sut.Dequeue();<br />

Assert.That(sut.enqueue, Is.Null);<br />

Assert.That(sut.dequeue, Is.Null);<br />

}<br />

Listing 14<br />

Ein weiteres Element hinzufügen.<br />

[Test]<br />

public void<br />

Nach_Aufnahme_eines_weiteren_Elements_zeigt_dequeue_immer_noch_auf_das_erste_Element() {<br />

sut.Enqueue("b");<br />

Assert.That(sut.dequeue.Data, Is.EqualTo("a"));<br />

}<br />

[Test]<br />

public void Nach_Aufnahme_eines_weiteren_Elements_zeigt_enqueue_auf_das_neue_Element() {<br />

sut.Enqueue("b");<br />

Assert.That(sut.enqueue.Data, Is.EqualTo("b"));<br />

}<br />

[Test]<br />

public void Nach_Aufnahme_eines_weiteren_Elements_zeigt_dequeue_prev_auf_das_neue_Element() {<br />

sut.Enqueue("b");<br />

Assert.That(sut.dequeue.Prev.Data, Is.EqualTo("b"));<br />

}<br />

schlange dann wieder leer, siehe Listing 13.<br />

Die Implementierung war bis hierher trivial.<br />

Als Nächstes ging es wieder um eine Warteschlange,<br />

die bereits ein Element enthält.<br />

Wenn nämlich ein weiteres Element in die<br />

Warteschlange gegeben wird, unterscheiden<br />

sich enqueue- und dequeue-Zeiger.<br />

Ferner muss der Prev-Zeiger des schon<br />

enthaltenen Elements gesetzt werden, siehe<br />

Listing 14.<br />

Im Anschluss habe ich Tests ergänzt, welche<br />

nur die öffentliche Schnittstelle verwen-<br />

den und demonstrieren, wie sich eineWarteschlange<br />

verhält, siehe Listing 15. Listing 16<br />

zeigt zu guter Letzt die Implementierung.<br />

Auch hier war, ähnlich wie beim Stack,<br />

die Implementierung keine große Sache.<br />

Die Skizze sowie die Vorüberlegungen zu<br />

den Prev-/Next-Zeigern haben sich gelohnt,<br />

da die Implementierung dadurch<br />

leicht von der Hand ging.<br />

Im Anschluss habe ich noch das Umkehren<br />

der Elementreihenfolge implementiert.<br />

Dabei zeigte sich wieder die große Stärke<br />

42 dotnetpro.dojos.2011 www.dotnetpro.de


Listing 15<br />

Die öffentliche Schnittstelle<br />

verwenden.<br />

[Test]<br />

public void FIFO_verschachtelt() {<br />

sut.Enqueue(1);<br />

Assert.That(sut.Dequeue(),<br />

Is.EqualTo(1));<br />

sut.Enqueue(2);<br />

Assert.That(sut.Dequeue(),<br />

Is.EqualTo(2));<br />

sut.Enqueue(3);<br />

sut.Enqueue(4);<br />

Assert.That(sut.Dequeue(),<br />

Is.EqualTo(3));<br />

Assert.That(sut.Dequeue(),<br />

Is.EqualTo(4));<br />

}<br />

von automatisierten Tests. Die Skizze einer<br />

Warteschlange im Vorher-Nachher-Vergleich<br />

war schnell erstellt, siehe Abbildung 4.<br />

Doch bis das Umdrehen der Zeiger korrekt<br />

lief, mussten die Tests einige Male<br />

durchlaufen. Ich habe wirklich keine Ahnung,<br />

wie man so etwas ohne automatisierte<br />

Tests hinkriegen will. Na ja, ich habe<br />

irgendwann auch mal ohne automatisierte<br />

Tests entwickelt. Aber das ist glücklicherweise<br />

schon lange her. Listing 17 zeigt den<br />

Test für die Umkehr der Reihenfolge.<br />

Bei der Implementierung habe ich mit<br />

zwei Zeigern gearbeitet, die jeweils auf das<br />

aktuelle in Arbeit befindliche Element (current)<br />

sowie das nächste Element (next) verweisen.<br />

Da die Elemente jeweils mit Prev<br />

auf ihren Vorgänger verweisen, wird die<br />

Warteschlange von hinten nach vorne abgearbeitet.<br />

Daher ist jeweils das Element, welches<br />

im zurückliegenden Schleifendurch-<br />

[Abb. 4] Die Reihenfolge der<br />

Elemente umkehren.<br />

Listing 16<br />

lauf bearbeitet wurde, das nächste Element<br />

im Sinne der normalenVorwärts-Reihenfolge.<br />

Am Ende sind noch enqueue und dequeue<br />

zu vertauschen, siehe Listing 18.<br />

Die Methode habe ich über die öffentliche<br />

Schnittstelle getestet. Man hätte sicherlich<br />

auch hier die interne Repräsentation<br />

heranziehen können, ich glaube aber, dass<br />

die Tests dadurch schlecht lesbar geworden<br />

wären. Daher wird die Beispielwarteschlange<br />

mit enqueue aufgebaut, anschließend<br />

[Abb. 3] Prev-Zeiger bei der Queue.<br />

Die Queue implementieren.<br />

public class Queue<br />

{<br />

internal Element enqueue;<br />

internal Element dequeue;<br />

public void Enqueue(TElement element) {<br />

var newElement = new<br />

Element {Data = element};<br />

if (enqueue != null) {<br />

enqueue.Prev = newElement;<br />

}<br />

enqueue = newElement;<br />

if (dequeue == null) {<br />

dequeue = newElement;<br />

}<br />

}<br />

public TElement Dequeue() {<br />

if (dequeue == null) {<br />

throw new<br />

InvalidOperationException();<br />

}<br />

var result = dequeue.Data;<br />

dequeue = dequeue.Prev;<br />

if (dequeue == null) {<br />

enqueue = null;<br />

}<br />

return result;<br />

}<br />

}<br />

Listing 17<br />

umgedreht und dann mit dequeue überprüft,<br />

ob die Elemente in der richtigen, umgekehrten<br />

Reihenfolge geliefert werden.<br />

Fazit<br />

LÖSUNG<br />

Die Umkehr der Reihenfolge<br />

testen.<br />

[TestFixture]<br />

public class ReverseTests {<br />

private Queue sut;<br />

[SetUp]<br />

public void Setup() {<br />

sut = new Queue();<br />

}<br />

[Test]<br />

public void<br />

Eine_Queue_mit_drei_Elementen_kann<br />

_umgekehrt_werden() {<br />

sut.Enqueue(1);<br />

sut.Enqueue(2);<br />

sut.Enqueue(3);<br />

sut.Reverse();<br />

Assert.That(sut.Dequeue(),<br />

Is.EqualTo(3));<br />

Assert.That(sut.Dequeue(),<br />

Is.EqualTo(2));<br />

Assert.That(sut.Dequeue(),<br />

Is.EqualTo(1));<br />

}<br />

}<br />

Listing 18<br />

Die Reihenfolge umkehren.<br />

public void Reverse() {<br />

var current = dequeue;<br />

Element next = null;<br />

while(current != null) {<br />

var prev = current.Prev;<br />

current.Prev = next;<br />

next = current;<br />

current = prev;<br />

}<br />

var dummy = enqueue;<br />

enqueue = dequeue;<br />

dequeue = dummy;<br />

}<br />

Bei der testgetriebenen Entwicklung hat<br />

sich für mich bestätigt, wie nützlich die<br />

Planung auf Papier ist. Mir ist deutlich geworden,<br />

dass die Auswahl der Reihenfolge<br />

der Tests bei TDD wichtig ist. Daher sollte<br />

man in schwierigeren Szenarien immer<br />

erst Testfälle sammeln und diese dann in<br />

eine sinnvolle Reihenfolge bringen, bevor<br />

man mit dem ersten Test beginnt. [ml]<br />

www.dotnetpro.de dotnetpro.dojos.2011 43


Wer übt, gewinnt<br />

AUFGABE<br />

Infrastruktur<br />

Wie zähmt man den Dämon?<br />

In der Unix-Welt heißen sie Dämonen: die Dienste, die im Hintergrund ihre Arbeit verrichten.<br />

Stefan, stell doch mal eine Aufgabe, die in die Unterwelt der Windows-Dienste führt.<br />

dnpCode: A1010DojoAufgabe<br />

In jeder dotnetpro finden Sie<br />

eine Übungsaufgabe von<br />

Stefan Lieser, die in maximal<br />

drei Stunden zu lösen sein<br />

sollte. Wer die Zeit investiert,<br />

gewinnt in jedem Fall – wenn<br />

auch keine materiellen Dinge,<br />

so doch Erfahrung und Wissen.<br />

Es gilt:<br />

❚ Falsche Lösungen gibt es<br />

nicht. Es gibt möglicherweise<br />

elegantere, kürzere<br />

oder schnellere Lösungen,<br />

aber keine falschen.<br />

❚ Wichtig ist, dass Sie reflektieren,<br />

was Sie gemacht<br />

haben. Das können Sie,<br />

indem Sie Ihre Lösung mit<br />

der vergleichen, die Sie<br />

eine Ausgabe später in<br />

dotnetpro finden.<br />

Übung macht den Meister.<br />

Also − los geht’s. Aber Sie<br />

wollten doch nicht etwa<br />

sofort Visual Studio starten…<br />

Ein Windows-Dienst ist aus .NET-Sicht<br />

eine Konsolenanwendung. Da Dienste<br />

im System im Hintergrund laufen,<br />

auch wenn kein Benutzer am System<br />

angemeldet ist, gelten einige Besonderheiten. So<br />

kann ein Dienst logischerweise nicht über eine<br />

grafische Benutzerschnittstelle verfügen. Kein Benutzer,<br />

keine Interaktion, so einfach ist das.<br />

Damit der Dienst vom Betriebssystem ohne Zutun<br />

eines Benutzers gestartet werden kann, müssen<br />

in der Registry einige Angaben zum Dienst<br />

hinterlegt werden. Die wichtigste Information ist,<br />

unter welchem Benutzer der Dienst laufen soll,<br />

weil sich daraus die Rechte ableiten, die dem<br />

Dienst zur Verfügung stehen. Weitere Einstellungen<br />

betreffen das Startverhalten: Soll der Dienst<br />

beim Systemstart automatisch mit gestartet werden?<br />

Ist der Dienst von anderen Diensten abhängig,<br />

die dann zuerst gestartet werden müssen?<br />

Bevor ein Windows-Dienst gestartet werden<br />

kann, muss er installiert werden. Dazu gibt es im<br />

.NET Framework eine entsprechende Infrastruktur.<br />

Die Details sind nicht kompliziert, dennoch<br />

ist es lästig, für jeden Dienst erneut den Installationsvorgang<br />

entwickeln zu müssen. Daher geht<br />

es in diesem Monat darum, eine wiederverwendbare<br />

Infrastruktur zu entwickeln, mit der Windows-Dienste<br />

erstellt werden können.<br />

Mit „wiederverwendbar“ und „Infrastruktur“<br />

stecken in dieser Übung gleich zwei Fallen, die<br />

man im Blick behalten sollte. Das Ziel der Wiederverwendbarkeit<br />

zieht sich zwar durch die Literatur<br />

zur objektorientierten Programmierung.<br />

Es birgt jedoch die Gefahr, zu viel tun zu wollen.<br />

Denn es droht das Risiko, maximal flexibel zu<br />

sein, dadurch aber auch maximalen Aufwand<br />

zu betreiben.<br />

Der Infrastrukturaspekt birgt das Risiko, „von<br />

unten“ zu beginnen. Statt also Anforderungen<br />

„von oben“ aus Sicht des Anwenders zu definieren,<br />

wird beim Fokus auf Infrastruktur oft der<br />

Fehler gemacht, Dinge vorzusehen, die am Ende<br />

niemand braucht. Ohne klare Anforderungen<br />

bleibt nur der Blick in die Glaskugel. Die Herausforderung<br />

lautet daher, Wiederverwendbarkeit<br />

und Infrastruktur besonders kritisch im Blick zu<br />

behalten, um nicht in diese Fallen zu laufen.<br />

Die Anforderungen für die Übung lauten: Erstellen<br />

Sie eine Komponente, mit der ein Windows-Dienst<br />

realisiert werden kann. Als Anwender<br />

möchte ich den Dienst an der Konsole folgendermaßen<br />

bedienen können:<br />

❚ Mit myService.exe /install und myService.exe<br />

/uninstall kann der Dienst installiert beziehungsweise<br />

aus dem System entfernt werden.<br />

❚ myService.exe /run führt den Dienst als normale<br />

Konsolenanwendung aus, ohne dass er zuvor als<br />

Windows-Dienst registriert werden muss.<br />

Dies ist für Test und Fehlersuche sehr hilfreich.<br />

Das Starten und Stoppen des Dienstes kann zunächst<br />

mit den Windows-Bordmitteln bewerkstelligt<br />

werden:<br />

❚ net start myService<br />

❚ net start myService<br />

Eine denkbare Erweiterung wäre, den Dienst<br />

auch mit myService.exe /start starten zu können.<br />

Aber dies ist ein Feature, welches erst umgesetzt<br />

werden soll, wenn die anderen Anforderungen<br />

implementiert sind. Nicht zu viel auf einmal tun,<br />

lautet die Devise.<br />

Als Entwickler möchte ich möglichst wenig mit<br />

der Windows-Infrastruktur konfrontiert werden.<br />

Ich möchte den Namen des Dienstes angeben<br />

sowie zwei Methoden, die beim Starten und<br />

Stoppen des Dienstes aufgerufen werden. Dabei<br />

sollte sich die Infrastruktur nicht in meine Klassen<br />

drängeln. Die Klasse, welche die Logik des<br />

Dienstes implementiert, sollte nicht von einer<br />

vorgegebenen Basisklasse ableiten müssen.<br />

Die Herausforderung der Übung liegt in zwei<br />

Bereichen: Zum einen geht es um die Technologie<br />

von Windows-Diensten. Wer sich damit noch<br />

nicht befasst hat, kann sich mit einem Spike mit<br />

der Technologie vertraut machen. Der andere<br />

Bereich ist der Entwurf der Lösung. Hier geht es<br />

darum, die Balance zu finden zwischen zu viel<br />

und zu wenig. Zu viel wäre beispielsweise, wenn<br />

im ersten Entwurf schon überlegt wird, wie man<br />

den Dienst zur Laufzeit kontrollieren kann. Zu<br />

wenig wäre, wenn die Anforderungen an die Bedienung<br />

der Kommandozeile nicht umgesetzt<br />

wären oder alles in einer Klasse landet. [ml]<br />

44 dotnetpro.dojos.2011 www.dotnetpro.de


Windows-Dienste implementieren<br />

So beherrschen Sie den Dienst<br />

Ein Windows-Dienst ist eng in die Infrastruktur des Betriebssystems integriert.<br />

Das erschwert automatisierte Tests.Wenn Sie den eigentlichen Kern des Dienstes unabhängig von<br />

der Infrastruktur halten, ist er dennoch für automatisierte Tests zugänglich.<br />

In der Aufgabenstellung zu dieser<br />

Übung habe ich auf zwei Risiken<br />

hingewiesen, die bei Infrastrukturprojekten<br />

oftmals auftreten. Zum<br />

einen bergen sie das Risiko, den Aspekt der<br />

Wiederverwendbarkeit zu stark zu berücksichtigen.<br />

Zum anderen ist die Versuchung<br />

groß, „von unten nach oben“ zu entwickeln.<br />

Beides führt in der Tendenz dazu,<br />

dass man zu viel tut. Nun mögen Sie sich<br />

vielleicht fragen, was denn so schlimm daran<br />

ist, mehr zu tun als gefordert. Sicher, es<br />

wäre schlimmer, weniger zu tun als gefordert.<br />

Jedoch wird beim „mehr tun“ Aufwand<br />

getrieben, den am Ende niemand<br />

bezahlen möchte. Möglicherweise wird sogar<br />

der geplante Termin nicht gehalten,<br />

weil unterwegs hier und da noch zusätzliche<br />

„Schmankerl“ eingebaut wurden. Aus<br />

diesem Grund sollten Anforderungen möglichst<br />

exakt umgesetzt werden.<br />

Und damit sind wir bei einem weiteren<br />

Knackpunkt: Was sind denn eigentlich die<br />

Anforderungen? Solange die nicht klar sind,<br />

kann ein Entwickler bei der Implementierung<br />

eigentlich nur falschliegen. Folglich<br />

sollte er so oft wie nötig nachfragen, um<br />

unklare Anforderungen zu präzisieren.<br />

Nun stand ich Ihnen während der Übung<br />

nicht als Kunde unmittelbar zur Verfügung,<br />

aber in der Aufgabenstellung war ein Feature<br />

explizit als „denkbare Erweiterung“<br />

aufgeführt, nämlich das Starten und Stoppen<br />

des Windows-Dienstes. Folglich sollte<br />

die Implementierung so voranschreiten,<br />

dass dieses Feature nicht sofort von Anfang<br />

an umgesetzt wird. Andererseits darf es<br />

auch nicht aufwendiger sein, das Feature<br />

nachträglich zu ergänzen, statt es von vornherein<br />

vorzusehen. Hilfreich ist es deshalb,<br />

die möglichen Features zunächst zu sammeln.<br />

Dann können Kunde und Entwickler<br />

die Features priorisieren und in der „richtigen“<br />

Reihenfolge abarbeiten.<br />

Beim Sammeln von Features muss eine<br />

Kundensicht eingenommen werden. Alle<br />

Features sollen dem Kunden Nutzen bringen.<br />

Am Ende muss schließlich der Kunde<br />

das Feature als „fertig“ akzeptieren und ab-<br />

nehmen. Das ist nur möglich, wenn das<br />

Feature für den Kunden tatsächlich relevant<br />

ist. Ein Feature wie etwa „Das Programm<br />

von 32 Bit auf 64 Bit umstellen“<br />

spiegelt nicht den unmittelbaren Kundennutzen<br />

wider. Lautet das Feature jedoch<br />

„Das Programm kann mit sehr großen Datenmengen<br />

umgehen“, liegt der Fokus auf<br />

dem Kundennutzen statt auf einem technischen<br />

Detail.<br />

Featureliste<br />

Für die Aufgabenstellung„Windows-Dienst“<br />

könnten die Features wie folgt aussehen:<br />

z F1: Ein Windows-Dienst kann installiert<br />

und deinstalliert werden.<br />

z F2: Der Windows-Dienst kann auch als<br />

Konsolenanwendung gestartet werden,<br />

ohne dass man ihn vorher als Dienst installieren<br />

muss.<br />

z F3: Der Windows-Dienst kann gestartet<br />

und gestoppt werden.<br />

Abnahmekriterien<br />

Um die Anforderungen zu präzisieren, sollten<br />

Abnahmekriterien definiert werden.<br />

Dadurch weiß der Entwickler, wann er mit<br />

der Arbeit fertig ist − eben dann, wenn alle<br />

Abnahmekriterien erfüllt sind. Die Abnahmekriterien<br />

für Feature F1 lauten:<br />

z Wenn der Dienst mit myservice.exe /install<br />

installiert wird, ist er unter Systemsteuerung/Services<br />

sichtbar.<br />

z Der Dienst kann nach der Installation<br />

über Systemsteuerung/Services oder net<br />

start myservice gestartet werden (und<br />

natürlich auch gestoppt werden).<br />

z Wenn der Dienst mit myservice.exe/uninstall<br />

deinstalliert wird, taucht er unter<br />

Systemsteuerung/Services nicht mehr auf.<br />

Spätestens an dieser Stelle beschleicht<br />

mich der Verdacht, dass hier mit automatisierten<br />

Tests nicht viel auszurichten ist. Am<br />

Ende hilft es nichts, der Dienst muss mit<br />

dem Betriebssystem korrekt zusammenarbeiten.<br />

Das lässt sich nur durch einen Integrationstest<br />

wirklich sicherstellen. Das bedeutet<br />

nun nicht, dass wir gar keine auto-<br />

matisierten Tests sehen werden, aber es<br />

werden wenige sein.<br />

Entwurf<br />

LÖSUNG<br />

Für Feature F1 kann nun eine Architektur<br />

entworfen werden. Dabei ist einerseits zu<br />

berücksichtigen, dass möglicherweise nicht<br />

alle Features sofort implementiert werden.<br />

Ein Feature wird nach dem anderen implementiert,<br />

niedrig priorisierte möglicherweise<br />

gar nicht. Wird ein Feature nach dem<br />

anderen implementiert, bietet das für den<br />

Kunden sehr viel Flexibilität: Er kann die<br />

Priorisierung von Features jederzeit ändern.<br />

Und er kann Features streichen oder auch<br />

neue hinzunehmen. Logischerweise gilt dies<br />

nicht für Features, die bereits in Arbeit sind.<br />

Aber alle noch nicht begonnenen Features<br />

stehen zur Disposition. Folglich sollte ein<br />

Architekturentwurf nur die Features im<br />

Detail berücksichtigen, die konkret zur Implementierung<br />

anstehen. Da weitere mögliche<br />

Features schon bekannt sind, kann<br />

und sollte man diese beim Architekturentwurf<br />

im Blick behalten. Diese Features sollten<br />

jedoch keinen nennenswerten Einfluss<br />

auf die Architektur nehmen, denn es droht<br />

jederzeit das Risiko, dass sie doch nicht benötigt<br />

werden. Gefragt ist also ein Blick über<br />

den Tellerrand der anstehenden Features,<br />

ohne dass man dabei gleich zu viel tut. Orientierung<br />

liefert die Fokussierung auf den<br />

Kundennutzen. Für den Kunden ist es weitaus<br />

angenehmer, das wichtigste Feature<br />

einsatzbereit geliefert zu bekommen, als<br />

viele unvollendete Features, die nicht einsatzbereit<br />

sind.<br />

Um einen möglichst flexiblen Architekturentwurf<br />

zu erreichen, müssen die beteiligten<br />

Funktionseinheiten lose gekoppelt<br />

sein. So ist die Wahrscheinlichkeit groß,<br />

dass zusätzliche Features leicht integriert<br />

werden können. Es liegt also auf der Hand,<br />

hier Event-Based Components (EBC) zu<br />

verwenden. Doch bevor es so weit ist, müssen<br />

die Anforderungen weiter präzisiert<br />

werden, denn noch ist nicht klar, wie die<br />

geforderte Dienstinfrastruktur eingesetzt<br />

werden soll.<br />

www.dotnetpro.de dotnetpro.dojos.2011 45


LÖSUNG<br />

Listing 1<br />

Ein Interface für den Dienst.<br />

public interface IService<br />

{<br />

string Name { get; }<br />

string DisplayName { get; }<br />

string Description { get; }<br />

void OnStart();<br />

void OnStop();<br />

}<br />

Als Entwickler, der einen Windows-<br />

Dienst implementieren soll, möchte ich es<br />

so einfach wie möglich haben. Das bedeutet<br />

für mich unter anderem: geringe Abhängigkeiten.<br />

Denkbar wäre die Realisierung<br />

über ein Interface, das wie in Listing 1<br />

zu implementieren ist.<br />

Durch dieses Interface würden alle Informationen<br />

bereitgestellt, die relevant<br />

sind, damit man einen Windows-Dienst installieren<br />

und starten kann. Das könnte so<br />

wie in Listing 2 aussehen.<br />

Das Anlegen einer Klasse, die IService<br />

implementiert, geht zwar schnell von der<br />

Listing 2<br />

Grundlagen eines Dienstes.<br />

Hand. Noch schneller bin ich aber, wenn<br />

ich nicht für jeden Dienst eine Klasse implementieren<br />

muss. Stattdessen könnte ich<br />

eine generische Klasse verwenden, von der<br />

Instanzen angelegt werden, siehe Listing 3.<br />

Ich habe die Klasse EasyService genannt,<br />

weil es damit so schön einfach ist, die erforderlichen<br />

Angaben für einen Windows-<br />

Dienst zusammenzustellen. Es bleibt allerdings<br />

die Frage, wozu eine Instanz von<br />

EasyService angelegt werden muss. Das<br />

Objekt würde nur dazu dienen, die Dienstbeschreibung<br />

zu transportieren. Eigentlich<br />

wird aber kein Zustand benötigt, also genügt<br />

auch eine statische Methode Run innerhalb<br />

der Klasse, siehe Listing 4.<br />

Nachdem klar ist, wie der Kunde seine<br />

Software bedienen will, können Sie einen<br />

Architekturentwurf für Feature F1 angehen.<br />

Von ferne betrachtet, ist das Feature eine<br />

Funktionseinheit, die als Parameter die<br />

Kommandozeilenargumente sowie eine<br />

Dienstbeschreibung erhält. Die Dienstbeschreibung<br />

besteht vor allem aus dem Namen<br />

des Dienstes. Ferner sind zwei Lambda-Ausdrücke<br />

nötig, die beim Starten und<br />

Stoppen des Dienstes ausgeführt werden<br />

sollen. Abbildung 1 zeigt den Entwurf.<br />

public class MyService : IService<br />

{<br />

public string Name { get { return "myService"; }<br />

}<br />

public string DisplayName { get { return "Mein Service"; }<br />

}<br />

public string Description { get { return "Ein Service, der nichts tut."; }<br />

}<br />

public void OnStart() { Trace.WriteLine("OnStart aufgerufen");<br />

}<br />

public void OnStop() { Trace.WriteLine("OnStop aufgerufen");<br />

}<br />

}<br />

Listing 3<br />

Eine generische Klasse verwenden.<br />

var myService = new EasyService {<br />

Name = "myService",<br />

DisplayName = "Mein Service",<br />

Description = "Ein Service, der nichts tut.",<br />

OnStart = () => Trace.WriteLine("OnStart aufgerufen"),<br />

OnStop = () => Trace.WriteLine("OnStop aufgerufen")<br />

};<br />

myService.Run(args);<br />

[Abb. 1] Erster Entwurf.<br />

Der nächste Schritt besteht darin, diesen<br />

Entwurf zu verfeinern. Dazu wird die Funktionseinheit<br />

zerlegt. Ohne mir schon zu<br />

viele Gedanken um die Details der Dienstinstallation<br />

machen zu müssen, fällt es mir<br />

leicht, vier Funktionseinheiten zu identifizieren:<br />

z Argumente auswerten,<br />

z Dienst installieren,<br />

z Dienst deinstallieren,<br />

z Dienst ausführen.<br />

Diese Einheiten bilden die Verfeinerung<br />

meines Entwurfs, siehe Abbildung 2.<br />

Damit bin ich bereits auf einem Abstraktionsniveau<br />

angekommen, mit dem ich zufrieden<br />

bin. In die technischen Details der<br />

Dienstinstallation will ich bei diesem Entwurf<br />

nicht weiter hineinzoomen. Sicherlich<br />

werden da noch ein paar technische<br />

Details stecken, doch in der Rolle des Architekten<br />

gehe ich davon aus, dass der Entwickler<br />

diese Details in der Funktionseinheit<br />

„Dienst installieren“ sinnvoll unterbringen<br />

kann. Sollte sich während der Implementierung<br />

herausstellen, dass dem<br />

nicht so ist, muss der Entwurf möglicherweise<br />

weiter verfeinert werden. Aus dem<br />

Entwurf ergeben sich folgende Kontrakte:<br />

z Ein Datenmodell für die Dienstbeschreibung.<br />

z Ein Kontrakt für das Auswerten der Kommandozeilenargumente.<br />

z Je ein Kontrakt für die Installation, die<br />

Deinstallation und die Ausführung des<br />

Dienstes.<br />

Das Datenmodell wird anstelle von einfachen<br />

Typen wie string oder Ähnlichem<br />

verwendet, weil dadurch ein höheres Abstraktionsniveau<br />

erreicht wird. Im Entwurf<br />

werden bei den Input- und Outputpins<br />

ebenfalls Bezeichner verwendet, die aus<br />

der sogenannten allgegenwärtigen Sprache<br />

(Ubiquitous Language) stammen. Da<br />

es sich bei Begriffen wie „Dienstbeschreibung“<br />

um einen Begriff aus der Problemdomäne<br />

handelt, ist es gut, diese auch im<br />

Code wiederzufinden. Das erleichtert das<br />

Verständnis, da keine gedankliche Übersetzung<br />

erforderlich ist. Würde eine Me-<br />

46 dotnetpro.dojos.2011 www.dotnetpro.de


thode mehrere Parameter von einfachen<br />

Typen erwarten, müsste jemand, der den<br />

Code liest, daraus gedanklich die Dienstbeschreibung<br />

erst wieder zusammensetzen.<br />

Das Datenmodell für die Dienstbeschreibung<br />

sieht aus wie in Listing 5.<br />

Die Kontrakte für die Funktionseinheiten<br />

sind in EBC-Manier erstellt. Das bedeutet,<br />

dass sie über sogenannte Inputund<br />

Outputpins verfügen. Inputpins werden<br />

in Form von Methoden modelliert,<br />

Outputpins sind Events. Zu weiteren Details<br />

über EBCs lesen Sie am besten die<br />

Artikelserie von Ralf Westphal, zu finden<br />

unter [1] [2] [3]. Der Kontrakt für das Auswerten<br />

der Argumente sieht aus wie in Listing<br />

6. Auf dem Inputpin In_Process der<br />

Funktionseinheit werden die Kommando-<br />

Listing 4<br />

Eine statische Methode<br />

„Run” verwenden.<br />

EasyService.Run(<br />

args,<br />

"myService",<br />

"Mein Service",<br />

"Ein Service, der nichts tut.",<br />

() => Trace.WriteLine<br />

("OnStart aufgerufen"),<br />

() => Trace.WriteLine<br />

("OnStop aufgerufen")<br />

);<br />

Listing 5<br />

Datenmodell für die Dienstbeschreibung.<br />

public class ServiceBeschreibung<br />

{<br />

public string Name { get; set; }<br />

public string DisplayName { get; set; }<br />

public string Description { get; set; }<br />

}<br />

Listing 6<br />

Argumente auswerten.<br />

public interface IArgumenteAuswerten<br />

{<br />

void In_Process(params string[] args);<br />

event Action Out_Install;<br />

event Action Out_Uninstall;<br />

event Action Out_RunAsService;<br />

}<br />

[Abb. 2] Verfeinerung<br />

des Entwurfs.<br />

zeilenparameter in Form eines string-Arrays<br />

übergeben. Je nachdem, welcher Parameter<br />

übergeben wurde, wird daraufhin<br />

der korrespondierende Outputpin ausgelöst.<br />

Die beiden Kontrakte für die Installation<br />

und Deinstallation sind noch einfacher, da<br />

sie nicht über Outputpins verfügen, siehe<br />

Listing 7.<br />

Und zu guter Letzt ist da noch der Kontrakt<br />

für die Ausführung des Service, siehe<br />

Listing 8.<br />

Damit haben Sie nun alle Kontrakte zusammen<br />

und können mit der Implementierung<br />

beginnen. Wie schon angedeutet,<br />

sind automatisierte Tests an den Stellen<br />

schwierig, an denen die Windows-Infrastruktur<br />

relevant ist. Dies betrifft das Installieren<br />

und Deinstallieren des Dienstes. Ferner<br />

sollte der Kontrakt IServiceAusführen<br />

mit der abstrakten Klasse ServiceBase aus<br />

dem .NET Framework kombiniert werden.<br />

Diese stellt die erforderliche Infrastruktur<br />

für die Dienstausführung zur Verfügung.<br />

Damit die Dienste nicht von ServiceBase<br />

ableiten müssen und tatsächlich keine Abhängigkeiten<br />

zur Windows-Infrastruktur<br />

haben, verwende ich einen Dienstproxy.<br />

Diese Klasse leitet von ServiceBase ab und<br />

bietet zwei Events, die beim Starten bzw.<br />

Stoppen des Dienstes ausgeführt werden.<br />

Dadurch kann ein Lambda-Ausdruck verwendet<br />

werden, und der Dienst ist infra-<br />

Listing 7<br />

Installation und Deinstallation.<br />

public interface IServiceInstallieren<br />

{<br />

void In_Installieren(ServiceBeschreibung beschreibung);<br />

}<br />

public interface IServiceDeinstallieren<br />

{<br />

void In_Deinstallieren(ServiceBeschreibung beschreibung);<br />

}<br />

LÖSUNG<br />

strukturunabhängig. Der Dienstproxy sieht<br />

aus, wie in Listing 9 gezeigt wird.<br />

Diese Klasse ist so simpel, dass ich auf<br />

Tests verzichtet habe. Sie zu ergänzen würde<br />

im Übrigen auch erfordern, die protected<br />

Methoden OnStart und OnStop im<br />

Test aufzurufen. Aufgrund der Vererbung<br />

kann die Sichtbarkeit nicht zu internal geändert<br />

werden. Aufwand und Nutzen stünden<br />

daher in einem sehr ungünstigen Verhältnis.<br />

Da das gesamte Projekt ohnehin<br />

einen Integrationstest erfordert, wird der<br />

ServiceProxy dort mitgetestet.<br />

Sehr gut automatisiert zu testen ist die<br />

Implementierung der Argumentauswertung.<br />

Ich habe die Klasse ArgumenteAuswerten<br />

testgetrieben entwickelt. Listing 10<br />

zeigt zwei der Tests als Beispiele.<br />

Der erste Test prüft, ob beim Aufruf ohne<br />

Parameter der Outputpin Out_RunAs-<br />

Service ausgelöst wird. Dies ist wichtig, da<br />

Windows den Dienst so startet. Der zweite<br />

Test prüft, ob bei Aufruf mit /install der<br />

Event Out_Install ausgelöst wird.<br />

Die zugehörige Implementierung ist einfach<br />

gehalten. Zunächst wird geprüft, ob<br />

kein Argument übergeben wurde. In dem<br />

Fall wird der Event Out_RunAsService ausgeführt.<br />

Andernfalls wird über ein switch-<br />

Statement in die jeweiligen Events verzweigt.<br />

Diese Form der Argumentauswertung<br />

ist zwar zu einfach, um damit etwa zusätzliche<br />

Parameter zu den Optionen parsen<br />

www.dotnetpro.de dotnetpro.dojos.2011 47


LÖSUNG<br />

Listing 8<br />

Den Dienst ausführen und<br />

stoppen.<br />

public interface IServiceAusführen<br />

{<br />

void In_Start();<br />

void In_Stop();<br />

}<br />

zu können. Kommandos mit Parametern<br />

wie etwa /install myService2 erfordern etwas<br />

mehr Aufwand beim Parsen. Für Feature<br />

F1 genügt ein solch einfacher Parser<br />

aber völlig, warum also mehr tun. Tatsächlich<br />

ist sogar die Groß-/Kleinschreibung<br />

signifikant, das heißt /INSTALL würde zu<br />

einem Fehler führen. Aber auch diese Einschränkung<br />

ist in Ordnung, es sei denn, der<br />

Kunde würde explizit fordern, dass Groß-/<br />

Kleinschreibung zu ignorieren sei.<br />

Als Nächstes kommt das Installieren des<br />

Dienstes an die Reihe. Obwohl ich die<br />

Dienstinstallation schon mal implementiert<br />

habe, musste ich die Suchmaschine<br />

meiner Wahl bedienen, um verschiedene<br />

Details zusammenzusammeln. Auch automatisierte<br />

Tests schieden aus. So erinnerte<br />

mich die Implementierung dieser Funktionseinheit<br />

eher an einen Spike, und leichtes<br />

Unbehagen ließ sich nicht vermeiden.<br />

Um überhaupt etwas testen zu können,<br />

musste ich einen kleinen Minidienst implementieren.<br />

Der kann nun gleichzeitig<br />

als Beispiel dienen, wie man Dienste mithilfe<br />

der geschaffenen Infrastruktur imple-<br />

Listing 9<br />

Ein Proxy für den Dienst.<br />

public class ServiceProxy : ServiceBase, IServiceAusführen<br />

{<br />

public event Action Out_Start = delegate { };<br />

public event Action Out_Stop = delegate { };<br />

public void In_Start() {<br />

Out_Start();<br />

}<br />

public void In_Stop() {<br />

Out_Stop();<br />

}<br />

protected override void OnStart(string[] args) {<br />

Out_Start();<br />

}<br />

protected override void OnStop() {<br />

Out_Stop();<br />

}<br />

}<br />

mentiert. Dennoch war mir nicht wohl bei<br />

der Sache, da ich diese Arbeitsweise dank<br />

testgetriebener Entwicklung gar nicht<br />

mehr gewohnt bin. Aber es hilft nichts −<br />

die korrekte Installation eines Dienstes innerhalb<br />

des Betriebssystems lässt sich nun<br />

mal nicht anders überprüfen.<br />

Neben den Details der Dienstinstallation<br />

ging es auch um andere Details. Denn natürlich<br />

darf nicht jeder Benutzer einen<br />

Dienst im Betriebssystem registrieren, es<br />

sei denn, man arbeitet immer noch als Administrator<br />

und schaltet die User Account<br />

Control (UAC) aus. Folglich musste ich<br />

mich damit befassen, wie man dem Betriebssystem<br />

mitteilen kann, dass ein Programm<br />

Administratorberechtigungen benötigt.<br />

Das geht ganz einfach: Man fügt<br />

dem Projekt, mit dem man das Programm<br />

erstellt, also dem EXE-Projekt, eine Manifestdatei<br />

hinzu. In dieser XML-Datei kann<br />

die Administratorberechtigung angefordert<br />

werden, sodass Windows die sogenannte<br />

Elevation beim Benutzer anfordern<br />

kann. Der relevante Ausschnitt aus der Manifestdatei<br />

app.manifest sieht aus, wie in<br />

Listing 11 gezeigt wird.<br />

Für die Installation eines Dienstes stehen<br />

im .NET Framework die Klassen TransactedInstaller,<br />

ServiceProcessInstaller und<br />

ServiceInstaller zur Verfügung. Der TransactedInstaller<br />

ist dafür zuständig, eine Installation<br />

transaktional auszuführen. Das<br />

bedeutet, die angeforderte Installation<br />

wird entweder vollständig ausgeführt oder<br />

bei einem Fehler komplett wieder rückgängig<br />

gemacht. ServiceProcessInstaller und<br />

ServiceInstaller werden konfiguriert und<br />

zum TransactedInstaller hinzugefügt; die-<br />

Listing 10<br />

Argumente auswerten.<br />

[TestFixture]<br />

public class ArgumenteAuswertenTests<br />

{<br />

private IArgumenteAuswerten sut;<br />

[SetUp]<br />

public void Setup() {<br />

sut = new ArgumenteAuswerten();<br />

}<br />

[Test]<br />

public void Ohne_Argumente() {<br />

var count = 0;<br />

sut.Out_RunAsService += () => count++;<br />

sut.In_Process();<br />

Assert.That(count, Is.EqualTo(1));<br />

}<br />

[Test]<br />

public void Install_als_Argument() {<br />

var count = 0;<br />

sut.Out_Install += () => count++;<br />

sut.In_Process("/install");<br />

Assert.That(count, Is.EqualTo(1));<br />

}<br />

}<br />

ser wird anschließend angewiesen, die Installation<br />

durchzuführen. Da die Schritte<br />

für die Deinstallation alle gleich sind, liegt<br />

es auf der Hand, die beiden Kontrakte IServiceInstallieren<br />

und IServiceDeinstallieren<br />

in einer Klasse zusammenzufassen, wie<br />

Listing 12 zeigt.<br />

Nachdem die einzelnen Prozessschritte<br />

implementiert sind, müssen sie nur noch<br />

in EBC-Manier miteinander verbunden<br />

werden. Diese Arbeit übernimmt die statische<br />

Run-Methode in der Klasse EasyService.<br />

Ich habe mich auch hier dagegen entschieden,<br />

die „Verdrahtung“ der Bausteine<br />

automatisiert zu testen. Technisch wäre<br />

das natürlich möglich. Dazu müssten die<br />

einzelnen Bausteine durch ein Mock-Framework<br />

instanziert und in die Klasse Easy-<br />

Service injiziert werden. Anschließend<br />

könnte automatisiert geprüft werden, ob<br />

die Zuordnung von Methoden zu Events<br />

korrekt ist. Im Ergebnis ist der Nutzen<br />

abermals recht gering, da die Verdrahtung<br />

aufgrund der Event- und Methodensignaturen<br />

kaum falsch gemacht werden kann.<br />

Um nun tatsächlich feststellen zu können,<br />

ob Feature F1 fertiggestellt ist, müssen<br />

die Abnahmekriterien überprüft werden.<br />

Dazu muss ein exemplarischer Dienst implementiert<br />

werden, um so zu prüfen, ob<br />

dieser tatsächlich in der Systemsteuerung<br />

sichtbar ist und gestartet werden kann.<br />

48 dotnetpro.dojos.2011 www.dotnetpro.de


[Abb. 3] DebugView zeigt die Trace-Ausgaben.<br />

Hier stellte sich mir die Frage, wie man am<br />

einfachsten überprüft, ob der Dienst tatsächlich<br />

gestartet und gestoppt werden<br />

kann. Eine Ausgabe auf der Konsole scheidet<br />

aus, denn schließlich handelt es sich<br />

um einen Dienst ohne Benutzerinteraktion.<br />

Ich entschied mich für eine Ausgabe<br />

mittels System.Diagnostics.Trace aus dem<br />

.NET Framework. Diese kann nämlich mit<br />

dem Programm DebugView aus der SysInternals-Sammlung<br />

[4] angezeigt werden.<br />

Damit Trace-Ausgaben von Diensten angezeigt<br />

werden, muss im DebugView die Einstellung<br />

„Capture Global Win32“ aktiviert<br />

werden. Voraussetzung dafür ist wiederum,<br />

dass das Programm mit Administratorrechten<br />

gestartet wird. Abbildung 3 zeigt<br />

die Ausgabe von DebugView.<br />

Die Features F2 und F3 zu ergänzen ist<br />

dank der EBCs ganz leicht. Dazu musste<br />

ich lediglich die Auswertung der Argumente<br />

so ergänzen, dass weitere Kommandozeilenparameter<br />

erkannt werden. Das Ausführen<br />

des Dienstes von der Konsole aus,<br />

also ohne Installation im Betriebssystem,<br />

war ganz leicht und benötigte keine weitere<br />

Funktionseinheit. Für das Starten und<br />

Stoppen des Dienstes habe ich die Klasse<br />

ServiceStarter ergänzt. Anschließend konnte<br />

ich in der Klasse EasyService die Verdrahtung<br />

ergänzen, und das neue Feature war<br />

fertig.<br />

Fazit<br />

Bei dieser Übung ist die Testabdeckung<br />

recht gering ausgefallen. Dies hat mich natürlich<br />

nicht kaltgelassen. Allerdings gibt es<br />

zwei Dinge zu berücksichtigen: Zum einen<br />

wären automatisierte Tests aufgrund des<br />

sehr hohen Infrastrukturanteils sehr aufwendig,<br />

darauf habe ich weiter oben an<br />

den entsprechenden Stellen bereits hingewiesen.<br />

Zum anderen sorgt die hier vorgestellte<br />

Dienstinfrastruktur aber dafür, dass<br />

der eigentliche Kern des zu implementierenden<br />

Dienstes völlig befreit ist von Infrastrukturabhängigkeiten.<br />

Dadurch ist der<br />

Kern des Dienstes besser zu testen. Insofern<br />

kann ich akzeptieren, dass der Infrastrukturanteil<br />

mittels manueller Integra-<br />

Listing 11<br />

Die Administratorberechtigung anfordern.<br />

tionstests überprüft wird. Die Vorgehensweise,<br />

ausgehend von einer Featureliste<br />

über die EBC-Architektur hin zur Implementierung,<br />

hat sich bewährt. Wie sah das<br />

bei Ihnen aus? Schreiben Sie doch einmal<br />

einen Leserbrief zu Ihren Erfahrungen<br />

beim dotnetpro dojo! [ml]<br />

[1] Ralf Westphal, Zusammenstecken – funktioniert,<br />

Event-Based Components, dotnetpro 6/2010,<br />

LÖSUNG<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

Listing 12<br />

Den Service installieren und entfernen.<br />

public class ServiceInstallation : IServiceInstallieren, IServiceDeinstallieren<br />

{<br />

public void In_Installieren(ServiceBeschreibung name) {<br />

var transactedInstaller = CreateTransactedInstaller(name, "Install.log");<br />

transactedInstaller.Install(new Hashtable());<br />

}<br />

}<br />

public void In_Deinstallieren(ServiceBeschreibung name) {<br />

var transactedInstaller = CreateTransactedInstaller<br />

(name, "UnInstall.log");<br />

transactedInstaller.Uninstall(null);<br />

}<br />

private static TransactedInstaller CreateTransactedInstaller<br />

(ServiceBeschreibung name, string logFilePath) {<br />

var serviceProcessInstaller = new ServiceProcessInstaller {<br />

Account = ServiceAccount.LocalSystem<br />

};<br />

}<br />

var transactedInstaller = new TransactedInstaller();<br />

transactedInstaller.Installers.Add(serviceProcessInstaller);<br />

var path = string.Format("/assemblypath={0}",<br />

Assembly.GetEntryAssembly().Location);<br />

var installContext = new InstallContext(logFilePath, new[] {path});<br />

transactedInstaller.Context = installContext;<br />

var serviceInstaller = new ServiceInstaller {<br />

ServiceName = name.Name,<br />

DisplayName = name.DisplayName,<br />

Description = name.Description<br />

};<br />

transactedInstaller.Installers.Add(serviceInstaller);<br />

return transactedInstaller;<br />

S. 132ff., www.dotnetpro.de/<br />

A1006ArchitekturKolumne<br />

[2] Ralf Westphal, Stecker mit System,<br />

dotnetpro 7/2010, S. 126ff.,<br />

www.dotnetpro.de/A1007ArchitekturKolumne<br />

[3] Ralf Westphal, Nicht nur außen schön,<br />

dotnetpro 8/2010, S. 126ff.,<br />

www.dotnetpro.de/A1008ArchitekturKolumne<br />

[4] DebugView for Windows v4.76,<br />

www.dotnetpro.de/SL1011dojoLoesung1<br />

www.dotnetpro.de dotnetpro.dojos.2011 49


Wer übt, gewinnt<br />

AUFGABE<br />

Event-Based Components<br />

Wie baue ich einen Legostein?<br />

Softwarekomponenten so einfach wie Legosteine zusammenstecken zu können – mit diesem Versprechen<br />

tritt das Konzept der Event-Based Components an. Stefan, kannst du dazu eine Übung stellen?<br />

dnpCode: A1011DojoAufgabe<br />

In jeder dotnetpro finden Sie<br />

eine Übungsaufgabe von<br />

Stefan Lieser, die in maximal<br />

drei Stunden zu lösen sein<br />

sollte. Wer die Zeit investiert,<br />

gewinnt in jedem Fall – wenn<br />

auch keine materiellen Dinge,<br />

so doch Erfahrung und Wissen.<br />

Es gilt:<br />

z Falsche Lösungen gibt es<br />

nicht. Es gibt möglicherweise<br />

elegantere, kürzere<br />

oder schnellere Lösungen,<br />

aber keine falschen.<br />

z Wichtig ist, dass Sie reflektieren,<br />

was Sie gemacht<br />

haben. Das können Sie,<br />

indem Sie Ihre Lösung mit<br />

der vergleichen, die Sie<br />

eine Ausgabe später in<br />

dotnetpro finden.<br />

Übung macht den Meister.<br />

Also − los geht’s. Aber Sie<br />

wollten doch nicht etwa<br />

sofort Visual Studio starten…<br />

In Ergänzung zur Artikelserie von Ralf<br />

Westphal [1] [2] [3] [4] über Event-Based<br />

Components (EBC) lautet die Aufgabe in<br />

diesem Monat: Entwickeln Sie eine Textumbruchkomponente.<br />

Für die Silbentrennung<br />

können Sie die Komponente NHunspell [5] verwenden.<br />

Abbildung 1 zeigt, wie eine kleine Testanwendung<br />

aussehen könnte, die den Textumbruch<br />

als Komponente verwendet. Die Komponente<br />

soll über folgenden Kontrakt verfügen:<br />

public interface ITextumbruch {<br />

string Umbrechen(string text,<br />

int breiteInZeichen);<br />

}<br />

Der Text sowie die gewünschte Breite werden<br />

in die Methode gegeben, diese liefert den umbrochenen<br />

Text zurück. Die Breite des Textes<br />

wird der Einfachheit halber als Anzahl der Zeichen<br />

angegeben. Eine Angabe in Millimetern<br />

würde es erfordern, dass man die Laufweiten der<br />

jeweiligen Zeichen berücksichtigt. Das wäre für<br />

die Übung dann doch zu viel des Guten.<br />

So weit zum gewünschten API der Komponente.<br />

Diese ist damit eine Komponente im klassischen<br />

Sinne, also eine binäre Funktionseinheit<br />

mit separatem Kontrakt. Intern soll sie jedoch<br />

durch EBCs realisiert werden. Überlegen Sie sich<br />

dazu, welche Bearbeitungsschritte nötig sind,<br />

um den Text zu umbrechen. Entwerfen Sie dabei<br />

nicht gleich in Verantwortlichkeiten, sondern in<br />

Prozessschritten oder Aktionen. Die Verantwortlichkeiten<br />

ergeben sich daraus ganz von allein.<br />

Für die Silbentrennung mag es auf der Hand liegen,<br />

dass NHunspell die Verantwortlichkeit für<br />

diesen Prozessschritt übernimmt. Für alle anderen<br />

Schritte dürfte es nicht so offenkundig sein.<br />

Die Anforderungen an die Komponente sollten<br />

Sie sich vorher notieren. Auch mögliche Testfälle<br />

sollten Sie sammeln. In einem realen Projekt<br />

würden Sie solche Testfälle mit dem Kunden diskutieren.<br />

Bei dieser Übung treffen Sie selber<br />

sinnvolle Annahmen. So könnten Sie beispielsweise<br />

entscheiden, dass ein Wort, welches selbst<br />

nach der Silbentrennung zu lang ist, einfach<br />

übersteht. „Spielen“ Sie ein wenig mit verschiedenen<br />

Texten, es werden Ihnen sicher zahlreiche<br />

[Abb. 1] Testanwendung für den Textumbruch.<br />

interessante Szenarien auffallen. Dieses Mal sollen<br />

automatisierte Tests eine größere Rolle spielen<br />

als bei der vorherigen Übung zum Windows-<br />

Dienst.<br />

Die einzelnen Funktionseinheiten sollen möglichst<br />

isoliert getestet werden. Und natürlich dürfen<br />

ein paar Integrationstests nicht fehlen. Für<br />

das explorative Testen ist die Testanwendung gedacht.<br />

Damit können Sie ausprobieren, wie sich<br />

die Komponente bei bestimmten Konstellationen<br />

verhält. Ich wünsche viel Spaß und großen<br />

Erkenntnisgewinn! [ml]<br />

[1] Ralf Westphal, Zusammenstecken – funktioniert,<br />

Event-Based Components, dotnetpro 6/2010, S. 132ff.,<br />

www.dotnetpro.de/A1006ArchitekturKolumne<br />

[2] Ralf Westphal, Stecker mit System, Event-Based<br />

Components, dotnetpro 7/2010, S. 126ff.,<br />

www.dotnetpro.de/A1007ArchitekturKolumne<br />

[3] Ralf Westphal, Nicht nur außen schön, Event-Based<br />

Components, dotnetpro 8/2010, S. 126ff.,<br />

www.dotnetpro.de/A1008ArchitekturKolumne<br />

[4] Ralf Westphal, Staffel-Ende mit Happy End,<br />

Event-Based Components, dotnetpro 9/2010, S. 132ff.,<br />

www.dotnetpro.de/A1009ArchitekturKolumne<br />

[5] http://nhunspell.sourceforge.net/<br />

50 dotnetpro.dojos.2011 www.dotnetpro.de


Event-Based Components<br />

So trennt man Feu-er-wehr<br />

Einen Textumbruch mit Silbentrennung<br />

zu implementieren,<br />

hört sich schwierig an. Und das<br />

ist es auch, wenn man an eine<br />

Textverarbeitung wie Word denkt. Allerdings<br />

gab die Aufgabenstellung mit dem<br />

Hinweis auf NHunspell [1] einen Tipp für<br />

das Problem der Silbentrennung.<br />

Als Erstes sind die Anforderungen zu klären.<br />

Im Falle des Textumbruchs können die<br />

Anforderungen sehr gut anhand von Beispielen<br />

dargestellt werden. Der folgende<br />

Satz soll etwa auf eine Breite von zehn Zeichen<br />

umbrochen werden:<br />

Bauer Klaus erntet Kartoffeln.<br />

Dann soll das Ergebnis folgendermaßen<br />

aussehen:<br />

Bauer<br />

Klaus erntetKartoffeln.<br />

Das Beispiel enthält keine besonderen<br />

Schwierigkeiten oder Spezialfälle. Aber genau<br />

die gilt es natürlich ebenfalls in den<br />

Blick zu nehmen. So stellt sich beispielsweise<br />

die Frage, wie mit Zeilenumbrüchen<br />

verfahren werden soll, die im Eingangstext<br />

schon vorhanden sein können:<br />

Bauer Klaus<br />

erntet<br />

Kartoffeln.<br />

Das Ergebnis soll das gleiche sein wie<br />

oben. Bereits vorhandene Zeilenumbrüche<br />

werden also ignoriert. Das soll auch für<br />

mehrere hintereinander stehende Zeilenumbrüche<br />

gelten. Auch Absätze werden<br />

damit ignoriert. Diese Vereinfachung ist<br />

der Tatsache geschuldet, dass es hier nur<br />

um eine Übung geht.<br />

Als Nächstes ist Leerraum zu betrachten.<br />

Wenn im Satz zusätzliche Leerzeichen stehen,<br />

sollen diese erhalten bleiben, es soll<br />

also keine Normalisierung stattfinden. Allerdings<br />

sollen Leerzeichen am Anfang einer<br />

Zeile entfernt werden, weil das Ergebnis<br />

sonst doch sehr fragwürdig aussieht.<br />

Dazu ein Beispiel, in dem die Leerzeichen<br />

durch einen Punkt ersetzt sind, damit sie<br />

besser zu erkennen sind:<br />

Bauer•••••••Klaus••erntet•Kartoffeln.<br />

Wenn dieser Satz auf eine Breite von<br />

zehn Zeichen unter Beibehaltung der Leerzeichen<br />

umbrochen wird, ergibt sich zunächst<br />

folgendes Ergebnis:<br />

Bauer•••••<br />

••Klaus••<br />

erntet•<br />

Kartoffeln.<br />

Dabei sind die Leerzeichen in der zweiten<br />

Zeile vor dem Wort „Klaus“ jedoch störend.<br />

Folglich sollen sie entfernt werden,<br />

mit folgendem Ergebnis:<br />

Bauer•••••<br />

Klaus••erntet•Kartoffeln.<br />

Leerzeichen am Zeilenanfang werden also<br />

entfernt, innerhalb des Satzes oder auch<br />

am Ende bleiben sie erhalten. Um die Anforderungen<br />

für die Übung möglichst einfach<br />

zu halten, lassen wir es dabei zunächst<br />

bewenden.<br />

Algorithmus<br />

Nachdem die Anforderungen präzisiert<br />

sind, müssen Sie eine Idee für einen Algorithmus<br />

entwickeln. Dabei steht die Frage<br />

im Vordergrund, wie das Problem algorithmisch<br />

gelöst werden kann. Es geht noch<br />

nicht darum, wie der Algorithmus konkret<br />

zu implementieren ist und welche Funktionseinheiten<br />

dabei eine Rolle spielen.<br />

Die erste Idee für den Textumbruch sah<br />

bei mir folgendermaßen aus:<br />

❚ Zerlege den Text in Wörter.<br />

❚ Zerlege die Wörter in Silben.<br />

❚ Fasse die Silben neu zu Zeilen zusammen.<br />

Dabei bemerkte ich schnell, dass die<br />

Zerlegung des Textes in „Wörter“ nicht<br />

wirklich präzise beschreibt, was zu tun ist.<br />

Denn zwischen den Wörtern steht Leerraum,<br />

der erhalten bleiben muss. Als Oberbegriff<br />

für Wort und Leerraum habe ich<br />

Zeichenfolge gewählt. Der Text wird also<br />

zunächst in Zeichenfolgen zerlegt. Das<br />

lässt auch Spielraum für mögliche Erweiterungen.<br />

Schließlich können im Text auch<br />

Zahlen als Zeichenfolgen auftreten, die<br />

möglicherweise besonders behandelt werden<br />

müssen. Ein weiteres Beispiel sind<br />

Interpunktionszeichen, auch diese kann<br />

man unter den Überbegriff Zeichenfolgen<br />

stellen.<br />

Nachdem Zeichenfolgen in Silben zerlegt<br />

sind, müssen die Silben so zu Zeilen zusammengefasst<br />

werden, dass die einzelnen<br />

Zeilen höchstens die maximale Länge haben.<br />

Dabei stehen die einzelnen Silben natürlich<br />

nicht für sich. Denn Silben können<br />

nur innerhalb der Zeile einfach so aneinandergereiht<br />

werden. Am Zeilenende muss<br />

ein Trennstrich ergänzt werden, wenn die<br />

letzte Silbe der Zeile zum selben Wort gehört<br />

wie die erste Silbe der Folgezeile. Daher<br />

muss der Zusammenhang zwischen Silben<br />

und Wörtern erhalten bleiben. Offensichtlich<br />

genügt es daher nicht, das Zusammenfassen<br />

zu Zeilen auf Basis eines Stroms<br />

von Silben zu implementieren.<br />

Entwurf<br />

LÖSUNG<br />

Das Konzept der Event-Based Components einzuüben – das war das Ziel dieses dojos. Die konkrete Aufgabe bestand<br />

darin, eine Komponente für den Textumbruch mit Silbentrennung zu entwickeln. Zum Glück hat Stefan Lieser ein eigenes<br />

Test-GUI entwickelt, denn damit konnte er viele Fehler entdecken und beseitigen.<br />

Aus diesenVorüberlegungen entstand mein<br />

Entwurf für eine EBC-Architektur (Event-<br />

Based Components). Abbildung 1 zeigt die<br />

folgenden vier Aktionen:<br />

❚ Zerlegen in Zeichenfolgen,<br />

❚ Zeichenfolgen in Silben trennen,<br />

❚ Zusammenfassen zu Zeilen,<br />

❚ Zusammenfassen zu Text.<br />

Diese vier Aktionen sind zu einer EBC-<br />

Aktivität zusammengefasst, welche die Aktionen<br />

umschließt. Aufgabe der Aktivität ist<br />

es, die Input- und Outputpins der beteiligten<br />

Aktionen zu verbinden. Die Aktivität<br />

selbst verfügt über je einen Input- und Outputpin<br />

und verbirgt die internen Details der<br />

Realisierung. Sollten später Aktionen hin-<br />

www.dotnetpro.de dotnetpro.dojos.2011 51


LÖSUNG<br />

zukommen, können diese Änderungen lokal<br />

innerhalb der Aktivität gehalten werden.<br />

In der Abbildung verwende ich an den<br />

Pfeilen, welche einen Datenstrom von einem<br />

Output- zu einem Inputpin darstellen,<br />

die Sternnotation. Die Bezeichnung<br />

Zeile* bedeutet daher „mehrere Zeilen“. Ob<br />

dies am Ende durch ein Array, eine Liste<br />

oder ein IEnumerable realisiert wird, ist auf<br />

der Ebene des Architekturentwurfs nicht<br />

entscheidend. Wichtig ist, dass sich die<br />

nachfolgende Implementation an die Kardinalität<br />

hält. Wenn also beispielsweise die<br />

Aktion Zusammenfassen zu Zeilen im Entwurf<br />

mehrere Zeilen liefert, darf nicht in<br />

der Implementation ein einzelner string<br />

zurückkommen. Dies ergibt sich aus dem<br />

Prinzip „Implementation spiegelt Entwurf“<br />

[2], welches dafür sorgt, dass die Implementation<br />

besser verständlich ist. Würde<br />

man in der Implementation vom Entwurf<br />

abweichen, wäre für einen Entwickler, der<br />

später in den Code einsteigt, ein Übersetzungsaufwand<br />

erforderlich. Ein Blick in<br />

den Entwurf würde ihm dann nicht viel<br />

helfen, wenn die Implementation immer<br />

wieder davon abweicht.<br />

Um das in den Anforderungen beschriebene<br />

API bereitzustellen, kommt noch eine<br />

Klasse hinzu, in der die Aktivität verwendet<br />

wird. So ist die Realisierung als EBC außen<br />

nicht mehr sichtbar.<br />

Wo beginnen?<br />

Bei vier Aktionen, einer Aktivität und der<br />

API-Klasse stellt sich die Frage, wo man anfangen<br />

soll. Vereinfacht gesagt stehen Topdown<br />

oder Bottom-up-Vorgehensweisen<br />

zur Auswahl. Bei einer Top-down-Vorgehensweise<br />

beginnt man bei der Benutzerschnittstelle<br />

oder in diesem Fall beim API.<br />

Von dort arbeitet man sich „nach unten“<br />

durch. Andersherum beim Bottom-up-<br />

Vorgehen: Hier beginnt man mit den Aktionen<br />

und arbeitet sich langsam nach oben<br />

Listing 1<br />

Aktionen verdrahten.<br />

[Abb. 1] Der Entwurf in EBC-Architektur.<br />

zur Integration vor. Mein Favorit ist die<br />

Top-down-Vorgehensweise. Diese bietet<br />

den Vorteil, dass die Implementation jeweils<br />

aus der Sicht einesVerwenders erfolgt.<br />

Zu jeder Komponente, Klasse oder Methode,<br />

die so entsteht, gibt es dann bereits einen<br />

Verwender. Dieser stellt ganz konkrete<br />

Anforderungen. So wird die Gefahr minimiert,<br />

sich mögliche Anforderungen aus<br />

den Fingern zu saugen. Bei einer Bottomup-Vorgehensweise<br />

ist diese Gefahr nicht<br />

zu unterschätzen. Sie führt häufig dazu,<br />

dass Funktionalität implementiert wird,<br />

von der niemand weiß, ob sie benötigt wird.<br />

Um zu überprüfen, ob der Architekturentwurf<br />

der Problemstellung angemessen<br />

ist und funktioniert, ist es ganz wichtig, als<br />

Erstes einen Durchstich zu realisieren, bei<br />

dem alle entworfenen Funktionseinheiten<br />

beteiligt sind. Dieses sogenannte Tracer<br />

Bullet Feature soll keine echte Funktionalität<br />

erzeugen, sondern nur zeigen, dass die<br />

Integration der Funktionseinheiten funktioniert.<br />

Hier kann daher auch auf automatisierte<br />

Tests verzichtet werden. Im Falle eines<br />

APIs ist in Ermangelung einer anderen<br />

Benutzerschnittstelle lediglich ein Test erforderlich,<br />

der das API bedient.<br />

Das Tracer Bullet Feature sorgt dafür,<br />

dass die Entwurfsskizzen quasi in Code gegossen<br />

werden. Dadurch werden vor allem<br />

die Schnittstellen zwischen den Funktions-<br />

var zerlegenInZeichenfolgen = new ZerlegenInZeichenfolgen();<br />

var zeichenfolgenInSilbenTrennen = new ZeichenfolgenInSilbenTrennen();<br />

var zusammenfassenZuZeilen = new ZusammenfassenZuZeilen();<br />

var zusammenfassenZuText = new ZusammenfassenZuText();<br />

zerlegenInZeichenfolgen.Out_Result += zeichenfolgenInSilbenTrennen.In_Process;<br />

zeichenfolgenInSilbenTrennen.Out_Result += zusammenfassenZuZeilen.In_Process;<br />

zusammenfassenZuZeilen.Out_Result += zusammenfassenZuText.In_Process;<br />

zusammenfassenZuText.Out_Result += text => Out_Result(text);<br />

einheiten viel rigoroser unter die Lupe genommen,<br />

als dies am Whiteboard möglich<br />

ist. So werden Ungereimtheiten frühzeitig<br />

aufgedeckt.<br />

Im konkreten Fall des Textumbruchs habe<br />

ich also zunächst die API-Klasse Textumbruch<br />

sowie die Aktivität TextumbruchAktivität<br />

erstellt. Anschließend habe ich die<br />

einzelnen Aktionen mit ihren Input- und<br />

Outputpins erstellt und innerhalb der Aktivität<br />

verdrahtet. Um die Aktionen nicht einzeln<br />

ausimplementieren zu müssen, habe<br />

ich lediglich die Daten der Inputpins auf die<br />

Outputpins übertragen. Dabei müssen natürlich<br />

nach Bedarf entsprechende Datenobjekte<br />

erzeugt werden, um den Signaturen<br />

von Input- und Outputpins gerecht zu werden.<br />

Nach der Verdrahtung der Aktionen in<br />

der Aktivität konnte ich dann sehen, dass<br />

ein Text, der über den Inputpin in die Aktivität<br />

hineingegeben wird, tatsächlich am<br />

Outputpin wieder herauskommt. Wunderbar!<br />

Commit nicht vergessen!<br />

Zusätzlich kann man bei der Implementation<br />

des Tracer Bullet Features auch<br />

Trace-Ausgaben ergänzen. Dadurch lässt<br />

sich mit Tools wie DebugView [3] verfolgen,<br />

ob der Ablauf der einzelnen Aktionen<br />

korrekt erfolgt. Listing 1 zeigt die Verdrahtung<br />

der Aktionen in der Aktivität.<br />

Zunächst werden von den benötigten<br />

Aktionen Instanzen erstellt. Anschließend<br />

werden Input- und Outputpins gemäß dem<br />

Entwurf verbunden. Die letzte Zeile zeigt die<br />

Verbindung zum Outputpin der Aktivität.<br />

Doch wie erfolgt dieVerbindung zum Inputpin<br />

der Aktivität? Dazu wird im Konstruktor<br />

der Aktivität eine Action erstellt,<br />

die zur Signatur des Inputpins passt:<br />

process = (text, breiteInZeichen) => {<br />

zusammenfassenZuZeilen.<br />

In_SetzeBreite(breiteInZeichen);<br />

zerlegenInZeichenfolgen.In_Process(text);<br />

};<br />

Diese Action ist als Feld der Klasse deklariert<br />

und kann daher im Inputpin aufgerufen<br />

werden:<br />

public void In_Process(string text, int<br />

breiteInZeichen) {<br />

process(text, breiteInZeichen);<br />

}<br />

Dank dieses Kniffs müssen in der Klasse<br />

keine anderen Felder definiert werden, um<br />

auf die Aktionen zugreifen zu können.<br />

Und Action!<br />

Nach der Aktivität ging es an die Implementation<br />

der einzelnen Aktionen. Auch dabei<br />

52 dotnetpro.dojos.2011 www.dotnetpro.de


in ich immer in Durchstichen vorgegangen.<br />

Statt also die Zerlegung in Zeichenfolgen<br />

komplett fertigzustellen und erst dann<br />

mit der Silbentrennung zu beginnen, habe<br />

ich die Zerlegung erst nur ganz simpel realisiert<br />

und dann mit der Silbentrennung begonnen.<br />

Hier gilt es im realen Projekt abzuwägen,<br />

welche Teilfunktionalität dem Kunden<br />

den jeweils größten Nutzen bringt. Das<br />

kann bedeuten, eine Funktion komplett zu<br />

realisieren und andere nur rudimentär. Genauso<br />

gut kann es aber nützlich sein, an allen<br />

Stellen einen Teil der geforderten Leistung<br />

zu erbringen. Im Zweifel gilt: Reden<br />

hilft! Eine Rückfrage beim Kunden oder Product<br />

Owner sollte den Sachverhalt klären.<br />

Doch zurück zu den Aktionen. Das Aufteilen<br />

des Textes in Zeichenfolgen basiert<br />

im Wesentlichen auf der Anwendung von<br />

string.Split. Dadurch bleiben zwar nicht alle<br />

Leerzeichen erhalten, aber dies habe ich<br />

für die erste Version in Kauf genommen.<br />

Der erste Test befasst sich mit einem<br />

Text, der nur aus einem Wort besteht. Dieser<br />

Test diente mir dazu, den Testrahmen<br />

zu erstellen. Man beachte, dass es hier um<br />

den Test einer EBC-Aktion geht, bei der das<br />

Ergebnis über einen Event geliefert wird.<br />

Den Rahmen für den Test zeigt Listing 2.<br />

Die Rückgabe des Ergebnisses erfolgt bei<br />

EBCs über einen Outputpin. Outputpins<br />

werden über Events realisiert. Daher muss<br />

im Test geprüft werden, ob der Event das<br />

richtige Ergebnis liefert. Dazu erstelle ich<br />

im Setup der Testklasse eine Instanz des<br />

Prüflings und binde einen kleinen Lambda-Ausdruck<br />

an den Event. Der Lambda-<br />

Ausdruck kopiert das Argument des Events<br />

in das Feld result der Testklasse. So kann in<br />

den Testmethoden auf das Ergebnis des<br />

Events zugegriffen werden.<br />

Die Klasse Zeichenfolge ist trivial. Sie enthält<br />

lediglich eine Eigenschaft für den<br />

string, siehe Listing 3.<br />

Ferner ist eine Equals-Methode implementiert,<br />

damit Zeichenfolgen im Test verglichen<br />

werden können. Das Erzeugen von<br />

Equals und GetHashCode übernimmt für<br />

mich das ReSharper-Add-in.<br />

Im Prinzip wäre eine Implementation ohne<br />

die Datenklasse Zeichenfolge denkbar.<br />

Schließlich kapselt diese lediglich eine<br />

string-Eigenschaft. Durch die Einführung<br />

dieser Datenklasse ist die Typisierung der<br />

Input- und Outputpins jedoch strenger, da<br />

so nur Input- und Outputpins verbunden<br />

werden können, die eine Zeichenfolge als<br />

Argument erwarten. Das fördert nicht nur<br />

die Verständlichkeit, sondern vereinfacht<br />

auch ein automatisiertesVerdrahten der Ak-<br />

tionen, auch wenn das hier nicht verwendet<br />

wird. Ferner verwendet die Implementation<br />

so auch die Ubiquitous Language, die allgegenwärtige<br />

Sprache des Projektes, was<br />

ebenfalls zur Verständlichkeit beiträgt.<br />

Die Implementation der Textzerlegung<br />

sieht am Ende so aus wie in Listing 4.<br />

Was mich daran stört, ist der Umgang<br />

mit den Zeilenumbrüchen. Diese werden<br />

nämlich hier ebenfalls behandelt, obwohl<br />

die Aufgabe der Klasse das Zerlegen des<br />

Textes in Zeichenfolgen ist. Damit kümmert<br />

sich die Klasse um zwei Dinge und<br />

verstößt so gegen das Single Responsibility<br />

Principle [4]. In der nächsten Iteration würde<br />

ich das ändern und die Behandlung der<br />

Zeilenumbrüche herausziehen.<br />

Als Nächstes kam die Silbentrennung an<br />

die Reihe. Durch den Einsatz von NHunspell<br />

steht die Funktionalität bereits zur<br />

Verfügung. Es geht also lediglich darum,<br />

das NHunspell-API an unsere Bedürfnisse<br />

anzupassen. Die Implementation ist einfach,<br />

bedarf aufgrund der Verwendung von<br />

LINQ aber einer kurzen Erklärung, siehe<br />

Listing 5.<br />

Der Inputpin erhält eine Aufzählung von<br />

Zeichenfolgen. Für jede Zeichenfolge muss<br />

die Silbentrennung aufgerufen werden. Anschließend<br />

muss jeweils eine Instanz vom<br />

Typ GetrennteZeichenfolge erstellt werden.<br />

Das riecht nach einer Schleife, ist aber mit<br />

LINQ viel eleganter realisierbar. Doch zuvor<br />

habe ich mich auf die eigentliche Kernfunktionalität<br />

konzentriert, das Trennen einer<br />

einzelnen Zeichenfolge. Daher habe ich eine<br />

internal-Methode erstellt, welche eine<br />

einzelne Zeichenfolge in Silben trennt. Diese<br />

Methode habe ich isoliert getestet. Damit<br />

die Tests möglichst wenig Rauschen enthalten<br />

und dadurch gut verständlich sind, arbeitet<br />

die Methode mit strings statt mit Zeichenfolge<br />

und GetrennteZeichenfolge. Listing<br />

6 zeigt einen der Tests.<br />

Diese Vorgehensweise bietet den großen<br />

Vorteil, dass die beiden Concerns „Silbentrennung“<br />

und „Iterieren“ sauber getrennt<br />

sind. Das vereinfacht die Tests und schafft<br />

Übersichtlichkeit in der Implementation.<br />

Das Iterieren erledigt dann LINQ. Durch<br />

Einsatz von Select wird über die Aufzählung<br />

der Zeichenfolgen iteriert und jeweils eine<br />

Instanz von GetrennteZeichenfolge erzeugt.<br />

Zusammensetzen<br />

Die Silben müssen nun wieder zu Zeilen zusammengefasst<br />

werden. Dabei muss zum einen<br />

die gewünschte Breite der Zeilen berücksichtigt<br />

werden. Zum anderen müssen<br />

gegebenenfalls Trennstriche am Zeilenende<br />

Listing 2<br />

Ein erster Test.<br />

LÖSUNG<br />

private ZerlegenInZeichenfolgen sut;<br />

private IEnumerable result;<br />

[SetUp]<br />

public void Setup() {<br />

sut = new ZerlegenInZeichenfolgen();<br />

sut.Out_Result += wörter => result =<br />

wörter;<br />

}<br />

[Test]<br />

public void Einzelnes_Wort() {<br />

sut.In_Process("A");<br />

Assert.That(result, Is.EqualTo(new[] {<br />

new Zeichenfolge("A")<br />

}));<br />

}<br />

Listing 3<br />

Klasse für die Zeichenfolge.<br />

public class Zeichenfolge {<br />

public Zeichenfolge(string text) {<br />

Text = text;<br />

}<br />

public string Text { get; private set; }<br />

}<br />

ergänzt werden. Dazu muss der Zusammenhang<br />

von Silben und Wörtern bekannt sein.<br />

Am Ende stellte sich heraus, dass das Zusammenfassen<br />

der Silben zu Zeilen die<br />

schwierigste Funktionseinheit darstellt. Hier<br />

hatte ich mal wieder das Gefühl, dass ich ohne<br />

automatisierte Tests völlig aufgeschmissen<br />

wäre. Während der Implementation<br />

enthielt die Schleife sogar zeitweise ein goto.<br />

Doch die Community konnte mich über<br />

Twitter [5] überzeugen, dass dies keine gute<br />

Idee ist. Und am Ende ging es tatsächlich<br />

auch ganz leicht ohne dieses Konstrukt.<br />

Um aber zu dem hier in Listing 7 gezeigten<br />

Ergebnis zu kommen, waren einige Refaktorisierungen<br />

notwendig.<br />

Ich glaube, dass diese Methode auch ohne<br />

den Abdruck aller verwendeten privaten<br />

Methoden verständlich ist. Die Details können<br />

Sie sich wie gewohnt im Quellcode auf<br />

der Heft-DVD anschauen. Die hier gezeigte<br />

Methode realisiert das Zusammenfassen der<br />

Silben zu Zeilen auf relativ hohem Abstraktionsniveau.<br />

Die Bedingungen für die zahlreichen<br />

if-Statements sind konsequent als<br />

Methoden herausgezogen. Das bietet den<br />

Vorteil, einen sprechenden Namen verwen-<br />

www.dotnetpro.de dotnetpro.dojos.2011 53


LÖSUNG<br />

Listing 4<br />

Texte zerlegen.<br />

public class ZerlegenInZeichenfolgen {<br />

public void In_Process(string text) {<br />

Out_Result(TrenneInZeichenfolgen(text));<br />

}<br />

private static IEnumerable TrenneInZeichenfolgen(string text) {<br />

var textOhneZeilenumbruch = text.Replace(Environment.NewLine, " ");<br />

var zeichenfolgen = textOhneZeilenumbruch.Split(' ');<br />

for (var i = 0; i < zeichenfolgen.Length; i++) {<br />

var zeichenfolge = zeichenfolgen[i];<br />

yield return new Zeichenfolge(zeichenfolge);<br />

if (IstNichtDieLetzteZeichenfolge(i, zeichenfolgen.Length)) {<br />

yield return new Zeichenfolge(" ");<br />

}<br />

}<br />

}<br />

public event Action Out_Result;<br />

private static bool IstNichtDieLetzteZeichenfolge(int i, int anzahlZeichenfolgen) {<br />

return i + 1 < anzahlZeichenfolgen;<br />

}<br />

}<br />

Listing 5<br />

Die Silbentrennung implementieren.<br />

public class ZeichenfolgenInSilbenTrennen {<br />

private readonly Hyphen hyphen;<br />

public ZeichenfolgenInSilbenTrennen() {<br />

hyphen = new Hyphen("hyph_de_DE.dic");<br />

}<br />

public void In_Process(IEnumerable zeichenfolgen) {<br />

Out_Result(zeichenfolgen.Select(<br />

x => new GetrennteZeichenfolge(x.Text) {<br />

Silben = Silben(x.Text)<br />

}));<br />

}<br />

public event Action Out_Result;<br />

internal IEnumerable Silben(string zeichenfolge) {<br />

var result = hyphen.Hyphenate(zeichenfolge);<br />

return (result == null) ? new[]{""} : result.HyphenatedWord.Split('=');<br />

}<br />

}<br />

den zu können. So muss man beim Lesen<br />

nicht interpretieren, was die Bedingung eigentlich<br />

testet, sondern kann die Bedeutung<br />

aus dem Namen ableiten. Bei dieser Implementation<br />

wäre es interessant auszuprobieren,<br />

wie schwierig es ist, die Zeilenbreite<br />

nicht in Zeichen, sondern in Millimetern<br />

zu definieren. Dazu müsste die Schriftart<br />

des Textes herangezogen werden, um die<br />

tatsächliche Breite ermitteln zu können.<br />

Gerade bei proportionalen Schriftarten ist<br />

dies wichtig, da hier jedes Zeichen seine<br />

eigene Breite hat. Ferner kann man nicht<br />

einfach die Zeichenbreiten addieren, da<br />

bestimmte Zeichenkombinationen enger<br />

aneinandergestellt werden als andere.<br />

Die Entscheidung, ob eine Silbe noch in<br />

die Zeile passt oder die nächste Zeile begonnen<br />

wird, steht an genau einer Stelle in der<br />

Methode SilbePasstNochInDieZeile. Hier<br />

müsste man also mit einer entsprechenden<br />

Erweiterung ansetzen. Richtig spannend<br />

wird es bei solchen Erweiterungen ja immer<br />

dann, wenn nicht der ursprüngliche Autor<br />

des Codes diese Erweiterung vornimmt,<br />

sondern jemand, der den Code bislang noch<br />

nicht kennt.<br />

Ein Test-GUI<br />

Bei konsequentem Einsatz von automatisierten<br />

Unit-Tests verliert man schon mal die<br />

Integration aus den Augen. Aber auch diese<br />

muss getestet werden. Also habe ichTests ergänzt,<br />

welche „ganz oben“ auf dem öffentlichen<br />

API aufsetzen. So ist sichergestellt, dass<br />

die Integration von Aktivität und Aktionen<br />

korrekt funktioniert. Bei dieser einfachen<br />

Aufgabenstellung hat mir das Tracer Bullet<br />

Feature schon die Sicherheit gegeben, dass<br />

die Integration der einzelnen Funktionseinheiten<br />

korrekt ist. In komplexeren Szenarien<br />

sind dazu häufig mehrere automatisierte Integrationstests<br />

erforderlich.<br />

Aber selbst Unit-Tests plus Integrationstests<br />

genügen nicht. Man kommt nicht umhin,<br />

auch den Bereich der explorativen Tests<br />

abzudecken. Je leichter es fällt, die Funktionalität<br />

„mal eben auf die Schnelle“ auszuprobieren,<br />

desto größer ist die Wahrscheinlichkeit,<br />

Fehler zu finden. Aus gutem Grund<br />

sind Softwaretester nicht plötzlich überflüssig<br />

geworden, nur weil die Entwickler ihren<br />

Code endlich selber automatisiert testen.<br />

Ich habe also ein Test-GUI erstellt, ganz<br />

in Anlehnung an den Entwurf, der in der<br />

Aufgabenstellung abgedruckt war. Und<br />

schon der erste Versuch mit dem Test-GUI<br />

förderte einen Fehler zutage: Ich habe einfach<br />

mal auf den Umbrechen-Schalter geklickt,<br />

um einen leeren Text zu umbrechen.<br />

Dabei zeigte sich, dass die Silbentrennung<br />

NHunspell bei einem leeren Eingabetext<br />

null als Ergebnis liefert. Sehr unschön,<br />

aber so ist es nun mal. Was tun?<br />

Klar ist: Man muss null abfangen und eine<br />

leere Silbenliste zurückliefern. Aber vorher<br />

sollte das Problem durch einen automatisierten<br />

Test reproduziert werden. Dieser<br />

sollte so nah wie möglich an der für das<br />

Problem verantwortlichen Funktionseinheit<br />

ansetzen. Also bei der Aktion ZeichenfolgenInSilbenTrennen.<br />

So ist sichergestellt,<br />

dass der Test fokussiert und überschaubar<br />

bleibt. Hier zeigt sich übrigens, wie wichtig<br />

es ist, Funktionalität zu kapseln.<br />

Im Entwurf ist eine Schnittstelle für die<br />

Silbentrennung entstanden. Dabei habe<br />

ich keine Rücksicht auf NHunspell genommen<br />

(mir war dieses fragwürdige Verhalten<br />

vorher gar nicht bekannt). Hätte ich<br />

NHunspell direkt verwendet, ohne eine<br />

eigene Klasse drumherum zu legen, wäre<br />

es möglicherweise schwieriger, dieses unschöne<br />

Verhalten an einer Stelle zu beseitigen.<br />

Dies gilt übrigens auch für den Rückgabewert.<br />

NHunspell liefert als Ergebnis<br />

nicht etwa eine Liste der Silben, sondern<br />

einen string, in dem die Silben durch ein<br />

Gleichheitszeichen abgetrennt sind. Für<br />

Feuerwehrauto wird Feu=er=wehr=au=to<br />

geliefert. Dies muss dem Entwurf gemäß<br />

umgesetzt werden, in eine Liste von Silben.<br />

54 dotnetpro.dojos.2011 www.dotnetpro.de


Die zweite Erkenntnis aus den Versuchen<br />

mit dem Test-GUI: Zeilenumbrüche wurden<br />

nicht berücksichtigt. In den Anforderungen<br />

habe ich diese zwar aufgeführt, aber<br />

nicht sofort implementiert. Das war schnell<br />

nachgeholt, allerdings mit den bereits weiter<br />

oben erwähnten Einschränkungen.<br />

Dritte Erkenntnis: Nicht trennbare Wörter<br />

können länger als die maximale Zeilenlänge<br />

sein. Ferner können in trennbaren<br />

Wörtern Silben auftreten, die länger als die<br />

maximale Zeilenlänge sind. Dadurch drehte<br />

sich eine Schleife beim Zusammenfassen<br />

von Silben zu Zeilen im Kreis. Auch<br />

hier habe ich erst zwei Tests ergänzt, um<br />

den Fehler automatisiert reproduzieren zu<br />

können. Erst danach habe ich das Problem<br />

behoben. Die Vorgehensweise ist hierbei<br />

pragmatisch: Zu lange Wörter oder Silben<br />

werden nicht umbrochen.<br />

Das nächste Problem, das ich durch Ausprobieren<br />

mit dem Test-GUI identifiziert<br />

habe, hängt damit zusammen, wie NHunspell<br />

auf Interpunktionszeichen reagiert.<br />

Beim Zerlegen des Textes in Zeichenfolgen<br />

werden Interpunktionszeichen nicht gesondert<br />

betrachtet, sondern einfach an die<br />

Wörter mit angehängt. Dadurch entstanden<br />

aber merkwürdige Trennungen. So wurde<br />

„Welt“ in „Wel-t“ getrennt.<br />

Auch hier konnten automatisierte Tests<br />

das Verhalten von NHunspell reproduzieren.<br />

Hängt nämlich am Wort „Welt“ noch<br />

ein Punkt, also „Welt.“, trennt NHunspell<br />

es zu „Wel-t.“. Ich habe dieses Verhalten lediglich<br />

durch entsprechende Tests dokumentiert,<br />

an der Implementierung jedoch<br />

nichts geändert. Die Berücksichtigung von<br />

Interpunktionszeichen würde eine ganze<br />

Reihe von Änderungen nach sich ziehen,<br />

die den Umfang dieser Übung sprengen<br />

würden.<br />

Erweiterungen<br />

Einige Erweiterungsmöglichkeiten sind mir<br />

durch „Spielen“ mit dem Test-GUI aufgefallen.<br />

Der Umgang mit mehreren aufeinanderfolgenden<br />

Leerzeichen ist in meiner<br />

Implementation stark vereinfacht. Da zum<br />

Trennen desTextes in Zeichenfolgen die Methode<br />

string.Split verwendet wird, bleiben<br />

die Leerzeichen nicht ordnungsgemäß erhalten.<br />

Die Implementation müsste also erweitert<br />

werden, da string.Split doch zu simpel<br />

für die Aufgabe ist. Vermutlich wäre hier<br />

ein endlicher Automat besser geeignet.<br />

Eine weitere Vereinfachung betrifft Absätze.<br />

Zeilenumbrüche werden derzeit<br />

komplett entfernt, bevor mit der Trennung<br />

in Zeichenfolgen begonnen wird. Dadurch<br />

Listing 6<br />

Die Silbentrennung testen.<br />

[Test]<br />

public void Feuerwehrauto_wird_getrennt() {<br />

Assert.That(sut.Silben("Feuerwehrauto"),<br />

Is.EqualTo(new[] {"Feu", "er", "wehr", "au", "to"}));<br />

}<br />

Listing 7<br />

Silben zu Zeilen zusammensetzen.<br />

fallen Absätze natürlich ebenfalls unter<br />

den Tisch. Hier müssten also zwei hintereinander<br />

stehende Zeilenumbrüche anders<br />

behandelt werden. Auch das wäre mit einem<br />

endlichen Automaten realisierbar.<br />

Fazit und Nachtrag<br />

Einerseits bin ich überrascht, dass sich mit<br />

vergleichsweise wenig Aufwand doch eine<br />

recht leistungsfähige Textumbruchkomponente<br />

realisieren lässt. Andererseits<br />

zeigt sich, dass man für ein reales Projekt<br />

weitaus mehr Aufwand in die Analyse der<br />

Anforderungen und den Entwurf stecken<br />

müsste. Das wird beispielsweise beim Umgang<br />

mit Leerraum und Zeilenumbrüchen<br />

deutlich.<br />

Ich bin, nachdem dieser Artikel fertig<br />

war, noch der Frage nachgegangen, ob die<br />

Lösung tatsächlich so evolvierbar ist, dass<br />

eine Umstellung von Breite in Zeichen auf<br />

Breite in Millimetern leicht zu bewerkstelligen<br />

ist. Um ein aussagekräftigeres Ergebnis<br />

zu erreichen, habe ich Ralf Westphal gebe-<br />

LÖSUNG<br />

private IEnumerable ErzeugeZeilen(IEnumerable zeichenfolgen) {<br />

var zeile = "";<br />

foreach (var zeichenfolge in zeichenfolgen) {<br />

foreach (var silbe in zeichenfolge.Silben) {<br />

if (SilbeIstLeerraumAmZeilenanfang(zeile, silbe)) {<br />

continue;<br />

}<br />

if (SilbePasstNochInDieZeile(zeile, zeichenfolge, silbe) || IstLeerraum(zeile)) {<br />

zeile += silbe;<br />

continue;<br />

}<br />

if (TrennstrichErforderlich(silbe, zeichenfolge)) {<br />

zeile += "-";<br />

}<br />

yield return zeile;<br />

zeile = IstLeerraum(silbe) ? "" : silbe;<br />

}<br />

}<br />

if (ZeileIstNichtLeer(zeile)) {<br />

yield return zeile;<br />

}<br />

}<br />

ten, diese Änderung ebenfalls vorzunehmen.<br />

Dazu habe ich ihm lediglich den<br />

Code und eine Entwurfskizze zur Verfügung<br />

gestellt. Mein Ergebnis: Nach 50 Minuten<br />

war die Änderung fertig.<br />

Und erfreulicherweise hat auch Ralf es in<br />

dieser Zeit geschafft. Das lag zum einen daran,<br />

dass der Entwurf 1:1 in der Implementation<br />

zu finden ist. So konnte er anhand<br />

des Entwurfs verorten, wo Änderungen vorzunehmen<br />

sind. Zum anderen hat die konsequente<br />

Einhaltung der EBC-Konventionen<br />

geholfen. Ein schönes Ergebnis. [ml]<br />

[1] NHunspell, http://nhunspell.sourceforge.net<br />

[2] Blauer 5. Grad der Clean Code Developer,<br />

http://clean-code-developer.de/wiki/<br />

CcdBlauerGrad<br />

[3] DebugView for Windows v4.76,<br />

www.dotnetpro.de/SL1012dojoLoesung1<br />

[4] Oranger 2. Grad der Clean Code Developer,<br />

http://clean-code-developer.de/wiki/<br />

CcdOrangerGrad<br />

[5] http://twitter.com/stefanlieser<br />

www.dotnetpro.de dotnetpro.dojos.2011 55


Wer übt, gewinnt<br />

AUFGABE<br />

Algorithmen und Datenstrukturen<br />

Wie viele Blätter hat der Baum?<br />

Baumstrukturen sind in der Informatik allgegenwärtig. Wer selbst Bäume implementiert, lernt dabei<br />

viel über ihre Arbeitsweise. Stefan, kannst du dazu eine Übung stellen?<br />

dnpCode: A1012DojoAufgabe<br />

In jeder dotnetpro finden Sie<br />

eine Übungsaufgabe von<br />

Stefan Lieser, die in maximal<br />

drei Stunden zu lösen sein<br />

sollte. Wer die Zeit investiert,<br />

gewinnt in jedem Fall – wenn<br />

auch keine materiellen Dinge,<br />

so doch Erfahrung und Wissen.<br />

Es gilt:<br />

❚ Falsche Lösungen gibt es<br />

nicht. Es gibt möglicherweise<br />

elegantere, kürzere<br />

oder schnellere Lösungen,<br />

aber keine falschen.<br />

❚ Wichtig ist, dass Sie reflektieren,<br />

was Sie gemacht<br />

haben. Das können Sie,<br />

indem Sie Ihre Lösung mit<br />

der vergleichen, die Sie<br />

eine Ausgabe später in<br />

dotnetpro finden.<br />

Übung macht den Meister.<br />

Also − los geht’s. Aber Sie<br />

wollten doch nicht etwa<br />

sofort Visual Studio starten…<br />

Wenn man den sprichwörtlichen<br />

Wald vor lauter Bäumen nicht<br />

mehr sehen kann, mag es helfen,<br />

einmal über die Implementierung<br />

von Bäumen nachzudenken. Im<br />

.NET Framework gibt es dazu zwar keine generische<br />

Implementation, dennoch werden Bäume<br />

auch dort verwendet, nämlich an so prominenter<br />

Stelle wie LINQ. Genauer gesagt übersetzt der<br />

Compiler Lambda-Ausdrücke in sogenannte Expression<br />

Trees. Auf diese Weise ist es möglich, aus<br />

LINQ-Ausdrücken SQL-Code zu erzeugen. Mit<br />

den WCF-RIA-Services können LINQ-Ausdrücke<br />

sogar übers Netz übertragen werden.<br />

Doch wie implementiert man eine solche Datenstruktur?<br />

Dazu soll hier nur das API vorgegeben<br />

werden. Die Realisierung ist Ihre Aufgabe für<br />

diesen Monat. Ein Baum hat immer genau einen<br />

Wurzelknoten. Dieser und alle anderen Knoten<br />

können beliebig viele untergeordnete Knoten,<br />

sogenannte Kinder, haben.<br />

Das API für Bäume besteht aus zwei Interfaces,<br />

einem für den Baum, genannt ITree, sowie<br />

einem für die Knoten, genannt INode, siehe<br />

Listing 1.<br />

Das Interface für den Baum ist simpel, es enthält<br />

lediglich den Wurzelknoten des Baums.<br />

Beim Knoten sieht das schon anders aus. Jeder<br />

Listing 1<br />

Interfaces für die Baumstruktur.<br />

public interface ITree<br />

{<br />

INode Root { get; }<br />

}<br />

public interface INode<br />

{<br />

T Value { get; }<br />

Node Add(T nodeValue);<br />

IEnumerable Children { get; }<br />

IEnumerable ChildValues { get; }<br />

IEnumerable PreOrderValues();<br />

IEnumerable PostOrderValues();<br />

}<br />

[Abb. 1] Eine einfache Baumstruktur.<br />

Knoten hat einen Wert. Dieser ist vom generischen<br />

Typ T. Der Wert eines Knotens kann über<br />

die Eigenschaft Value gelesen werden. Um einem<br />

Knoten einen Kindknoten hinzuzufügen, rufen<br />

Sie die Add-Methode auf und übergeben den<br />

Wert des neuen Knotens. Für den Wert müssen<br />

Sie intern einen Knoten anlegen und in die Children-Liste<br />

aufnehmen. Mit der Eigenschaft<br />

ChildValues können Sie die Werte aller Kindknoten<br />

eines Knotens abrufen.<br />

Nun geht es an das Traversieren des Baums. Im<br />

Gegensatz zum Traversieren von Listen ist das<br />

Traversieren von Bäumen auf unterschiedliche<br />

Weise möglich. Bei Listen wird ein Element nach<br />

dem anderen geliefert. Bei Bäumen ist aber die<br />

Frage, ob zuerst der Knoten und dann seine Kinder<br />

geliefert werden sollen (Pre-Order) oder umgekehrt<br />

(Post-Order).<br />

Am einfachsten wird das an einem Beispiel<br />

deutlich. Abbildung 1 zeigt einen Baum. Bei der sogenannten<br />

Pre-Order-Traversierung wird der Knoten<br />

vor seinen Kindern geliefert. Für den Baum aus<br />

Abbildung 1 ergibt das folgende Reihenfolge:<br />

1, 2, 5, 6, 3, 7, 8, 4, 9, 10<br />

Bei der Post-Order-Traversierung werden zuerst<br />

die Kinder geliefert, dann der Knoten selbst.<br />

Für den Beispielbaum ergibt sich daher folgendes<br />

Ergebnis:<br />

5, 6, 2, 7, 8, 3, 9, 10, 4, 1<br />

Damit sind die Anforderungen klar. Implementieren<br />

Sie die Datenstruktur und die zugehörigen<br />

Traversierungsalgorithmen, natürlich inklusive<br />

automatisierter Unit-Tests. Happy Learning! [ml]<br />

56 dotnetpro.dojos.2011 www.dotnetpro.de


Algorithmen und Datenstrukturen<br />

So bauen Sie Bäume<br />

Im .NET Framework gibt es keine vordefinierte Datenstruktur für Bäume.Wer seine Daten in einer Baumstruktur<br />

ablegen will, muss sich diese Struktur selbst implementieren. Eine ideale Aufgabe für das dotnetpro.dojo!<br />

A<br />

lgorithmen und Datenstrukturen<br />

stellen auch heute noch eine<br />

wichtige Grundlage der Softwareentwicklung<br />

dar. Zwar sind viele der Datenstrukturen<br />

im Laufe der Zeit in die Frameworks<br />

gewandert, sodass man als Entwickler<br />

heute nur noch selten eine der<br />

klassischen Strukturen wie Listen oder<br />

Stacks selbst implementieren muss. Andererseits<br />

ist es für einen Entwickler wichtig,<br />

eine Vorstellung davon zu erlangen, was<br />

hinter den Kulissen geschieht.<br />

Nicht zuletzt eignen sich Datenstrukturen<br />

sehr gut dafür, die testgetriebene Entwicklung<br />

einzuüben. Und da die Datenstruktur<br />

„Baum“ im .NET Framework nicht<br />

zur Verfügung steht, lohnt es sich tatsächlich,<br />

eine solche Implementation vorzunehmen.<br />

Die Aufgabenstellung hat das API für<br />

Bäume vorgegeben, siehe Listing 1. Sie besteht<br />

aus zwei Interfaces: einem für den<br />

Baum, genannt ITree, und einem für<br />

die Knoten, genannt INode.<br />

Ein Baum besteht aus einem Wurzelknoten,<br />

Root genannt. Dieser hat selbst einen<br />

Wert (Value) sowie Kindknoten (Children).<br />

Dabei bezieht sich der Baum auf das Interface<br />

INode. Der Baum selbst fügt eigentlich<br />

keine Funktionalität hinzu, sondern bezieht<br />

diese aus den Knoten.<br />

Bei der Implementation habe ich mit<br />

einem einzelnen Knoten Node begonnen.<br />

Der erste Test betrifft die Value-Eigen-<br />

Listing 1<br />

Das API für die Bäume.<br />

public interface ITree {<br />

INode Root { get; }<br />

}<br />

public interface INode {<br />

T Value { get; }<br />

Node Add(T nodeValue);<br />

IEnumerable Children {get;}<br />

IEnumerable ChildValues { get; }<br />

IEnumerable PreOrderValues();<br />

IEnumerable PostOrderValues();<br />

}<br />

Listing 2<br />

Ein erster Test.<br />

[Test]<br />

public void Node_liefert_den_Konstruktorwert_als_Value() {<br />

var node = new Node(42);<br />

Assert.That(node.Value, Is.EqualTo(42));<br />

}<br />

Listing 3<br />

Die Children-Eigenschaft testen.<br />

[Test]<br />

public void Node_hat_nach_dem_Instanzieren_keine_Nachkommen() {<br />

var node = new Node("");<br />

Assert.That(node.Children, Is.Empty);<br />

}<br />

schaft für den Wert des Knotens. Da das Interface<br />

lediglich einen Getter erwartet,<br />

müssen Sie sich überlegen, wie ein Knoten<br />

zu seinem Wert kommt. Eine Möglichkeit<br />

wäre, die Eigenschaft zusätzlich mit einem<br />

Setter auszustatten. Allerdings wären die<br />

Knoten damit veränderbar. Solange Sie<br />

dies nicht benötigen, genügt ein privater<br />

Setter, um den Wert einmalig setzen zu<br />

können. Der Wert muss dann im Konstruktor<br />

zugewiesen werden, da private Setter<br />

nur innerhalb der Klasse verwendet werden<br />

können. Solche nicht änderbaren Objekte,<br />

sogenannte Immutable Objects, bieten<br />

den Vorteil, dass man damit den Problemen<br />

des parallelen Zugriffs bei Multithreading<br />

aus dem Weg geht.<br />

Mein erster Test prüft also, ob die Value-<br />

Eigenschaft den Wert liefert, der dem Knoten<br />

im Konstruktor übergeben wurde, siehe<br />

Listing 2. Dies ist nur ein Minischritt,<br />

und es ist fraglich, ob dieser Test von hohem<br />

Wert ist. Andererseits muss für diesen<br />

ersten Test die Klasse Node erzeugt<br />

werden. Es entsteht also das grobe Codegerüst<br />

der Klasse, welches erforderlich ist, um<br />

das Interface zu implementieren. Gerade<br />

Anfängern der testgetriebenen Entwick-<br />

LÖSUNG<br />

lung sei empfohlen, auch solche Minischritte<br />

zu gehen. Das flexible Anpassen<br />

der „Schrittweite“ beim testgetriebenen<br />

Entwickeln hat viel mit Erfahrung zu tun<br />

und muss daher geübt werden.<br />

Der nächste Test betrifft die Children-<br />

Eigenschaft. Nach dem Instanzieren eines<br />

neuen Knotens soll diese Liste leer sein.<br />

Ganz wichtig an dieser Stelle: Vergessen Sie<br />

null in dem Zusammenhang. Es ist keine<br />

gute Idee, hier null zu liefern anstelle einer<br />

leeren Liste, weil dann der Verwender der<br />

Klasse vor jedem Zugriff eine null-Prüfung<br />

vornehmen müsste. Die Tatsache, dass der<br />

Knoten keine Nachfolger hat, wird perfekt<br />

repräsentiert durch eine leere Liste. Diese<br />

kann beispielsweise auch ohne vorherige<br />

Prüfung mit foreach durchlaufen werden.<br />

Wenn die Liste leer ist, wird die Schleife<br />

halt nicht ausgeführt. Eine zusätzliche<br />

null-Prüfung würde den Code nur unnötig<br />

aufblähen. Listing 3 zeigt den Test für die<br />

Children-Eigenschaft.<br />

Die Implementation ist einfach, der Test<br />

treibt also auch hier noch nicht viel voran.<br />

Sie definieren damit jedoch den Initialzustand<br />

eines Knotens. Sich darüber klarzuwerden<br />

ist nicht ganz unwichtig.<br />

www.dotnetpro.de dotnetpro.dojos.2011 57


LÖSUNG<br />

Listing 4<br />

ChildValues testen.<br />

[Test]<br />

public void Node_hat_nach_dem_Instanzieren_keine_Nachkommenwerte() {<br />

var node = new Node(1);<br />

Assert.That(node.ChildValues, Is.Empty);<br />

}<br />

Listing 5<br />

Einen neuen Knoten testen.<br />

[Test]<br />

public void Für_einen_hinzugefügten_Wert_wird_ein_Knoten_angelegt() {<br />

var node = new Node(1);<br />

var child = node.Add(2);<br />

Assert.That(node.Children, Is.EquivalentTo(new[] {child}));<br />

}<br />

Weiter geht es mit der Eigenschaft Child-<br />

Values, siehe Listing 4.<br />

Auch hier ist die Implementation für einen<br />

Knoten ohne Nachkommen trivial.<br />

Immerhin haben Sie jetzt die Initialwerte<br />

aller Eigenschaften definiert und können<br />

sich einer neuen Aufgabe zuwenden.<br />

Die einzige ändernde Operation, die ein<br />

Knoten anbietet, ist das Hinzufügen eines<br />

weiteren Knotens. Dabei habe ich mich in<br />

der API-Definition dafür entschieden, dem<br />

Knoten einen weiteren Wert hinzuzufügen.<br />

Nehmen wir an, die Knoten hätten Zeichenketten<br />

als Werte. Dann gäbe es für das<br />

API die beiden folgenden Möglichkeiten:<br />

❚ Hinzufügen eines Knotens vom Typ<br />

Node.<br />

❚ Hinzufügen einesWertes vom Typ string.<br />

Das Hinzufügen eines Knotens sähe in<br />

der Anwendung wie folgt aus:<br />

node.Add(new Node("a");<br />

Der erforderliche Aufruf des Konstruktors<br />

kann entfallen, wenn das API die Möglichkeit<br />

bietet, direkt einen Wert hinzuzufügen:<br />

node.Add("a");<br />

Die Add-Methode muss folglich einen<br />

neuen Knoten anlegen und mit dem übergebenen<br />

Wert initialisieren, siehe Listing 5.<br />

Als Ergebnis liefert die Add-Methode den<br />

neu eingefügten Knoten als Rückgabewert<br />

zurück. So können Sie nach dem Hinzufügen<br />

beobachten, dass der neue Knoten in<br />

der Liste der Nachkommen vorhanden ist.<br />

Ferner muss der Wert des Knotens in der<br />

Liste der Nachkommenswerte (ChildValues)<br />

auftauchen. Dass der neu erzeugte Knoten<br />

von Add als Ergebnis zurückgeliefert wird,<br />

ist übrigens nicht der Testbarkeit geschuldet.<br />

Beim Aufbauen eines Baumes ist es<br />

handlich, auf die jeweils erzeugten Knoten<br />

zugreifen zu können, weil auf diesen dann<br />

weitere Methoden aufgerufen werden können.<br />

Damit wird das API für den Anwender<br />

komfortabel in der Benutzung.<br />

Übrigens habe ich diesen Test in einer<br />

weiteren Testklasse angelegt. Die ersten<br />

drei Tests befassen sich mit dem Initialzustand<br />

von Knoten, daher habe ich die Testklasse<br />

Node_Instanzieren_Tests genannt.<br />

Listing 7<br />

Listing 6<br />

Einen neuen Knoten<br />

hinzufügen.<br />

public Node Add(T nodeValue) {<br />

var node = new Node(nodeValue);<br />

children.Add(node);<br />

return node;<br />

}<br />

Das Hinzufügen von Knoten mit der Add-<br />

Methode teste ich in der Klasse Node_ Add_<br />

Tests. Die Implementation der Add-Methode<br />

verwendet eine interne Liste aller Nachkommen<br />

des Knotens. Dieser Liste wird bei<br />

Add ein neues Element hinzugefügt, siehe<br />

Listing 6.<br />

Der nächste Test prüft, ob der hinzugefügte<br />

Wert in der Eigenschaft ChildValues<br />

vertreten ist, siehe Listing 7. Mithilfe von<br />

LINQ ist die Implementation der Child-<br />

Values-Eigenschaft simpel, siehe Listing 8.<br />

Die Select-Methode aus dem Namespace<br />

System.Linq ist eine Extension Method auf<br />

IEnumerable. Dadurch steht sie auf allen<br />

Aufzählungen zur Verfügung. Ergebnis<br />

der Select-Methode ist wieder eine Aufzählung.<br />

Der übergebene Lambda-Ausdruck<br />

gibt an, wie die Werte für die Ergebnisaufzählung<br />

gebildet werden sollen. Im vorliegenden<br />

Fall sollen aus der Aufzählung von<br />

Knoten alle Werte extrahiert werden. Folglich<br />

gibt der Lambda-Ausdruck an, dass<br />

node.Value geliefert werden soll.<br />

Wenn die Knoten implementiert sind,<br />

können Sie sich dem Baum zuwenden. Da<br />

Das erfolgreiche Hinzufügen eines Knotens testen.<br />

[Test]<br />

public void Der_hinzugefügte_Wert_wird_in_die_ChildValues_aufgenommen() {<br />

var node = new Node('x');<br />

node.Add('y');<br />

Assert.That(node.ChildValues, Is.EquivalentTo(new[]{'y'}));<br />

}<br />

Listing 8<br />

LINQ nutzen.<br />

public IEnumerable ChildValues {<br />

get { return Children.Select(node => node.Value); }<br />

}<br />

58 dotnetpro.dojos.2011 www.dotnetpro.de


Listing 9<br />

Einen neu angelegten Baum prüfen.<br />

[Test]<br />

public void Tree_mit_einem_Wurzelknoten_initialisieren() {<br />

var tree = new Tree("Wurzel");<br />

Assert.That(tree.Root.Value, Is.EqualTo("Wurzel"));<br />

}<br />

dieser nur die Eigenschaft des Wurzelknotens<br />

Root hat, sollte die Implementation<br />

leicht von der Hand gehen. Ähnlich wie<br />

beim Wert eines Knotens hat auch die<br />

Root-Eigenschaft nur einen Getter. Auch<br />

hier habe ich mich entschieden, den Konstruktor<br />

des Baumes für die Initialisierung<br />

zu verwenden. Da beim Knoten im Konstruktor<br />

der Wert des Knotens übergeben<br />

wird, ist es konsequent, beim Baum ebenso<br />

zu verfahren. Der Konstruktor des Baumes<br />

legt also den Wurzelknoten an und initialisiert<br />

diesen mit dem übergebenen Wert.<br />

Listing 9 zeigt den zugehörigen Test. Auch<br />

hier bietet es sich an, den Test in eine eigene<br />

Testklasse zu schreiben. Schließlich geht<br />

es nicht um Knoten, sondern um Bäume.<br />

Ich habe die Testklasse Tree_Tests genannt.<br />

Traversieren<br />

Nun geht es an das Traversieren der Bäume.<br />

Dazu sind im API die Methoden PreOr-<br />

Listing 10<br />

Pre-Order-Traversierung testen.<br />

[Test]<br />

public void Ein_Knoten_mit_Nachkommen() {<br />

var node = new Node(1);<br />

node.Add(2);<br />

node.Add(3);<br />

Assert.That(node.PreOrderValues(),<br />

Is.EqualTo(new[]{1, 2, 3}));<br />

}<br />

Listing 11<br />

Pre-Order-Traversierung<br />

durchführen.<br />

public IEnumerable PreOrderValues(){<br />

yield return Value;<br />

foreach (var child in Children) {<br />

yield return child.Value;<br />

}<br />

}<br />

derValues und PostOrderValues zu implementieren.<br />

Die beiden Methoden müssen<br />

jeweils den Baum durchlaufen und alle<br />

Knotenwerte in Form einer Aufzählung<br />

IEnumerable liefern. Auch hier stand<br />

die Entscheidung an, ob die Knoten oder<br />

deren jeweilige Werte geliefert werden sollen.<br />

Alternativ wäre das Ergebnis also vom<br />

Typ IEnumerable. Sollte sich<br />

später zeigen, dass eine solche Aufzählung<br />

benötigt wird, können die zugehörigen<br />

Methoden leicht ergänzt werden.<br />

Zunächst steht wieder die Frage an, wie<br />

der erste Test aussehen soll. Einen „leeren“<br />

Baum zu traversieren ist nicht wirklich<br />

spannend. Vor allem würde durch diesen<br />

Test die Implementation nicht wirklich vorangetrieben.<br />

Schließlich besteht ein neu<br />

initialisierter Baum lediglich aus einem<br />

Knoten, der keine Nachkommen hat. Am<br />

anderen Ende stehen Tests, für die gleich<br />

die komplette Traversierung implementiert<br />

werden muss. Ein Test „dazwischen“<br />

wäre gut: mehr als nur ein einzelner Knoten,<br />

aber weniger als gleich ein ganzer<br />

Baum. Ein einzelner Knoten mit Nachkommen<br />

dürfte das Kriterium erfüllen. Damit<br />

ergeben sich folgende Testszenarien:<br />

❚ Traversieren eines Knotens, der keine<br />

Nachkommen hat;<br />

❚ Traversieren eines Knotens, der Nachkommen<br />

hat;<br />

❚ Traversieren eines Knotens, dessen Nachkommen<br />

ebenfalls Nachkommen haben.<br />

Ob man nun mit dem ersten oder dem<br />

zweiten Szenario beginnt, ist Geschmacksache.<br />

Das Traversieren eines einzelnen<br />

Knotens ohne Nachkommen ist eigentlich<br />

so trivial, dass man es nicht gesondert testen<br />

muss. Spürt man aber Unsicherheit bei<br />

der Implementation, hilft es möglicherweise,<br />

in solch kleinen Minischritten voranzuschreiten.<br />

Wichtig ist jedoch festzuhalten,<br />

dass man sich vor dem ersten Test Gedanken<br />

über die Testdaten machen muss. Dabei<br />

hilft es, Beispiele zu sammeln und diese<br />

anschließend in Äquivalenzklassen zu-<br />

LÖSUNG<br />

sammenzufassen. So liegt beispielsweise<br />

das Traversieren eines Knotens mit drei<br />

Nachkommen in der gleichen Äquivalenzklasse<br />

wie das Traversieren von fünf Nachkommen.<br />

Haben die Nachkommen jedoch<br />

selbst wieder Nachkommen, so ergibt sich<br />

eine andere Äquivalenzklasse, denn hier<br />

muss plötzlich rekursiv vorgegangen werden.<br />

Würde man gleich mit einem ersten<br />

Test beginnen, ohne vorher über die Testdaten<br />

und eine sinnvolle Reihenfolge der<br />

Implementation nachzudenken, wäre der<br />

anstehende Implementationsaufwand für<br />

den nächsten Schritt möglicherweise zu<br />

groß. Nachdenken hilft!<br />

Die Tests zur Traversierung habe ich wieder<br />

in eine eigene Testklasse abgelegt. Sie<br />

heißt Node_Pre_Order_Traversieren_Tests.<br />

Für die Implementation dieses Tests von<br />

Listing 10 ist es lediglich notwendig, erst<br />

den eigenen Knotenwert und anschließend<br />

die Werte der Nachkommen zu liefern.<br />

Unter tatkräftiger Mithilfe von yield<br />

return sieht die Implementation dazu wie<br />

in Listing 11 aus.<br />

Ergänzt man nun einen Test für das erste<br />

Szenario, bei dem ein Knoten keine<br />

Nachkommen hat, wird man feststellen,<br />

dass dieser sofort erfolgreich verläuft, ohne<br />

dass an der Implementation etwas geändert<br />

werden muss.<br />

Doch nun geht es ans Eingemachte. Der<br />

letzte Test befasst sich mit dem Szenario<br />

eines Knotens, dessen Nachkommen ebenfalls<br />

Nachkommen haben, siehe Listing 12.<br />

Das riecht doch sehr nach Rekursion!<br />

Die Implementation könnte folgendermaßen<br />

aussehen: Gib den eigenen Knotenwert<br />

aus, und rufe anschließend die Traversierung<br />

für jeden Nachkommen auf. Dazu<br />

müssen Sie allerdings die bisherige Implementation<br />

zunächst in eine Methode auslagern,<br />

die einen Knoten als Parameter erhält.<br />

Die Methode ist dann dafür verantwortlich,<br />

Listing 12<br />

Einen Baum testen.<br />

[Test]<br />

public void Ein_Knoten_deren_Nachkommen_auch_Nachkommen_haben()<br />

{<br />

var node1 = new Node(1);<br />

var node2 = node1.Add(2);<br />

var node3 = node2.Add(3);<br />

var node4 = node2.Add(4);<br />

Assert.That(node1.PreOrderValues(),<br />

Is.EqualTo(new[] { 1, 2, 3, 4 }));<br />

www.dotnetpro.de dotnetpro.dojos.2011 59<br />

}


LÖSUNG<br />

Listing 13<br />

PreOrderValues refaktorisieren.<br />

public IEnumerable PreOrderValues() {<br />

return TraversePreOrder(this);<br />

}<br />

private static IEnumerable<br />

TraversePreOrder(INode node) {<br />

yield return node.Value;<br />

foreach (var child in node.Children) {<br />

yield return child.Value;<br />

}<br />

}<br />

einen einzelnen Knoten zu traversieren und<br />

kann sich dabei rekursiv aufrufen. Der erste<br />

Schritt ist also eine Refaktorisierung, bei der<br />

eine Methode mit einem Knoten als Parameter<br />

eingeführt wird, siehe Listing 13.<br />

Nach dieser Refaktorisierung können Sie<br />

die Rekursion in der Methode TraversePre-<br />

Order einführen, siehe Listing 14.<br />

Durch Verwendung von yield return<br />

kann hier das Ergebnis des rekursiven Aufrufs<br />

nicht direkt als Resultat zurückgegeben<br />

werden. Schließlich ist das Ergebnis<br />

vom Typ IEnumerable. yield return erwartet<br />

aber einzelne Elemente vom Typ T.<br />

Daher muss das Ergebnis in einer Schleife<br />

durchlaufen werden und Element für Element<br />

an yield return übergeben werden.<br />

Und jetzt andersrum<br />

Um die Post-Order-Traversierung zu implementieren,<br />

ist nur eine kleine Änderung<br />

notwendig. Sie müssen lediglich die Reihenfolge<br />

der Bearbeitung umstellen. Statt<br />

zuerst den Knotenwert auszugeben und<br />

dann die Nachkommen zu bearbeiten,<br />

werden erst die Nachkommen bearbeitet,<br />

und danach wird der Knotenwert ausgegeben,<br />

siehe Listing 15.<br />

Vergessen Sie bei diesem Copy-Paste-<br />

Vorgang nicht, den rekursiven Aufruf anzupassen.<br />

Hier muss TraversePostOrder rekursiv<br />

aufgerufen werden. Bei der Kontrolle<br />

kann mal wieder JetBrains ReSharper behilflich<br />

sein. In Abbildung 1 sehen Sie einen<br />

Ausschnitt aus der Methode. ReSharper erkennt<br />

den rekursiven Aufruf und markiert<br />

diesen am linken Rand durch den kreisför-<br />

Listing 14<br />

Rekursion einführen.<br />

private static IEnumerable TraversePreOrder(INode rootNode) {<br />

yield return rootNode.Value;<br />

foreach (var child in rootNode.Children) {<br />

foreach (var childValue in TraversePreOrder(child)) {<br />

yield return childValue;<br />

}<br />

}<br />

}<br />

Listing 15<br />

Post-Order-Traversierung implementieren.<br />

private static IEnumerable TraversePostOrder(INode rootNode) {<br />

foreach (var child in rootNode.Children) {<br />

foreach (var childValue in TraversePostOrder(child)) {<br />

yield return childValue;<br />

}<br />

}<br />

yield return rootNode.Value;<br />

}<br />

migen Pfeil. Die Tests für die Post-Order-<br />

Traversierung befinden sich in der Klasse<br />

Node_Post_Order_Traversieren_Tests.<br />

Zuletzt habe ich die Beispiele aus der Aufgabenstellung<br />

noch in einer Testklasse mit<br />

dem Namen Integrationstests überprüft.<br />

Alternativ hätte ich sie auch Akzeptanztests<br />

nennen können, da es sich um die Beispiele<br />

handelt, die quasi zwischen Kunde und<br />

Auftragnehmer besprochen wurden. Wenn<br />

man als Entwickler mit dem Kunden Beispielfälle<br />

durchgeht mit dem Ziel, die Anforderungen<br />

zu verstehen, dann sollte man<br />

diese Beispiele als Akzeptanztests festhalten.<br />

So ist sichergestellt, dass man nach der<br />

Implementation das Gespräch mit dem<br />

Kunden leicht wieder aufnehmen kann.<br />

Zeigt man ihm dabei anhand der automatisierten<br />

Tests, dass die besprochenen Beispiele<br />

erfolgreich implementiert sind, so<br />

schafft dies eine gute Vertrauensbasis.<br />

Fazit<br />

Datenstrukturen machen Spaß! Das Schöne<br />

an der Implementation von Datenstrukturen<br />

ist, dass sich diese meist gut automa-<br />

[Abb. 1] Rekursion mit ReSharper<br />

visualisieren.<br />

tisiert testen lassen. Dadurch kann man<br />

sich bei solchen Übungen darauf konzentrieren,<br />

eine geeignete Reihenfolge für die<br />

Tests zu finden. Voraussetzung ist, dass<br />

man vor dem ersten Test Testfälle sammelt.<br />

Beim Zusammenstellen der Testfälle sollte<br />

man möglichst darauf achten, ob diese in<br />

dieselbe Äquivalenzklasse fallen. Schließlich<br />

genügt es, jeweils einen Repräsentanten<br />

der Äquivalenzklasse für einen Test herauszugreifen.<br />

Bezüglich der Addition sind<br />

etwa die beiden zu addierenden Zahlen 2<br />

und 3 sowie 4 und 5 in derselben Äquivalenzklasse.<br />

Es genügt daher, für eines der<br />

beiden Zahlenpaare die Addition zu testen.<br />

Ein zusätzlicher Test mit einem weiteren<br />

Repräsentanten derselben Äquivalenzklasse<br />

würde keinen weiteren Erkenntnisgewinn<br />

bringen.<br />

Beim Traversieren genügt es, einen Knoten<br />

zu testen, dessen Nachkommen ebenfalls<br />

einen Nachkommen haben. Es ist<br />

nicht nötig, einen Baum mit vielen Ebenen<br />

zu testen. Durch die rekursive Implementation<br />

ist dies auch ganz offensichtlich. In<br />

anderen Fällen mag es nicht so offensichtlich<br />

sein, wie die Äquivalenzklassen der<br />

Testdaten aussehen. Dann muss man möglicherweise<br />

länger darüber nachdenken<br />

und mehr Testdaten sammeln. Einfach<br />

drauflos mit den Tests zu beginnen ist aber<br />

so oder so wenig hilfreich, da man dann<br />

Gefahr läuft, Testfälle zu übersehen. [ml]<br />

60 dotnetpro.dojos.2011 www.dotnetpro.de


.NET Framework Grundlagen<br />

Wie funktioniert LINQ?<br />

Manche Grundlagen versteht man besser, wenn man sie einmal selbst implementiert hat.<br />

Stefan, kannst du dazu eine Übung stellen?<br />

LINQ ist nun schon seit einiger Zeit Bestandteil<br />

des .NET Frameworks sowie<br />

der C#- und VB.NET-Compiler [1]–[4].<br />

Dennoch geht dazu bei Entwicklern<br />

noch oft genug einiges durcheinander. Angefangen<br />

bei der Frage, wie LINQ ausgesprochen wird,<br />

bis zur Verwechslung von LINQ mit SQL oder einem<br />

Object Relational Mapper. Da lohnt es sich<br />

doch mal aufzuräumen. Beginnen wir mit der<br />

Aussprache: LINQ wird gesprochen wie „Link“.<br />

So einfach ist das. Und obwohl LINQ in Verbindung<br />

mit Object Relational Mappern verwendet<br />

wird, ist es nicht selbst ein Mapper. Mit LINQ to<br />

Objects arbeitet LINQ auf allem, was aufzählbar<br />

ist, sprich IEnumerable implementiert.<br />

LINQ besteht aus zwei Teilen:<br />

❚ der sogenannten Query Comprehension Syntax,<br />

die so sehr an SQL erinnert;<br />

❚ einer Reihe von Extension Methods im .NET<br />

Framework aus dem Namespace System.Linq.<br />

Die Query Comprehension Syntax sorgt dafür,<br />

dass Sie im Quellcode Abfragen schreiben können<br />

wie beispielsweise diese:<br />

var query = from kunde in kunden where<br />

kunde.Ort == "Köln" select kunde;<br />

Kurz und knackig und für die meisten Entwickler<br />

gut zu lesen. Aber schon nachdem der<br />

Compiler sein Werk verrichtet hat, ist von der<br />

Query Comprehension nichts mehr übrig: Der<br />

Compiler übersetzt diese Query nämlich in<br />

äquivalente Aufrufe der Extension Methods aus<br />

dem Namespace System.Linq. Das sieht dann etwa<br />

so aus:<br />

var query = kunden<br />

.Where(kunde => kunde.Ort == "Köln")<br />

.Select(kunde => kunde);<br />

Diese Compilermagie ist dem C#- sowie dem<br />

VB.NET-Compiler spendiert worden. Die CLR<br />

hat also von LINQ keine Ahnung und musste dazu<br />

nicht verändert werden. Die eigentliche Funktionalität<br />

von LINQ steckt folglich in den Extension<br />

Methods. Diese haben nämlich die Aufgabe,<br />

den jeweiligen Teil der Query auszuführen. So<br />

wird die Where-Klausel einer Query in einen Auf-<br />

ruf der Where-Methode übersetzt. Gleiches geschieht<br />

für Select, Order, Group by etc. Um dabei<br />

möglichst flexibel zu bleiben, ist LINQ auf dem<br />

Interface IEnumerable definiert.<br />

Die Signatur der Where-Methode sieht wie<br />

folgt aus:<br />

static IEnumerable Where(this<br />

IEnumerable values, Predicate<br />

predicate);<br />

Where arbeitet also auf einer Aufzählung und<br />

liefert eine solche zurück. Damit stellt die Where-<br />

Methode eine Selektion oder Filterung dar. Für<br />

jedes Element der Aufzählung wird nämlich mithilfe<br />

des Prädikats geprüft, ob die Bedingung für<br />

das Element zutrifft. Und ausschließlich Elemente,<br />

für die das Prädikat true zurückgibt, landen im<br />

Ergebnis.<br />

Um sich mit der Funktionsweise von LINQ<br />

auseinanderzusetzen, lautet die Aufgabe dieses<br />

Mal daher: Implementieren Sie LINQ Extension<br />

Methods. Beginnen Sie mit Where und Select,<br />

beide sind recht einfach. Etwas kniffliger wird es<br />

beim Gruppieren. Schauen Sie sich dazu die Signatur<br />

der GroupBy-Methode im Framework an,<br />

und überlegen Sie, wie Sie eine Gruppierung implementieren<br />

können. Anschließend gehen Sie<br />

testgetrieben vor.<br />

Weitere interessante Herausforderungen finden<br />

Sie in den Methoden Distinct, Union, Intersect<br />

und Except. Oder versuchen Sie sich an<br />

Count, Min, Max und Average. Langeweile dürfte<br />

so schnell nicht aufkommen. Viel Spaß! [ml]<br />

[1] Mirko Matytschak, Was steckt hinter LINQ?<br />

Language Integrated Queries: Neue Sprachmerkmale<br />

für C# und VB. dotnetpro 2/2006, Seite 97ff.<br />

www.dotnetpro.de/A0602Linq<br />

[2] Ralf Westphal, dotnetpro.tv: LINQ – Language<br />

Integrated Query, dotnetpro 11/2006, Seite 46,<br />

www.dotnetpro.de/A0611dotnetpro.tv<br />

[3] Patrick A. Lorenz, Kochen mit Patrick, zum Thema<br />

LINQ, dotnetpro 8/2008, Seite 116ff.,<br />

www.dotnetpro.de/A0808Kochstudio<br />

[4] Christian Liensberger, LINQ to Foo, Ein LINQ-Provider<br />

für den eigenen Datenspeicher, dotnetpro 8/2008,<br />

Seite 72ff., www.dotnetpro.de/A0808LINQ2X<br />

AUFGABE<br />

dnpCode: A1101DojoAufgabe<br />

In jeder dotnetpro finden Sie<br />

eine Übungsaufgabe von<br />

Stefan Lieser, die in maximal<br />

drei Stunden zu lösen sein<br />

sollte. Wer die Zeit investiert,<br />

gewinnt in jedem Fall – wenn<br />

auch keine materiellen Dinge,<br />

so doch Erfahrung und Wissen.<br />

Es gilt:<br />

❚ Falsche Lösungen gibt es<br />

nicht. Es gibt möglicherweise<br />

elegantere, kürzere<br />

oder schnellere Lösungen,<br />

aber keine falschen.<br />

❚ Wichtig ist, dass Sie reflektieren,<br />

was Sie gemacht<br />

haben. Das können Sie,<br />

indem Sie Ihre Lösung mit<br />

der vergleichen, die Sie<br />

eine Ausgabe später in<br />

dotnetpro finden.<br />

Übung macht den Meister.<br />

Also − los geht’s. Aber Sie<br />

wollten doch nicht etwa<br />

sofort Visual Studio starten…<br />

www.dotnetpro.de dotnetpro.dojos.2011 61<br />

Wer übt, gewinnt


LÖSUNG<br />

LINQ im Eigenbau<br />

So geLINQt es<br />

Grundlagen muss man gut verstanden haben. Wer sie besonders gut verstehen will, sollte sie nachbauen. Bei dem<br />

Versuch, LINQ selbst zu implementieren, hat auch Stefan wieder etwas dazugelernt.<br />

Ob man eine Technologie wirklich<br />

versteht und beherrscht, merkt<br />

man, sobald man versucht, sie<br />

selbst zu implementieren. Im Fall von LINQ<br />

liegt der Schwerpunkt auf der reinen Funktionalität.<br />

Unterschätzen Sie nicht, wie viel<br />

Sie lernen können, wenn Sie vorhandene<br />

Funktionalität nachbauen. Wissen Sie beispielsweise,<br />

wie generische Methoden definiert<br />

werden?<br />

Die Reise durch LINQ soll bei der Where-<br />

Methode starten. Der erste Schritt ist die<br />

Signatur. Where ist eine Extension Method<br />

auf dem Typ IEnumerable. Die Anforderungen<br />

an eine Extension Method sind<br />

überschaubar:<br />

❚ Die Methode muss in einer statischen<br />

Klasse deklariert sein. Dadurch ist sie<br />

selbst ebenfalls statisch.<br />

❚ Der erste Parameter der Methode muss<br />

zusätzlich mit dem Schlüsselwort this<br />

gekennzeichnet werden.<br />

Für die Where-Methode kommt hinzu,<br />

dass sie generisch sein muss. Das bedeutet,<br />

dass der Typ der Aufzählung nicht fix ist,<br />

sondern als generischer Typparameter angegeben<br />

werden kann. Lässt man die Be-<br />

Listing 1<br />

Seid ihr alle da?<br />

[Test]<br />

public void Prädikat_liefert_immer_true() {<br />

var values = new[] {1, 2, 3}.Where(x => true);<br />

Assert.That(values, Is.EqualTo(new[] {1, 2, 3}));<br />

}<br />

Listing 2<br />

Daten selektieren.<br />

dingung der Where-Methode fürs Erste<br />

einmal weg, ergibt sich folgende Signatur:<br />

IEnumerable Where(this<br />

IEnumerable values);<br />

[Test]<br />

public void Prädikat_liefert_nur_für_gerade_Werte_true() {<br />

var values = new[] {1, 2, 3, 4}.Where(x => x % 2 == 0);<br />

Assert.That(values, Is.EqualTo(new[] {2, 4}));<br />

}<br />

Die Methode arbeitet also auf einer Aufzählung<br />

vom Typ T und liefert eine ebensolche<br />

zurück. Der generische Typparameter<br />

T muss syntaktisch beim Methodennamen<br />

deklariert werden.<br />

Doch es fehlt noch das Prädikat. In der<br />

Logik liefert ein Prädikat für ein Element<br />

einen booleschen Wert. Den erforderlichen<br />

Typ gibt es natürlich im .NET Framework,<br />

aber das gilt ja auch für die Where-Methode.<br />

Daher hier die Definition von Predicate:<br />

public delegate bool Predicate(T t);<br />

Die delegate-Deklaration definiert den<br />

Typ einer Methode. Der Name dieses Methodentyps<br />

lautet Predicate. Methoden<br />

dieses Typs sind generisch, der Typparameter<br />

T ist daher beim Methodentyp definiert.<br />

Des Weiteren definiert diese delegate-<br />

Deklaration, dass der Rückgabewert der<br />

Methode vom Typ bool ist und dass der<br />

Methode ein Parameter vom generischen<br />

Typ T übergeben werden muss.<br />

Damit haben Sie alle Zutaten für die Signatur<br />

der Where-Methode zusammen:<br />

public static IEnumerable<br />

Where(this IEnumerable values,<br />

Predicate predicate);<br />

Mein erster Test für die Implementation<br />

prüft, ob alle Elemente geliefert werden,<br />

wenn das Prädikat immer true liefert, siehe<br />

Listing 1. Die zugehörige Implementation<br />

ist leicht: Die Eingabe wird einfach zurückgeliefert<br />

und das Prädikat ignoriert. Der<br />

nächste Test erfordert dann bereits das Iterieren<br />

und elementweise Auswerten des<br />

Prädikats. Der in Listing 2 gezeigte Test filtert<br />

die Aufzählung nach geraden Werten.<br />

Die Implementation ist dank des Operators<br />

yield return sehr überschaubar, siehe<br />

Listing 3.<br />

Listing 3<br />

yield return nutzen.<br />

public static IEnumerable Where(this<br />

IEnumerable values, Predicate<br />

predicate) {<br />

foreach (var value in values) {<br />

if (predicate(value)) {<br />

yield return value;<br />

}<br />

}<br />

}<br />

Wer yield return bislang nicht kannte,<br />

wird die Werte, für die das Prädikat true liefert,<br />

vermutlich in einer List gesammelt<br />

haben. Doch Vorsicht, neben dem eleganteren,<br />

weil kürzeren Äußeren unterscheiden<br />

sich die beiden Lösungen deutlich in<br />

der Semantik. Verwendet man eine Liste, in<br />

der das Ergebnis zusammengestellt wird,<br />

erfolgt das Zusammenstellen komplett innerhalb<br />

der Where-Methode. Das bedeutet<br />

vor allem, dass alle Elemente gleichzeitig<br />

im Speicher Platz finden müssen. Bei kleinen<br />

Datenmengen ist das sicher kein Problem<br />

– aber überlegen Sie, was passiert,<br />

wenn Sie auf diesem Weg eine etwas größe-<br />

62 dotnetpro.dojos.2011 www.dotnetpro.de


Listing 4<br />

Berechnen mit Select.<br />

[Test]<br />

public void String_Länge_wird_ermittelt() {<br />

var values = new[] {"abc", "a",<br />

"ab"}.Select(s => s.Length);<br />

Assert.That(values, Is.EqualTo(new[]{3,<br />

1, 2}));<br />

}<br />

re Datei einlesen und dann filtern. Bei Verwendung<br />

von yield return sorgt der Compiler<br />

für etwas Magie. Denn er erzeugt einen<br />

endlichen Automaten für das Zusammenstellen<br />

der Aufzählung. Dieser sorgt dafür,<br />

dass die Methode immer nur dann aufgerufen<br />

wird, wenn wieder ein Element benötigt<br />

wird. Es muss quasi erst jemand an<br />

der Aufzählung „ziehen“, damit ein Element<br />

durch das Prädikat überprüft wird.<br />

Die Auswertung des Prädikats erfolgt somit<br />

Element für Element statt für die gesamte<br />

Eingabe auf einmal. Somit können Sie mit<br />

yield return potenziell unendlich große<br />

Datenmengen bearbeiten.<br />

Select<br />

Danach steht die Select-Methode an. Sie<br />

dient dazu, den Elementtyp der Aufzählung<br />

zu transformieren. Enthält die ursprüngliche<br />

Aufzählung beispielsweise Adressen,<br />

können Sie mit Select eine einzelne Eigenschaft<br />

selektieren. Dabei können natürlich<br />

auch Berechnungen erfolgen, wie der Test<br />

in Listing 4 zeigt.<br />

Der Test liefert zu jedem Eingangsstring<br />

dessen Länge zurück. Dazu müssen Sie die<br />

Aufzählung durchlaufen und die Select-<br />

Funktion auf jedes Element anwenden. Mit<br />

yield return ist das ebenfalls keine Hexerei,<br />

siehe Listing 5. Für die Select-Methode benötigen<br />

Sie wiederum eine delegate-Deklaration.<br />

Diesmal definieren Sie einen Methodentyp<br />

mit einem Eingangsparameter<br />

und einem Rückgabewert. Beide sind von<br />

generischem Typ. Es handelt sich damit<br />

um eine Funktion, die ein Element vom<br />

Eingabetyp TInput in den Ausgabetyp<br />

TOutput transformiert. In der Select-Methode<br />

wird diese Funktion innerhalb einer<br />

Schleife auf jedes Element der Aufzählung<br />

angewandt. Das Ergebnis wird mit yield return<br />

an den Aufrufer geliefert.<br />

GroupBy<br />

Kommen wir nun zu den etwas kniffligeren<br />

Methoden. Die GroupBy-Methode grup-<br />

Listing 5<br />

Select anwenden.<br />

public delegate TOutput Func(TInput input);<br />

piert die Elemente einer Aufzählung nach<br />

einem Schlüsselwert und liefert eine neue<br />

Aufzählung zurück. Die Ergebnisaufzählung<br />

enthält für jeden Schlüsselwert der<br />

Eingabe ein Element. Ein Beispiel: Sie möchten<br />

die Zahlen von 1 bis 10 danach gruppieren,<br />

ob sie gerade oder ungerade sind.<br />

Das Ergebnis von GroupBy(x => x % 2 == 0)<br />

würde dann folgendermaßen aussehen:<br />

new[]{<br />

new[] { 1, 3, 5, 7, 9 },<br />

new[] { 2, 4, 6, 8, 10}<br />

}<br />

Das Ergebnis ist also eine Aufzählung,<br />

die wiederum zwei Aufzählungen enthält.<br />

Der Elementtyp dieser Aufzählung lautet<br />

LÖSUNG<br />

public static IEnumerable Select(this IEnumerable<br />

values, Func selector) {<br />

foreach (var value in values) {<br />

yield return selector(value);<br />

}<br />

}<br />

Listing 6<br />

IGrouping implementiert IEnumerable.<br />

public interface IGrouping : IEnumerable {<br />

TKey Key { get; }<br />

}<br />

Listing 7<br />

Gruppierung ermöglichen.<br />

public class Grouping : IGrouping {<br />

private readonly TKey key;<br />

private readonly IEnumerable values;<br />

public Grouping(TKey key, IEnumerable values) {<br />

this.key = key;<br />

this.values = values;<br />

}<br />

public IEnumerator GetEnumerator() {<br />

return values.GetEnumerator();<br />

}<br />

IEnumerator IEnumerable.GetEnumerator() {<br />

return GetEnumerator();<br />

}<br />

public TKey Key {<br />

get { return key; }<br />

}<br />

}<br />

IGrouping. Der Trick an der Stelle<br />

ist: IGrouping implementiert<br />

IEnumerable. Dadurch<br />

sind die einzelnen Elemente der<br />

Aufzählung ebenfalls aufzählbar. Aber<br />

IGrouping hat noch eine weitere Eigenschaft,<br />

wie das Interface in Listing 6 zeigt.<br />

Über die Eigenschaft Key kann der Schlüsselwert<br />

ermittelt werden, der zu diesem<br />

Element der Gruppierung gehört.<br />

Listing 7 zeigt das Interface. Im Konstruktor<br />

werden Schlüssel und zugehörige Werte<br />

übergeben und in Feldern abgelegt. Doch<br />

wie erfolgt nun die Gruppierung der Eingangsdaten?<br />

Schauen Sie sich dazu zunächst die Signatur<br />

der GroupBy-Methode an:<br />

www.dotnetpro.de dotnetpro.dojos.2011 63


LÖSUNG<br />

Listing 8<br />

Zeichenketten gruppieren.<br />

[Test]<br />

public void GroupBy_Länge_des_Wortes() {<br />

var values = new[] {"abc", "a", "ab", "a", "abc"};<br />

var groups = values.GroupBy(x => x.Length);<br />

Assert.That(groups, Is.EqualTo(new[]{new[]{"abc", "abc"}, new []{"a", "a"},<br />

new[]{"ab"}}));<br />

}<br />

Listing 9<br />

GroupBy, selbst gebaut.<br />

public static IEnumerable GroupBy<br />

(this IEnumerable values, Func keyFunction) {<br />

var dictionary = new Dictionary();<br />

foreach (var value in values) {<br />

if (!dictionary.ContainsKey(keyFunction(value))) {<br />

dictionary[keyFunction(value)] = new List();<br />

}<br />

dictionary[keyFunction(value)].Add(value);<br />

}<br />

foreach (var d in dictionary) {<br />

yield return new Grouping(d.Key, d.Value);<br />

}<br />

}<br />

public static<br />

IEnumerable<br />

GroupBy(this<br />

IEnumerable values,<br />

Func keyFunction)<br />

Die Methode erhält neben den Eingangsdaten<br />

eine Funktion, die zu einem<br />

Element den zugehörigen Schlüsselwert<br />

liefert. Aufgabe der GroupBy-Methode ist<br />

es nun, die Eingangsdaten Element für<br />

Element zu durchlaufen und jeweils den<br />

Schlüssel des Elements zu ermitteln. Anschließend<br />

muss das Element in die zu<br />

seinem Schlüssel gehörige Aufzählung eingereiht<br />

werden. Gruppiert man beispielsweise<br />

Zeichenketten nach ihrer Länge,<br />

muss das Element „a“ in die Aufzählung<br />

zum Schlüsselwert 1 eingereiht werden.<br />

Listing 8 zeigt einen Test, der Zeichenketten<br />

nach ihrer Länge gruppiert.<br />

Wenn man nun überlegt, wie man diese<br />

Funktionalität implementieren kann, wird<br />

klar, dass die Eingangsdaten innerhalb der<br />

GroupBy-Methode vollständig behandelt<br />

werden müssen, ehe das Ergebnis geliefert<br />

werden kann. Das Ergebnis kann nicht Element<br />

für Element gebildet werden, weil die<br />

Elemente des Ergebnisses selbst wieder<br />

Aufzählungen sind. Um die erste gruppierte<br />

Liste herausgeben zu können, müssen<br />

die Schlüssel aller Eingangselemente geprüft<br />

worden sein. Also ist es für diese Methode<br />

angemessen, eine Variable zu verwenden,<br />

in der das Ergebnis erst vollständig<br />

gebildet wird.<br />

Die GroupBy-Methode ist übrigens nicht<br />

die einzige, bei der das Ergebnis vollständig<br />

gebildet werden muss, ehe es als Rückgabewert<br />

herausgegeben werden kann. Das Sortieren<br />

der Elemente ist ein weiteres Beispiel.<br />

Für die Gruppierung bietet es sich an,<br />

mit einem Dictionary zu arbeiten. Darin<br />

können Sie die Schlüsselwerte der Elemente<br />

als Schlüssel im Dictionary verwenden.<br />

Der zugehörige Wert der Dictionary-Einträge<br />

ist dann jeweils eine Liste von Elementen.<br />

Das Dictionary ist daher von folgendem<br />

Typ:<br />

var dictionary = new Dictionary();<br />

Damit sieht die Implementation der<br />

GroupBy-Methode wie in Listing 9 aus.<br />

Die Methode besteht aus zwei Teilen. Im<br />

ersten Teil werden die Elemente in die zu ihrem<br />

Schlüssel gehörige Liste eingereiht. Dabei<br />

ist jeweils zu prüfen, ob die Liste bereits<br />

Listing 10<br />

Wer ist der Erste?<br />

[Test]<br />

public void Drei_Elemente() {<br />

Assert.That(new[] {1, 2, 3}.First(),<br />

Is.EqualTo(1));<br />

}<br />

Listing 11<br />

Mit MoveNext auf den 1.Platz.<br />

public static T First<br />

(this IEnumerable values) {<br />

var enumerator =<br />

values.GetEnumerator();<br />

enumerator.MoveNext();<br />

return enumerator.Current;<br />

}<br />

existiert. Immer wenn ein Schlüssel zum<br />

ersten Mal auftritt, wird das Element im<br />

Dictionary angelegt. Im zweiten Teil wird<br />

das fertige Dictionary durchlaufen und für<br />

jedes Element ein Grouping zurückgegeben.<br />

First<br />

Beim Ausprobieren der GroupBy-Methode<br />

aus dem .NET Framework habe ich in einem<br />

Test die First-Methode verwendet.<br />

Diese liefert das erste Element einer Aufzählung,<br />

siehe Listing 10.<br />

Für die Implementation ist es wichtig zu<br />

wissen, wie ein Enumerator funktioniert.<br />

Er muss nämlich vor dem ersten Zugriff auf<br />

das aktuelle Element durch einen Aufruf<br />

von MoveNext initialisiert werden. Mit dieser<br />

Kenntnis ist die Implementation einfach,<br />

siehe Listing 11.<br />

Ganz wichtig ist hier übrigens die lokale<br />

Variable für den Enumerator. Die Methode<br />

GetEnumerator liefert nämlich bei jedem<br />

Aufruf einen neuen Enumerator. Da Move-<br />

Next und Current jedoch auf demselben<br />

Enumerator aufgerufen werden müssen,<br />

ist das Zwischenspeichern in einer Variablen<br />

notwendig.<br />

Distinct<br />

Weiter geht’s mit der Methode Distinct. Sie<br />

liefert jedes Element einer Aufzählung nur<br />

genau einmal. Enthält die Aufzählung ein<br />

Element mehrfach, wird es nur einmal weitergeleitet.<br />

Dazu ist es erforderlich, dass<br />

sich die Methode merkt, welche Elemente<br />

64 dotnetpro.dojos.2011 www.dotnetpro.de


sie bereits geliefert hat. Mithilfe dieses<br />

„Merkzettels“ ist es möglich, jedes Element<br />

einzeln zu behandeln. Im Gegensatz zu<br />

GroupBy oder Sort muss also nicht die gesamte<br />

Aufzählung auf einmal bearbeitet<br />

werden. Listing 12 zeigt die ersten Tests.<br />

Den „Merkzettel“ habe ich über eine<br />

List realisiert. Der eine oder andere<br />

Leser wird vermutlich sofort zusammenzucken<br />

und sich über die Performance Gedanken<br />

machen. Doch Obacht! Keep it<br />

Simple Stupid (KISS) lautet die Devise. Sollte<br />

sich später wirklich ein Performance-<br />

Engpass zeigen, kann immer noch eine effizientere<br />

Implementation gesucht werden.<br />

Listing 13 zeigt meine Implementation.<br />

Min<br />

Listing 12<br />

Distinct testen.<br />

[Test]<br />

public void Zwei_mal_das_gleiche_Element() {<br />

Assert.That(new[] {1, 1}.Distinct(),<br />

Is.EqualTo(new[] {1}));<br />

}<br />

[Test]<br />

public void<br />

Mehrere_mehrfach_auftretende_Elemente() {<br />

Assert.That(new[] {1, 2, 1, 2,<br />

3}.Distinct(),<br />

Is.EqualTo(new[] {1, 2, 3}));<br />

}<br />

Fortgesetzt habe ich meinen kleinen LINQ-<br />

Ausflug mit der Min-Methode. Denn dabei<br />

stellt sich eine weitere Herausforderung:<br />

Wie kann man Elemente eines beliebigen<br />

generischen Typs miteinander vergleichen?<br />

Für die bislang gezeigten Methoden genügte<br />

es, dass zwei Elemente auf Gleichheit<br />

überprüft werden konnten. Da die<br />

Equals-Methode zum Bestandteil der Basisklasse<br />

object gehört, war dies bislang<br />

Listing 13<br />

Distinct realisieren.<br />

kein Problem. Doch um das kleinste Element<br />

einer Aufzählung zu ermitteln, müssen<br />

die Elemente verglichen werden können.<br />

Listing 14 zeigt dazu einen Test.<br />

Für den Vergleich zweier Elemente haben<br />

Sie mehrere Möglichkeiten:<br />

❚ Sie können mehrere Überladungen der<br />

Min-Methode anbieten. Die dabei verwendeten<br />

Elementtypen müssen einen<br />

Vergleichsoperator definieren.<br />

❚ Sie können den generischen Elementtyp<br />

T mit einem Constraint versehen, welches<br />

dafür sorgt, dass der Typ T das Interface<br />

IComparable implementieren muss.<br />

❚ Sie können die Klasse Comparer aus dem<br />

.NET Framework verwenden.<br />

Die erste Variante wird im .NET Framework<br />

zwar verwendet, doch das hat sicherlich<br />

seinen Grund in besserer Performance.<br />

Ich habe daher zunächst nach Variante<br />

zwei implementiert und den Elementtyp<br />

mit einem Constraint versehen, siehe Listing<br />

15. Das Constraint where T : IComparable<br />

sorgt dafür, dass der Compiler überprüft,<br />

ob der Typ T das Interface IComparable<br />

implementiert. Damit wird zur Kompilierzeit<br />

sichergestellt, dass CompareTo<br />

aufgerufen werden kann.<br />

Etwas unschön am generischen Typ ist,<br />

dass man ohne ein weiteres Constraint<br />

nicht davon ausgehen kann, dass es sich<br />

um einen Referenztyp handelt. Damit kann<br />

die Variable minimum, die das bislang<br />

kleinste gefundene Element hält, nicht mit<br />

null initialisiert werden, um anzuzeigen,<br />

dass es noch kein Minimum gibt. Den Typ<br />

T mit einem Constraint auf Referenztypen<br />

einzuschränken wäre auch nicht sinnvoll,<br />

denn dann könnte kein Minimum einer Integer-Aufzählung<br />

ermittelt werden. Daher<br />

habe ich eine boolescheVariable minimum-<br />

Gefunden eingeführt, in der festgehalten<br />

wird, ob bereits ein Minimum gefunden<br />

wurde. Nur dann darf das aktuelle Element<br />

gegen das bislang gefundene kleinste Ele-<br />

public static IEnumerable Distinct(this IEnumerable elements) {<br />

var distinctElements = new List();<br />

foreach (var element in elements) {<br />

if (!distinctElements.Contains(element)) {<br />

distinctElements.Add(element);<br />

yield return element;<br />

}<br />

}<br />

}<br />

Listing 14<br />

LÖSUNG<br />

Die kleinste Zahl finden.<br />

[Test]<br />

public void Mehrere_ints() {<br />

Assert.That(new[] {4, 2, 3,<br />

1}.Min(), Is.EqualTo(1));<br />

}<br />

Listing 15<br />

Daten vergleichen.<br />

public static T Min(this<br />

IEnumerable elements) where T :<br />

IComparable {<br />

var minimumGefunden = false;<br />

var minimum = default(T);<br />

foreach (var element in elements) {<br />

if (!minimumGefunden) {<br />

minimumGefunden = true;<br />

minimum = element;<br />

}<br />

else if (element.Compare-<br />

To(minimum) < 0) {<br />

minimum = element;<br />

}<br />

}<br />

if (!minimumGefunden) {<br />

throw new InvalidOperation-<br />

Exception();<br />

}<br />

return minimum;<br />

}<br />

ment verglichen werden. Zu Variante drei<br />

sei verraten, dass ich darauf auch erst<br />

durch einen Blick in den .NET-Framework-<br />

Quellcode gekommen bin. Bislang wusste<br />

ich nicht, dass es die statische Klasse Comparer<br />

gibt. Mit ihrer Hilfe kann man sich zu<br />

einem Typ einen Comparer liefern lassen:<br />

var comparer = Comparer.Default;<br />

Dadurch kann das Typconstraint entfallen.<br />

Wieder was dazugelernt!<br />

www.dotnetpro.de dotnetpro.dojos.2011 65<br />

Fazit<br />

Ich habe bei dieser Übung zwei Dinge gelernt:<br />

Obwohl ich GroupBy schon oft verwendet<br />

habe, waren mir die Details von<br />

IGrouping nicht klar. Und dass man sich<br />

beim .NET Framework einfach so einen<br />

Comparer abholen kann, war mir auch neu.<br />

Also hat sich die kleine Übung gelohnt! [ml]<br />

[1] Patrick A. Lorenz, Kochen mit Patrick zum<br />

Thema LINQ, dotnetpro 8/2008, Seite 116ff.,<br />

www.dotnetpro.de/A0808Kochstudio


Wer übt, gewinnt<br />

AUFGABE<br />

Einen Twitterticker realisieren<br />

Was pfeifen die Spatzen?<br />

Gute Übungsaufgaben müssen cool sein. Sonst macht das Herumtüfteln keinen Spaß.Also, Stefan: Kannst du<br />

eine Aufgabe stellen, bei der ein cooles Programm entsteht, das zugleich technisch herausfordernd ist?<br />

dnpCode: A1102DojoAufgabe<br />

In jeder dotnetpro finden Sie<br />

eine Übungsaufgabe von<br />

Stefan Lieser, die in maximal<br />

drei Stunden zu lösen sein<br />

sollte. Wer die Zeit investiert,<br />

gewinnt in jedem Fall – wenn<br />

auch keine materiellen Dinge,<br />

so doch Erfahrung und Wissen.<br />

Es gilt:<br />

❚ Falsche Lösungen gibt es<br />

nicht. Es gibt möglicherweise<br />

elegantere, kürzere<br />

oder schnellere Lösungen,<br />

aber keine falschen.<br />

❚ Wichtig ist, dass Sie reflektieren,<br />

was Sie gemacht<br />

haben. Das können Sie,<br />

indem Sie Ihre Lösung mit<br />

der vergleichen, die Sie<br />

eine Ausgabe später in<br />

dotnetpro finden.<br />

Übung macht den Meister.<br />

Also − los geht’s. Aber Sie<br />

wollten doch nicht etwa<br />

sofort Visual Studio starten…<br />

Twitterwalls erfreuen sich auf Veranstaltungen<br />

wachsender Beliebtheit.<br />

Eine Twitterwall zeigt regelmäßig aktualisiert<br />

Tweets, die ein vordefiniertes<br />

Hashtag enthalten. Die gefundenen Tweets<br />

werden an eine Wand projiziert. Auf diese Weise<br />

können Teilnehmer der Veranstaltung Tweets zur<br />

Veranstaltung absetzen und die Twitterwall wie<br />

ein Schwarzes Brett nutzen.<br />

Doch in diesem dotnetpro.dojo soll es nicht<br />

um eine Twitterwall gehen, sondern um ein Twitterband.<br />

Auch das Twitterband soll Tweets mit einem<br />

definierten Hashtag suchen und darstellen.<br />

Allerdings sollen die Tweets wie ein visuelles<br />

Laufband dargestellt werden, ähnlich den Börsentickern<br />

bei einschlägigen Nachrichtensendern.<br />

Der Suchbegriff soll dem Programm über die<br />

Kommandozeile übergeben werden. Anschließend<br />

soll das Programm die Tweets abrufen und<br />

darstellen. Dabei sollen die üblichen Angaben<br />

visualisiert werden:<br />

❚ Text des Tweets,<br />

❚ Benutzername,<br />

❚ Profilfoto,<br />

❚ Zeitstempel,<br />

❚ Client, mit dem der Tweet abgesetzt wurde.<br />

Zu Anfang können Sie natürlich den Funktionsumfang<br />

reduzieren und zunächst nur den<br />

Text des Tweets anzeigen.<br />

In regelmäßigen Abständen muss das Programm<br />

die Tweets aktualisieren. Dazu muss erneut<br />

eine Anfrage an Twitter abgesetzt werden.<br />

Die Benutzerschnittstelle soll während der Abfrage<br />

nicht einfrieren. Hier kommt also Multithreading<br />

ins Spiel. Ein Timer, der in regelmäßigen<br />

Abständen ein Ereignis auslöst, kann hier<br />

zum Einsatz kommen. Doch Obacht! Die beiden<br />

Threads müssen dann synchronisiert werden,<br />

damit die Aktualisierung der Benutzerschnittstelle<br />

auf dem UI-Thread erfolgt.<br />

Den Zugriff auf das Twitter-API könnten Sie<br />

natürlich selbst entwickeln. Hier empfehle ich<br />

jedoch, eines der vorhandenen Open-Source-<br />

Frameworks einzusetzen. Andernfalls wird Sie<br />

diese Übung längere Zeit beschäftigen. Ich habe<br />

gute Erfahrungen gemacht mit Twitterizer [1].<br />

Die Benutzerschnittstelle können Sie mit Windows<br />

Forms oder WPF angehen. Auch eine Silverlight-Anwendung<br />

wäre denkbar. Abbildung 1 zeigt<br />

einen groben Entwurf der Benutzerschnittstelle.<br />

Die einzelnen Tweets sollen nebeneinander angezeigt<br />

werden und von rechts nach links durchs<br />

Fenster laufen. Zusammengenommen besteht<br />

die Herausforderung dieser Übung darin, ein Modell<br />

für die Implementation zu entwickeln und<br />

dieses umzusetzen. Bei der Umsetzung geht es<br />

vor allem um Multithreading und die damit verbundene<br />

Synchronisation. Aber auch in der Benutzerschnittstelle<br />

stecken Herausforderungen.<br />

Zur Modellierung und Umsetzung empfehle ich,<br />

Event-Based Components einzusetzen. Ralf Westphal<br />

hat dazu in zurückliegenden Heften einiges<br />

veröffentlicht [2]–[4]. Probieren Sie es doch mal<br />

aus! Im nächsten Heft gibt’s meine Lösung. [ml]<br />

[1] http://www.twitterizer.net/<br />

[2] Ralf Westphal, Zusammenstecken – funktioniert,<br />

Event-Based Components, dotnetpro 6/2010, S. 132ff.,<br />

www.dotnetpro.de/A1006ArchitekturKolumne<br />

[3] Ralf Westphal, Stecker mit System, Event-Based<br />

Components, dotnetpro 7/2010, S. 126ff.,<br />

www.dotnetpro.de/A1007ArchitekturKolumne<br />

[4] Ralf Westphal, Nicht nur außen schön, Event-Based<br />

Components, dotnetpro 8/2010, S. 126ff.,<br />

www.dotnetpro.de/A1008ArchitekturKolumne<br />

66 dotnetpro.dojos.2011 www.dotnetpro.de<br />

[Abb. 1]<br />

Ein Twitter-<br />

ticker.


Einen Twitter-Ticker realisieren<br />

Der Zwitscherfinder<br />

Hat da jemand 'piep' gesagt? Der Zwitscherfinder weiß die Antwort.Alle paar Minuten checkt er die Twitter-Website<br />

nach dem gesuchten Schlüsselwort und präsentiert das Ergebnis. Übrigens lässt sich auch ein Zwitscherfinder<br />

vorteilhaft über Event-Based Components realisieren.<br />

A<br />

m Anfang dieser kleinen Anwendung<br />

stand für mich ein Spike. Ich<br />

hatte nämlich keine klare Vorstellung<br />

davon, wie das Open-Source-Framework<br />

Twitterizer [1] zu verwenden ist. Ferner<br />

wusste ich nicht so ganz genau, wie das<br />

Ergebnis der Twitter-Suche, eine Liste von<br />

Tweets, mit WPF visualisiert werden kann.<br />

Dass das irgendwie mit Databinding und<br />

einem Item-Template in einer ListView gehen<br />

würde, war mir klar. Aber wie genau?<br />

Also habe ich eine Spike-Solution erstellt.<br />

In der ist sozusagen alles erlaubt. Logik im<br />

UI, keine Tests, alles in Ordnung. Solange<br />

das Ergebnis am Ende lediglich dazu verwendet<br />

wird, Erkenntnisse zu gewinnen.<br />

Das bedeutet nicht zwangsläufig wegwerfen.<br />

Häufig dient eine Spike-Solution auch<br />

später noch mal dazu, wieder in ein Thema<br />

reinzukommen. Oder ein Kollege möchte<br />

sich mit der Technik vertraut machen. Legen<br />

Sie daher Ihre Spikes ruhig im Versionskontrollsystem<br />

ab, natürlich gut gekennzeichnet<br />

in einem separaten Verzeichnis.<br />

Nachdem ich durch den Spike herausgefunden<br />

hatte, wie das Twitter-API zu bedienen<br />

ist, habe ich begonnen, die Lösung des<br />

Problems zu modellieren. Dabei bin ich in<br />

zwei Iterationen vorgegangen: In einem<br />

ersten Modell habe ich das periodische Aktualisieren<br />

der Tweets weggelassen. Statt<br />

also nach einer gewissen Zeit erneut bei<br />

Twitter nach Tweets zu suchen, wird dort<br />

nur einmal gesucht und die so gefundenen<br />

Tweets werden angezeigt. Dadurch wurde<br />

das Modell einfacher und ich konnte mich<br />

auf den Kern der Anwendung konzentrieren:<br />

Ausgehend von einem Suchbegriff<br />

werden die gefundenen Tweets in einem<br />

Laufband visualisiert.<br />

Das vereinfachte Modell und dessen<br />

Umsetzung bietet schon einen großen<br />

Nutzen für den potenziellen Kunden: Die<br />

Anwendung kann in dieser abgespeckten<br />

Form sicherlich schneller entwickelt werden<br />

als in der Komplettversion. Damit<br />

steht das Feedback des Kunden auch<br />

schneller zur Verfügung. Ich komme auf<br />

diesen Aspekt später zurück.<br />

[Abb. 1] Erster Schritt mit zwei Funktionseinheiten.<br />

Die Modellierung habe ich in Form von<br />

Datenflüssen vorgenommen. Pfeile bedeuten<br />

hier den Fluss von Daten, Kreise stehen<br />

für Funktionseinheiten. Eine Funktionseinheit<br />

kann atomar oder zusammengesetzt<br />

sein. In Anlehnung an die Elektrotechnik<br />

und die Umsetzung des Datenflussmodells<br />

mittels Event-Based Components werden<br />

die atomaren Funktionseinheiten Bauteile<br />

(engl. Parts) genannt, die zusammengesetzten<br />

heißen Platinen (engl. Boards) [2]. Ein<br />

Bauteil enthält Logik, während eine Platine<br />

dafür zuständig ist, Funktionseinheiten zu<br />

verdrahten. Das können wieder Bauteile<br />

oder Platinen sein. Im Modell sieht man<br />

den Funktionseinheiten nicht an, ob sie<br />

Bauteil oder Platine sind. Das ist gut so,<br />

denn es ermöglicht die spätere Verfeinerung<br />

durch Hierarchisierung. Eine ursprünglich<br />

als Bauteil modellierte Funktionseinheit<br />

kann später zu einer Platine<br />

verfeinert werden. Dadurch ist am ursprünglichen<br />

Diagramm nichts zu ändern,<br />

sondern es entsteht ein weiteres Diagramm,<br />

welches das Innenleben der Platine<br />

zeigt. Es handelt sich dabei also um ein<br />

hierarchisches Modell. Die Schachtelung<br />

kann beliebig tief erfolgen.<br />

Im Modell der Twitterband-Anwendung<br />

habe ich zunächst mit zwei Funktionseinheiten<br />

begonnen, wie Abbildung 1 zeigt:<br />

❚ Die Funktionseinheit UI steht für die Benutzerschnittstelle,<br />

also alle Elemente,<br />

über die der Benutzer mit der Anwendung<br />

interagiert. Ob dabei alles in einer<br />

Form realisiert wird oder zusätzlich User<br />

Controls eingesetzt werden, spielt bei der<br />

Modellierung noch keine Rolle, denn es<br />

ist ein Implementationsdetail.<br />

❚ Die Funktionseinheit Tweets suchen enthält<br />

die Logik der Anwendung. Ausge-<br />

LÖSUNG<br />

hend von einem Suchbegriff als Eingabe<br />

produziert diese Funktionseinheit mehrere<br />

Tweets als Ausgabe.<br />

Im Modell bedeutet der Datenfluss<br />

(Tweet*), dass mehrere Tweets geliefert werden.<br />

Dies wird durch den Stern angezeigt.<br />

Ob dies später als Array, List oder IEnumerable<br />

realisiert wird, spielt im Modell<br />

keine Rolle. Wichtig ist hier lediglich auszudrücken,<br />

dass es mehrere Tweets sind. Die<br />

Daten sind in Klammern notiert, um deutlich<br />

zu machen, dass dies die Daten der<br />

Nachricht sind. Eine Benennung der Nachricht<br />

fehlt, weil sich diese aus dem Namen<br />

der Funktionseinheit Tweets suchen ergibt.<br />

Würde die Funktionseinheit beispielsweise<br />

Twitter heißen, wäre es notwendig, den<br />

Datenfluss zu benennen. So könnte die<br />

Eingabe dann beispielsweise Tweets suchen<br />

(Suchbegriff) heißen, während die<br />

Ausgabe Ergebnis liefern (Tweet*) heißen<br />

könnte.<br />

Hier wird der Unterschied deutlich zwischen<br />

einer Modellierung mit Aktivitäten<br />

wie Tweets suchen und Akteuren wie Twitter.<br />

Bei der Modellierung mit Aktivitäten<br />

kann die Benennung der Nachricht meist<br />

entfallen, während sie bei Modellierung<br />

mit Akteuren für das Verständnis notwendig<br />

ist. In der Praxis hat sich gezeigt, dass<br />

die konsequente Modellierung in Aktivitäten<br />

natürlicher ist und nach einer kurzen<br />

Gewöhnungsphase leichter fällt. Die Gewöhnungsphase<br />

ist vor allem für Entwickler<br />

erforderlich, die sehr in der Objektorientierung<br />

verhaftet sind. Sie sind eher gewohnt,<br />

die Substantive zu suchen, um daraus<br />

Akteure zu machen.<br />

Für die Tweets, die von der Funktionseinheit<br />

Tweets suchen zum UI geliefert wer-<br />

www.dotnetpro.de dotnetpro.dojos.2011 67


LÖSUNG<br />

[Abb. 2] Die Funktionseinheit „Tweets suchen“ weiter zerlegen.<br />

den, habe ich einen eigenen Datentyp<br />

Tweet vorgesehen. Alternativ hätte ich den<br />

Datentyp Tweet aus dem Twitterizer-Framework<br />

verwenden können. Dann wäre das<br />

UI jedoch von dieser Infrastruktur abhängig.<br />

Im UI wäre dann eine Referenz auf die<br />

Twitterizer-Assembly erforderlich gewesen.<br />

Das wollte ich in jedem Fall vermeiden,<br />

schließlich sind Benutzerschnittstelle<br />

und Ressourcenzugriffe völlig unterschiedliche<br />

Concerns und sollten daher getrennt<br />

werden. Ferner bietet ein eigener Datentyp<br />

die Möglichkeit, die Daten bereits so aufzubereiten,<br />

dass sie vom UI direkt verwendet<br />

werden können. Das führt dazu, dass<br />

das UI die Daten nicht deuten muss. Damit<br />

bleibt das UI extrem dünn und enthält keinerlei<br />

Logik. Ein automatisiertes Testen des<br />

UIs kann entfallen.<br />

Aus der Tatsache, dass Tweets suchen die<br />

gefundenen Tweets in einem Datenmodell<br />

ablegt, folgt, dass sich Tweets suchen um<br />

zwei Belange kümmert: Zum einen findet<br />

hier der Zugriff auf Twitter über das Twitterizer-API<br />

statt. Zum anderen ist die Funktionseinheit<br />

dafür zuständig, die Daten aus<br />

dem Twitterizer-Datentyp in meinen eigenen<br />

Datentyp zu mappen und dabei gegebenenfalls<br />

aufzubereiten.<br />

Da diese Erkenntnis bereits während des<br />

Modellierens zutage trat, habe ich die Funktionseinheit<br />

Tweets suchen weiter zerlegt.<br />

Um auf dem gleichen Abstraktionsniveau<br />

zu bleiben und das bisherige Modell nicht<br />

mit Details zu verwässern, die auf der Ebene<br />

nicht relevant sind, habe ich die Verfeinerung<br />

in einem weiteren Diagramm modelliert.<br />

Die Funktionseinheit Tweets suchen<br />

zerfällt dadurch intern in weitere<br />

Funktionseinheiten, ist demnach also eine<br />

Platine und kein Bauteil. Diese hierarchische<br />

Zerlegung ist durch die Modellierung<br />

in Datenflüssen und die Umsetzung<br />

mit Event-Based Components auf einfache<br />

Weise möglich. Vor allem kann die Schachtelung<br />

in beliebiger Tiefe erfolgen, ohne<br />

dass dadurch bei der späteren Implementation<br />

Probleme auftreten.<br />

Abbildung 2 zeigt die Zerlegung von<br />

Tweets suchen. Die äußere Schnittstelle ist<br />

logischerweise gleich geblieben, andernfalls<br />

würde die Funktionseinheit nicht<br />

mehr in das sie umgebende Modell passen.<br />

Der Suchbegriff wird an die Funktionseinheit<br />

Twitter abfragen übergeben. Diese liefert<br />

daraufhin eine Liste der gefundenen<br />

Tweets im Datentyp des Twitterizer-Frameworks.<br />

Die Tweets werden von der Funktionseinheit<br />

Tweets mappen in das eigene<br />

Datenmodell übersetzt. Dabei werden unter<br />

anderem Eigenschaften aus dem Originaltweet<br />

zu Zeichenketten zusammengefasst,<br />

damit das UI diese Informationen<br />

nicht deuten muss, sondern sie direkt per<br />

Databinding anzeigen kann. Nun liegen in<br />

unserem Modell zwei verschiedene Arten<br />

von Funktionseinheiten vor:<br />

❚ Bauteile, die nicht weiter verfeinert sind<br />

und Logik enthalten.<br />

❚ Platinen, die weitere Funktionseinheiten<br />

enthalten und für deren Verbindungen<br />

zuständig sind.<br />

Die Funktionseinheiten UI, Twitter abfragen<br />

und Tweets mappen sind Bauteile.<br />

Dagegen ist die Funktionseinheit Tweets<br />

suchen durch die Verfeinerung jetzt eine<br />

Platine. Sie ist dafür zuständig, die beiden<br />

enthaltenen Bauteile zu verdrahten, und<br />

enthält selbst keine Logik.<br />

In dieser ersten Modellierung habe ich<br />

das regelmäßige Aktualisieren der Tweets<br />

Listing 1<br />

Die Bauteile verdrahten.<br />

public class TwitterSearch {<br />

private readonly Action search;<br />

public TwitterSearch() {<br />

var twitter = new Twitter();<br />

var mapper = new Mapper();<br />

twitter.Out_Result += mapper.In_Map;<br />

mapper.Out_Result += tweets => Out_Update(tweets);<br />

search = query => twitter.In_Search(query);<br />

}<br />

public event Action Out_Update;<br />

public void In_Search(string query) {<br />

search(query);<br />

}<br />

}<br />

bewusst weggelassen. Das hat mehrere<br />

Vorteile. Zum einen wird dadurch die Modellierung<br />

vereinfacht. Das gilt natürlich<br />

nur unter der Prämisse, dass spätere Ergänzungen<br />

einfach möglich sind. Durch<br />

die Modellierung mit Datenflüssen ist das<br />

gegeben. Wäre eine spätere Ergänzung des<br />

Modells mit hohem Änderungsaufwand<br />

verbunden, wäre die vorläufige Vereinfachung<br />

teuer eingekauft.<br />

Der zweite Vorteil ist darin zu sehen,<br />

dass nun dieses Modell bereits implementiert<br />

werden kann. Dabei wird zwar noch<br />

nicht die gesamte geforderte Funktionalität<br />

umgesetzt, aber auch hier gilt, dass man<br />

diese später ergänzen kann, ohne dabei die<br />

bereits implementierten Funktionseinheiten<br />

ändern zu müssen. Das liegt maßgeblich<br />

daran, dass ich zur Implementation<br />

des Modells Event-Based Components verwende.<br />

Somit bietet die Vorgehensweise<br />

den Vorteil der iterativen Entwicklung mit<br />

sehr kurzer Iterationsdauer. Dadurch kann<br />

der Kunde oder Product Owner sehr früh<br />

Feedback geben.<br />

Und so wie das gesamte Modell iterativ<br />

entwickelt werden kann, kann man auch<br />

bei der Implementation iterativ vorgehen:<br />

❚ Die Übergabe des Suchbegriffs als Kommandozeilenparameter<br />

kann zunächst<br />

weggelassen werden. Stattdessen wird<br />

ein fester Suchbegriff verwendet, der in<br />

der Anwendung hart codiert ist.<br />

❚ Statt direkt auf das Twitter-API zuzugreifen<br />

und die eingehenden Tweets zu<br />

mappen, kann zunächst eine hart codierte<br />

Liste von Tweets zurückgegeben<br />

werden. Dadurch lässt sich die Entwicklung<br />

des Controls zur Anzeige der Tweets<br />

vorantreiben.<br />

❚ Umgekehrt kann auch zunächst auf das<br />

Control verzichtet werden. Stattdessen<br />

68 dotnetpro.dojos.2011 www.dotnetpro.de


werden die Tweets ganz simpel in einem<br />

Label als Text angezeigt.<br />

Es bieten sich also zahlreiche Möglichkeiten,<br />

das Feature Tweets suchen und anzeigen<br />

in kleinen Schritten umzusetzen. Ganz<br />

wichtig dabei: Es handelt sich trotzdem immer<br />

um Längsschnitte durch alle Funktionseinheiten.<br />

Dadurch kann man die Anwendung<br />

bereits sehr früh an den Kunden übergeben,<br />

um Feedback einzuholen.<br />

Für die Implementation von Tweets suchen<br />

müssen zwei Bauteile und eine Platine<br />

implementiert werden. Das eine Bauteil<br />

ermittelt mithilfe des Twitter-APIs die Liste<br />

der Tweets. Das andere bringt die gefundenen<br />

Tweets in eine Form, die vom UI unmittelbar<br />

verwendet werden kann. Um beide<br />

Bauteile herum liegt eine Platine, die für<br />

die Verdrahtung der Bauteile zuständig ist.<br />

Dazu muss der eingehende Methodenaufruf<br />

an das erste Bauteil der Platine weitergereicht<br />

werden. Das Ergebnis der Suche<br />

muss zum Mapper weitergeleitet werden.<br />

Und schließlich muss das Ergebnis des<br />

Mappers als Endergebnis der Platine zurückgegeben<br />

werden. Diese Verdrahtung<br />

zeigt Listing 1.<br />

Hier werden die Bauteile im Konstruktor<br />

unmittelbar instanziert, statt sie per Parameter<br />

zu injizieren. Da das Board lediglich<br />

für die Verdrahtung zuständig ist, verzichte<br />

ich auf einen automatisierten Test dieser<br />

Verdrahtung. Dieser wäre relativ aufwendig,<br />

weil dazu mittels Attrappen überprüft<br />

werden müsste, ob die Verdrahtung korrekt<br />

erfolgt ist. Ebenso verzichte ich auf einen<br />

automatisierten Test der Twitter-Suche,<br />

weil das Bauteil lediglich einen Aufruf des<br />

Twitter-APIs kapselt.<br />

Ich habe allerdings je einen Test ergänzt,<br />

der explizit gestartet werden muss, um damit<br />

zu überprüfen, ob das Twitter-API prinzipiell<br />

korrekt verwendet wird und irgendein<br />

Ergebnis zurückgeliefert wird. Einen<br />

vergleichbaren Test habe ich für die Platine<br />

ebenfalls erstellt. So kann überprüft werden,<br />

ob die Verdrahtung korrekt erfolgt ist,<br />

ohne dass dazu die komplette Anwendung<br />

gestartet werden muss. Allerdings handelt<br />

es sich hierbei nicht um Unit-, sondern um<br />

Integrationstests, die dazu noch vom realen<br />

Twitter-API abhängig sind. Da jedoch Platine<br />

und Twitter-Bauteil praktisch keine Logik<br />

enthalten, halte ich das Vorgehen für<br />

angemessen. Das Explicit-Attribut an den<br />

Testmethoden sorgt dafür, dass diese Tests<br />

nur ausgeführt werden, wenn dies explizit<br />

angefordert wird, siehe Listing 2. So kann<br />

man weiterhin alle Tests der Assembly aus-<br />

Listing 2<br />

Die Anwendung testen.<br />

[TestFixture]<br />

public class TwitterTests {<br />

private Twitter sut;<br />

private TwitterSearchResultCollection<br />

result;<br />

[SetUp]<br />

public void Setup() {<br />

sut = new Twitter();<br />

sut.Out_Result += x => result = x;<br />

}<br />

[Test, Explicit]<br />

public void<br />

Suche_nach_einem_Hashtag() {<br />

sut.In_Search("#ccd");<br />

Assert.That(result.Count,<br />

Is.GreaterThan(0));<br />

}<br />

}<br />

führen, ohne dabei jedesmal lange auf die<br />

Antwort von Twitter warten zu müssen.<br />

Für das Mappen der Daten vom Twitterizer-Datentyp<br />

in den eigenen Datentyp<br />

können natürlich Tests geschrieben werden,<br />

da diese Operation ja lediglich auf Daten<br />

arbeitet.<br />

Für das Laufband-Control habe ich die<br />

Suchmaschine meiner Wahl befragt. Dies<br />

führte in der Tat zu einem WPF-Treffer.<br />

Den gefundenen Quellcode habe ich dahingehend<br />

angepasst, dass die Breite des<br />

durchlaufenden Contents mit in die Laufzeit<br />

der Animation eingeht. Ohne diese<br />

Modifikation liefen Suchergebnisse mit<br />

vielen Treffern sehr schnell durch das Fenster,<br />

während Treffer mit nur einem Tweet<br />

sehr langsam angezeigt wurden. Die gewählte<br />

Lösung der Animation scheint mir<br />

[Abb. 3] Einen Timer ergänzen.<br />

LÖSUNG<br />

relativ viel Prozessorzeit zu verbraten. Ich<br />

gestehe erneut, dass ich kein WPF-Spezialist<br />

bin. Wenn einem Leser eine bessere<br />

Lösung für das Marquee-Control einfällt,<br />

möge er sich bitte melden.<br />

Nachdem das Feature Tweets suchen und<br />

anzeigen umgesetzt ist, muss das nächste<br />

Feature modelliert werden: Periodisches<br />

Aktualisieren der Suche. Dabei will ich natürlich<br />

auf dem schon vorhandenen Modell<br />

aufsetzen und dieses erweitern. Durch<br />

die Modellierung mit Datenflüssen ist das<br />

einfach möglich, da zusätzliche Funktionseinheiten<br />

leicht in einen schon vorhandenen<br />

Datenfluss eingesetzt werden können.<br />

Um die Suche periodisch zu aktualisieren,<br />

habe ich zwischen UI und Tweets suchen<br />

eine weitere Funktionseinheit gesetzt,<br />

siehe Abbildung 3. Dieser Periodic Dispenser<br />

hat die Aufgabe, beim Eintreffen von<br />

Daten einen Timer zu starten und die erhaltenen<br />

Daten periodisch herauszugeben.<br />

So wird aus einem einmaligen Datenfluss<br />

ein sich periodisch wiederholender.<br />

Das Schöne dabei: An den vorhandenen<br />

Funktionseinheiten müssen keine Änderungen<br />

vorgenommen werden. Einzige<br />

Ausnahme stellt die Platine dar, die für die<br />

Verdrahtung der Funktionseinheiten zuständig<br />

ist. Diese erhält zusätzliche Funktionseinheiten,<br />

und die Verdrahtung muss<br />

abgeändert werden.<br />

Ein weiterer schöner Effekt: Der Timer<br />

hat nichts mit einer Twittersuche zu tun.<br />

Das Bauteil ist also völlig generisch und<br />

kann auch in einem anderen Kontext eingesetzt<br />

werden.<br />

Wenn man den Periodic Dispenser in die<br />

Anwendung integriert hat, stellt man fest,<br />

dass nun die aktualisierten Tweets auf einem<br />

anderen Thread beim UI eintreffen als<br />

bislang. Vor dem Einsatz des Timers gab es<br />

www.dotnetpro.de dotnetpro.dojos.2011 69


LÖSUNG<br />

Listing 3<br />

Periodische Aktualisierung ermöglichen.<br />

public class PeriodicDispenser {<br />

private T t;<br />

private readonly int timerIntervalInSeconds;<br />

public PeriodicDispenser()<br />

: this(60) {<br />

}<br />

internal PeriodicDispenser(int timerIntervalInSeconds) {<br />

this.timerIntervalInSeconds = timerIntervalInSeconds;<br />

Timer_konfigurieren();<br />

}<br />

public void In_Event(T t) {<br />

this.t = t;<br />

Out_Event(t);<br />

}<br />

public event Action Out_Event;<br />

private void Timer_konfigurieren() {<br />

var timer = new Timer {<br />

Interval = timerIntervalInSeconds * 1000<br />

};<br />

timer.Elapsed += (sender, e) => Out_Event(t);<br />

timer.Start();<br />

}<br />

}<br />

Listing 4<br />

Die Synchronisierung durchführen.<br />

public class Synchronizer {<br />

private readonly SynchronizationContext synchronizationContext;<br />

public Synchronizer() {<br />

synchronizationContext = SynchronizationContext.Current ?? new<br />

SynchronizationContext();<br />

}<br />

public void In_Event(T t) {<br />

synchronizationContext.Send(state => Out_Event(t), null);<br />

}<br />

public event Action Out_Event;<br />

}<br />

in der Anwendung nur einen einzigen<br />

Thread, nun sind es zwei. Damit das UI<br />

nicht meckert, müssen die Aktualisierungen<br />

der UI-Controls auf dem UI-Thread<br />

vorgenommen werden.<br />

Nun könnte man dazu Anpassungen im<br />

UI vornehmen und dort das Wechseln des<br />

Threads implementieren. Einfacher ist es<br />

allerdings, auch hier wieder eine zusätzliche<br />

Funktionseinheit in den Datenfluss<br />

einzusetzen, die den Threadwechsel vornimmt.<br />

Diese Synchronization-Funktionseinheit<br />

lässt sich mit einem SynchronizationContext<br />

aus dem .NET Framework einfach<br />

realisieren.<br />

Diese Vorgehensweise führt dazu, dass<br />

die unterschiedlichen Belange sowohl im<br />

Modell als auch in der Implementation<br />

sauber getrennt bleiben.<br />

Ferner taucht der Aspekt der Synchronisation<br />

im Modell explizit auf und sorgt damit<br />

für bessere Verständlichkeit. Würde<br />

man die Synchronisation im UI implementieren,<br />

indem dort das übliche Muster von<br />

InvokeRequired-Abfrage und Invoke-Aufruf<br />

angewandt würde, wäre der Aspekt in der<br />

Implementation verborgen. Dadurch würde<br />

das Verständnis der Implementation erschwert.<br />

Hinzu kommt, dass nun die Funktionseinheiten<br />

Periodic Dispenser und Synchronization<br />

zu Standardbauteilen werden, die<br />

auch in anderen Anwendungen zum Einsatz<br />

kommen können.<br />

Damit ist das Thema Timer und Threadsynchronisation<br />

abgehakt. Und das in einer<br />

Form, die es erlaubt, den Aspekt im<br />

Modell zu visualisieren. Damit dient das<br />

Modell wirklich dem Verständnis der Implementation.<br />

Wäre die Synchronisation<br />

im UI auf die übliche Art und Weise realisiert<br />

worden, würde die Implementation<br />

den Entwurf nicht widerspiegeln. Einziger<br />

Ausweg: den Aspekt im Modell weglassen.<br />

Damit wäre aber niemandem gedient.<br />

Der PeriodicDispenser ist als generische<br />

Klasse implementiert, siehe Listing 3. Dabei<br />

gibt der generische Typ an, von welchem<br />

Typ der Datenfluss ist. Durch den parameterlosen<br />

Defaultkonstruktor wird der<br />

Timer fix auf 60 Sekunden eingestellt. Für<br />

einen automatisierten Test habe ich einen<br />

weiteren Konstruktor ergänzt, der allerdings<br />

nur internal sichtbar ist. Durch das<br />

Attribut InternalsVisibleTo ist der Konstruktor<br />

auch in der Testassembly sichtbar.<br />

Beim eingehenden Pin In_Event werden<br />

die Daten in einem Feld abgelegt. Dadurch<br />

stehen sie in der Timerroutine zur Verfügung,<br />

um periodisch mit dem Out_Event<br />

wieder ausgeliefert zu werden. Die Synchronisation<br />

erfolgt mithilfe des SynchronizationContext<br />

aus dem .NET Framework,<br />

siehe Listing 4. Dieser wird im Konstruktor<br />

angelegt. Daher muss das Bauteil auf dem<br />

Thread erzeugt werden, auf den später die<br />

eingehenden Ereignisse synchronisiert<br />

werden sollen. Auch dieses Bauteil ist generisch,<br />

um den Typ des ein- und ausgehenden<br />

Datenflusses angeben zu können.<br />

70 dotnetpro.dojos.2011 www.dotnetpro.de<br />

Fazit<br />

Durch die Zerlegung der Gesamtanwendung<br />

in Features und deren getrennte Modellierung<br />

konnte die Gesamtaufgabe in<br />

Iterationen aufgeteilt werden. Die weitere<br />

Zerlegung der Features in Featurescheiben<br />

bot weitere Möglichkeiten, iterativ vorzugehen.<br />

Das gibt beim Entwickeln ein gutes<br />

Gefühl, weil man den Fortschritt sieht und<br />

immer wieder etwas wirklich fertig wird.<br />

Durch den Einsatz von Funktionseinheiten<br />

für die periodische Wiederholung und<br />

die Synchronisation der Threads sind diese<br />

beiden wichtigen Aspekte im Modell sichtbar.<br />

Das fördert das Verständnis und erhöht<br />

den Nutzen der Modellierung als Dokumentation<br />

der Implementation. [ml]<br />

[1] www.twitterizer.net<br />

[2] Ralf Westphal, Zusammenstecken –<br />

funktioniert, Event-Based Components,<br />

dotnetpro 6/2010, S. 132 ff.,<br />

www.dotnetpro.de/A1006ArchitekturKolumne


Algorithmen und Datenstrukturen zu Graphen<br />

Wie hängt alles zusammen?<br />

AUFGABE<br />

Mit einem Graphen kann man darstellen, wie die Dinge miteinander zusammenhängen. Weil aber alles mit allem<br />

irgendwie zusammenhängt, kann man mit Graphen eigentlich alles darstellen. Das ist interessant, und deswegen<br />

gibt es hier dazu eine Übung.<br />

Graphen sind eine Datenstruktur, mit<br />

der sich viele Probleme auf einfache<br />

und elegante Art lösen lassen. Nehmen<br />

wir als Beispiel das Referenzieren<br />

von Projekten und Assemblies in Visual-Studio-Projekten.<br />

Dabei ergibt sich die Frage, in welcher<br />

Reihenfolge die Projekte einer Solution<br />

übersetzt werden müssen. Die Fragestellung ist<br />

leicht zu lösen, wenn man von einer Baumstruktur<br />

ausgeht. Solange also zirkuläre Referenzen<br />

unterbunden werden, genügt es, alle referenzierten<br />

Projekte in einer Baumstruktur abzulegen<br />

und anschließend den Baum zu traversieren. Allerdings<br />

muss die Projektstruktur nicht zwingend<br />

einen einzigen Baum ergeben, sondern es können<br />

mehrere Bäume sein. Ferner stellt sich die<br />

Frage, wie man erkennt, ob es bei den Referenzen<br />

zu Kreisen kommt.<br />

Hier kommt die Datenstruktur Graph ins Spiel.<br />

Ein Graph besteht ganz allgemein gesagt aus<br />

Knoten und Kanten. Eine Kante setzt zwei Knoten<br />

in eine Beziehung. Dabei ist zu unterscheiden,<br />

ob die Kanten gerichtet oder ungerichtet<br />

sind. Bei ungerichteten Kanten werden einfach<br />

zwei Knoten in Beziehung gesetzt, ohne dabei eine<br />

Traversierungsrichtung mit abzulegen. Bei gerichteten<br />

Graphen hat jede Kante eine Richtung,<br />

zeigt also von einem Quellknoten auf einen Zielknoten.<br />

Hierbei ist eine Traversierung in Kantenrichtung<br />

oder auch gegen die Kantenrichtung<br />

möglich. Enthält ein Graph nur ungerichtete<br />

Kanten, spricht man von einem ungerichteten<br />

Graphen. Enthält er gerichtete Kanten, spricht<br />

man von einem gerichteten Graphen.<br />

Das Beispiel der Projektreferenzen lässt sich<br />

mit einem gerichteten Graphen abbilden. Die<br />

Richtung der Kante definiert, wer wen referenziert.<br />

Eine Kante von A nach B bedeutet in dem<br />

Fall, dass Projekt A das Projekt B referenziert.<br />

Normalerweise geht man bei Graphen davon<br />

aus, dass es maximal eine Kante zwischen denselben<br />

Knoten geben kann. Sollen mehrere Kanten<br />

möglich sein, so spricht man von einem Multigraphen.<br />

Das Schöne an Graphen ist, dass in der Literatur<br />

zahlreiche Algorithmen zu finden sind, um<br />

beispielsweise herauszufinden, ob zwei Knoten<br />

miteinander verbunden sind. Im Falle der Projektreferenzen<br />

würde das eine Abhängigkeit der<br />

betroffenen Projekte bedeuten. Durch eine topologische<br />

Sortierung lässt sich die Reihenfolge der<br />

Übersetzung von abhängigen Projekten herausfinden.<br />

Und auch für die Frage nach Kreisen gibt<br />

es Algorithmen, die herausfinden, ob ein Graph<br />

kreisfrei ist.<br />

In vorangegangenen dotnetpro-dojos war beim<br />

Thema Datenstruktur jeweils ein API vorgegeben.<br />

Diesmal ist es ein Bestandteil der Übung: Entwerfen<br />

Sie ein API für den Umgang mit gerichteten<br />

Graphen. Anschließend implementieren Sie die<br />

Datenstruktur. Dazu gibt es in der Literatur genügend<br />

Vorschläge.<br />

Im Anschluss sollten Sie sich einen der Algorithmen<br />

vornehmen und implementieren. Dabei<br />

geht es nicht darum, einen Graphenalgorithmus<br />

selbst zu„erfinden“, sondern es geht lediglich um<br />

die Umsetzung. Beginnen Sie beispielweise mit<br />

der topologischen Sortierung. Da die Algorithmen<br />

meist von einer bestimmten Art und Weise<br />

der Implementation der Datenstruktur ausgehen,<br />

beispielsweise einer Adjazenzmatrix, sollten Sie<br />

sich mit dem Algorithmus befassen, bevor Sie die<br />

Datenstruktur umsetzen.<br />

Eine spannende Ergänzung zur Implementation<br />

der Datenstruktur ist natürlich die Visualisierung<br />

eines Graphen. Auch dabei muss man<br />

das Rad nicht neu erfinden, sondern kann Bibliotheken<br />

wie das freie GraphViz [1] oder das lizenzpflichtige<br />

MSAGL [2] verwenden. MSAGL ist<br />

inzwischen in der MSDN Subscription enthalten.<br />

Im Downloadbereich findet man es unter Automatic<br />

Graph Layout.<br />

Genügend Stoff, um Neues zu lernen. Oder wie<br />

sagte der Professor in derVorlesung über Graphentheorie?<br />

„Das ganze Leben ist ein Graph.“ [ml]<br />

[1] www.graphviz.org/<br />

[2] http://research.microsoft.com/en-us/projects/msagl/<br />

dnpCode: A1103DojoAufgabe<br />

In jeder dotnetpro finden Sie<br />

eine Übungsaufgabe von<br />

Stefan Lieser, die in maximal<br />

drei Stunden zu lösen sein<br />

sollte. Wer die Zeit investiert,<br />

gewinnt in jedem Fall – wenn<br />

auch keine materiellen Dinge,<br />

so doch Erfahrung und Wissen.<br />

Es gilt:<br />

❚ Falsche Lösungen gibt es<br />

nicht. Es gibt möglicherweise<br />

elegantere, kürzere<br />

oder schnellere Lösungen,<br />

aber keine falschen.<br />

❚ Wichtig ist, dass Sie reflektieren,<br />

was Sie gemacht<br />

haben. Das können Sie,<br />

indem Sie Ihre Lösung mit<br />

der vergleichen, die Sie<br />

eine Ausgabe später in<br />

dotnetpro finden.<br />

Übung macht den Meister.<br />

Also − los geht’s. Aber Sie<br />

wollten doch nicht etwa<br />

sofort Visual Studio starten…<br />

www.dotnetpro.de dotnetpro.dojos.2011 71<br />

Wer übt, gewinnt


LÖSUNG<br />

Algorithmen und Datenstrukturen zu Graphen<br />

Wie die Welt zusammenhält<br />

„Adjazenz“ bezeichnet keinen geistlichen Würdenträger und ist auch kein militärischer Dienstgrad, sondern steht für<br />

die Beziehung zwischen Knoten und Kanten. Über adjazente, also miteinander verbundene Knoten kann man<br />

Zusammenhänge modellieren und erforschen. dotnetpro macht einen Ausflug in die Graphentheorie, für die es viele<br />

praktische Anwendungen gibt.<br />

dnpCode: A1104DojoLoesung<br />

Stefan Lieser ist Softwareentwickler<br />

aus Leidenschaft. Nach<br />

seinem Informatikstudium mit<br />

Schwerpunkt auf Softwaretechnik<br />

hat er sich intensiv mit<br />

Patterns und Principles auseinandergesetzt.<br />

Er arbeitet als<br />

Berater und Trainer, hält zahlreiche<br />

Vorträge und hat gemeinsam<br />

mit Ralf Westphal die Clean<br />

Code Developer Initiative ins<br />

Leben gerufen. Sie erreichen ihn<br />

unter stefan@lieser-online.de<br />

oder lieser-online.de/blog.<br />

[Abb. 1] Ein Graph mit zugehöriger<br />

Adjazenzmatrix.<br />

Literatur zu Graphenalgorithmen zu finden<br />

ist nicht schwer. Ich habe Robert<br />

Sedgewicks „Algorithms in Java“ [1] herangezogen.<br />

Die Tatsache, dass die Beispiele in<br />

Java vorliegen, sollte Sie nicht abschrecken. Java<br />

ist nicht so weit weg von C#, dass man die Beispiele<br />

nicht problemlos übernehmen könnte.<br />

Bevor es an die Algorithmen geht, muss man<br />

sich natürlich Gedanken über die zugrunde liegende<br />

Datenstruktur machen. Auch hierbei hilft<br />

die Literatur. Sie enthält Vorschläge, wie Graphen<br />

implementiert werden können. Ich habe mit der<br />

Repräsentation in Form einer sogenannten Adjazenzmatrix<br />

begonnen. Der Begriff Adjazenz steht<br />

für die Beziehung zwischen Knoten oder Kanten.<br />

Zwei Knoten sind adjazent, wenn sie durch eine<br />

Kante verbunden sind. Zwei Kanten sind adjazent,<br />

wenn sie einen gemeinsamen Knoten haben.<br />

Die Idee einer Adjazenzmatrix ist ganz einfach:<br />

In Form einer Matrix wird festgehalten, ob zwei<br />

Knoten durch eine Kante verbunden sind. Die<br />

Adjazenzmatrix enthält also die Information über<br />

die Kanten. Repräsentiert wird sie durch ein zweidimensionales<br />

Array von booleschen Werten:<br />

private readonly bool[,] adjacency =<br />

new bool[VertexMaxCount,VertexMaxCount];<br />

Eine Kante zwischen den Knoten 5 und 7 wird<br />

also dargestellt, indem im Array an Position [5, 7]<br />

der Wert true steht. Knoten werden durch Integerzahlen<br />

repräsentiert und können daher als Index<br />

in der Adjazenzmatrix verwendet werden.<br />

Ein Beispiel für einen Graphen und die zugehörige<br />

Adjazenzmatrix zeigt Abbildung 1.<br />

Die Adjazenzmatrix mag dem ein oder anderen<br />

Leser etwas verschwenderisch mit dem Speicherplatz<br />

umgehen. Allemal, wenn ein Graph nur wenige<br />

Kanten enthält und dadurch die meisten<br />

Einträge in der Matrix auf false stehen. Hier sei<br />

der Hinweis auf das Prinzip „Vorsicht vor Optimierungen“<br />

erlaubt [2]. Wenn nicht gerade Graphen<br />

mit Tausenden von Knoten bearbeitet werden<br />

sollen, ist der Speicherbedarf vernachlässigbar.<br />

ImVordergrund steht dieVerständlichkeit der<br />

Implementation, und die ist hier definitiv gegeben.<br />

Um beispielsweise zu ermitteln, ob eine<br />

Kante vom Knoten v zum Knoten w existiert, genügt<br />

es, in der Adjazenzmatrix nachzuschauen:<br />

if(adjacency[v, w]) {<br />

...<br />

}<br />

Wer sich dennoch Sorgen um den Speicherplatz<br />

macht, kann auch die Klasse BitArray aus dem<br />

.NET Framework verwenden. Allerdings muss<br />

man sich die Matrix dann selbst zusammenbasteln,<br />

da BitArrays nur eindimensional sind.<br />

In meiner Implementation habe ich die Adjazenzmatrix<br />

mit einer fixen Größe von willkürlich<br />

50 Knoten angelegt. Es ist ein Leichtes, dies durch<br />

einen zusätzlichen Konstruktor konfigurierbar<br />

zu machen. Und natürlich könnte man das Array<br />

bei Bedarf auch in der Größe anpassen. Dazu<br />

muss lediglich geprüft werden, ob noch Platz für<br />

den zu ergänzenden Knoten ist. Da dann allerdings<br />

ein Umkopieren der Werte erforderlich ist,<br />

sollte man gut überlegen, ob es nicht besser ist,<br />

gleich die „richtige“ Größe zu verwenden.<br />

API und Implementation des Graphen habe<br />

ich aus dem erwähnten Buch übernommen. Allerdings<br />

habe ich dabei einige der Bezeichner geändert,<br />

weil mir diese im Original zu stark abgekürzt<br />

waren. Das erschwert für mich die Lesbarkeit,<br />

daher bevorzuge ich ausgeschriebene Begriffe.<br />

Lediglich bei den Eigenschaften für die<br />

Anzahl der Knoten und Kanten habe ich es bei V<br />

und E in Großbuchstaben belassen, weil dies die<br />

in der Literatur allgemein verwendeten Symbole<br />

sind (abgeleitet von Vertex und Edge). Aber auch<br />

über diese Bezeichner ließe sich natürlich reden.<br />

Listing 1 zeigt die Implementation der Datenstruktur.<br />

Die beiden Properties V und E liefern jeweils<br />

die Anzahl von Knoten und Kanten zurück.<br />

Zu beachten ist dabei, dass V jeweils die maximale<br />

Anzahl möglicher Knoten im Graph liefert.<br />

Das liegt daran, dass in der Adjazenzmatrix lediglich<br />

die Kanten verwaltet werden. Diese werden<br />

beim Einfügen und Löschen zusätzlich noch<br />

gezählt, damit für die Ermittlung ihrer Anzahl E<br />

kein Zählen in der Matrix erforderlich ist.<br />

Für das Hinzufügen und Löschen von Kanten<br />

wird die Datenklasse Edge verwendet. Diese hat<br />

72 dotnetpro.dojos.2011 www.dotnetpro.de


Listing 1<br />

Die Datenstruktur für einen<br />

Graphen.<br />

public class Graph {<br />

private const int VertexMaxCount = 50;<br />

private readonly bool[,] adjacency =<br />

new bool[VertexMaxCount,VertexMaxCount];<br />

private int edgeCount;<br />

public int V {<br />

get { return VertexMaxCount; }<br />

}<br />

public int E {<br />

get { return edgeCount; }<br />

}<br />

public void Insert(Edge e) {<br />

if (adjacency[e.v, e.w]) {<br />

return;<br />

}<br />

adjacency[e.v, e.w] = true;<br />

edgeCount++;<br />

}<br />

public void Remove(Edge e) {<br />

if (!adjacency[e.v, e.w]) {<br />

return;<br />

}<br />

adjacency[e.v, e.w] = false;<br />

edgeCount--;<br />

}<br />

public bool Edge(int v, int w) {<br />

return adjacency[v, w];<br />

}<br />

public IEnumerable<br />

AdjacentVertices(int v) {<br />

for (var i = 0; i <<br />

VertexMaxCount; i++) {<br />

if (Edge(v, i)) {<br />

yield return i;<br />

}<br />

}<br />

}<br />

}<br />

public class Edge {<br />

public int v { get; set; }<br />

public int w { get; set; }<br />

}<br />

lediglich zwei Properties für Start- und<br />

Zielknoten. Auch hier habe ich es bei den<br />

Kleinbuchstaben v und w belassen, da diese<br />

in der Literatur sehr oft als Symbole für<br />

Knoten verwendet werden.<br />

Neben den Methoden zum Verändern<br />

des Graphen stehen die beiden Methoden<br />

Edge und AdjacentVertices zur Verfügung.<br />

Die Methode Edge liefert für zwei Knoten<br />

die Information, ob diese durch eine Kante<br />

verbunden sind. Dazu ist lediglich ein<br />

Zugriff auf die Adjazenzmatrix erforderlich,<br />

bei dem die beiden Knoten als Indizes verwendet<br />

werden. Der Laufzeitaufwand ist<br />

somit konstant O(1). Details zur sogenann-<br />

Listing 2<br />

Graphen verändern.<br />

public class GraphTests {<br />

private Graph g;<br />

[SetUp]<br />

public void Setup() {<br />

g = new Graph();<br />

}<br />

[Test]<br />

public void Edge_is_retrievable() {<br />

g.Insert(new Edge {v = 0, w = 1});<br />

Assert.That(g.Edge(0, 1), Is.True);<br />

Assert.That(g.E, Is.EqualTo(1));<br />

}<br />

[Test]<br />

public void Edge_can_be_removed() {<br />

g.Insert(new Edge {v = 0, w = 1});<br />

g.Remove(new Edge {v = 0, w = 1});<br />

Assert.That(g.Edge(0, 1), Is.False);<br />

Assert.That(g.E, Is.EqualTo(0));<br />

}<br />

}<br />

ten O-Notation – auch Landau-Symbol genannt<br />

– finden Sie unter [3].<br />

Mit AdjacentVertices können zu einem<br />

gegebenen Knoten v sämtliche adjazenten<br />

Knoten ermittelt werden. Das sind Knoten,<br />

die durch eine von v ausgehende Kante mit<br />

v verbunden sind sind. Dabei kommt ein<br />

Iterator zum Einsatz, den ich mit yield return<br />

implementiert habe. Der Laufzeitaufwand<br />

für diese Methode beträgt O(n), ist<br />

also linear. Das bedeutet, dass die Laufzeit<br />

linear mit der Größe der Adjazenzmatrix<br />

ansteigt.<br />

Natürlich habe ich zu dieser Datenstruktur<br />

einige automatisierte Tests erstellt. Diese<br />

können gleichzeitig auch als Beispiele<br />

für die Verwendung des APIs dienen. Listing<br />

2 zeigt ein Beispiel für die Veränderung<br />

eines Graphen.<br />

Listing 3 zeigt einen Test zur Traversierung<br />

der benachbarten Knoten.<br />

Der erste Test zeigt, dass die Aufzählung<br />

der adjazenten Knoten leer ist, wenn keine<br />

Kanten hinzugefügt wurden. Im zweiten<br />

Test sieht man, dass die über Kanten unmittelbar<br />

erreichbaren Zielknoten aufgelistet<br />

werden.<br />

Im Rausch der Tiefe<br />

Dieses Beispiel führt uns wieder zur Frage<br />

aus der Aufgabenstellung zurück: Wie kann<br />

man ermitteln, ob es eine Verbindung zwischen<br />

zwei Knoten im Graph gibt, die über<br />

mehrere Kanten verläuft? Da hilft die Tiefensuche:<br />

Man besucht, ausgehend vom<br />

Startknoten, so lange alle erreichbaren<br />

Listing 3<br />

Benachbarte Knoten<br />

traversieren.<br />

LÖSUNG<br />

[Test]<br />

public void<br />

Adjacent_vertices_without_edge() {<br />

Assert.That(g.AdjacentVertices(0),<br />

Is.Empty);<br />

}<br />

[Test]<br />

public void Adjacent_vertices() {<br />

g.Insert(new Edge {v = 1, w = 1});<br />

g.Insert(new Edge {v = 1, w = 3});<br />

g.Insert(new Edge {v = 1, w = 7});<br />

Assert.That(g.AdjacentVertices(1),<br />

Is.EqualTo(new[] {1, 3, 7}));<br />

}<br />

Knoten, bis man entweder den Zielknoten<br />

gefunden hat oder es nicht mehr weitergeht.<br />

Um sich dabei nicht im Kreis zu drehen,<br />

werden alle Knoten, die einmal besucht<br />

wurden, markiert. Wird ein solcher<br />

Knoten während der weiteren Suche erneut<br />

erreicht, wird dieser Pfad nicht weiter<br />

verfolgt. Listing 4 zeigt den in der Klasse<br />

PathSearch umgesetzten Algorithmus.<br />

Die eigentliche Suche nach einem Pfad<br />

von v nach w übernimmt die rekursive Methode<br />

SearchRecursive. Ihr Abbruchkriterium<br />

ist erreicht, wenn die beiden Knoten<br />

v und w dieselben sind, denn ein Weg von<br />

einem Knoten zu sich existiert natürlich immer.<br />

Danach wird der Startknoten dieser<br />

Suche markiert, damit er später nicht erneut<br />

berücksichtigt wird. Dann werden in einer<br />

Schleife alle adjazenten Knoten besucht,<br />

falls das nicht bereits zuvor geschehen ist.<br />

Beim Markieren der schon besuchten Knoten<br />

ist es wieder nützlich, dass die Knoten<br />

als Zahlen repräsentiert werden. Dadurch<br />

kann nämlich der Knoten selbst als Index<br />

in ein boolesches Array verwendet werden.<br />

Die Implementation des Algorithmus ist<br />

getrennt von der Implementation der Datenstruktur.<br />

Im Konstruktor werden die nötigen<br />

Angaben übergeben: der Graph, in<br />

dem gesucht werden soll, und die beiden<br />

Knoten, zwischen denen ein Pfad gesucht<br />

werden soll. Das Ergebnis der Suche wird<br />

in einem Feld abgelegt und kann über die<br />

Eigenschaft Exists abgefragt werden, wie<br />

der Test in Listing 5 zeigt.<br />

Auf diese Weise ist es übrigens auch<br />

leicht, den Algorithmus so zu erweitern,<br />

dass der gefundene Pfad auch ermittelt<br />

wird statt nur seine Existenz zu überprü-<br />

www.dotnetpro.de dotnetpro.dojos.2011 73


LÖSUNG<br />

Listing 4<br />

In Verzweigungen abtauchen.<br />

public class PathSearch {<br />

private readonly Graph g;<br />

private readonly bool found;<br />

private readonly bool[] visited;<br />

public PathSearch(Graph g, int v, int w) {<br />

this.g = g;<br />

visited = new bool[g.V];<br />

found = SearchRecursive(v, w);<br />

}<br />

privateboolSearchRecursive(intv,intw){<br />

if (v == w) {<br />

return true;<br />

}<br />

visited[v] = true;<br />

foreach (var t in<br />

g.AdjacentVertices(v)) {<br />

if (visited[t]) {<br />

continue;<br />

}<br />

if (SearchRecursive(t, w)) {<br />

return true;<br />

}<br />

}<br />

return false;<br />

}<br />

public bool Exists {<br />

get { return found; }<br />

}<br />

}<br />

Listing 5<br />

Ergebnis der Suche ablegen.<br />

[Test]<br />

public void Existing_path() {<br />

g.Insert(new Edge{v = 0, w = 1});<br />

g.Insert(new Edge{v = 1, w = 2});<br />

g.Insert(new Edge{v = 2, w = 3});<br />

var path = new PathSearch(g, 0, 3);<br />

Assert.That(path.Exists, Is.True);<br />

}<br />

fen. Die Klasse kann dazu einfach mit einer<br />

weiteren Eigenschaft versehen werden, die<br />

während der Suche die traversierten Knoten<br />

aufsammelt.<br />

Natürlich wäre es auch denkbar, den Algorithmus<br />

nicht direkt von der Klasse Graph<br />

abhängig zu machen. Dazu müsste Graph<br />

nur mit einem Interface versehen werden.<br />

So könnten unterschiedliche Implementationen<br />

der Datenstruktur vom selben Algorithmus<br />

verwendet werden. Im Sinne von<br />

KISS, Keep it simple, stupid [2], habe ich<br />

darauf aber verzichtet, denn zurzeit habe<br />

ich nur eine einzige Datenstruktur Graph<br />

implementiert. Bei Bedarf ist die Umstellung<br />

auf ein Interface keine große Tat.<br />

Visualisierung<br />

Ich wollte dann noch prüfen, ob der Algorithmus<br />

auch mit Kreisen (Zyklen) umgehen<br />

kann. Darunter versteht man einen<br />

Graphen, in dem zwei Knoten so über Kanten<br />

verbunden sind, dass man bei der Traversierung<br />

wieder am Ursprungsknoten<br />

ankommt. Natürlich dürfen dabei mehrere<br />

andere Knoten traversiert werden.<br />

Der Test dazu war schnell erstellt. Doch<br />

kam der Wunsch auf, den zu testenden<br />

Graphen visualisieren zu können: So ist sichergestellt,<br />

dass die Testdaten tatsächlich<br />

den gewünschten Graphen repräsentieren.<br />

Dies visuell zu prüfen ist eben viel einfacher,<br />

als eine Liste von Kanten zu interpretieren.<br />

Also habe ich mir die MSAGL-Bibliotheken<br />

aus den MSDN Subscription Downloads<br />

besorgt und in ein neues Projekt eingebunden<br />

[4]. MSAGL verwendet zur Visualisierung<br />

eine eigene Graph-Klasse. Also galt es<br />

zunächst, meine Repräsentation eines Graphen<br />

in die Repräsentation aus dem<br />

MSAGL-Framework zu überführen Dies<br />

habe ich als Extension-Methode implementiert.<br />

Dadurch bleibt meine Graph-<br />

Klasse weiterhin frei von unnötigen Abhängigkeiten,<br />

siehe Listing 6.<br />

In einer Schleife werden alle Knoten des<br />

Graphen durchlaufen. Für jeden Knoten<br />

werden dann die adjazenten Knoten ermittelt.<br />

Für je zwei adjazente Knoten wird anschließend<br />

eine Kante im MSAGL-Graphen<br />

angelegt. Ein Knoten wird durch MSAGL<br />

standardmäßig als Kästchen visualisiert;<br />

deshalb ändere ich hier auch gleich den<br />

Shape zu einem Kreis.<br />

Danach habe ich ein Windows-Forms-<br />

Projekt erstellt und darin eine neue Form<br />

angelegt. In die Form habe ich ein MSAGL-<br />

GViewer-Control eingefügt und eine Methode<br />

ergänzt, mit welcher der zu visualisierende<br />

Graph an die Form übergeben<br />

Listing 6<br />

Die MSAGL-Bibliothek einbeziehen.<br />

wird. Listing 7 zeigt, wie ein Graph nun angezeigt<br />

werden kann.<br />

Auch diese Methode habe ich als Extension-Methode<br />

implementiert. Somit kann<br />

ich nun den nicht kreisfreien Graphen im<br />

Test visualisieren, siehe Listing 8.<br />

Das Ergebnis der Visualisierung zeigt Abbildung<br />

2. Natürlich sollte die Visualisierung<br />

nicht in einem automatisierten Test<br />

aufgerufen werden. Nach der visuellen<br />

Kontrolle meiner Testdaten habe ich den<br />

Aufruf g.ShowGraph() daher auskommentiert.<br />

Wer es ganz richtig machen möchte,<br />

implementiert den Viewer als Visual Studio<br />

Debugger Extension. Dann kann jeder<br />

Graph im Debugger zur Kontrolle angezeigt<br />

werden. Eine solche Debugger Extension<br />

zu realisieren ist nicht schwer.<br />

Topologische Sortierung<br />

Nun sind wir mithilfe der Pfadsuche in der<br />

Lage zu ermitteln, ob ein Pfad von einem<br />

Startknoten zu einem Zielknoten existiert.<br />

Damit können wir beispielsweise ermitteln,<br />

ob eine Assembly von einer anderen<br />

Assembly abhängig ist, auch wenn sich die-<br />

public static class GraphExtensions {<br />

public static Microsoft.Msagl.Drawing.Graph ToMsaglGraph(this Graph g) {<br />

var msaglGraph = new Microsoft.Msagl.Drawing.Graph();<br />

for (var v = 0; v < g.V; v++) {<br />

foreach (var w in g.AdjacentVertices(v)) {<br />

var e = msaglGraph.AddEdge(v.ToString(), w.ToString());<br />

e.SourceNode.Attr.Shape = Shape.Circle;<br />

e.TargetNode.Attr.Shape = Shape.Circle;<br />

}<br />

}<br />

return msaglGraph;<br />

}<br />

}<br />

74 dotnetpro.dojos.2011 www.dotnetpro.de<br />

[Abb. 2]<br />

Zyklische<br />

Bezüge<br />

visualisieren.


Listing 7<br />

Einen Graphen visualisieren.<br />

public static void ShowGraph(this<br />

Graph g) {<br />

var viewer = new Viewer();<br />

viewer.SetGraph(g);<br />

viewer.ShowDialog();<br />

}<br />

Listing 8<br />

Zyklische Bezüge visualisieren.<br />

[Test]<br />

public void Path_with_circles() {<br />

g.Insert(new Edge{v = 0, w = 1});<br />

g.Insert(new Edge{v = 1, w = 2});<br />

g.Insert(new Edge{v = 2, w = 3});<br />

g.Insert(new Edge{v = 1, w = 0});<br />

g.Insert(new Edge{v = 2, w = 0});<br />

g.Insert(new Edge{v = 3, w = 0});<br />

g.ShowGraph();<br />

var path = new PathSearch(g, 0, 3);<br />

Assert.That(path.Exists, Is.True);<br />

}<br />

se Abhängigkeit über mehrere Assemblies<br />

hinweg ergibt. Doch wie können wir die<br />

Build-Reihenfolge mehrerer Projekte ermitteln,<br />

die untereinander Abhängigkeiten haben?<br />

Die Lösung liegt in der topologischen<br />

Sortierung eines entsprechenden Graphen.<br />

Unter der topologischen Sortierung eines<br />

Graphen versteht man eine Knotenreihenfolge,<br />

bei der jeder Knoten vor allen Knoten<br />

angeordnet ist, auf die er mittels Kanten<br />

verweist. Für das Bestimmen der Build-<br />

Reihenfolge benötigen wir die umgekehrte<br />

topologische Sortierung: Projekte, die von<br />

anderen benötigt werden, müssen vor diesen<br />

übersetzt werden.<br />

Der Algorithmus zur umgekehrten topologischen<br />

Sortierung basiert, wie schon<br />

die Pfadermittlung, auf einer Tiefensuche.<br />

Auch diesen Algorithmus habe ich wieder<br />

als eigenständige Klasse implementiert,<br />

siehe Listing 9.<br />

Auch hier ist die Tiefensuche wieder als<br />

Rekursion implementiert. Damit Knoten<br />

nicht mehrfach besucht werden, wird das<br />

Array pre mit -1 initialisiert. Nur Knoten,<br />

für die der Initialwert noch im Array steht,<br />

werden besucht. Die beiden Arrays order<br />

und relabel nehmen das Ergebnis der Sortierung<br />

auf.<br />

❚ Order liefert für einen gegebenen Index<br />

die Knotennummer.<br />

Listing 9<br />

Umgekehrte topologische<br />

Sortierung.<br />

public class ReverseTopologicalSort {<br />

private readonly Graph g;<br />

private int cnt;<br />

private int tcnt;<br />

private readonly int[] pre;<br />

private readonly int[] relabel;<br />

private readonly int[] order;<br />

public ReverseTopologicalSort(Graph g) {<br />

this.g = g;<br />

pre = new int[g.V];<br />

relabel = new int[g.V];<br />

order = new int[g.V];<br />

for (var i = 0; i < g.V; i++) {<br />

pre[i] = -1;<br />

relabel[i] = -1;<br />

order[i] = -1;<br />

}<br />

for (var i = 0; i < g.V; i++) {<br />

if (pre[i] == -1) {<br />

SortReverse(i);<br />

}<br />

}<br />

}<br />

private void SortReverse(int v) {<br />

pre[v] = cnt++;<br />

foreach (var w in<br />

g.AdjacentVertices(v)) {<br />

if (pre[w] == -1) {<br />

SortReverse(w);<br />

}<br />

}<br />

relabel[v] = tcnt;<br />

order[tcnt++] = v;<br />

}<br />

public int Order(int v) {<br />

return order[v];<br />

}<br />

public int Relabel(int v) {<br />

return relabel[v];<br />

}<br />

}<br />

❚ Relabel liefert zu einer gegebenen Knotennummer<br />

den Index des Knotens.<br />

Listing 10 zeigt einen Test des Algorithmus<br />

für einen Graphen, bei dem drei Knoten<br />

hintereinander angeordnet sind. Man<br />

sieht bereits, dass die beiden Methoden<br />

Order und Relabel invers zueinander sind.<br />

Natürlich gibt es für manche Graphen<br />

mehrere mögliche Ergebnisse. Schon bei<br />

einem Knoten mit zwei Nachfolgern stellt<br />

sich die Frage, welcher Nachfolger zuerst<br />

besucht werden soll. Hier gibt es also zwei<br />

mögliche Ergebnisse bei der topologischen<br />

Sortierung. Für die Reihenfolge beim Übersetzen<br />

von Visual-Studio-Projekten spielt<br />

das natürlich keine Rolle.<br />

Listing 10<br />

Graphen testen.<br />

Sich im Kreis drehen<br />

LÖSUNG<br />

[Test]<br />

public void Graph_with_two_edges_in_line() {<br />

g.Insert(new Edge {v = 0, w = 1});<br />

g.Insert(new Edge {v = 1, w = 2});<br />

var sut = new ReverseTopologicalSort(g);<br />

Assert.That(sut.Order(0), Is.EqualTo(2));<br />

Assert.That(sut.Order(1), Is.EqualTo(1));<br />

Assert.That(sut.Order(2), Is.EqualTo(0));<br />

Assert.That(sut.Relabel(0), Is.EqualTo(2));<br />

Assert.That(sut.Relabel(1), Is.EqualTo(1));<br />

Assert.That(sut.Relabel(2), Is.EqualTo(0));<br />

}<br />

Beim Ermitteln der Build-Reihenfolge bleibt<br />

abschließend noch die Frage, ob zyklische<br />

Abhängigkeiten existieren. Wenn A von B<br />

abhängt und B von A, gibt es keine Build-<br />

Reihenfolge, bei der jedes Projekt lediglich<br />

einmal übersetzt wird. Solche Situationen<br />

sollten erkannt werden, um Referenzen zu<br />

verhindern, die zu Kreisen führen würden.<br />

Die Vorgehensweise ist demnach wie<br />

folgt: Zunächst wird ein Graph mit den bereits<br />

vorhandenen Projekten und Referenzen<br />

erzeugt. Danach wird die neu hinzuzufügende<br />

Referenz in den Graphen eingefügt.<br />

Bevor die Referenz tatsächlich in das<br />

Visual-Studio-Projekt aufgenommen wird,<br />

muss der Graph auf Kreisfreiheit überprüft<br />

werden. Würde durch die zusätzliche Referenz<br />

ein Kreis entstehen, muss diese Referenz<br />

logischerweise abgelehnt werden.<br />

Die Lösung des Problems kann man zurückführen<br />

auf die Frage, ob zwischen zwei<br />

Knoten eine sogenannte starkeVerbindung<br />

existiert.Wenn für zwei Knoten v und w eine<br />

starke Verbindung existiert, bedeutet das,<br />

dass auch eine Verbindung zwischen w<br />

und v existiert. Ist das der Fall, liegt ein Zyklus<br />

(oder auch Kreis) vor. Die Menge von<br />

zusammenhängenden Knoten in einem<br />

Graph, die über starke Verbindungen erreichbar<br />

sind, werden starke Komponenten<br />

genannt. Und das führt uns zu einem Algorithmus<br />

für die Frage nach der Kreisfreiheit<br />

eines Graphen: Wenn die Anzahl starker<br />

Komponenten in einem Graph der Anzahl<br />

seiner Knoten entspricht, ist keiner der<br />

Knoten mit einem anderen stark verbunden.<br />

Das heißt, es liegen keine Kreise vor.<br />

Um herauszufinden, ob ein Graph Kreise<br />

enthält, ermittelt man also die Anzahl der<br />

starken Komponenten. Ist diese gleich der<br />

Anzahl der Knoten, ist der Graph kreisfrei.<br />

www.dotnetpro.de dotnetpro.dojos.2011 75


LÖSUNG<br />

Listing 11<br />

Auf kreisfreie Graphen testen.<br />

[Test]<br />

public void Graph_without_cycles() {<br />

g.Insert(new Edge{v = 0, w = 1});<br />

g.Insert(new Edge{v = 1, w = 2});<br />

g.Insert(new Edge{v = 2, w = 3});<br />

}<br />

var sc = new StrongComponents(g);<br />

Assert.That(sc.Count, Is.EqualTo(g.V));<br />

Assert.That(sc.StronglyReachable(0, 3),<br />

Is.False);<br />

[Test]<br />

public void Graph_with_cycle_over_2_vertices() {<br />

g.Insert(new Edge{v = 0, w = 1});<br />

g.Insert(new Edge{v = 1, w = 0});<br />

}<br />

var sc = new StrongComponents(g);<br />

Assert.That(sc.Count, Is.EqualTo(g.V - 1));<br />

Assert.That(sc.StronglyReachable(0, 1),<br />

Is.True);<br />

Sie suchen<br />

.NET Entwickler?<br />

MitunsfindenSieIhren<br />

Wunschkandidaten!<br />

Im Beispielcode auf der Heft-CD ist der<br />

Algorithmus von Tarjan in der Klasse<br />

StrongComponents implementiert. Er ist<br />

ebenfalls aus Sedgewick [1] entnommen.<br />

Zwei Tests sollen das API demonstrieren,<br />

siehe Listing 11.<br />

Das erste Beispiel betrifft einen kreisfreien<br />

Graphen. In diesem Fall ist die Anzahl<br />

der starken Komponenten sc.Count gleich<br />

der Anzahl der Knoten g.V. Zwischen den<br />

beiden Knoten besteht keine starke Verbindung,<br />

da sie nur in einer Richtung verbunden<br />

sind. Im zweiten Beispiel enthält der<br />

Graph zwei Knoten, die in beiden Richtungen<br />

miteinander verbunden sind. Somit<br />

entspricht die Anzahl der starken Komponenten<br />

nicht der Anzahl der Knoten. Der<br />

Algorithmus liefert uns also die Aussage,<br />

dass der Graph nicht kreisfrei ist.<br />

Fazit<br />

Ich habe mich während dieser Übung an<br />

mein Studium erinnert. Die Vorlesungen<br />

zu Graphenalgorithmen waren immer sehr<br />

lehrreich, weil grundlegende Dinge wie Rekursion,<br />

Tiefensuche etc. erklärt wurden.<br />

Wer solche gedanklichen „Zeitreisen“ eher<br />

mit negativen Gefühlen verbindet, sei getröstet:<br />

Die Algorithmen konnte ich unverändert<br />

bei Sedgewick „abschreiben“. Die<br />

Übersetzung von Java nach C# war leicht.<br />

Das hat für mich wieder bestätigt, dass<br />

man als .NET-Entwickler bei der Suche<br />

nach Literatur durchaus Java-Literatur in<br />

den Blick nehmen sollte, von Java-spezifischen<br />

Themen vielleicht abgesehen.<br />

Graphenalgorithmen sind grundlegend<br />

für viele Problemstellungen. Daher sollte<br />

man zumindest die wichtigsten Begriffe<br />

und Konzepte kennen. Bei der Umsetzung<br />

eines vorhandenen Algorithmus geschieht<br />

dies ganz praxisnah nebenbei. Befasst man<br />

sich mit Algorithmen und Datenstrukturen<br />

regelmäßig im Rahmen der persönlichen<br />

Weiterbildung, ist man für zukünftige Herausforderungen<br />

gerüstet. [ml]<br />

[1] Robert Sedgewick, Algorithms in Java, Part 5,<br />

Graph Algorithms, ISBN 0-201-36121-3<br />

[2] http://www.clean-code-developer.de/<br />

Roter-Grad.ashx<br />

[3] http://de.wikipedia.org/wiki/Landau-Symbole<br />

[4] http://research.microsoft.com/<br />

en-us/projects/msagl/<br />

NeueMediengesellschaft<br />

UlmmbH<br />

IhreAnsprechpartnerfür<br />

denStellenmarkt<br />

AngelikaHochmuth<br />

Anzeigenleitung<br />

Tel:089/74117-125<br />

angelika.hochmuth@nmg.de<br />

76 dotnetpro.dojos.2011 www.dotnetpro.de


Eine interaktive Anwendung mit Datenflüssen modellieren<br />

Wie fließen die Daten?<br />

Software modellieren: Ja, dem gehört die Zukunft.Aber was soll man eigentlich genau modellieren?<br />

Datenflüsse oder Abhängigkeiten von Funktionseinheiten? Stefan, kannst du dazu eine Übung stellen?<br />

Bei einer algorithmischen Fragestellung<br />

liegt es nahe, Datenflüsse zu modellieren.Wer<br />

Datenflüsse modelliert,<br />

verdeutlicht den Ablauf der Anwendung.<br />

Bei der Modellierung von Abhängigkeiten<br />

ist das hingegen meistens nicht der Fall. Zwar<br />

sieht man bei einem Abhängigkeitsdiagramm,<br />

dass eine Funktionseinheit die Dienste anderer<br />

Funktionseinheiten in Anspruch nimmt und damit<br />

von diesen Funktionseinheiten abhängig<br />

wird. Wie die Interaktion der Funktionseinheiten<br />

aber genau aussieht, lässt sich meist nur erahnen.<br />

Für Aufgabenstellungen ohne Benutzerinteraktion<br />

sind Datenflussdiagramme naheliegend.<br />

Das Zerlegen einer Zeichenkette in Konfigurationswerte,<br />

die in ein Dictionary übernommen<br />

werden, wäre ein Beispiel. Doch kann man auch<br />

eine interaktive Anwendung auf diese Weise modellieren?<br />

Um diese Frage zu beantworten, nehmen<br />

wir diesen Monat eine kleine To-do-Listenverwaltung<br />

auf der To-do-Liste. Im Vordergrund<br />

stehen die Modellierung und Implementation der<br />

Benutzerinteraktionen. Ziel ist, die GUI möglichst<br />

frei von Logik zu halten. Somit müssen alle Entscheidungen,<br />

die sich auf die GUI auswirken,<br />

außerhalb der GUI getroffen werden. Diese Form<br />

der Interaktion lässt sich mit Datenflüssen sehr<br />

gut modellieren. Doch diese Denkweise fällt vor<br />

allem eingefleischten OOP-Verfechtern schwer.<br />

Wie sehen die Anforderungen an die Anwendung<br />

aus? Ich habe dazu eine Featureliste erstellt.<br />

Die Features können bei einer konsequenten Datenflussmodellierung<br />

einzeln modelliert werden.<br />

Das Zusammenfügen der dabei entstandenen<br />

Modelle ist bei Datenflussdesigns kein Problem.<br />

Das liegt daran, dass die beteiligten Funktionseinheiten<br />

keine Abhängigkeiten mehr haben.<br />

Featureliste<br />

F1: Ein neues To-do hinzufügen.<br />

Interaktion: Button Neu wird angeklickt.<br />

Feedback: Das neue To-do wird in der Liste der<br />

To-dos angezeigt und ist im Bearbeiten-Modus.<br />

F2: Bearbeiten eines To-dos beenden.<br />

Interaktion: Eingabetaste oder Anklicken eines<br />

anderen To-dos.<br />

Feedback: Das To-do wird im normalen Modus<br />

angezeigt.<br />

F3: Vorhandenes To-do zum Bearbeiten öffnen.<br />

Interaktion: Doppelklick auf ein To-do.<br />

Feedback: To-do wird im Bearbeiten-Modus angezeigt<br />

und kann geändert werden.<br />

F4: To-dos persistieren.<br />

Interaktion: Beenden und Starten der App.<br />

Feedback: Nach Starten der Anwendung werden<br />

die To-dos aus dem vorhergehenden Lauf wieder<br />

angezeigt.<br />

Ressource: Datei todoliste.daten.<br />

Da die einzelnen Features möglicherweise recht<br />

umfangreich sind, sollte man sich Gedanken machen,<br />

ob man sie weiter zerlegen kann. Bei dieser<br />

Zerlegung in sogenannte Featurescheiben oder<br />

Slices ist es wichtig, weiterhin bei Längsschnitten<br />

zu bleiben, also vertikal zu zerlegen. Würde man<br />

Feature F1 beispielsweise in den GUI-Anteil und<br />

den „Rest“ zerlegen, so wäre der GUI-Anteil kein<br />

eigenständiger Längsschnitt. Besser ist es, die Zerlegung<br />

so zu wählen, dass sie durch alle Funktionseinheiten<br />

hindurch verläuft. Dann wird ein Teil<br />

des GUIs implementiert, aber auch ein Teil des<br />

„Rests“, sodass eine voll funktionsfähige Teilfunktionalität<br />

zur Verfügung steht. Weitere Informationen<br />

zum Entwicklungsprozess siehe [1]. Abbildung<br />

1 zeigt eine mögliche Benutzerschnittstelle.<br />

Die Aufgabe für diesen Monat besteht darin,<br />

die Features zu modellieren und anschließend zu<br />

implementieren. Bei der Implementation mag<br />

eine Beschränkung auf Featurescheiben sinnvoll<br />

sein, damit man nicht zu viel Zeit in ein einzelnes<br />

Feature investiert. Am Ende ist es spannender,<br />

von mehreren Features jeweils einen kleinen Ausschnitt<br />

zu implementieren, anstatt nur ein einziges<br />

Feature komplett zu implementieren. Auch in<br />

der Praxis empfiehlt sich diese Vorgehensweise,<br />

um möglichst früh Feedback vom Kunden zu erhalten.<br />

Happy modeling! [ml]<br />

[1] Ralf Westphal, Elf Schritte bis zum Code, Von den<br />

Anforderungen zum fertigen Programm,<br />

Teil 1, dotnetpro 10/2010, Seite 126ff.,<br />

www.dotnetpro.de/A1010ArchitekturKolumne<br />

Teil 2, dotnetpro 11/2010, Seite 130ff.,<br />

www.dotnetpro.de/A1011Architektur<br />

Teil 3, dotnetpro 12/2010, Seite 130ff.,<br />

www.dotnetpro.de/A1012Architektur<br />

AUFGABE<br />

dnpCode: A1104DojoAufgabe<br />

In jeder dotnetpro finden Sie<br />

eine Übungsaufgabe von<br />

Stefan Lieser, die in maximal<br />

drei Stunden zu lösen sein<br />

sollte. Wer die Zeit investiert,<br />

gewinnt in jedem Fall – wenn<br />

auch keine materiellen Dinge,<br />

so doch Erfahrung und Wissen.<br />

Es gilt:<br />

❚ Falsche Lösungen gibt es<br />

nicht. Es gibt möglicherweise<br />

elegantere, kürzere<br />

oder schnellere Lösungen,<br />

aber keine falschen.<br />

❚ Wichtig ist, dass Sie reflektieren,<br />

was Sie gemacht<br />

haben. Das können Sie,<br />

indem Sie Ihre Lösung mit<br />

der vergleichen, die Sie<br />

eine Ausgabe später in<br />

dotnetpro finden.<br />

Übung macht den Meister.<br />

Also − los geht’s. Aber Sie<br />

wollten doch nicht etwa<br />

sofort Visual Studio starten…<br />

[Abb. 1] So könnte die To-do-Liste<br />

aussehen.<br />

www.dotnetpro.de dotnetpro.dojos.2011 77<br />

Wer übt, gewinnt


LÖSUNG<br />

Eine interaktive Anwendung mit Datenflüssen modellieren<br />

Wissen, was zu tun ist<br />

MVVM-Pattern? Kennt man. Flow Design? Schon mal gehört. Event-Based Components? Klar, das ist die Spezialität<br />

von Ralf und Stefan.Aber alles zusammen auf einmal? Ist noch nicht da gewesen. Geht aber, auch wenn Stefan<br />

bei der Umsetzung ins Schwitzen kam.<br />

dnpCode: A1105DojoLoesung<br />

Stefan Lieser ist Softwareentwickler<br />

aus Leidenschaft. Nach<br />

seinem Informatikstudium mit<br />

Schwerpunkt auf Softwaretechnik<br />

hat er sich intensiv mit<br />

Patterns und Principles auseinandergesetzt.<br />

Er arbeitet als<br />

Berater und Trainer, hält zahlreiche<br />

Vorträge und hat gemeinsam<br />

mit Ralf Westphal die<br />

Initiative Clean Code Developer<br />

ins Leben gerufen. Sie erreichen<br />

ihn unter stefan@lieser-online.de<br />

oder lieser-online.de/blog.<br />

Das Modellieren der ToDo-Listenanwendung<br />

in Form eines Flow Designs ist<br />

eine nützliche Übung. Es stellt sich die<br />

herausfordernde Aufgabe, das MVVM-Pattern<br />

mit Flow Design und Event-Based Components<br />

(EBC) unter einen Hut zu bringen. Hier sind viele<br />

Buzzwords vereint, da tut Aufklärung not.<br />

Das Kürzel MVVM steht für Model-View-<br />

ViewModel. Das Pattern dient der Implementation<br />

grafischer Benutzerschnittstellen. Die Grundidee<br />

besteht darin, ein ViewModel zu definieren,<br />

welches die View optimal bedient. Als View wird<br />

hierbei die Benutzerschnittstelle bezeichnet. Das<br />

ViewModel enthält alle Daten, die von der View<br />

anzuzeigen sind. Das Ziel dabei ist es, die View so<br />

dünn wie möglich zu halten. Sie soll die Daten<br />

des ViewModels nicht deuten müssen. Der<br />

Grund liegt zum einen in der Testbarkeit. Views<br />

sind typischerweise schwierig automatisiert zu<br />

testen, daher sollte dort möglichst kein Code untergebracht<br />

werden. Zum anderen darf die View<br />

keine Domänenlogik enthalten. Es ist Aufgabe<br />

der Domänenlogik, die Daten im ViewModel so<br />

aufzubereiten, wie die View sie benötigt.<br />

Das ViewModel wird an die View übergeben.<br />

Es gibt also eine Abhängigkeit der View vom<br />

ViewModel. Für WPF- und Silverlight-Anwendungen<br />

bedeutet dies vor allem, dass das<br />

ViewModel optimal für Data Binding geeignet<br />

sein sollte. Dazu sollten beispielsweise die Eigenschaften<br />

des ViewModels die INotifyProperty-<br />

Changed-Schnittstelle bedienen. Statt also wie<br />

beim Model-View-Controller-(MVC)- oder Model-View-Presenter-(MVP)-Pattern<br />

der View jeweils<br />

mitzuteilen, welche Änderungen durchgeführt<br />

werden sollen, wird bei MVVM das View-<br />

Model so an die View gebunden, dass direkt das<br />

ViewModel manipuliert werden kann. Durch das<br />

Data Binding werden bei Änderungen am<br />

[Abb. 1] ViewModel im Datenfluss. [Abb. 2] Neues ToDo einfügen.<br />

ViewModel die notwendigen Aktualisierungen<br />

der View durch die Infrastruktur übernommen.<br />

Auf der anderen Seite desViewModels steht das<br />

Model. Das Model steht für die eigentliche Geschäftslogik.<br />

Die Geschäftslogik soll frei sein von<br />

Abhängigkeiten zur View und ihren technischen<br />

Details. Als Mittler zwischen Model undView steht<br />

das ViewModel. Das Model kann das ViewModel<br />

erzeugen bzw. verändern, worauf die View mit<br />

einer Aktualisierung der Darstellung reagiert.<br />

Hochzeit von MVVM und Flow Design<br />

Die Herausforderung beim „Verheiraten“ von<br />

MVVM mit Flow Design liegt in der Frage, wie<br />

Änderungen am ViewModel modelliert werden.<br />

Wird in der View vom Benutzer eine Interaktion<br />

gestartet, sind für die Abarbeitung der zugehörigen<br />

Logik in der Regel Informationen aus dem<br />

ViewModel erforderlich. Umgekehrt führt die Abarbeitung<br />

der Geschäftslogik meist zu Änderungen,<br />

die in der View visualisiert werden müssen.<br />

Eine Möglichkeit wäre, das ViewModel als Ergebnis<br />

einer Änderung als Datenfluss zur View zu<br />

übertragen, so wie es Abbildung 1 zeigt.<br />

Damit würde die View allerdings jedes Mal<br />

eine neue Instanz des ViewModels erhalten, was<br />

das Data Binding ad absurdum führen würde.<br />

Sendet man immer dieselbe Instanz des<br />

ViewModels, stellt sich die Frage, wie man dies<br />

im Modell deutlich macht.<br />

Abbildung 2 zeigt dazu ein Beispiel. Die Aktion<br />

Neues ToDo erzeugen liefert an die View ein geändertes<br />

ViewModel, in das ein zusätzliches ToDo<br />

eingefügt wurde.<br />

Daraus ergeben sich nun gleich zwei Fragen:<br />

❚ Woher kennt die Aktion Neues ToDo erzeugen<br />

den vorhergehenden Zustand desViewModels?<br />

❚ Wie stellt dieView sicher, dass das Data Binding<br />

funktioniert?<br />

78 dotnetpro.dojos.2011 www.dotnetpro.de


Die Frage nach dem Zustand führt zur<br />

Lösung: Das ViewModel ist ein Zustand,<br />

der von der View und der Aktion gemeinsam<br />

verwendet wird. Beide sind von diesem<br />

Zustand abhängig. Wenn dem so ist,<br />

stellt sich erneut die Frage, ob es sinnvoll<br />

ist, das ViewModel als Datenfluss zur View<br />

zu übertragen. Wenn nämlich View und<br />

Aktion ein gemeinsames ViewModel verwenden,<br />

kann die Aktion das ViewModel<br />

manipulieren und muss der View keine<br />

Daten mehr in Form eines Datenflusses<br />

liefern. Schließlich erfolgt die Aktualisierung<br />

der View durch das Data Binding.<br />

Für solche Abhängigkeiten haben Ralf<br />

Westphal und ich die Notation des Flow<br />

Designs um eine weitere Pfeilart ergänzt.<br />

Ein Pfeil mit einem Punkt am Ende steht<br />

für eine Abhängigkeit. Abbildung 3 zeigt,<br />

wie man das ViewModel als gemeinsame<br />

Abhängigkeit zwischen die View und eine<br />

Aktion stellen kann. Abbildung 4 zeigt erneut<br />

das Feature Neues ToDo erzeugen,<br />

diesmal jedoch mit einer Abhängigkeit anstelle<br />

eines Datenflusses.<br />

Auf diese Weise ist es nun bequem möglich,<br />

die Abhängigkeit von einem Zustand<br />

zu modellieren. Durch die Modellierung<br />

des gemeinsamen Zustands als Abhängigkeit<br />

gehen die Datenflüsse in den folgenden<br />

Abbildungen jeweils vom GUI aus zu<br />

den einzelnen Aktionen. Die Aktionen ändern<br />

bei Bedarf das ViewModel, und per<br />

Data Binding gelangen diese Änderungen<br />

zum GUI.<br />

Features realisieren<br />

Beginnen wir mit dem ersten Feature: F1:<br />

Ein neues ToDo hinzufügen. Das Modell<br />

dazu ist in Abbildung 4 zu sehen. Das Feature<br />

ist im Prinzip recht simpel zu modellieren:<br />

Ausgehend von einem Datenfluss<br />

vom GUI zur Aktion Neues ToDo erzeugen<br />

wird das ViewModel um ein neues ToDo<br />

ergänzt, fertig. Durch die gemeinsame Abhängigkeit<br />

von View und Aktion vom View-<br />

Model wird die Änderung im ViewModel<br />

per Data Binding in der View visualisiert.<br />

Bleibt noch die Frage zu klären, auf welchem<br />

Weg GUI und Aktion das ViewModel<br />

initial erhalten. Es muss einmal instanziert<br />

und in die beiden Funktionseinheiten hineingereicht<br />

werden. Ich habe mich entschieden,<br />

dies jeweils im Konstruktor von<br />

GUI und Aktion zu realisieren. Dazu erhalten<br />

die Konstruktoren einen Parameter<br />

vom Typ SharedState. Dass beide Funktionseinheiten<br />

vom ViewModel abhängig<br />

sind, geht aus dem Flow-Design-Modell<br />

hervor. Wie diese Abhängigkeit initialisiert<br />

wird, ist ein Implementationsdetail. Nach<br />

der Modellierung habe ich begonnen, das<br />

Feature zu implementieren. Mir war klar,<br />

dass ich am GUI die meiste Zeit zubringen<br />

werde. Allerdings habe ich mich zunächst<br />

konsequent mit einer Minimalimplementation<br />

des GUIs zufriedengegeben und die<br />

restlichen Funktionseinheiten realisiert,<br />

damit am Ende ein Längsschnitt fertig wird<br />

statt nur ein schickes GUI.<br />

Nach den Minimalimplementationen<br />

standen App-Projekt und Buildskript an.<br />

Das App-Projekt ist dafür zuständig, die<br />

benötigten Funktionseinheiten zu instanzieren<br />

und alles zusammenzustecken<br />

(Build und Bind). Und da es bei komponentenorientierter<br />

Vorgehensweise nicht<br />

eine einzige Solution gibt, die das gesamte<br />

Programm ausspuckt, muss ein Buildskript<br />

her, welches alle Solutions in der richtigen<br />

Reihenfolge übersetzt.<br />

Das hört sich aufwendiger an, als es in<br />

der Praxis ist. Denn das Buildskript besteht<br />

aus den immer gleichen vier Schritten:<br />

1. Übersetzen der Kontrakte,<br />

2. Übersetzen aller Komponenten in beliebiger<br />

Reihenfolge,<br />

3. Ausführen der Tests,<br />

4. Übersetzen der App.<br />

Buildskripte erstelle ich nach wie vor mit<br />

FinalBuilder [1]. Das Übersetzen der Komponenten<br />

erfolgt in einer Schleife, in der<br />

einfach alle Komponentenwerkbänke aufgesammelt<br />

werden. Da die Buildreihenfolge<br />

der Komponenten egal ist, muss dieser<br />

Teil beim Hinzufügen weiterer Komponen-<br />

LÖSUNG<br />

[Abb. 3] View und Aktion sind vom<br />

ViewModel abhängig.<br />

[Abb. 4] Neues ToDo erzeugen.<br />

[Abb. 5] Geöffnetes und geschlossenes ToDo.<br />

ten nicht angepasst werden. Dadurch ist<br />

das Erstellen des Buildskripts ein einmaliger<br />

Vorgang zu Beginn des Projekts.<br />

Das App-Projekt kann erst erstellt werden,<br />

wenn von allen Komponenten zumindest<br />

eine Minimalimplementation vorhanden<br />

ist. Das liegt daran, dass die App alle<br />

Komponenten binär referenzieren muss,<br />

um sie instanzieren zu können. Aus diesem<br />

Grund erstelle ich von allen Komponenten<br />

zunächst eine sogenannte Tracer-Bullet-<br />

Implementation. Die so implementierten<br />

Funktionseinheiten machen noch nicht<br />

wirklich etwas, außer Traceausgaben zu<br />

produzieren. So sind zwei Fliegen mit einer<br />

Klappe geschlagen: Zum einen kann das<br />

App-Projekt erstellt werden, zum anderen<br />

kann man anhand der Traceausgaben bereits<br />

feststellen, ob die einzelnen Funktionseinheiten<br />

korrekt zusammenspielen.<br />

Nachdem ich die einzelnen Funktionseinheiten<br />

implementiert hatte, habe ich<br />

begonnen, das GUI aufzumotzen. Ich<br />

möchte erreichen, dass ein ToDo in der<br />

Liste in zwei verschiedenen Zuständen angezeigt<br />

werden kann:<br />

❚ geöffnet zur Bearbeitung,<br />

❚ geschlossen zum Anzeigen.<br />

Abbildung 5 zeigt die Form mit je einem<br />

geöffneten und einem geschlossenen ToDo.<br />

Im geöffneten Zustand soll der Text des<br />

Eintrags editiert werden können. Ferner<br />

soll man das ToDo mit einer Checkbox als<br />

erledigt kennzeichnen können. In einem<br />

späteren Schritt soll auch die Eingabe von<br />

Tags möglich sein. Somit stand schnell die<br />

www.dotnetpro.de dotnetpro.dojos.2011 79


LÖSUNG<br />

Listing 1<br />

Einen Data Trigger verwenden.<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

Idee im Raum, dazu eine ListView mit zwei<br />

unterschiedlichen DataTemplates zu verwenden.<br />

Die Frage war nur, wie man es erreicht,<br />

dass das DataTemplate in Abhängigkeit<br />

von einer Eigenschaft im ViewModel<br />

ausgewählt wird. Denn meine Idee war,<br />

dass jedes einzelne ToDo eine Eigenschaft<br />

InBearbeitung erhalten soll. Abhängig davon,<br />

ob diese Eigenschaft auf true oder<br />

false gesetzt ist, soll das passende Data-<br />

Template ausgewählt werden.<br />

Die Lösung liegt in der Verwendung eines<br />

Data Triggers, der an die Eigenschaft<br />

im ViewModel gebunden wird. Listing 1<br />

zeigt den relevanten XAML-Ausschnitt.<br />

Um das Tüfteln an der Form zu beschleunigen,<br />

braucht man einen Testrahmen.<br />

Müsste man erst die gesamte Anwendung<br />

vollständig übersetzen, um die Form in Aktion<br />

sehen zu können, wäre die Entwicklung<br />

stark ausgebremst. Die Komponentenorientierung<br />

will ja gerade erreichen, dass<br />

die einzelnen Komponenten isoliert entwi-<br />

Listing 2<br />

Das GUI testen.<br />

[SetUp]<br />

public void Setup() {<br />

toDoListe = new ToDoListe();<br />

toDoListe.ToDos.Add(new ToDo {<br />

Text = "ToDo Nummer 1",<br />

IsSelected = true});<br />

// ...<br />

state = new SharedState();<br />

state.Write(toDoListe);<br />

sut = new Main(state);<br />

sut.ToDo_schliessen += delegate { };<br />

}<br />

[Test, RequiresSTA, Explicit]<br />

public void Liste_mit_ToDos_anzeigen() {<br />

sut.ShowDialog();<br />

}<br />

ckelt werden können. Das soll auch für das<br />

GUI möglich sein. Folglich habe ich in der<br />

GUI-Solution neben dem Projekt mit der<br />

Implementation noch ein Testprojekt angelegt.<br />

In einem normalen NUnit-Test instanziere<br />

ich die Form und fülle sie mit entsprechenden<br />

Testdaten. So ist ein zügiges<br />

Arbeiten möglich, weil zur visuellen Kontrolle<br />

lediglich ein Test gestartet werden<br />

muss. Listing 2 zeigt einen der GUI-Tests.<br />

Da dieser Test mit ShowDialog eine WPF-<br />

Form öffnet und erst weiterläuft, wenn die<br />

Form wieder geschlossen ist, habe ich das<br />

NUnit-Attribut Explicit ergänzt. Es sorgt dafür,<br />

dass der Test nur ausgeführt wird, wenn<br />

er explizit gestartet wird. Beim Ausführen<br />

aller Tests der Assembly, beispielsweise auf<br />

dem Continuous-Integration-Server, werden<br />

die expliziten Tests ignoriert. Des Weiteren<br />

habe ich das Attribut RequiresSTA ergänzt,<br />

damit den Anforderungen von Windows<br />

Forms bzw. WPF entsprochen wird.<br />

Das Austüfteln des XAML-Codes hat am<br />

Ende doch einige Zeit in Anspruch genommen.<br />

Durch die klare Trennung des GUIs<br />

vom Rest der Anwendung, durch Einsatz<br />

der Komponentenorientierung, wäre es jedoch<br />

leicht möglich gewesen, diese Details<br />

von einem erfahrenen WPF-Entwickler<br />

vornehmen zu lassen. Dazu hätte dieser<br />

nicht die gesamte Anwendung benötigt,<br />

sondern lediglich die Visual-Studio-Solution<br />

mit der WPF-Form und dem zugehörigen<br />

Testrahmen.<br />

ToDos bearbeiten<br />

Wenn ein neues ToDo in die Liste aufgenommen<br />

wird, befindet es sich im Modus<br />

Bearbeiten. Dazu ist die Eigenschaft InBearbeitung<br />

im ViewModel auf true gesetzt.<br />

Damit immer nur ein einzelnes ToDo geöffnet<br />

dargestellt wird, muss die View einen<br />

Event auslösen, sobald ein anderes To-<br />

do selektiert wird. Daraufhin muss die In-<br />

Bearbeitung-Eigenschaft des selektierten<br />

Elements auf false geändert werden. Durch<br />

Data Binding sorgt WPF dann dafür, dass<br />

das Listenelement mit dem anderen Data-<br />

Template im geschlossenen Zustand visualisiert<br />

wird.<br />

Umgekehrt muss es möglich sein, ein<br />

vorhandenes ToDo zur Bearbeitung zu öffnen.<br />

Laut Feature F3 soll dazu ein Doppelklick<br />

auf ein ToDo-Element als Interaktion<br />

verwendet werden. Auch hier ist also in der<br />

View ein Event auszulösen, wenn ein Eintrag<br />

der Liste per Doppelklick ausgewählt<br />

wird. Die Umsetzung der Logik ist trivial:<br />

Es muss lediglich die InBearbeitung-Eigenschaft<br />

angepasst werden.<br />

Persistenz<br />

Das nächste Feature hat es in sich: Die To-<br />

Do-Einträge sollen von einem zum anderen<br />

Programmstart erhalten bleiben. Dazu<br />

habe ich zunächst identifiziert, bei welchen<br />

bereits umgesetzten Features die To-<br />

Do-Liste so geändert wird, dass die Daten<br />

erneut persistiert werden müssen. Das ist<br />

bei folgenden Interaktionen der Fall:<br />

❚ Ein neues ToDo wird angelegt.<br />

❚ Der Text eines vorhandenen ToDos wird<br />

geändert.<br />

❚ Die Erledigt-Eigenschaft eines vorhandenen<br />

ToDos wird geändert.<br />

Anschließend habe ich im vorhandenen<br />

Modell nachgesehen, ob diese drei Interaktionen<br />

dort bereits sichtbar sind. Bei zweien<br />

ist das der Fall, lediglich das Ändern der<br />

Erledigt-Eigenschaft über die Checkbox ist<br />

nicht im Modell sichtbar, da dies vollständig<br />

mittels Data Binding gelöst ist. Daher<br />

habe ich im GUI einen ausgehenden Datenfluss<br />

ergänzt, der mitteilt, dass ein ToDo<br />

geändert wurde.<br />

Ausgehend von den Aktivitäten ToDo geändert,<br />

ToDo-Bearbeitung beenden und<br />

Neues ToDo ergänzen wird die Aktivität View-<br />

Model in ToDo-Liste übersetzen gestartet.<br />

Sie mappt das ViewModel auf ein Datenmodell<br />

für die Persistenz. Beide Modelle sind<br />

strikt zu trennen, damit es nicht zu Abhängigkeiten<br />

zwischen View und Persistenz<br />

kommt. Zum Mappen des ViewModels auf<br />

das Datamodell habe ich das Open-Source-<br />

Framework AutoMapper [2] verwendet.<br />

Als letzter Schritt muss das Datenmodell<br />

persistiert werden. Dazu habe ich es nach<br />

XML serialisiert. Weil der XML Serializer<br />

aus dem .NET Framework nur mit konkreten<br />

Typen umgehen kann und beispielsweise<br />

nicht mit IEnumerable klarkommt,<br />

80 dotnetpro.dojos.2011 www.dotnetpro.de


[Abb. 6] Die ToDo-<br />

Listenlogik.<br />

habe ich dazu das Open-Source-Framework<br />

sharpSerializer verwendet [3]. Wenn<br />

der XML Serializer aus dem .NET Framework<br />

Sie mal wieder ärgert, sollten Sie<br />

sharpSerializer ausprobieren.<br />

Zum Speichern gehört auch das Laden<br />

der ToDo-Liste. Die Implementation dieser<br />

Funktionseinheit ging schnell von der<br />

Hand. Eingesetzt wird sie im Mainboard<br />

der Anwendung. Das Mainboard bietet eine<br />

Run-Methode, die beim Starten der App<br />

aufgerufen wird. Auch dieser Schritt lässt<br />

sich im Rahmen von Flow Design generalisieren.<br />

Nach Build, Bind und Inject folgt<br />

Init. In der Beispielanwendung, die Sie auf<br />

der beiliegenden Heft-DVD finden, wird<br />

die Run-Methode des Mainboards allerdings<br />

direkt aufgerufen.<br />

Spaßeshalber habe ich noch einen weiteren<br />

Persistenzmechanismus implementiert.<br />

Statt die ToDo-Liste in eine Datei zu<br />

schreiben, kann man sie auch in einer<br />

Amazon-SimpleDB-Datenbank ablegen.<br />

Das bringt beim Speichern allerdings Latenzzeiten<br />

mit sich. Damit entstand der<br />

Wunsch, das Speichern in den Hintergrund<br />

zu verlagern. Zusätzlich sollten mehrere<br />

Speicheraufträge, die in kurzer zeitlicher<br />

Folge eintreffen, zu einem einzigen zusammengefasst<br />

werden. Das Speichern soll so<br />

lange verzögert werden, bis für eine gewisse<br />

Zeit keine Änderungen mehr eintreffen.<br />

Die Umsetzung dieser Anforderung ist<br />

mit Flow Design und EBC ein Leichtes. Zunächst<br />

habe ich die Funktionseinheit ToDo-<br />

Liste speichern von einem Bauteil in eine<br />

Platine geändert. Anschließend habe ich in<br />

der Platine vor das eigentliche Speichern<br />

einen Standardbaustein Throttle eingesetzt.<br />

Dieser verzögert einen eingehenden<br />

Datenfluss für fünf Sekunden. Trifft das Ereignis<br />

in diesem Zeitraum mehrfach ein,<br />

wird der interne Timer dadurch wieder zurückgesetzt.<br />

Diesen Standardbaustein sowie<br />

einige andere finden Sie im Open-<br />

Source-Projekt ebclang [4].<br />

Um die Benutzerschnittstelle über das<br />

Speichern zu informieren, liefert die Platine<br />

drei Datenflüsse, die anzeigen, dass mit dem<br />

Speichern begonnen wurde, dass es beendet<br />

ist bzw. dass ein Fehler aufgetreten ist. Damit<br />

diese Datenflüsse im GUI verwendet werden<br />

können, um eine Meldung in der Statusleiste<br />

auszugeben, müssen sie auf den GUI-<br />

Thread synchronisiert werden. Auch das<br />

übernehmen Standardbausteine innerhalb<br />

der Platine. Abbildung 6 zeigt den Kern der<br />

Anwendung, die ToDo-Listenlogik.<br />

Das in der ToDo-App eingesetzte Tooling<br />

ist Open Source und unter [4] zu finden.<br />

Die Platinen sind im Contracts-Projekt jeweils<br />

in XML-Dateien beschrieben. Aus<br />

diesen ebc.xml-Dateien wird durch den<br />

ebc.compiler C#-Code generiert. Ein Vorteil<br />

dieser Vorgehensweise liegt darin, dass die<br />

ebc.xml-Dateien visualisiert werden können.<br />

Dadurch ist eine visuelle Kontrolle<br />

möglich, die sicherstellt, dass die Modellierung<br />

korrekt in die ebc.xml-Dateien übernommen<br />

wurde. Zurzeit sehen die generierten<br />

Graphen noch nicht so richtig<br />

schick aus. Aber Ralf Westphal und ich arbeiten<br />

bereits an einem Nachfolger.<br />

ILmerge<br />

Als die Anwendung in Grundzügen fertig<br />

war, dachte ich mir, es sei eine gute Idee,<br />

alle DLLs der App mithilfe von ILmerge zu<br />

einer einzigen EXE-Datei zusammenzufassen.<br />

Das habe ich schon öfter gemacht und<br />

bin dabei nie auf Probleme gestoßen. Diesmal<br />

aber schon. Der Grund: ILmerge versagt<br />

seinen Dienst, sobald WPF-Assemblies<br />

mit im Spiel sind. Das liegt nicht an<br />

ILmerge, sondern daran, dass WPF beim<br />

Laden von Ressourcen vollqualifizierte Assemblynamen<br />

verwendet. Diese ändern<br />

sich dummerweise durch ILmerge.<br />

Aber da es ja darum ging, etwas zu lernen,<br />

habe ich mich nicht damit abgefunden,<br />

sondern nach Abhilfe gesucht. Die Lösung<br />

findet sich im Hinweistext zu ILmerge,<br />

LÖSUNG<br />

in dem dasWPF-Problem beschrieben wird.<br />

Hier steht auch gleich der Link zu einer Lösung<br />

[5]. Allerdings funktionierte die dort<br />

beschriebene Lösung nicht auf Anhieb. Das<br />

lag daran, dass bei mehrfachem Laden derselben<br />

Assembly mehrere Instanzen der Assembly<br />

geliefert wurden. Ein kleiner Cache,<br />

implementiert mit einem Dictionary, schafft<br />

Abhilfe. Die ILmerge-Alternative basiert darauf,<br />

dass alle erforderlichen DLLs als Ressource<br />

in das EXE-Projekt eingebettet werden.<br />

Ein Assembly-Loader, der die Assemblies<br />

aus der Ressource holt, sorgt dafür,<br />

dass die Anwendung komplett in der EXE-<br />

Datei enthalten ist. Wer sich das Verfahren<br />

genauer anschauen mag, sollte die Solution<br />

im Verzeichnis source.app öffnen.<br />

www.dotnetpro.de dotnetpro.dojos.2011 81<br />

Fazit<br />

Die Umsetzung der kompletten ToDo-App<br />

hat länger gedauert als geplant. Das lag an<br />

drei Bereichen:<br />

❚ Das Modellieren mit MVVM und Abhängigkeiten<br />

brauchte noch etwas Feinschliff.<br />

❚ Der XAML-Code des GUIs ist recht aufwendig<br />

geraten.<br />

❚ Es gab Probleme mit ILmerge.<br />

Trotz der Herausforderungen hatte ich<br />

zwischendurch immer wieder Versionen,<br />

die ich hätte liefern können. Konsequentes<br />

Fokussieren auf Längsschnitte sowie schrittweises<br />

Verfeinern sind Schlüssel zum entspannten<br />

Arbeiten. [ml]<br />

[1] Best of Breed, Die Lieblingstools der dotnetpro-Autoren,<br />

dotnetpro 3/2011, S.16,<br />

www.dotnetpro.de/A1103LieblingsTools<br />

[2] AutoMapper, http://automapper.codeplex.com/<br />

[3] sharpSerializer,<br />

http://www.sharpserializer.com/<br />

[4] Event-Based Components Tooling,<br />

http://ebclang.codeplex.com/<br />

[5] Microsoft Research, ILMerge,<br />

http://research.microsoft.com/enus/people/mbarnett/ilmerge.aspx


GRUNDLAGEN<br />

MVVM und Flow-Design kombinieren<br />

Alles unter einem Hut<br />

Das Data Binding von WPF bietet beeindruckende Möglichkeiten. MVVM ist für WPF-Anwendungen das geeignete<br />

Konzept. Flow-Design erlaubt eine sehr natürliche Art der Modellierung. Und Event-Based Components stellen<br />

ein universales Konzept für Modellierung und Implementierung. dotnetpro zeigt, wie Sie diese Konzepte gemeinsam<br />

nutzen können.<br />

Auf einen Blick<br />

Stefan Lieser ist Berater und<br />

Trainer und hat mit Ralf Westphal<br />

die Initiative „Clean Code<br />

Developer“ ins Leben gerufen.<br />

Sie erreichen ihn unter<br />

stefan@lieser-online.de oder<br />

unter lieser-online.de/blog.<br />

Inhalt<br />

➡ Flow-Design für den Entwurf<br />

einer grafischen Oberfläche<br />

verwenden.<br />

➡ Model und View in Abhängigkeit<br />

von einer einzelnen<br />

Instanz des ViewModels<br />

modellieren.<br />

➡ Die Phasen des Konzepts der<br />

Event-Based Components bei<br />

der Implementierung umsetzen.<br />

dnpCode<br />

A1105MVVM<br />

Für Anwendungen mit<br />

einer visuellen Benutzerschnittstelle<br />

stellt<br />

Alles ist im Fluss<br />

An dieser Stelle kommt das<br />

Flow-Design ins Spiel. Flowsich<br />

die zentrale Frage, wie die<br />

Design ist eine Art der Model-<br />

Daten in die View gelangen.<br />

lierung, bei der Datenflüsse die<br />

Die gleiche Frage stellt sich auf<br />

zentrale Rolle spielen. Die Da-<br />

dem Rückweg: Wie gelangen<br />

ten fließen hier zwischen<br />

die vom Benutzer eingegebe-<br />

Funktionseinheiten, die in der<br />

nen Daten von der View zur<br />

Regel in Form einer Aktion be-<br />

Programmlogik? Ferner: Wie<br />

schrieben sind. Wenn also bei-<br />

werden Kommandos des Bespielsweise<br />

in einer Anwennutzers<br />

mitgeteilt? Seit das Dadung<br />

zur Pflege von Kundenta<br />

Binding mit der Einführung<br />

daten eine Änderung an einer<br />

von WPF deutlich an Leis-<br />

Kundenadresse vorgenommen<br />

tungsfähigkeit gewonnen hat,<br />

werden soll, fließen die geän-<br />

liegt es auf der Hand, diese<br />

derten Adressdaten von der<br />

Leistungsfähigkeit auch auszu-<br />

Benutzerschnittstelle zu einer<br />

nutzen. Zwar verfügte auch<br />

Funktionseinheit Kundenadres-<br />

schon Windows Forms über<br />

se ändern. Von dort fließen die<br />

Data-Binding-Funktionalität, [Abb. 1] Ein Datenfluss mit GUI.<br />

Daten weiter zu einer Aktion<br />

doch hat sich der Einsatz nicht<br />

Geänderte Kundendaten per-<br />

wirklich durchgesetzt.<br />

sistieren, um in einer Daten-<br />

Wer bei WPF oder Silverlight die Möglichkeiten bank abgelegt zu werden. Abbildung 1 zeigt das<br />

des Data Bindings einmal gesehen hat, wird auf Modell dazu.<br />

ihren Einsatz nicht verzichten wollen. So kann Dieser Fluss von Daten und das Aneinander-<br />

beispielsweise eine Textbox mit einer Dropreihen von Aktionen ist eine sehr natürliche Art<br />

downliste verbunden werden, und der Daten- der Modellierung. Das liegt daran, dass es einem<br />

austausch erfolgt in beiden Richtungen. Wer das Denken in Prozessschritten sehr nahe kommt.<br />

ohne Data Binding selbst implementieren müss- Denn wenn wir uns als Entwickler eine Anfordete,<br />

wäre längere Zeit beschäftigt.<br />

rung wie das skizzierte Ändern einer Kunden-<br />

Akzeptieren wir also für den Moment, dass das adresse anschauen, analysieren wir meist ge-<br />

Model-View-ViewModel-Pattern (MVVM) im Zudanklich, aus welchen Prozessschritten das<br />

sammenhang mit WPF und Silverlight gesetzt ist, Feature besteht. Diese Prozessschritte sofort als<br />

auch wenn eine detailliertere Betrachtung des Modell zu verwenden, liegt nahe.<br />

Patterns erst später folgt [1] [2]. Trotz MVVM Die Umsetzung solcher Flow-Designs in eine<br />

bleibt aber die Frage offen, wie der Rest der An- Implementation ist mithilfe von Event-Based<br />

wendung strukturiert wird. Schließlich muss ir- Components (EBC) möglich. Dabei liegt der grogendwer<br />

das ViewModel mit Daten befüllen beße Vorteil in der Tatsache, dass das Modell 1:1 in<br />

ziehungsweise vom Benutzer eingegebene Da- Code übersetzt werden kann. Man findet also alten<br />

verarbeiten. Zu glauben, mit MVVM wäre der le Artefakte des Modells leicht im Code wieder.<br />

gesamte Aufbau einer Anwendung bereits vorge- Und umgekehrt gilt auch, dass alle Artefakte der<br />

geben, ist ein Trugschluss, denn die Anwen- Implementation leicht im Modell zu verorten<br />

dungslogik dürfte den Hauptteil jeder nicht tri- sind. Damit kann ein Flow-Design-Modell tatvialen<br />

Anwendung ausmachen. MVVM ist nur sächlich die vornehmste Aufgabe eines Modells<br />

ein Pattern für bestimmte Teile der Anwendung übernehmen, die darin besteht, zu abstrahieren.<br />

und gibt noch nicht das Modell für den wesent- Das Modell ist eine abstrakte Darstellung der Imlich<br />

umfangreicheren Rest der Anwendung vor. plementation. Damit dient das Modell einerseits<br />

82 dotnetpro.dojos.2011 www.dotnetpro.de


als Vorlage für die Implementation, andererseits<br />

auch als Dokumentation derselben.<br />

Ohne den Einsatz von MVVM ist das<br />

Einbeziehen der Benutzerschnittstelle in<br />

das Flow-Design ganz leicht: Daten fließen<br />

von der Benutzerschnittstelle zur Logik<br />

und werden dort verarbeitet. Und in vielen<br />

Fällen fließt aus der Logik ein Resultat zurück<br />

zur Benutzerschnittstelle, um dort visualisiert<br />

zu werden. Doch wie bezieht<br />

man nun Data Binding und ViewModels in<br />

das Flow-Design ein?<br />

ViewModels<br />

Dazu muss zunächst geklärt werden, was<br />

es mit dem ViewModel auf sich hat. Ein<br />

ViewModel enthält alle Daten, die eine<br />

View darstellen soll. Das ViewModel hat<br />

keine Abhängigkeit zu irgendeiner Infrastruktur<br />

wie etwa WPF oder Silverlight. Es<br />

sind einfache Klassen, die sich gut automatisiert<br />

testen lassen. Weil die Testbarkeit ein<br />

unverzichtbares Kriterium bei der modernen<br />

Softwareentwicklung darstellt, ist es<br />

wichtig, diesen Vorteil deutlich herauszustellen.<br />

Wenn das Ergebnis einer Logikoperation<br />

direkt in einer WPF-View angezeigt<br />

wird, ist es ungleich schwerer, dies automatisiert<br />

zu testen. Ist das Ergebnis der<br />

Operation jedoch ein ViewModel, das quasi<br />

aus der Operation hinausfließt, sind diese<br />

Tests sehr leicht zu automatisieren. Daraus<br />

folgt eine wichtige Erkenntnis: Views<br />

sollen die Daten nicht deuten müssen.<br />

Dann wäre nämlich in der View Code enthalten,<br />

der getestet werden muss. Liegen<br />

die Daten aber bereits im ViewModel fix<br />

und fertig aufbereitet vor, sodass die View<br />

diese Daten lediglich anzeigt, ohne sie erst<br />

deuten zu müssen, ist das Testen einfach.<br />

Dazu ein Beispiel: Häufig sind Controls in<br />

einer View nur unter bestimmten Umständen<br />

eingeschaltet. Es kann sein, dass ein<br />

Control zwar immer sichtbar ist, die Eingabe<br />

jedoch deaktiviert ist. Es kann aber auch<br />

sein, dass eine Gruppe von Controls nur in<br />

einem bestimmten Kontext angezeigt wird.<br />

Hier stellt sich also die Frage, wer dafür verantwortlich<br />

ist, Controls abhängig von einem<br />

Zustand auszublenden. Würde dies<br />

durch die View bewerkstelligt, indem dort<br />

eine entsprechende Prüfung des Zustands<br />

mit anschließendem Deaktivieren der Controls<br />

programmiert ist, wäre dies problematisch<br />

in Bezug auf die Testbarkeit. Viel leichter<br />

zu testen ist ein solches Szenario, wenn<br />

die zu testenden Daten bereits im ViewModel<br />

vorhanden sind. Dort sollte also für dieses<br />

Szenario für jedes Control eine Eigenschaft<br />

vorhanden sein, die anzeigt, ob das<br />

Listing 1<br />

Ein Control ein- und ausblenden.<br />

Control deaktiviert werden muss beziehungsweise<br />

gar nicht angezeigt werden soll.<br />

Listing 1 zeigt dazu ein Beispiel.<br />

Mittels ViewModel und Data Binding<br />

lässt sich in der View der Effekt erzielen,<br />

dass das Control nur aktiviert ist, wenn die<br />

Eigenschaft im ViewModel true ist.<br />

<br />

Der Vorteil dieser Vorgehensweise liegt<br />

darin, dass die Deutung des Zustands bereits<br />

durch die Logikeinheit erfolgt. Ferner<br />

lässt sich das Ergebnis im ViewModel ablesen<br />

und damit leicht automatisiert testen.<br />

Auch die View lässt sich auf diese Weise<br />

leicht testen. Im Test können einfach<br />

ViewModels mit unterschiedlichen Daten<br />

instanziert und an die View übergeben werden.<br />

Anschließend wird die View mit Show-<br />

Dialog angezeigt und visuell überprüft. So<br />

ist sichergestellt, dass auch die in XAML formulierten<br />

Data Bindings schnell überprüft<br />

werden können. Es ist nicht mehr notwendig,<br />

die komplette Anwendung zu starten<br />

und mit Testdaten zu füttern, um die unterschiedlichen<br />

visuellen Effekte der Views zu<br />

überprüfen.<br />

View und Logik verbinden<br />

Das ViewModel bildet die Verbindung zwischen<br />

View und Logik. Im MVVM-Pattern<br />

wird die Logik als Modell bezeichnet. Das<br />

finde ich irreführend, weil im Rahmen des<br />

Entwurfs das Ergebnis der Modellierung<br />

GRUNDLAGEN<br />

public class ViewModel : INotifyPropertyChanged {<br />

private bool kontonummerEnabled;<br />

public bool KontonummerEnabled {<br />

get { return kontonummerEnabled; }<br />

set {<br />

kontonummerEnabled = value;<br />

PropertyChanged(this, new PropertyChangedEventArgs("KontonummerEnabled"));<br />

}<br />

}<br />

public event PropertyChangedEventHandler PropertyChanged = delegate { };<br />

}<br />

[Abb. 2] ViewModel als Datenfluss austauschen.<br />

ebenfalls Modell heißt. Daher spreche ich<br />

lieber von Logik. Eigentlich müsste es dann<br />

also Logik-View-ViewModel heißen.<br />

View und Logik sind beide vom ViewModel<br />

abhängig. Dadurch wird eine saubere<br />

Trennung zwischen View und Logik erreicht,<br />

denn zwischen ihnen gibt es keine<br />

direkte Abhängigkeit. Allerdings bleibt<br />

noch die Frage zu klären, wie beim Flow-<br />

Design das ViewModel konkret von View<br />

und Logik zu verwenden ist.<br />

Eine Möglichkeit wäre, das ViewModel<br />

als einen Datenfluss zwischen View und<br />

Logik aufzufassen. Das bedingt allerdings,<br />

dass View und Logik entweder jeweils ein<br />

neues ViewModel instanzieren oder einer<br />

von beiden dieses als Zustand hält. In beiden<br />

Fällen wird dies aus einem reinen Datenflussdiagramm<br />

nicht deutlich. Abbildung<br />

2 zeigt das an einem Beispiel.<br />

Hier ist nicht ersichtlich, ob es sich beim<br />

Datenfluss um ein und dasselbe ViewModel<br />

handelt oder ob es jeweils eine neue Instanz<br />

ist. Abhilfe schafft eine Erweiterung<br />

der Flow-Design-Diagramme. DasViewModel<br />

stellt eine Abhängigkeit dar. SowohlView<br />

als auch Logik sind vom ViewModel abhängig.<br />

Und wenn sich beide auf dieselbe Instanz<br />

beziehen würden, wäre es nicht mehr<br />

nötig, das ViewModel in Form eines Datenflusses<br />

zwischen beiden zu transportieren.<br />

Abbildung 3 zeigt das im Beispiel. Der Datenfluss<br />

von View zu Logik dient nun lediglich<br />

dazu, die Logik über die Interaktion zu<br />

informieren, damit dort die entsprechende<br />

www.dotnetpro.de dotnetpro.dojos.2011 83


GRUNDLAGEN<br />

Funktionalität ausgeführt wird. Der Datenfluss<br />

trägt allerdings keine Daten mehr, was<br />

durch das leere Klammerpaar ausgedrückt<br />

wird. Stattdessen sind nun View und Logik<br />

vom ViewModel abhängig, was durch die<br />

„Pfeile“ mit Kringel am Ende angezeigt wird.<br />

Ein weiterer Vorteil dieser Vorgehensweise<br />

liegt darin, dass das ViewModel leicht eine<br />

Instanz sein kann, die nur einmalig beim<br />

Öffnen der View erzeugt wird. Damit wird<br />

das Data Binding erleichtert, da das Binden<br />

nur einmalig erfolgen muss. Fließt jeweils<br />

eine neue Instanz des ViewModels von der<br />

Logik zur View, muss dieses jeweils neu gebunden<br />

werden. Dieser Nachteil wird beseitigt,<br />

wenn View und Logik die ganze Zeit<br />

auf demselben ViewModel arbeiten.<br />

Um zu sehen, wie das ViewModel zu den<br />

beiden davon abhängigen Funktionseinheiten,<br />

View und Logik, gelangt, ist es notwendig,<br />

darzustellen, wie Funktionseinheiten instanziert<br />

und verbunden werden. Das Konzept<br />

der Event-Based Components sieht<br />

mehrere Phasen vor, die durchlaufen werden,<br />

bevor Daten zwischen den Funktionseinheiten<br />

fließen können:<br />

❚ Build,<br />

❚ Bind,<br />

❚ Inject,<br />

❚ Config,<br />

❚ Run.<br />

Die Build-Phase ist dafür verantwortlich,<br />

Funktionseinheiten zu instanzieren. Für<br />

Bauteile ist dies ganz einfach, da die Konstruktoren<br />

von Bauteilen keine Parameter<br />

haben. Das liegt daran, dass Bauteile keine<br />

Abhängigkeiten aufweisen. Bei Platinen ist<br />

es jedoch erforderlich, die Funktionseinheiten,<br />

die von der Platine verbunden werden,<br />

vor der Platine zu instanzieren, da diese im<br />

Konstruktor an die Platine übergeben werden.<br />

Die Build-Phase kann entweder manuell<br />

ausprogrammiert werden oder von einem<br />

Dependency-Injection-Container wie<br />

StructureMap übernommen werden.<br />

[Abb. 3] View und Logik sind vom ViewModel abhängig.<br />

[Abb. 4] MVVM mit Flow verheiratet.<br />

Aufgabe der Platinen ist es, Funktionseinheiten<br />

zu verbinden, indem Input- und<br />

Output-Pins zusammengesteckt werden.<br />

Dazu müssen Input-Pin-Methoden an<br />

Output-Pin-Events gebunden werden. Insofern<br />

erfolgt die Bind-Phase innerhalb der<br />

Build-Phase in den jeweiligen Konstruktoren<br />

der Platinen.<br />

In der Inject-Phase geht es darum, die<br />

Abhängigkeiten in die betroffenen Funktionseinheiten<br />

zu injizieren. Hier ist es<br />

wichtig, zu differenzieren. Es geht nicht<br />

um die Abhängigkeiten von Platinen zu<br />

den in ihnen enthaltenen Funktionseinheiten.<br />

Diese sind ja bereits durch die<br />

Build-Phase abgehandelt und wurden<br />

durch Konstruktorinjektion aufgelöst. In<br />

der Inject-Phase geht es um Abhängigkeiten,<br />

die explizit als solche modelliert wurden.<br />

Diese werden nun durch Aufruf einer<br />

Methode in die abhängigen Funktionseinheiten<br />

hineingereicht.<br />

In der vorletzten Phase, der Config-Phase,<br />

sind alle Funktionseinheiten bereits betriebsbereit.<br />

Sie sind verdrahtet und ihre<br />

Abhängigkeiten sind erfüllt. Bevor die Anwendung<br />

am sogenannten Entry Point<br />

startet, mögen manche Funktionseinheiten<br />

noch eine Konfiguration oder Initialisierung<br />

benötigen. So könnte es beispielsweise<br />

notwendig sein, die Daten der Anwendung<br />

durch einen Persistenzmechanismus<br />

zu laden.<br />

Zum Schluss ist eine Funktionseinheit dafür<br />

zuständig, die Ausführung der Anwendung<br />

zu beginnen. Meist ist dies das Hauptformular<br />

der Anwendung. Diese Funktionseinheit<br />

stellt eine Run-Methode bereit.<br />

Für die Phasen Inject, Config und Run<br />

sollten Interfaces verwendet werden, um<br />

diese Aspekte bei beliebigen Funktionseinheiten<br />

definieren zu können. Listing 2 zeigt<br />

das Interface für die Inject-Phase. In der Inject-Phase<br />

müssen alle Funktionseinheiten<br />

aufgesucht werden, die IDependsOn<br />

implementieren. Diesen muss dann die er-<br />

forderliche Abhängigkeit durch Aufruf der<br />

Inject-Methode übergeben werden.<br />

Nun haben wir für die gemeinsame Verwendung<br />

eines ViewModels alles zusammen.<br />

Beim Programmstart werden alle<br />

Funktionseinheiten instanziert und die Datenflüsse<br />

verbunden. Anschließend werden<br />

die ViewModels an den entsprechenden<br />

Stellen injiziert und die Anwendung kann<br />

loslaufen.<br />

Die View weist das ViewModel dem DataContext<br />

zu. Dadurch werden die im<br />

XAML-Code definierten Bindings wirksam.<br />

Ändert eine Funktionseinheit Daten im<br />

ViewModel, werden diese Änderungen<br />

durch das Data Binding visualisiert. Ändert<br />

der Benutzer gebundene Daten über die<br />

Listing 2<br />

Interface für die Inject-Phase.<br />

public interface IDependsOn {<br />

void Inject(T independent);<br />

}<br />

Listing 3<br />

Interface für Commands.<br />

public interface ICommand {<br />

event EventHandler CanExecuteChanged;<br />

bool CanExecute(object parameter);<br />

void Execute(object parameter);<br />

}<br />

Listing 4<br />

Interface eines Kommandos.<br />

public interface IFlowCommand {<br />

event Action ExecuteAction;<br />

void SetCanExecute(bool newValue);<br />

}<br />

84 dotnetpro.dojos.2011 www.dotnetpro.de


entsprechenden Controls, landen die Änderungen<br />

im ViewModel. Doch genau an<br />

dieser Stelle fehlt noch ein kleiner Baustein:<br />

Wie signalisiert eine View der zugehörigen<br />

Logik eine Interaktion? Wie sind<br />

Buttons, Toolbars und Menüs an die Logik<br />

anzubinden?<br />

Kommandos<br />

WPF verwendet das Konzept der Commands.<br />

Dahinter steht das Interface ICommand,<br />

siehe Listing 3. Ein Kommando<br />

muss zunächst eine Execute-Methode bereitstellen,<br />

die von WPF aufgerufen wird,<br />

um das Kommando auszuführen. Natürlich<br />

sollte der Code, der für das Kommando<br />

auszuführen ist, auf keinen Fall im Kommando<br />

abgelegt werden. Das wäre nicht<br />

viel besser, als würde die Logik direkt in der<br />

View untergebracht. Um den IsEnabled-<br />

Zustand eines Buttons oder Menüpunkts<br />

passend setzen zu können, ruft WPF die<br />

CanExecute-Funktion auf. Diese muss true<br />

liefern, wenn das Kommando ausgeführt<br />

werden kann. Ändert sich der Zustand von<br />

CanExecute, muss dies durch CanExecute-<br />

Changed signalisiert werden.<br />

Ein Kommando, welches ICommand implementiert,<br />

kann in WPF per Data Binding<br />

gebunden werden. Für einen Button sieht<br />

das beispielsweise so aus:<br />

Überweisung<br />

<br />

Doch wie bringt man ein Kommando mit<br />

einem Flow zusammen? Dazu muss das<br />

Kommando einen Ausgang haben, an dem<br />

ein Datenfluss beginnen kann. Ausgehende<br />

Flüsse werden bei EBC mit Events implementiert.<br />

Folglich muss das Kommando bei<br />

Aufruf der Execute-Methode einen Event<br />

auslösen, der dann einen Datenfluss startet.<br />

Umgekehrt muss es möglich sein, durch einen<br />

eingehenden Datenfluss zu bestimmen,<br />

ob ein Kommando weiterhin eingeschaltet<br />

ist. Eingehende Datenflüsse werden bei EBC<br />

durch Methoden realisiert. Somit sieht das<br />

Interface eines Kommandos aus EBC-Sicht<br />

so aus, wie es Listing 4 zeigt.<br />

ExecuteAction ist der vom Kommando<br />

ausgehende Datenfluss, der initiiert werden<br />

muss, wenn WPF die Execute-Methode aufruft.<br />

SetCanExecute ist der eingehende Datenfluss,<br />

mit dem ein Logikbauteil das Kommando<br />

ein- oder ausschalten kann. Eine Implementation<br />

der beiden Interfaces ICommand<br />

und IFlowCommand zeigt Listing 5.<br />

Damit kann das Kommando nun sowohl<br />

WPF-konform verwendet werden als auch<br />

Listing 5<br />

ICommand und IFlowCommand nutzen.<br />

public class FlowCommand : ICommand, IFlowCommand<br />

{<br />

private bool canExecute = true;<br />

public void Execute(object parameter) {<br />

ExecuteAction();<br />

}<br />

public bool CanExecute(object parameter) {<br />

return canExecute;<br />

}<br />

public event EventHandler CanExecuteChanged = delegate { };<br />

public event Action ExecuteAction = delegate { };<br />

}<br />

Listing 7<br />

Datenflüsse verdrahten.<br />

GRUNDLAGEN<br />

public class Mainboard<br />

{<br />

public Mainboard(MainWindow mainWindow, Logik logik, FlowCommand überweisungCmd) {<br />

überweisungCmd.ExecuteAction += logik.ÜberweisungAktivieren;<br />

logik.ÜberweisungAktiviert += überweisungCmd.SetCanExecute;<br />

}<br />

public void SetCanExecute(bool newValue) {<br />

canExecute = newValue;<br />

CanExecuteChanged(this, EventArgs.Empty);<br />

}<br />

Listing 6<br />

Kommandos mit im ViewModel ablegen.<br />

public class ViewModel : INotifyPropertyChanged<br />

{<br />

private bool kontonummerEnabled;<br />

private string kontonummer;<br />

}<br />

public ViewModel() {<br />

ÜberweisungCmd = new FlowCommand();<br />

}<br />

public FlowCommand ÜberweisungCmd { get; private set; }<br />

public bool KontonummerEnabled {<br />

get { return kontonummerEnabled; }<br />

set {<br />

kontonummerEnabled = value;<br />

PropertyChanged(this, new PropertyChangedEventArgs("KontonummerEnabled"));<br />

}<br />

}<br />

public string Kontonummer {<br />

get { return kontonummer; }<br />

set {<br />

kontonummer = value;<br />

PropertyChanged(this, new PropertyChangedEventArgs("Kontonummer"));<br />

}<br />

}<br />

}<br />

www.dotnetpro.de dotnetpro.dojos.2011 85


GRUNDLAGEN<br />

in einem Datenfluss stehen. Für die WPF-<br />

Seite ist ICommand zuständig, für die EBC-<br />

Seite IFlowCommand.<br />

Für das Data Binding ist es sinnvoll, die<br />

Kommandos mit im ViewModel abzulegen.<br />

So kann der DataContext der Form für alle<br />

Bindings verwendet werden. Das ViewModel<br />

für ein einfaches Beispiel zeigt Listing 6.<br />

Auf diese Weise können in der XAML-<br />

Datei die Eigenschaften ÜberweisungCmd,<br />

Kontonummer und KontonummerEnabled<br />

gebunden werden.<br />

Listing 8<br />

Build, Bind und Inject.<br />

var mainWindow = new MainWindow();<br />

var logik = new Logik();<br />

var viewModel = new ViewModel();<br />

var mainBoard = new Mainboard(mainWindow,<br />

logik, viewModel.ÜberweisungCmd);<br />

mainWindow.Inject(viewModel);<br />

logik.Inject(viewModel);<br />

Auf der EBC-Seite werden View und Logik<br />

sowie das Kommando in eine Platine injiziert.<br />

Die Platine kann dann die Datenflüsse<br />

verdrahten, siehe Listing 7. Hier wird das<br />

Kommando so verdrahtet, dass es mit dem<br />

Eingang ÜberweisungAktivieren der Logik<br />

verbunden ist. Die Logik-Funktionseinheit<br />

kann daraufhin die notwendige Funktionalität<br />

ausführen. Durch die Verbindung<br />

von ÜberweisungAktiviert zu SetCanExecute<br />

wird nach Ausführung des Kommandos<br />

durch die Logik bestimmt, ob das Kommando<br />

nach wie vor eingeschaltet bleibt.<br />

Nach Build und Bind wird das ViewModel<br />

in die betroffenen Bauteile injiziert.<br />

Build, Bind und Inject sehen damit so aus<br />

wie in Listing 8. Abbildung 4 zeigt, wie die<br />

Funktionseinheiten zusammenspielen.<br />

Die View ist abhängig vom ViewModel.<br />

Dies ist notwendig, damit die View per Data<br />

Binding auf das ViewModel zugreifen<br />

kann. Auch die Logik ist vom ViewModel<br />

abhängig, damit sie Daten zur Visualisierung<br />

im ViewModel ablegen und Benutzereingaben<br />

von dort entnehmen kann.<br />

Zusätzlich zu den Abhängigkeiten steht<br />

ein Teil des ViewModels, nämlich die darin<br />

enthaltenen Kommandos, im Flow. Dabei<br />

können Datenflüsse zum einen von Kommandos<br />

ausgehen, um eine Benutzerinteraktion<br />

zu signalisieren. Zum anderen können<br />

sie im Kommando enden, um ein<br />

Kommando ein- oder auszuschalten.<br />

Fazit<br />

Durch die „Hochzeit“ zwischen MVVM<br />

und Flows steht einer WPF-konformen<br />

Umsetzung von Anwendungen, die auf Datenflüssen<br />

basieren, nichts mehr im Weg.<br />

So können Sie die tollen Möglichkeiten des<br />

Data Bindings voll ausschöpfen. Und natürlich<br />

können so auch die tollen Möglichkeiten<br />

des Flow-Designs und der Umsetzung<br />

mit Event-Based Components zum<br />

Einsatz kommen. [ml]<br />

[1] Torsten Zimmermann, Ein Rahmen für Feinheiten,<br />

Die Funktionsweise von MVVM-Frameworks<br />

für WPF, dotnetpro 1/2011, Seite 14 ff.,<br />

www.dotnetpro.de/A1101FrameworkTest<br />

[2] Torsten Zimmermann, Geschickt verbunden,<br />

Microsofts Framework für das Entwurfsmuster<br />

MVVM, dotnetpro 1/2011, Seite 40 ff.,<br />

www.dotnetpro.de/A1101WAF


Synchronisation über die Cloud<br />

Was steht in den Wolken?<br />

AUFGABE<br />

Über die Cloud wurde genügend spekuliert. Es wird Zeit, sie konkret anzuwenden. Stefan, kannst du zu diesem<br />

wolkigen Thema eine möglichst handfeste Übung stellen?<br />

Die Idee zu dieser Übung hängt mit<br />

der Aufgabe im vorhergehenden<br />

Heft zusammen, deren Lösung auf<br />

den folgenden Seiten präsentiert<br />

wird. Dort geht es um eine To-do-Listenanwendung,<br />

die ihre Daten lokal in einer Datei persistiert.<br />

Dass die Daten lokal persistiert werden, hat<br />

den großen Vorteil, dass sie auch dann zur Verfügung<br />

stehen, wenn das entsprechende Gerät<br />

mal gerade nicht mit dem Internet verbunden<br />

ist. Dennoch besteht oft der Wunsch, die Daten<br />

über die Cloud mit einem anderen Gerät zu synchronisieren.<br />

Diese Möglichkeit fehlt mir beispielsweise bei<br />

Things [1], einer To-do-Listenanwendung für<br />

Mac und iPhone/iPad. Leider kommt der Hersteller<br />

dieser Software schon seit mehreren Monaten<br />

nicht dem Versprechen nach, eine Synchronisation<br />

über die Cloud anzubieten. Zwar<br />

kann man die Daten per WLAN synchronisieren.<br />

Viel einfacher wäre aber eine Synchronisation<br />

über einen Service in der Cloud: Dann könnte<br />

die Software im Hintergrund selbstständig synchronisieren,<br />

und zwar unabhängig davon, ob<br />

die betreffenden Geräte gerade eingeschaltet<br />

sind oder nicht.<br />

Durch die Wolke stechen<br />

Doch wie schwierig ist die Realisierung einer solchen<br />

Synchronisation? Das herauszufinden ist<br />

die Übung für diesen Monat. Es geht dabei um<br />

einen sogenannten Spike. Ein Spike hat den Erkenntnisgewinn<br />

zum Ziel. Es geht also nicht darum,<br />

einen produktionsreifen Synchronisationsdienst<br />

zu entwickeln, sondern darum herauszufinden,<br />

wie ein solcher technisch realisierbar wäre.<br />

Um Daten über die Cloud zu synchronisieren,<br />

benötigt man einen Datenspeicher in der Cloud.<br />

Dieser ist von allen Clients aus erreichbar und<br />

kann somit verwendet werden, um darüber Daten<br />

auszutauschen. Auf der einen Seite schreibt<br />

ein Client seine Änderungen in diesen Cloudspeicher.<br />

Auf der anderen Seite holt sich ein anderer<br />

Client aus dem Cloudspeicher die Änderungen<br />

des ersten Clients. Beide Clients arbeiten<br />

also mit denselben Daten. Am Ende darf allerdings<br />

kein Client die Änderungen des anderen<br />

überschreiben. Findet ein Client in der Cloud<br />

neue oder geänderte Daten, müssen diese in den<br />

lokalen Speicher eingepflegt werden. Ferner<br />

muss der Client seine lokalen Änderungen an<br />

den Cloudspeicher melden.<br />

Natürlich können beim Synchronisieren Konflikte<br />

entstehen. Das passiert, wenn beide Clients<br />

dieselben Daten ändern und dann synchronisieren.<br />

Solche Konflikte sollten mindestens erkannt<br />

werden. Die Lösung des Konflikts könnte darin<br />

bestehen, den Anwender entscheiden zu lassen,<br />

welchen Stand er als aktuell akzeptieren möchte.<br />

Treten solche Konflikte häufig auf, könnte die Lösung<br />

darin bestehen, die Änderungen zusammenzufassen.<br />

Dies ist allerdings relativ aufwendig<br />

und vor allem nicht generisch lösbar. Es wird<br />

hier nicht weiter betrachtet.<br />

Als Cloudspeicher kann beispielsweise Amazon<br />

SimpleDB [2] zum Einsatz kommen. Dieser<br />

Service bietet ausreichend kostenlose Kapazitäten,<br />

um damit zu experimentieren. Mit dem<br />

Open-Source-Framework Simple Savant [3] steht<br />

ferner ein einfach zu bedienendes API zur Verfügung.<br />

Wer mag, kann auch eine Lösung mit<br />

Windows Azure versuchen [4].<br />

Bleibt am Ende die Frage, wie man die Synchronisation<br />

algorithmisch löst. Klar ist, dass jeder<br />

Datensatz über eine eindeutige ID verfügen<br />

muss. Nimmt man dazu einen GUID, ist sichergestellt,<br />

dass die IDs auch über mehrere Clients<br />

hinweg eindeutig sind. So ist es schon mal einfach<br />

zu erkennen, ob ein Datensatz in der Cloud<br />

und/oder lokal vorhanden ist.<br />

Für die Synchronisation der Änderungen sind<br />

Fälle interessant, in denen sowohl in der Cloud<br />

als auch lokal eineVersion der Daten vorliegt. Um<br />

dann zu erkennen, in welcher Richtung ein Update<br />

erfolgen muss, benötigt man eine Versionsnummer<br />

der Daten. Viel Spaß beim Tüfteln! [ml]<br />

[1] Cultured Code, Things,<br />

http://culturedcode.com/things/<br />

[2] Amazon SimpleDB,<br />

http://aws.amazon.com/de/simpledb/<br />

[3] Simple Savant, http://simol.codeplex.com/<br />

[4] Windows Azure, http://www.microsoft.com/<br />

germany/net/WindowsAzure/<br />

dnpCode: A1105DojoAufgabe<br />

In jeder dotnetpro finden Sie<br />

eine Übungsaufgabe von<br />

Stefan Lieser, die in maximal<br />

drei Stunden zu lösen sein<br />

sollte. Wer die Zeit investiert,<br />

gewinnt in jedem Fall – wenn<br />

auch keine materiellen Dinge,<br />

so doch Erfahrung und Wissen.<br />

Es gilt:<br />

❚ Falsche Lösungen gibt es<br />

nicht. Es gibt möglicherweise<br />

elegantere, kürzere<br />

oder schnellere Lösungen,<br />

aber keine falschen.<br />

❚ Wichtig ist, dass Sie reflektieren,<br />

was Sie gemacht<br />

haben. Das können Sie,<br />

indem Sie Ihre Lösung mit<br />

der vergleichen, die Sie<br />

eine Ausgabe später in<br />

dotnetpro finden.<br />

Übung macht den Meister.<br />

Also − los geht’s. Aber Sie<br />

wollten doch nicht etwa<br />

sofort Visual Studio starten…<br />

www.dotnetpro.de dotnetpro.dojos.2011 87<br />

Wer übt, gewinnt


LÖSUNG<br />

Synchronisation über die Cloud<br />

Nicht ohne meine Wolke<br />

Seine Daten will man am liebsten überall von verschiedenen Geräten aus verfügbar haben. Kein Problem, wenn man<br />

sie über die Cloud synchronisiert. Und das ist gar nicht so schwer.<br />

dnpCode: A1106DojoLoesung<br />

Stefan Lieser ist Softwareentwickler<br />

aus Leidenschaft. Nach<br />

seinem Informatikstudium mit<br />

Schwerpunkt auf Softwaretechnik<br />

hat er sich intensiv mit Patterns<br />

und Principles auseinandergesetzt.<br />

Er arbeitet als Berater und<br />

Trainer, hält zahlreiche Vorträge<br />

und hat gemeinsam mit<br />

Ralf Westphal die Initiative Clean<br />

Code Developer ins Leben<br />

gerufen. Sie erreichen ihn unter<br />

stefan@lieser-online.de oder<br />

lieser-online.de/blog.<br />

Die Synchronisierung von Daten wird<br />

immer wichtiger. Immer mehr Anwender<br />

nutzen Smartphones und Tablets,<br />

die ihrerseits immer leistungsfähiger werden.<br />

Damit wächst der Wunsch, alle relevanten Daten<br />

auch offline, also ohne Verbindung zum Internet,<br />

auf dem Gerät zur Verfügung zu haben. Aber natürlich<br />

müssen die Daten mit anderen Geräten<br />

synchronisiert werden, da die meisten Nutzer<br />

von Smartphones wohl zusätzlich einen Arbeitsplatzrechner<br />

und/oder ein Notebook verwenden.<br />

Ein zweiter Trend verstärkt die Nachfrage nach<br />

Synchronisationslösungen: die Cloud. Dienste in<br />

der Cloud werden ebenfalls immer leistungsfähiger,<br />

gleichzeitig sinken die Preise. Was liegt also<br />

näher, als die Synchronisation der Daten über die<br />

Cloud auszuführen? Die Idee dabei: Alle beteiligten<br />

Geräte synchronisieren sich mit einem Dienst,<br />

der in der Cloud läuft, siehe Abbildung 1.<br />

Dadurch muss man nicht mehr zwei Geräte<br />

miteinander verbinden, um Daten zu synchronisieren.<br />

Denn das ist lästig. Wenn ich mit dem<br />

Notebook arbeite, möchte ich alle Änderungen<br />

von dort in die Cloud synchronisieren. Anschließend<br />

„Deckel zu“ und Smartphone raus: Die Daten<br />

sollen nun von der Cloud zum Smartphone<br />

übertragen werden. Dabei kann zwischen dem<br />

Wechsel von einem zum anderen Gerät auch mal<br />

ein längerer Zeitraum vergehen. Das ist komfortabler,<br />

als wenn man zum Synchronisieren beide<br />

Geräte gleichzeitig verfügbar haben muss.<br />

Damit sich die Geräte synchronisieren können,<br />

müssen die Daten über die Cloud erreich-<br />

[Abb. 1] Daten über einen Cloud-Service synchronisieren. [Abb. 2] Synchronisieren als Flow.<br />

bar sein. Das bedingt noch nicht, dass sie in der<br />

Cloud gespeichert werden. Ein Webservice, der<br />

über das Internet erreichbar ist und der seine<br />

Daten auf einem firmeneigenen Server ablegt,<br />

kann den Zweck ebenso erfüllen. Damit die Lösung<br />

hier nicht zu umfangreich ausfällt, habe ich<br />

mich jedoch dazu entschieden, direkt auf einen<br />

Datenspeicher in der Cloud zu setzen. Dadurch<br />

ist es nicht erforderlich, eine eigene Infrastruktur<br />

in der Cloud aufzubauen. Der Datenspeicher<br />

muss lediglich über das Internet erreichbar sein.<br />

Ein zusätzlicher Webservice entfällt. Die Clients<br />

greifen zum Synchronisieren direkt auf den Speicherservice<br />

in der Cloud zu.<br />

Amazon SimpleDB<br />

Wie in der Aufgabenstellung angedeutet, setzte<br />

ich bei meiner Lösung auf Amazon SimpleDB.<br />

Amazon bietet kostenlos ein ausreichend großes<br />

monatliches Kontingent an, sodass bei den Experimenten<br />

keine Kosten anfallen. Man muss<br />

sich lediglich für die Nutzung von SimpleDB bei<br />

Amazon anmelden [1]. Als API habe ich das<br />

Open-Source-Framework Simple Savant [2] verwendet.<br />

Es vereinfacht das Speichern und Laden<br />

von Objekten im SimpleDB Storage.<br />

Synchronisation<br />

Doch ehe wir zu den Details des Cloud-Speichers<br />

kommen, muss eine Strategie entwickelt werden,<br />

nach der die Synchronisation erfolgen soll. Es ist<br />

naheliegend, dass die einzelnen Datensätze einen<br />

eindeutigen Identifier benötigen. Und natürlich ist<br />

88 dotnetpro.dojos.2011 www.dotnetpro.de


es naheliegend, dazu den .NET-Datentyp<br />

Guid zu verwenden. Der Vorteil: Das Erzeugen<br />

einer ID für einen neuen Datensatz<br />

kann dezentral erfolgen, da der Guid-Algorithmus<br />

sicherstellt, dass die generierten<br />

IDs eindeutig sind.<br />

Für den Synchronisationsvorgang habe<br />

ich die einzelnen zu synchronisierenden<br />

Datensätze zusätzlich mit einer Versionsnummer<br />

versehen. Durch diese Nummer<br />

kann erkannt werden, ob Daten zwischenzeitlich<br />

auf einem anderen Client aktualisiert<br />

wurden: Ist die lokal vorhandene Version<br />

kleiner als die in der Cloud, muss offensichtlich<br />

eine Übertragung von der<br />

Cloud in den lokalen Speicher erfolgen.<br />

Damit das Verfahren funktioniert, muss sichergestellt<br />

werden, dass die Versionsnummer<br />

beim Speichern einer Änderung in der<br />

Cloud jeweils erhöht wird. Nur beim Speichern<br />

in der Cloud darf die Versionsnummer<br />

verändert werden! So können Clients<br />

leicht feststellen, ob in der Cloud eine<br />

neuere Version eines Datensatzes existiert.<br />

Allerdings genügt die Versionsnummer<br />

noch nicht, um alle möglichen Fälle abzudecken.<br />

Zusätzlich muss jeder Client an<br />

den Datensätzen festhalten, ob sie lokal geändert<br />

wurden. Sind die Versionsnummern<br />

nämlich gleich und liegen lokale Änderungen<br />

vor, müssen diese zur Cloud übertragen<br />

werden. Alle Szenarien sind in der Tabelle<br />

1 abgebildet.<br />

Die beiden ersten Fälle sind einfach:<br />

Wenn Daten lokal, aber nicht entfernt vorhanden<br />

sind, muss der Datensatz in die<br />

Cloud übertragen und dort eingefügt werden.<br />

Sind die Daten umgekehrt nur in der<br />

Cloud, aber nicht lokal vorhanden, müssen<br />

sie lokal eingefügt werden.<br />

Der dritte Fall liegt vor, wenn Daten lokal<br />

geändert wurden. Beide Versionsnummern<br />

sind gleich, das heißt, lokal lag vor den Änderungen<br />

der Stand vor, der derzeit in der<br />

Cloud liegt. Daher müssen die lokalen Änderungen<br />

in die Cloud übertragen werden.<br />

Wurden Daten auf einem anderen Client<br />

geändert und in die Cloud übertragen, liegt<br />

der vierte Fall vor: Die entfernte Versionsnummer<br />

ist höher als die lokale, und es<br />

gibt lokal keine Änderungen. In diesem Fall<br />

werden die Änderungen aus der Cloud lokal<br />

übernommen.<br />

Im fünften Fall sind die Versionsnummern<br />

gleich, und es liegen lokal keine Änderungen<br />

vor. In diesem Fall ist nichts zu tun.<br />

Beim sechsten Fall kommt es zu einem<br />

Konflikt: Hier liegen Änderungen sowohl<br />

lokal wie entfernt vor. Die lokalen Änderungen<br />

werden am Changed Flag erkannt,<br />

[Tabelle 1] Mögliche Fälle<br />

bei der Synchronisation.<br />

die entfernten daran, dass entfernt eine<br />

höhere Versionsnummer als lokal vorliegt.<br />

In diesem Konfliktfall müssen die Änderungen<br />

zusammengeführt werden, sofern<br />

dies möglich ist. Im einfachsten Fall wird<br />

der Anwender informiert und um eine Entscheidung<br />

gebeten, welche Daten herangezogen<br />

werden sollen.<br />

Zuletzt bleiben noch zwei Fälle in der Tabelle<br />

übrig, die nicht eintreten können,<br />

wenn alles richtig implementiert ist: Die lokale<br />

Versionsnummer ist größer als die entfernte.<br />

Dieser Fall kann nicht eintreten, sofern<br />

die Versionsnummer nur beim Spei-<br />

Listing 1<br />

Definition der Platine.<br />

<br />

LÖSUNG<br />

chern von Daten auf dem entfernten System<br />

erhöht wird.<br />

Löschen<br />

Die Tabelle sieht einfach und übersichtlich<br />

aus, die Implementation dürfte eigentlich<br />

keine Schwierigkeiten bereiten. Doch der<br />

Teufel steckt im Detail: Das Verfahren eignet<br />

sich nicht für das Synchronisieren von<br />

Löschungen. Überhaupt ist das Löschen<br />

von Daten die größte Herausforderung<br />

beim Synchronisieren.<br />

Dazu ein Beispiel: Zunächst wird auf<br />

dem Client ein Datensatz angelegt und in<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

<br />

www.dotnetpro.de dotnetpro.dojos.2011 89


LÖSUNG<br />

Listing 2<br />

Key und Passwort auslesen.<br />

public class Secrets {<br />

public string AwsAccessKeyId { get; set; }<br />

public string AwsSecretAccessKey { get; set; }<br />

public static Secrets LoadFromFile(string filename) {<br />

var serializer = new SharpSerializer();<br />

return (Secrets)serializer.Deserialize(filename);<br />

}<br />

public void SaveToFile(string filename) {<br />

var serializer = new SharpSerializer();<br />

serializer.Serialize(this, filename);<br />

}<br />

}<br />

Listing 3<br />

Das Simple-Savant-API initialisieren.<br />

var secrets = Secrets.LoadFromFile("aws.secrets");<br />

var config = new SavantConfig {<br />

ReadConsistency = ConsistencyBehavior.Immediate<br />

};<br />

Savant = new SimpleSavant(secrets.AwsAccessKeyId, secrets.AwsSecretAccessKey, config);<br />

Listing 4<br />

Festlegen der zu speichernden Attribute.<br />

var itemNameMapping = AttributeMapping.Create("Id", typeof(Guid));<br />

itemMapping = ItemMapping.Create(domainName, itemNameMapping);<br />

itemMapping.AttributeMappings.Add(AttributeMapping.Create("Text", typeof(string)));<br />

itemMapping.AttributeMappings.Add(AttributeMapping.Create("Erledigt", typeof(bool)));<br />

itemMapping.AttributeMappings.Add(AttributeMapping.Create("Version", typeof(int)));<br />

itemMapping.AttributeMappings.Add(AttributeMapping.Create("Deleted", typeof(bool)));<br />

die Cloud synchronisiert. Damit steht der<br />

Datensatz nun mit der gleichen Versionsnummer<br />

lokal und entfernt zur Verfügung.<br />

Wird er nun lokal gelöscht und anschließend<br />

synchronisiert, landen wir bei Fall<br />

zwei der Tabelle: Der Datensatz wird aus<br />

der Cloud wieder zum Client übertragen,<br />

das Löschen damit rückgängig gemacht.<br />

Ich habe die Synchronisation in die To-<br />

Do-Listenanwendung integriert, die ich für<br />

die Übung aus dem vorhergehenden Heft<br />

erstellt habe. Für das Synchronisieren der<br />

Löschungen habe ich die Anwendung zunächst<br />

so umgestellt, dass das Löschen<br />

nicht hart auf den Daten ausgeführt wird,<br />

sondern nur soft, indem ein Marker in den<br />

Daten gesetzt wird. BeimVisualisieren ignoriert<br />

der Client alle als gelöscht markierten<br />

Daten. Allerdings werden die gelöschten<br />

Daten weiterhin in der Datendatei abge-<br />

legt. Damit ist das Problem der Löschsynchronisation<br />

auf das normale Synchronisieren<br />

von Änderungen zurückgeführt.<br />

Auf dem Client führt beim Synchronisieren<br />

von Löschungen ohnehin kein Weg daran<br />

vorbei, über die Löschungen bis zum<br />

nächsten Synchronisieren Buch zu führen.<br />

Insofern ist das softe Löschen eine guteVorbereitung<br />

für eine leistungsfähigere Implementation.<br />

Schließlich würden sich im Laufe<br />

der Zeit einige Daten ansammeln, wenn<br />

die als gelöscht markierten Daten nie entfernt<br />

würden. In der vorliegenden Implementation<br />

werden einfach alle als gelöscht<br />

markierten Daten dauerhaft gespeichert<br />

und synchronisiert. In einem späteren<br />

Schritt kann man dies so erweitern, dass die<br />

als gelöscht markierten Daten nur in der<br />

Cloud gehalten werden. Auf den Clients<br />

könnten die Löschungen nach dem Syn-<br />

chronisieren entfernt werden, wenn sichergestellt<br />

ist, dass diese Daten beim Synchronisieren<br />

von der Cloud zum Client nicht<br />

wieder auf dem Client angelegt werden.<br />

Show me your Code!<br />

Doch nun zur Implementation. Zunächst<br />

habe ich die Synchronisation der ToDo-<br />

Liste auf relativ hohem Abstraktionsniveau<br />

durch einen Flow abgebildet. In diesem<br />

Flow werden die lokalen sowie die entfernten<br />

Daten gelesen und dann zu einer synchronisierten<br />

Liste zusammengefasst. Diese<br />

zusammengefasste Liste wird anschließend<br />

lokal und in der Cloud gespeichert.<br />

Ferner wird sie in das ViewModel gemappt,<br />

um im GUI visualisiert zu werden.<br />

Abbildung 2 zeigt den Flow. Alle Pfeile in<br />

der Abbildung repräsentieren Datenflüsse,<br />

Abhängigkeiten sind durch eine Verbindungslinie<br />

mit einem Punkt am Ende dargestellt.<br />

Die Abbildung ist aus einer XML-<br />

Datei generiert. Gleichzeitig werden aus<br />

dieser Datei die Interfaces für alle Funktionseinheiten<br />

und die Implementation der<br />

Platinen generiert. Das dabei verwendete<br />

Tooling ist im Open-Source-Projekt ebclang<br />

unter [3] zu finden. Listing 1 zeigt die<br />

Definition der Platine.<br />

Das Synchronisieren beginnt, indem die<br />

lokale und die entfernte ToDo-Liste geladen<br />

werden. Die beiden Ergebnisse der Ladevorgänge<br />

werden durch einen Join-Baustein<br />

zu einem Tuple zusammengefasst.<br />

Ganz wichtig bei diesem Join: Erst wenn<br />

beide Eingänge über Daten verfügen, wird<br />

der Datenstrom am Ausgang in Gang gesetzt.<br />

Bei einem erneuten Durchlaufen der<br />

Synchronisation müssen wieder beide Eingänge<br />

des Joins anliegen. Daher wird hier<br />

ein ResetJoin-Baustein aus dem ebclang-<br />

Projekt verwendet. Bei diesem müssen jedes<br />

Mal beide Eingänge anliegen, bevor<br />

der Ausgang freigeschaltet wird. So ist sichergestellt,<br />

dass bei jedem Synchronisationsvorgang<br />

sowohl die lokalen als auch die<br />

entfernten Daten ermittelt werden.<br />

Der zweite wichtige Aspekt des Flows<br />

liegt in der Abhängigkeit des Bauteils To-<br />

Do_Listen_synchronisieren von SharedState.<br />

Diese Abhängigkeit ist im<br />

Kontext der ToDo-Listenanwendung erforderlich,<br />

damit das Modell der ToDo-Liste<br />

im Speicher aktualisiert wird. Dieses Modell<br />

repräsentiert den gesamten Zustand<br />

der Anwendung.<br />

Bauteile implementieren<br />

Nachdem das Flow-Design der Synchronisation<br />

erstellt war, habe ich begonnen, die<br />

90 6. 2011 www.dotnetpro.de


Listing 5<br />

Die ToDo-Liste aus dem Cloud-Speicher lesen.<br />

einzelnen Bauteile zu implementieren. Das<br />

Laden der lokalen ToDo-Liste aus einer Datei<br />

war schon fertig und konnte wiederverwendet<br />

werden. Auch das lokale Speichern<br />

und das Übersetzen in das ViewModel hatte<br />

ich bereits im Rahmen der ToDo-Anwendung<br />

implementiert. Wer die Lösung<br />

der ToDo-Listenanwendung im vorhergehenden<br />

Heft studiert hat, sollte sich die<br />

aktuelle Version der Anwendung noch mal<br />

anschauen − es hat sich einiges geändert.<br />

Zur Implementation standen also an:<br />

❚ Laden und Speichern der entfernten<br />

ToDo-Liste in einer SimpleDB-Datenbank<br />

in der Cloud.<br />

❚ Das eigentliche Synchronisieren zweier<br />

Listen.<br />

Über den Wolken<br />

Beginnen wir beim Laden und Speichern<br />

in der Cloud. Um Amazons SimpleDB nutzen<br />

zu können, muss man bei jedem API-<br />

Aufruf einen Key und ein Passwort übergeben.<br />

Simple Savant erwartet beide als Konstruktorparameter,<br />

sodass man die Angaben<br />

nur an einer Stelle hinterlegen muss.<br />

Natürlich haben solch vertrauliche Daten<br />

nichts im Quellcode zu suchen, sondern<br />

müssen in eine Konfigurationsdatei ausgelagert<br />

werden. Und tun Sie sich gleich den<br />

Gefallen, den Dateinamen in die Ignore-<br />

Liste der Versionsverwaltung aufzunehmen.<br />

Sonst erhöht sich das Risiko, dass Ihre<br />

Zugangsdaten plötzlich nicht mehr so<br />

geheim sind, wie sie es verdient haben.<br />

Für das Lesen der Zugangsdaten habe<br />

ich eine Klasse implementiert, die einen<br />

XML-Serialisierer verwendet, um die Daten<br />

aus der Datei zu lesen, siehe Listing 2.<br />

Damit ist die Initialisierung der Simple<br />

Savant API ganz einfach, siehe Listing 3.<br />

Danach wird definiert, welche Attribute in<br />

den SimpleDB-Datensätzen abgelegt wer-<br />

den sollen, siehe Listing 4. Anschließend<br />

können Sie bereits Objekte im Cloud-Speicher<br />

ablegen. Das Lesen der ToDo-Liste<br />

aus dem Cloud-Speicher zeigt Listing 5.<br />

Das Schreiben der Daten in den Cloud-<br />

Speicher sieht ähnlich aus. Den kompletten<br />

Quellcode für den Cloud-Speicherzugriff<br />

LÖSUNG<br />

private IEnumerable LoadToDos() {<br />

using (new ConsistentReadScope()) {<br />

var selectStatement = new SelectCommand(simpleDb.ItemMapping, string.Format("select * from {0}", simpleDb.DomainName));<br />

var results = simpleDb.Savant.SelectAttributes(selectStatement);<br />

foreach (var propertyValues in results) {<br />

var toDo = (ToDo)PropertyValues.CreateItem(simpleDb.ItemMapping, typeof(ToDo), propertyValues);<br />

Trace.TraceInformation(" Id: {0}", toDo.Id);<br />

yield return toDo;<br />

}<br />

}<br />

}<br />

Listing 6<br />

ToDo-Listen synchronisieren.<br />

finden Sie auf der beiliegenden Heft-DVD. Er<br />

befindet sich innerhalb des Projektverzeichnisses<br />

in der Solution source\todo.simpledbadapter\todo.simpledbadapter.sln.<br />

Ferner wird in der kommenden dotnetpro<br />

ein Artikel zum Einsatz von SimpleDB<br />

erscheinen.<br />

public void Process(Tuple message) {<br />

Trace.TraceInformation("ToDo_Listen_synchronisieren.Process");<br />

Trace.TraceInformation(" Local count: {0}", message.Item1.ToDos.Count());<br />

Trace.TraceInformation(" Remote count: {0}", message.Item2.ToDos.Count());<br />

var distinctIds = GetDistinctIds(message.Item1.ToDos, message.Item2.ToDos);<br />

var pairs = GetPairs(distinctIds, message.Item1.ToDos, message.Item2.ToDos);<br />

var actions = GetSyncActions(pairs);<br />

var result = GetResult(actions);<br />

sharedState.Write(result);<br />

Result(result);<br />

}<br />

Listing 7<br />

LINQ nutzen.<br />

internal static IEnumerable GetDistinctIds(IEnumerable localToDos,<br />

IEnumerable remoteToDos) {<br />

return (from l in localToDos select l.Id)<br />

.Union(from r in remoteToDos select r.Id)<br />

.Distinct()<br />

.ToList();<br />

}<br />

internal static IEnumerable GetPairs(IEnumerable<br />

distinctIds, IEnumerable localToDos, IEnumerable remoteToDos) {<br />

return (from id in distinctIds<br />

let local = localToDos.FirstOrDefault(x => x.Id == id)<br />

let remote = remoteToDos.FirstOrDefault(x => x.Id == id)<br />

select new Tuple(local, remote))<br />

.ToList();<br />

}<br />

www.dotnetpro.de 6. 2011 91


LÖSUNG<br />

Listing 8<br />

Das Ermitteln der Synchronisationsaktion testen.<br />

[TestFixture]<br />

public class SyncResultTests {<br />

private readonly Guid guid_1 =<br />

new Guid("11111111-1111-1111-1111-111111111111");<br />

private readonly Guid guid_2 =<br />

new Guid("22222222-2222-2222-2222-222222222222");<br />

[Test]<br />

public void Only_local_data_found() {<br />

var syncResult = Syncing.GetSyncResult(<br />

new ToDo(),<br />

null);<br />

Assert.That(syncResult, Is.EqualTo(SyncResult.InsertRemote));<br />

}<br />

[Test]<br />

public void Only_remote_data_found() {<br />

var syncResult = Syncing.GetSyncResult(<br />

null,<br />

new ToDo());<br />

Assert.That(syncResult, Is.EqualTo(SyncResult.InsertLocal));<br />

}<br />

[Test]<br />

public void Local_changes() {<br />

var syncResult = Syncing.GetSyncResult(<br />

new ToDo { Id = guid_1, Version = 1, Changes = true},<br />

new ToDo { Id = guid_1, Version = 1 });<br />

Assert.That(syncResult, Is.EqualTo(SyncResult.UpdateRemote));<br />

}<br />

[Test]<br />

public void No_local_changes() {<br />

var syncResult = Syncing.GetSyncResult(<br />

new ToDo { Id = guid_1, Version = 1 },<br />

new ToDo { Id = guid_1, Version = 1 });<br />

Assert.That(syncResult, Is.EqualTo(SyncResult.Nothing));<br />

}<br />

[Test]<br />

public void Local_and_remote_changes() {<br />

Synchronisieren<br />

Das Synchronisieren arbeitet auf zwei Listen.<br />

Eine enthält den lokalen Stand der Daten, die<br />

andere den Stand der Daten in der Cloud.<br />

Um die Listen zu synchronisieren, müssen<br />

immer je zwei zusammengehörige Einträge<br />

verglichen werden. Dabei sind zunächst<br />

drei Fälle zu unterscheiden:<br />

❚ ein Datensatz ist nur lokal vorhanden,<br />

❚ ein Datensatz ist nur entfernt vorhanden,<br />

❚ ein Datensatz ist sowohl lokal als auch<br />

entfernt vorhanden.<br />

Schon bei dieser ersten Überlegung wird<br />

klar, dass der komplette Vorgang der Synchronisation<br />

der beiden Listen nicht sinnvoll<br />

in einer einzigen Methode unterzubringen<br />

ist. Ferner ist die Aufgabenstellung zu<br />

kompliziert, um sofort draufloszucodieren.<br />

Nachdenken hilft bekanntlich, also habe<br />

ich vor der Implementation einen weiteren<br />

Flow entworfen. Abbildung 3 zeigt, wie die<br />

beiden Listen synchronisiert werden.<br />

Im ersten Schritt (GetDistinctIds) wird eine<br />

Liste aller IDs gebildet, die in den beiden<br />

Datenlisten auftreten. Die Liste der IDs<br />

wird im zweiten Schritt (GetPairs) verwendet,<br />

um jeweils Paare von Datensätzen zu<br />

bilden. Dabei werden jeweils die Datensätze<br />

aus der lokalen und entfernten Liste mit<br />

gleicher ID zusammengestellt.<br />

Der dritte Schritt (GetSyncActions) ermittelt<br />

zu jedem der Paare die Aktion, die auszuführen<br />

ist. Dabei werden die in der Tabelle<br />

gezeigten Regeln umgesetzt. Zuletzt (GetRe-<br />

var syncResult = Syncing.GetSyncResult(<br />

new ToDo {Id = guid_1, Version = 1, Changes = true},<br />

new ToDo {Id = guid_1, Version = 2});<br />

Assert.That(syncResult, Is.EqualTo(SyncResult.Conflict));<br />

}<br />

[Test]<br />

public void Remote_changes() {<br />

var syncResult = Syncing.GetSyncResult(<br />

new ToDo { Id = guid_1, Version = 1 },<br />

new ToDo { Id = guid_1, Version = 2 });<br />

Assert.That(syncResult, Is.EqualTo(SyncResult.UpdateLocal));<br />

}<br />

[Test]<br />

public void Invalid_local_version_with_local_changes() {<br />

Assert.Throws(() =><br />

Syncing.GetSyncResult(<br />

new ToDo {Version = 2, Changes = true},<br />

new ToDo {Version = 1}));<br />

}<br />

[Test]<br />

public void Invalid_local_version_without_local_changes() {<br />

Assert.Throws(() =><br />

Syncing.GetSyncResult(<br />

new ToDo {Version = 2},<br />

new ToDo {Version = 1}));<br />

}<br />

[Test]<br />

public void Different_ids() {<br />

Assert.Throws(() =><br />

Syncing.GetSyncResult(<br />

new ToDo {Id = guid_1},<br />

new ToDo {Id = guid_2}<br />

));<br />

}<br />

92 dotnetpro.dojos.2011 www.dotnetpro.de<br />

}<br />

sult) werden die ermittelten Aktionen auf<br />

das zugehörige Paar angewandt, und es entsteht<br />

eine synchronisierte Liste. Der dargestellte<br />

Datenfluss ist kein astreiner Flow,<br />

das sei hier zugestanden. Die Umsetzung habe<br />

ich mit Methoden gelöst, siehe Listing 6.<br />

Der Vorteil dieser Aufteilung auf Methoden<br />

liegt in jedem Fall in der Testbarkeit.<br />

Die einzelnen Schritte der Synchronisation<br />

lassen sich isoliert testen. Am Ende genügen<br />

dann wenige Integrationstests, die den<br />

Datenfluss vollständig durchlaufen.<br />

[Abb. 3] Zwei Listen<br />

synchronisieren.


In den einzelnen Methoden habe ich<br />

LINQ wieder einmal schätzen gelernt. Get-<br />

DistinctIds und GetPairs sind mit LINQ<br />

schnell implementiert, siehe Listing 7.<br />

Die Musik spielt dann am Ende an zwei<br />

Stellen: beim Ermitteln der Synchronisationsaktion<br />

und beim Zusammenstellen der<br />

Ergebnisliste. Die Synchronisationsaktion<br />

habe ich testgetrieben stur nach der Tabelle<br />

umgesetzt: ersten Fall der Tabelle genommen,<br />

in einem Test abgebildet, implementiert;<br />

zweiten Fall abgebildet, implementiert.<br />

Und so fort. Das ging leicht von der Hand.<br />

Und wie sich später zeigte, war dieser Teil<br />

auch von Anfang an korrekt. Auch die anderen<br />

Einzelteile waren sofort korrekt. Lediglich<br />

beim Join habe ich anfangs den falschen<br />

verwendet, weshalb mehrfaches<br />

Synchronisieren nicht auf Anhieb funktionierte.<br />

Listing 8 zeigt die Tests für das Ermitteln<br />

der Synchronisationsaktion.<br />

Die Umsetzung besteht dann nur aus ein<br />

paar Bedingungen, die in der richtigen Reihenfolge<br />

überprüft werden müssen, siehe<br />

Listing 9. Für das Zusammenstellen der Ergebnisliste<br />

müssen nun die Aktionen jeweils<br />

pro Datenpaar ausgeführt werden,<br />

siehe Listing 10.<br />

Je nach ermittelter Aktion wird der lokale<br />

oder der entfernte Datensatz in die Ergebnisliste<br />

übernommen. Synchronisationskonflikte<br />

habe ich hier nicht berücksichtigt,<br />

da dazu eine Interaktion mit dem Benutzer<br />

erforderlich ist. Die Konflikte müssten also<br />

vor dem Bilden der Ergebnisliste behandelt<br />

werden. Doch die Übung ist ohnehin wieder<br />

etwas länglich geraten, daher habe ich<br />

die Konfliktbehandlung weggelassen.<br />

Fazit<br />

Das Synchronisieren von Datenbeständen<br />

mehrerer Clients mittels eines Cloud-Speichers<br />

ist kein Hexenwerk. Die Lösung hat<br />

wenige Stunden in Anspruch genommen.<br />

Sie ist sicher noch nicht robust genug, um in<br />

ein Produkt aufgenommen zu werden. Doch<br />

der Schritt dahin ist nicht wirklich aufwendig.<br />

Für den Einsatz einer Synchronisation<br />

in einem Produkt will man dem Benutzer<br />

sicher auch nicht zumuten, seine Amazon-<br />

SimpleDB-Credentials zurVerfügung zu stellen.<br />

Das heißt, dass man einen Webservice<br />

ergänzen müsste, über den die Synchronisation<br />

erfolgt. Auch das ist nicht mit Zauberei<br />

verbunden. Ich frage mich also am<br />

Ende weiterhin, wieso einzelne Produkte<br />

trotz lange zurückliegender Ankündigung<br />

immer noch keine Cloud-Synchronisation<br />

anbieten. An der technischen Herausforderung<br />

kann es jedenfalls nicht liegen.<br />

Listing 9<br />

Datensätze synchronisieren.<br />

LÖSUNG<br />

public class Syncing {<br />

public static SyncResult GetSyncResult(ToDo local, ToDo remote) {<br />

if (remote == null) {<br />

return SyncResult.InsertRemote;<br />

}<br />

if (local == null) {<br />

return SyncResult.InsertLocal;<br />

}<br />

if (local.Id != remote.Id) {<br />

throw new InvalidOperationException("Ids must be the same");<br />

}<br />

if (local.Version == remote.Version) {<br />

if (local.Changes) {<br />

return SyncResult.UpdateRemote;<br />

}<br />

return SyncResult.Nothing;<br />

}<br />

if (local.Version < remote.Version) {<br />

if (local.Changes) {<br />

return SyncResult.Conflict;<br />

}<br />

return SyncResult.UpdateLocal;<br />

}<br />

throw new InvalidOperationException("Version numbers are corrupted");<br />

}<br />

}<br />

Listing 10<br />

Ergebnisliste zusammenstellen.<br />

private static IEnumerable GetResultList(IEnumerable tuples) {<br />

foreach (var tuple in tuples) {<br />

if (tuple.Item1 == SyncResult.InsertLocal || tuple.Item1 == SyncResult.UpdateLocal) {<br />

Trace.TraceInformation(" Local Id: {0}, Sync: {1}", tuple.Item2.Item2.Id,<br />

tuple.Item1);<br />

tuple.Item2.Item2.Changes = false;<br />

yield return tuple.Item2.Item2;<br />

}<br />

else if (tuple.Item1 == SyncResult.InsertRemote || tuple.Item1 ==<br />

SyncResult.UpdateRemote) {<br />

Trace.TraceInformation(" Remote Id: {0}, Sync: {1}", tuple.Item2.Item1.Id,<br />

tuple.Item1);<br />

tuple.Item2.Item1.Version++;<br />

tuple.Item2.Item1.Changes = false;<br />

yield return tuple.Item2.Item1;<br />

}<br />

else if (tuple.Item1 == SyncResult.Nothing) {<br />

Trace.TraceInformation(<br />

" Nothing Id: {0}, Sync: {1}", tuple.Item2.Item1.Id, tuple.Item1);<br />

yield return tuple.Item2.Item1;<br />

}<br />

}<br />

}<br />

Ein weiterer Aspekt ist mir bei der<br />

Übung erneut aufgefallen: Durch die Modellierung<br />

und Umsetzung mit Flows lässt<br />

sich die Anwendung einfach erweitern. Die<br />

Wiederverwendung einzelner Bausteine<br />

war problemlos möglich, und die Integra-<br />

tion der zusätzlichen Funktionalität war<br />

leicht. [ml]<br />

[1] http://aws.amazon.com/de/simpledb<br />

[2] http://simol.codeplex.com<br />

[3] http://ebclang.codeplex.com<br />

www.dotnetpro.de dotnetpro.dojos.2011 93


Imprint<br />

Dojos für Entwickler<br />

Stefan Lieser, Tilman Börner<br />

published by: epubli GmbH, Berlin,<br />

www.epubli.de<br />

Copyright: © 2012 Stefan Lieser, Tilman Börner<br />

Impressum


GRUNDLAGEN<br />

Coding Dojo : Mit Spaß lernen<br />

Lass uns einen lernen gehen<br />

Auch nach der Ausbildung gehört Lernen zum Berufsbild des Softwareentwicklers. Aber Lernen kann auch richtig<br />

Spaß machen: In lockerer Runde eine Programmieraufgabe lösen macht Laune.<br />

W<br />

as fällt Ihnen bei folgenden zwei Begriffen<br />

ein: Übung und Meister? Genau,<br />

das ist der Spruch, den wir suchen.<br />

Auch bei der Programmierung gilt, dass stetesTrainieren<br />

wichtig ist. Ja, man kann sogar sagen,<br />

dass Lernen wie in keiner anderem Branche überlebenswichtig<br />

ist. In kaum einem anderen Gebiet<br />

dreht sich das Rad mit neuen Technologien so<br />

schnell wie in der heiligen Softwareentwicklung.<br />

„Ja, das stimmt, aber ich übe jeden Tag, wenn ich<br />

für unsere Kunden Software schreibe“, mag Ihr<br />

Kommentar lauten. Das ist aber nicht richtig. Wer<br />

lernen will, muss spielen und ausprobieren.<br />

Spielen heißt im Fall von Programmieren<br />

schlicht, sich auch mal an etwas anderem versuchen,<br />

was nicht zur täglichen Arbeit gehört.<br />

Benötigen Sie in Ihrer täglichen Arbeit je Asynchronizität?<br />

Oder wie viele Male haben Sie schon<br />

die Rx [1] benutzt? Sind Sie fit mit Event-Based<br />

Components, oder wissen Sie, wie Sie Code tatsächlich<br />

clean machen? Mit TDD alles klar?<br />

Projekte, mit denen Sie im weitesten Sinne Ihr<br />

Einkommen verdienen, sind meist viel zu groß,<br />

um einen sinnvollen Anreiz für das Spielen zu geben.<br />

Eine Lernübung hingegen sollte vom Umfang<br />

überschaubar sein und nicht mehr als ein,<br />

zwei oder drei Stunden in Anspruch nehmen.<br />

Aufgaben stellen ist nicht einfach<br />

Der gravierendste und am häufigsten von Softwareentwicklern<br />

begangene Fehler liegt in folgenden<br />

Sätzen: „Das sind nur ein paar Zeilen<br />

Code“ oder „Das haben wir gleich“. Meist werden<br />

Aufwände komplett unterschätzt.<br />

Mit dem Ausdenken von Übungsaufgaben verhält<br />

es sich da nicht anders. „Aufgaben zu finden<br />

ist doch nicht schwer.“ Dieser Satz ist falsch,<br />

denn es müssen so viele Randbedingungen eingehalten<br />

werden: Idee, Machbarkeit, Dauer und<br />

so weiter. Finden Sie eine Aufgabe, die in etwa<br />

zwei bis drei Stunden lösbar ist, haben Sie eine<br />

sogenannte Kata erfunden.<br />

Wollen Sie sich die Mühe aber nicht machen,<br />

eigene Katas zu erfinden, können Sie auch welche<br />

aus dem Internet verwenden [2], [3]. Weiter<br />

unten listen wir die bekanntesten Katas auf.<br />

Kata und Dojo<br />

Der Begriff Kata ist der fernöstlichen Kampfkunst<br />

entlehnt und bedeutet so viel wie Übung. Wer<br />

Kampfsportarten lernt, läuft Katas. Das sind<br />

strikt vorgegebene Bewegungsfolgen, die Koordination,<br />

Bewegung und Exaktheit trainieren sollen.<br />

Insofern passt der Begriff eigentlich nicht<br />

hundertprozentig, denn es geht bei einer Coding<br />

Kata nicht darum, die gleiche Aufgabe immer<br />

und immer wieder zu lösen, sondern um das<br />

Eruieren von Neuem.<br />

Auch der Begriff Dojo kommt aus Fernost. Er<br />

bezeichnet den Platz, an dem Kampfsportarten<br />

geübt werden. In der Softwareentwicklung ist mit<br />

Coding Dojo nicht der Raum an sich gemeint,<br />

sondern das Event des Zusammenkommens und<br />

Lösens der Aufgabe.<br />

Es geht los – fast<br />

Am Anfang eines Coding Dojos bestimmt die<br />

Gruppe, welche Aufgabe gelöst werden soll. Sowohl<br />

Moderator als auch Teilnehmer können<br />

Vorschläge unterbreiten. Die Anwesenden einigen<br />

sich auf eine Kata, und los geht’s.<br />

Na ja, noch nicht ganz. Was gern vergessen<br />

wird, ist, zu prüfen, ob die Kata auch komplett<br />

von allen verstanden wurde. Was sind die Anforderungen?<br />

Wie sieht beispielsweise ein Use Case<br />

aus? Welche Nebenbedingungen sind zu erfüllen?<br />

Was ist eigentlich zu erzeugen? Ist das Ergebnis<br />

eine Klasse oder eine Methode?<br />

Erst wenn die Aufgabenstellung klar ist, sollte<br />

es losgehen. Und das heißt: programmieren nach<br />

Test-Driven Development. Das wiederum bedeutet,<br />

dass es eine Schnittstelle gibt, deren Funktionalität<br />

über Unit-Tests getestet wird. Es muss somit<br />

keine Kompilierung laufen, nur ein Testframework<br />

und ein Testrunner sind nötig. Auch das<br />

ist wieder so eine Randbedingung der Aufgabenstellung:<br />

Die Kata darf keine Abhängigkeiten haben.<br />

Sie muss losgelöst getestet werden können.<br />

Die Spielarten<br />

Es gibt verschiedene Arten von Coding Dojos,<br />

über deren Sinn oder Unsinn schon trefflich gestritten<br />

wurde. Oft läuft ein Dojo aber so ab: Die<br />

Aufgabe wird an einem Rechner gelöst, dessen<br />

Bild per Beamer an die Wand projiziert wird. Es<br />

gibt einen Code Monkey, der im Prinzip das in<br />

die Tasten klopft, was die Runde der Anwesenden<br />

wünscht.<br />

In einer anderen Art des Coding Dojos kann jeder,<br />

der möchte, für eine gewisse Zeit am Dojo-<br />

Auf einen Blick<br />

Tilman Börner ist Diplomphysiker<br />

und Chefredakteur der<br />

dotnetpro. Programmieren ist<br />

für ihn ein kreativer Akt, für den<br />

er leider viel zu wenig Zeit hat.<br />

Inhalt<br />

➡ In Coding Dojos löst man<br />

gemeinsam eine Programmieraufgabe<br />

per TDD.<br />

➡ Die populärsten Übungsaufgaben<br />

vorgestellt.<br />

➡ Spaß und Spiel gehören<br />

unbedingt mit zum Lernen.<br />

dnpCode<br />

A11DOJOKatas<br />

www.dotnetpro.de dotnetpro.dojos.2011 95


GRUNDLAGEN _Coding Dojo: Mit Spaß programmieren lernen<br />

Rechner sitzen. Damit ist er in der Lage, die<br />

Aufgabe so zu lösen, wie er möchte. Meist<br />

sorgt eine Zeitbegrenzung dafür, dass möglichst<br />

viele mal ihre Ideen zeigen können.<br />

Der Nachteil: Der Nachfolgende kann den<br />

Code des Vorgängers löschen und durch eigenen<br />

ersetzen.<br />

In beiden Formen ist es von Vorteil,<br />

wenn es einen Moderator gibt, der versucht,<br />

einen gewissen Konsens im einen<br />

Fall und eine gewisse Stringenz im anderen<br />

Fall durchzusetzen. Vor allem muss der<br />

Moderator auf die Zeit achten, denn Diskussionen<br />

unter Programmierern ufern<br />

bekanntlich schnell aus.<br />

Die Teilnehmer denken sich nun einen<br />

ersten Test aus, der die Schnittstelle gemäß<br />

einer Anforderung überprüft. Eine erste Implementierung<br />

des System UnderTest (SUT)<br />

muss dann den ersten Test grün machen.<br />

Weitere Anforderungen werden in Form<br />

von Unit-Tests beschrieben und das SUT<br />

so implementiert, dass die Tests alle grün<br />

werden.<br />

Ein möglicher Streitpunkt ist, wann eine<br />

Refaktorisierung erfolgen soll und ob das<br />

überhaupt zu den Aufgaben des Coding<br />

Dojos gehört. Wenn Code nach TDD<br />

wächst, empfiehlt es sich nach gewissen<br />

Schritten, den Code so umzustrukturieren,<br />

dass er wieder lesbarer wird. Aber ist das<br />

tatsächlich eine Anforderung, die an die<br />

Software gestellt wird, oder soll der Code<br />

nur die funktionale Ebene erfüllen, und es<br />

ist egal, wie er aussieht?<br />

Coding Dojo = Spaß + Lernen<br />

Dialog und Diskussion gehören zum Coding<br />

Dojo. Aber Vorsicht vor zu langen Diskussionen.<br />

Meistens entzünden sie sich an<br />

kleinen Dingen wie Benennungen von Methoden.<br />

Hier muss der Moderator rechtzeitig<br />

eingreifen – obwohl auch die richtige<br />

Benennung von Methoden und Variablen<br />

durchaus geübt werden muss.<br />

Kata BankOCR<br />

In dieser Kata geht es um das Lesen von<br />

Zahlen: Gesucht ist ein Algorithmus, der<br />

aus einer Eingabe eine Zahl erzeugt. Die<br />

Eingabe sind in dem Fall Strings, die Ausgabe<br />

soll ein Ganzzahlenformat sein. Wie<br />

der Name schon sagt, soll die Zahl per Optical<br />

Character Recognition (OCR) erkannt<br />

werden. Die Eingabe ist die Darstellung der<br />

Zahl in Form einer sogenannten Siebensegmentanzeige.<br />

Die Ziffern von 0 bis 9<br />

würden dann so aussehen:<br />

_ _ _ _ _ _ _ _<br />

| | | _| _||_||_ |_ ||_||_|<br />

|_| ||_ _| | _||_| ||_| _|<br />

[Abb. 1] Coding<br />

Dojo auf der<br />

.NET DevCon 2011<br />

in Nürnberg mit<br />

Ilker Cetinkaya<br />

(stehend).<br />

Schließlich ist die vorgegebene Zeit um,<br />

und die Aufgabe ist nicht gelöst. War das<br />

Coding Dojo deshalb ein Misserfolg?<br />

Diese Frage kann nur jeder für sich beantworten.<br />

Schließlich und endlich geht es ja darum,<br />

etwas mitzunehmen und etwas mit<br />

Spaß zu lernen (siehe auch Abbildung 2,<br />

Das Code Kata Manifesto). Und das kann<br />

schlicht die Erkenntnis sein, dass ein<br />

Coding Dojo manchmal einfach zu chaotisch<br />

verläuft.<br />

Die Klassiker: Ausgewählte Katas<br />

Diese Zahl lässt sich durch drei Zeilen<br />

Text darstellen. Darin markieren Unterstriche,<br />

vertikale Striche und Leerzeichen die<br />

einzelnen Segmente einer Ziffer. Nach den<br />

drei Zeilen kommt eine Leerzeile, die eine<br />

Zahl von der nächsten trennt. Jede Ziffer<br />

besteht damit aus drei Zeilen und ist drei<br />

Zeichen breit. Die Ziffern sind innerhalb<br />

des Rechtecks aus drei Zeilen und drei Zeichen<br />

rechtsbündig ausgerichtet. Das bedeutet,<br />

die 1, die ja nur ein Zeichen breit<br />

ist, erhält links noch zwei Leerzeichen.<br />

Jede Zahl ist 9 Ziffern breit.<br />

Damit ist jede Zahl 27 Zeichen breit und<br />

drei Zeilen hoch.<br />

96 dotnetpro.dojos.2011 www.dotnetpro.de


Aufgabe: Schreiben Sie einen Algorithmus,<br />

der aus einer Eingabe, die aus Strings<br />

besteht, Zahlen erzeugt. Die Menge der zu<br />

erkennenden Zahlen ist nicht besonders<br />

groß. Gehen Sie von 500 Zahlen aus. Die<br />

Zahlen sind alle fehlerfrei. Eine Fehlererkennung<br />

ist im ersten Schritt nicht nötig.<br />

Diese Kata lässt sich noch erweitern, etwa<br />

indem über eine Checksumme geprüft wird,<br />

ob die Zahl in Ordnung ist. Meist reicht aber<br />

für den ersten Teil die Zeit gerade so.<br />

Kata FizzBuzz<br />

Diese Kata geht auf ein Spiel zurück, bei<br />

dem die Konzentration eine wichtige Rolle<br />

spielt. Eine Gruppe Menschen steht zusammen<br />

und zählt der Reihe nach oben. 1,<br />

2, 3 … und so weiter. So weit ist das noch<br />

einfach. Jetzt kommt aber die Verschärfung:<br />

Ist eine Zahl durch drei teilbar, ruft<br />

der Mensch statt des Zahlenwerts „Fizz“,<br />

ist sie durch fünf teilbar, dann „Buzz“, und<br />

ist sie durch drei und fünf teilbar, dann<br />

muss derjenige „FizzBuzz“ rufen. Sie dürfen<br />

sich selbst ausmalen, was derjenige<br />

machen muss, der nicht richtig Zahl, Fizz,<br />

Buzz oder FizzBuzz ruft.<br />

Aufgabe: Schreiben Sie einen Algorithmus,<br />

der jedes Mal, wenn er aufgerufen<br />

wird, entweder eine Zahl, Fizz, Buzz oder<br />

FizzBuzz zurückgibt. Startpunkt soll bei 1<br />

liegen.<br />

Eine Folge von Aufrufen gibt also das<br />

Folgende zurück:<br />

1, 2, Fizz, 4, Buzz, Fizz, 7, 8, Fizz, Buzz…<br />

Kata Potter<br />

In dieser Kata geht es um den Verkauf von<br />

Büchern. Genauer gesagt: von Harry-Potter-Büchern.<br />

Eine Buchhandlung möchte<br />

als Werbeaktion besondere Buchbundles<br />

anbieten. Frei nach dem Motto: Kauf zwei,<br />

bekomm das dritte geschenkt. Aber so einfach<br />

geht es hier nicht zu.<br />

Ein Buch aus der Harry-Potter-Reihe soll<br />

acht Euro kosten. So weit wäre die Kata<br />

zwar sehr einfach, die Werbeaktion aber<br />

ein Fiasko. Also kommt Rabatt ins Spiel.<br />

Wer zwei verschiedene Bücher aus der Reihe<br />

erwirbt, erhält 5 Prozent Rabatt auf die<br />

beiden Bücher. Wer drei verschiedene Bücher<br />

kauft, erhält 10 Prozent Rabatt. Bei<br />

vier verschiedenen Büchern sind es schon<br />

20 Prozent Rabatt. Kauft ein Kunde fünf<br />

verschiedene Bücher aus der Reihe, bekommt<br />

er 25 Prozent Rabatt.<br />

Knifflig wird es, wenn die Bücher nicht<br />

unterschiedlich sind. Wer beispielsweise<br />

drei Exemplare kauft, wobei es sich aber<br />

GRUNDLAGEN<br />

[Abb. 2] Das Code Kata Manifesto fasst alle wichtigen Regeln für eine Kata zusammen.<br />

nur um zwei verschiedene handelt (also<br />

ein Buch kauft er zweimal), erhält er nur<br />

auf die zwei verschiedenen 5 Prozent Rabatt.<br />

Das doppelte Buch kostet weiterhin 8<br />

Euro.<br />

Aufgabe: Schreiben Sie einen Algorithmus,<br />

der für beliebig viele Exemplare den<br />

Preis berechnet, wobei der den maximalen<br />

Rabatt geben soll. Bei dieser Aufgabe ist es<br />

extrem wichtig, sich durch einige Fälle mit<br />

der Problematik vertraut zu machen. Die<br />

Optimierung bezüglich geringstem Preis<br />

ist nicht so einfach, wie das auf den ersten<br />

Blick scheint.<br />

Kata Tennis<br />

Tennisspieler sind anders. Sie spielen auf<br />

Sand oder Rasen, zu zweit oder zu viert<br />

und hauen sich mit Schlägern den Ball gegenseitig<br />

um die Ohren. Am seltsamsten ist<br />

aber die Zählweise, die den Punktestand in<br />

einem Tennismatch festhält.<br />

0, 15, 30, 40, Spiel lautet die Zählweise. Erreichen<br />

die Gegner beide den Punktestand<br />

40, entsteht ein sogenannter Einstand. Wer<br />

nach dem Einstand einen Ballwechsel für<br />

sich entscheiden kann, erhält den „Vorteil“.<br />

Aber er hat das Spiel noch nicht gewonnen.<br />

Erst mit einem erneuten gewonnenen Ballwechsel<br />

gewinnt er das Match. Verliert er<br />

den Ballwechsel, herrscht wieder Einstand.<br />

Wie gesagt: Das Tennisspiel ist seltsam.<br />

Nichtsdestotrotz gibt das eine schöne Aufgabe<br />

für einen Coding Dojo.<br />

Aufgabe: Schreiben Sie einen Algorithmus,<br />

der die Zählweise des Tennis nachahmt.<br />

Dabei bekommt Spieler A oder Spieler<br />

B den Gewinn des Ballwechsels zugesprochen.<br />

Der Algorithmus soll den aktuellen<br />

Spielstand zurückgeben.<br />

Kata Römische Zahlen<br />

Das darf nicht fehlen: Wenn es seltsame<br />

Zahlensysteme gibt, dann gehört das römische<br />

mit dazu. Gesehen haben Sie solche<br />

Zahlen sicher schon, vor allem als Jahreszahlangabe<br />

auf Gräbern oder Häusern.<br />

MCMLI steht etwa für 1951. Die Zahlen sind<br />

so aufgebaut: Es gibt Zeichen, mit denen<br />

sich alle Zahlen darstellen lassen. Diese Zeichen<br />

sind: I, V, X, L, C, D, M gemäß 1, 5, 10,<br />

50, 100, 500, 1000.<br />

Eine Zahl setzt sich nun aus mehreren<br />

solcher Zeichen zusammen:<br />

I steht für 1<br />

II steht für 2<br />

III steht für 3<br />

IV steht für 4<br />

V steht für 5<br />

VI steht für 6<br />

VII steht für 7<br />

VIII steht für 8<br />

IX steht für 9<br />

X für 10.<br />

Aufgabe: Schreiben Sie einen Algorithmus,<br />

der eine dezimale Zahl als String einer<br />

römischen Zahl zurückgibt. Achten Sie<br />

darauf, dass die römische Zahl auch valide<br />

www.dotnetpro.de dotnetpro.dojos.2011 97


GRUNDLAGEN _Coding Dojo: Mit Spaß programmieren lernen<br />

ist. IM beispielsweise für 999 ist falsch.<br />

Richtig wäre CMXCIX.<br />

Kata Taschenrechner Römische<br />

Zahlen<br />

Wer konvertieren kann, kann auch rechnen.<br />

Somit kann man die Kata Römische<br />

Zahlen so erweitern, dass daraus ein Taschenrechner<br />

wird.<br />

Aber Vorsicht: Ist das Konvertieren überhaupt<br />

nötig? Oder lässt sich das ganze<br />

auch ohne Konversion erledigen. Es gibt<br />

nämlich die folgende Erleichterung: Der<br />

Taschenrechner soll nur addieren. Subtraktion,<br />

Division und Multiplikation sind<br />

nicht zu implementieren.<br />

Aufgabe: Schreiben Sie einen Algorithmus,<br />

der zwei römische Zahlen addiert. Also:<br />

VIII + LXX = LXXVIII oder<br />

CXI + XII = CXXIII<br />

Kata Spiel des Lebens<br />

Das „Spiel des Lebens“ ist wohl ein Klassiker,<br />

den John Horton Conway schon 1970<br />

erfunden hat. Auf Basis einer Verteilung<br />

von Zellen auf einem zweidimensionalen<br />

Raster wird die nächste Generation an Zellen<br />

berechnet. Welche Zelle in der nächsten<br />

Generation weiterlebt, stirbt oder von<br />

den Toten aufgeweckt wird, richtet sich danach,<br />

wie viele Zellen sie als Nachbarn hat.<br />

Nach folgenden Bedingungen berechnet<br />

sich die nächste Generation:<br />

1. Jede Zelle, die weniger als zwei Nachbarn<br />

hat, stirbt wegen Vereinsamung.<br />

2. Jede Zelle, die mehr als drei Nachbarn hat,<br />

stirbt wegen Überbevölkerung.<br />

3. Jede Zelle, die zwei oder drei Nachbarn<br />

hat, lebt auch in der nächsten Generation<br />

weiter.<br />

4. Jede tote Zelle mit genau drei Nachbarn<br />

wird reanimiert und lebt in der nächsten<br />

Generaton wieder.<br />

Besonders zu beachten ist die Bedingung<br />

4: Wir werden zu Frankensteins, die<br />

Leben erschaffen können.<br />

Hier ein Beispiel für drei aufeinanderfolgende<br />

Generationen. Drei Zellen in einer<br />

Reihe werden zu einem Oszillator, der von<br />

einer Generation zur nächsten die Ausrichtung<br />

von vertikal zu horizontal und wieder<br />

zurück bildet.<br />

Generation 1<br />

........<br />

....*...<br />

....*...<br />

....*...<br />

[Abb. 3] Eine Kata von Ilker Cetincayas Website [4]. Ilker hat die Coding Dojos in Deutschland in die<br />

.NET-Community gebracht.<br />

Generation 2<br />

........<br />

........<br />

...***..<br />

........<br />

Generation 3<br />

........<br />

....*...<br />

....*...<br />

....*...<br />

Aufgabe: Schreiben Sie einen Algorithmus,<br />

der aus einem zweidimensionalen Array<br />

mit lebenden und toten Zellen auf Basis<br />

der vier Regeln die nächste Generation berechnet.<br />

Der Algorithmus soll ein zweidimensionales<br />

Array zurückgeben, das dann<br />

wieder zur Berechnung der nächsten Generation<br />

verwendet werden kann.<br />

Hinweis:Teilen Sie die Randbereiche ab.<br />

Diese bedürfen einer eigenen Behandlung.<br />

Kata Anagram<br />

Gegeben sei ein Wort, das der Schlüssel<br />

zum Schloss eines sagenhaften Schatzes<br />

ist. Nur leider sind die Buchstaben dieses<br />

Wortes durcheinandergeraten. Sie müssen<br />

so lange probieren, bis Sie die richtige<br />

Kombination gefunden haben. Wäre das<br />

Wort etwa ei, dann ist das schnell erledigt,<br />

denn es gibt nur noch ie als Permutation.<br />

Bei „ein“ sind es schon sechs mögliche:<br />

ein<br />

eni<br />

ine<br />

ien<br />

nei<br />

nie<br />

Aufgabe: Schreiben Sie einen Algorithmus,<br />

der von einem eingegebenen Wort alle<br />

Buchstabenpermutationen erzeugt. Eingabe<br />

ist ein String, Ausgabe ist eine Liste<br />

von Strings. Aber aufpassen: Die Zahl der<br />

Permutationen wächst mit der Fakultät der<br />

Zahl der Buchstaben. Also sind es bei einem<br />

Wort mit vier Buchstaben schon 24<br />

mögliche Wörter. [tib]<br />

[1] http://msdn.microsoft.com/enus/data/gg577609<br />

[2] http://codingkata.org/<br />

[3] http://codingdojo.org/<br />

[4] ilker.de/code-kata-pickakin<br />

98 dotnetpro.dojos.2011 www.dotnetpro.de


#1<br />

dotnetpro.de<br />

facebook.de/dotnetpro<br />

twitter.com/dotnetpro_mag<br />

gplus.to/dotnetpro

Hurra! Ihre Datei wurde hochgeladen und ist bereit für die Veröffentlichung.

Erfolgreich gespeichert!

Leider ist etwas schief gelaufen!