b2aat6n
b2aat6n
b2aat6n
Erfolgreiche ePaper selbst erstellen
Machen Sie aus Ihren PDF Publikationen ein blätterbares Flipbook mit unserer einzigartigen Google optimierten e-Paper Software.
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