Читать книгу: «Dojos für Entwickler 2», страница 3

Шрифт:

Listing 3
Dateinamen suchen.

public class Dateinamen_suchen { public void Process(Tuple<string, string> path_und_SearchPattern) { var path = path_und_SearchPattern.Item1; var searchPattern = path_und_SearchPattern.Item2; var filenames = Directory.EnumerateFiles(path, searchPattern, SearchOption.AllDirectories); Result(filenames); } public event Action< IEnumerable<string>> Result; }

Es ergibt sich nun die Frage, wie man das Bauteil automatisiert testen kann. Seine Aufgabe ist es, zu einem gegebenen Pfad und einem Suchmuster die Liste der gefundenen Dateinamen zu liefern. Folglich werden für einen automatisierten Test Testdaten benötigt. Diese habe ich im Testprojekt angelegt, wie Abbildung 3 zeigt.


[Abb. 3]

Testdaten zum automatisierten Testen der Dateisuche.

Ganz wichtig: Bei den Testdateien test1.txt, test2.txt et cetera muss die Eigenschaft Copy to Output Directory auf Copy always oder ­Copy if newer eingestellt werden. Dadurch werden die Testdateien samt Verzeichnisstruktur in das Ausgabeverzeichnis des Projektes kopiert. Im Test können die Dateien dann über den relativen Pfad testdaten angesprochen werden, wie Listing 4 zeigt. Wird im Verzeichnis testdaten nach den Dateien *.txt gesucht, müssen drei Dateinamen geliefert werden. Glücklicherweise liefert Directory.EnumerateFiles die Dateinamen als relative Namen. Wären es absolute Pfade, wäre eine Automatisierung schwieriger, da dann der absolute Pfad der jeweiligen Testumgebung zu berücksichtigen wäre.

Listing 4
Test: Dateinamen

[TestFixture] public class Dateinamen_suchen_Tests { private Dateinamen_suchen sut; private IEnumerable<string> result; [SetUp] public void Setup() { sut = new Dateinamen_suchen(); sut.Result += x => result = x; } [Test] public void Txt_Dateien_rekursiv_suchen() { sut.Process( new Tuple<string, string>( @"testdaten", "*.txt")); Assert.That(result.ToArray(), Is.EqualTo(new[] { @"testdaten\test1.txt", @"testdaten\test2.txt", @"testdaten\ebene2\test4.txt" })); } }

Der Spike zahlt sich aus

Als Nächstes kam das Bauteil Alle_Stich­wörter_ermitteln an die Reihe. Auch hier habe ich erst implementiert und dann getestet. Zur Enttäuschung der Test-first-Verfechter muss ich auch hier mitteilen, dass das nachträgliche Schreiben der automatisierten Tests keine Probleme bereitet hat. Das verwundert mich natürlich nicht wirklich, denn vor der Implementation stand ja der Entwurf mit Flow-Design. Ich möchte hier nicht den Eindruck erwecken, dass Test-first eine überflüssige Praktik sei. Allerdings bringt es eben auch keinen Vorteil, sich sklavisch an diese Praktik zu halten. In vielen Fällen ist eine Test-first-Vorgehensweise sinnvoll, nämlich immer dann, wenn Unsicherheiten bei der Implementation vorhanden sind. Durch den Entwurf und den Spike war mir allerdings zu Beginn der Implementation klar, was auf mich zukommt. Während der Übungszeit Praktiken bewusst einmal anzuwenden und ein anderes Mal nicht, halte ich für eine nützliche Form der Reflexion.

Beim Ermitteln der Stichwörter habe ich auf die Erkenntnisse aus dem Spike zurückgegriffen. Die Aufgabe bestand darin, zu einer Aufzählung von Dateinamen alle verwendeten Stichwörter zu liefern. Durch die Verwendung von yield return sieht die Implementation recht übersichtlich aus, wie Listing 5 zeigt.

Listing 5
Stichwörter ermitteln.

public class Alle_Stichwörter_ermitteln { public void Process(IEnumerable< string> dateinamen) { Result(Stichwörter_ermitteln(dateinamen)); } private IEnumerable<string> Stichwörter_ermitteln( IEnumerable<string> dateinamen) { foreach (var dateiname in dateinamen) { var stichwörter = Stichwörter _ermitteln(dateiname); foreach (var stichwort in stichwörter) { yield return stichwort; } } } private IEnumerable<string< Stichwörter_ermitteln(string dateiname) { BitmapSource img = BitmapFrame.Create( new Uri(dateiname, UriKind.Relative)); var metadata = (BitmapMetadata)img.Metadata; if (metadata.Keywords == null) { yield break; } foreach (var keyword in metadata.Keywords) { yield return keyword; } } public event Action<IEnumerable< string>> Result; }

Beim Aufruf der Process-Methode wird die gesamte Aufzählung der Dateinamen an die Methode Stichwörter_ermitteln übergeben. Diese hat einen Return-Wert vom Typ IEnumerable<string>, sodass yield return verwendet werden kann. Das war mein Ziel. Doch die Methode erledigt nicht die gesamte Arbeit, sondern lässt eine Überladung von Stichwörter_ermitteln die Arbeit für eine einzelne Datei ausführen. Dafür habe ich mich entschieden, damit die Methoden übersichtlich und klein bleiben. Die erste Überladung iteriert über alle Dateinamen und über die gelieferten Stichwörter in Form von zwei geschachtelten foreach-Schleifen. Listing 6 zeigt, dass das mithilfe von LINQ deutlich knackiger geht. Ich höre allerdings schon die Kritiker rufen: „Das versteht doch keiner mehr.“ Mag sein. Wer sich bislang nicht intensiv mit LINQ auseinandergesetzt hat, wird mit dieser knappen Formulierung möglicherweise Probleme haben. Aber wir programmieren ja auch nicht mehr in Assembler, nur weil irgendjemand C-Code nicht lesen kann.

Listing 6
LINQ nutzen.

private IEnumerable<string> Stichwörter_ermitteln( IEnumerable<string> dateinamen) { return dateinamen.SelectMany( Stichwörter_ermitteln); }

Um nun dieses Bauteil automatisiert testen zu können, bedarf es wieder Testdaten in Form von JPEG-Dateien. Auch diese Dateien habe ich im Projekt abgelegt, wie Abbildung 3 zeigt. Es handelt sich um vier JPEG-Dateien aus dem Lieferumfang von Windows 7. Nachdem ich diese in das Projekt kopiert hatte, habe ich wieder mit dem Windows Explorer Stichwörter vergeben, siehe Abbildung 4.


[Abb. 4]

Mehrere Stichwörter vergeben.

Auch diese Testdateien werden in das Ausgabeverzeichnis des Testprojektes kopiert, sodass sie im Test über einen relativen Dateinamen angesprochen werden können, siehe Listing 7.

Listing 7
Test: Stichwörter ermitteln.

[TestFixture] public class Alle_Stichwörter_ermitteln_Tests { private Alle_Stichwörter_ermitteln sut; private IEnumerable<string> result; [SetUp] public void Setup() { sut = new Alle_Stichwörter_ermitteln(); sut.Result += x => result = x; } [Test] public void Ein_einzelner_Dateiname() { sut.Process(new[] { @"testdaten\Lighthouse.jpg" }); Assert.That(result.ToArray(), Is.EqualTo(new[] { "A", "B", "C" })); } [Test] public void Mehrere_Dateinamen() { sut.Process(new[] { @"testdaten\Lighthouse.jpg", @"testdaten\Penguins.jpg", @"testdaten\Tulips.jpg" }); Assert.That(result.ToArray(), Is.EqualTo(new[] { "A", "B", "C", "C", "D", "E" })); } [Test] public void Eine_Datei_ohne_Stichwörter() { sut.Process(new[] { @"testdaten\Desert.jpg" }); Assert.That(result.ToArray(), Is.EqualTo(new string[] {})); } }

Der erste Test stellt sicher, dass die Stichwörter einer einzelnen Datei gelesen werden können. Durch den zweiten Test wird überprüft, ob dies auch für mehrere Dateien möglich ist. Dabei sieht man, dass doppelt verwendete Stichwörter hier noch nicht herausgefiltert werden.

Der letzte Test ist erst hinzugekommen, nachdem ich die ganze Anwendung fertig hatte und das Programm zum ersten Mal auf einem „echten“ Bildverzeichnis lief. Dabei zeigte sich, dass sich das Programm bei Dateien, die kein Stichwort enthalten, auf die Nase legte. Das liegt daran, dass meta­data.Key­words den Wert null liefert anstelle einer leeren Aufzählung. Nachdem ich das Problem durch den dritten Test in Listing 7 reproduziert hatte, konnte ich die Implementation entsprechend korrigieren. Dort wird auf null geprüft und in diesem Fall durch yield break eine leere Aufzählung geliefert.

Dieses Problem wäre übrigens auch nicht bei einer Test-first-Vorgehensweise zum Vorschein gekommen. Nach der Implementation durch den Entwickler sind trotz automatisierter Tests immer noch explorative Tests erforderlich. Das hier konkret vorliegende Problem hätte wohl auch im realen Leben ein Entwickler gefunden. In größeren Zusammenhängen wäre es möglicherweise aber auch erst bei der Qualitätskontrolle durch das Testteam aufgefallen.

Das dritte und letzte Bauteil war dank LINQ rasch realisiert: Eine Aufzählung musste so gefiltert werden, dass keine doppelten Einträge mehr auftauchen. Darum kümmert sich Distinct, siehe Listing 8. Die Tests dazu sind trivial, wie Listing 9 zeigt.

Listing 8
Doppelte Einträge herausfiltern.

public class Eindeutige_Stichwörter_filtern { public void Process(IEnumerable<string> stichwörter) { Result(stichwörter.Distinct()); } public event Action<IEnumerable<string>> Result; }

Listing 9
Test auf eindeutige Stichwörter.

[TestFixture] public class Eindeutige_Stichwörter_filtern_Tests { private Eindeutige_Stichwörter_filtern sut; private IEnumerable<string> result; [SetUp] public void Setup() { sut = new Eindeutige_Stichwörter_filtern(); sut.Result += x => result = x; } [Test] public void Nicht_eindeutige_Stichwörter() { sut.Process( new[]{"A", "B", "A", "C", "C", "B"}); Assert.That(result.ToArray(), Is.EqualTo(new[]{"A", "B", "C"})); } [Test] public void Ohnehin_schon _eindeutige_Stichwörter() { sut.Process(new[]{"A", "B"}); Assert.That(result.ToArray(), Is.EqualTo(new[]{"A", "B"})); } }

Nun fehlen nur noch eine Platine, die den Flow zusammensetzt, sowie ein Kommandozeilenprogramm, welches die Pla­tine entsprechend aufruft. Weiter geht es ­also in Listing 10 mit der Platine.

Listing 10
Den Flow zusammensetzen.

public class Eindeutige_Stichwörter_ermitteln { private Action<Tuple<string, string>> process; public Eindeutige_Stichwörter_ermitteln() { var dateinamen_suchen = new Dateinamen_suchen(); var alle_Stichwörter_ermitteln = new Alle_Stichwörter_ermitteln(); var eindeutige_Stichwörter_filtern = new Eindeutige_Stichwörter_filtern(); dateinamen_suchen.Result += alle_Stichwörter_ermitteln.Process; alle_Stichwörter_ermitteln.Result += eindeutige_Stichwörter_filtern.Process; eindeutige_Stichwörter_filtern.Result += x => Result(x); process = path_und_SearchPattern => dateinamen_suchen.Process( path_und_SearchPattern); } public void Process(Tuple<string, string> path_und_SearchPattern) { process(path_und_SearchPattern); } public event Action<IEnumerable< string>> Result; }

Ich habe hier bewusst keine Dependency Injection betrieben, sondern in der Platine werden die benötigten Bauteile direkt mit new instanziert. Die Abhängigkeiten über den Konstruktor zu injizieren hätte mir den Vorteil gebracht, dass ich dann im Test gegen Attrappen hätte testen können. Doch das ist mir für Platinen viel zu mühsam. Denn es liefe dann auf Tests hinaus, in denen ein Mock-Framework zum Einsatz kommen muss, um damit die Attrappen zu erzeugen. Weil die Platine selbst keine Logik enthält, sondern nur für die Verdrahtung der Bauteile zuständig ist, begnüge ich mich mit einem ohnehin notwendigen Integrationstest. Der Erkenntnisgewinn wäre durch einen Unit-Test der Platine, der dann auf Attrappen setzen müsste, nicht größer.

Das Verdrahten der Bauteile geschieht im Konstruktor der Platine. Nachdem die benötigten Bauteile instanziert sind, werden sie in der richtigen Reihenfolge verdrahtet, indem jeweils eine Process-Methode an einen Result-Event gebunden wird. Am Ende der Kette muss der Result-Event des letzten Bauteils den Result-Event der Platine auslösen. Da zum Zeitpunkt der Verdrahtung innerhalb des Konstruktors noch niemand an den Result-Event der Platine gebunden sein kann, erfolgt die Weiterleitung an diesen Event mittels Lambda-Expression. Um beim Aufruf der Process-Methode der Platine die Process-Methode des ersten Bauteils aufrufen zu können, wird der korrekte Aufruf in eine Action als Feld der Klasse abgelegt.

Der Integrationstest zur Platine arbeitet auf denselben Testdaten wie die Tests der Bauteile. Dadurch war der Integrationstest in Listing 11 schnell fertiggestellt. Der Test ermittelt die Stichwörter aller JPEG-Dateien im Verzeichnis testdaten. Anschließend wird über Assert überprüft, ob die korrekten Stichwörter vorliegen.

Listing 11
Integrationstest.

[TestFixture] public class Eindeutige _Stichwörter_ermitteln_Tests { private Eindeutige _Stichwörter_ermitteln sut; private IEnumerable<string> result; [SetUp] public void Setup() { sut = new Eindeutige _Stichwörter_ermitteln(); sut.Result += x => result = x; } [Test] public void Integrationstest() { sut.Process(new Tuple<string, string>("testdaten", "*.jpg")); Assert.That(result.ToArray(), Is.EqualTo(new[] { "A", "B", "C", "D", "E" })); } }

Eine App

Als letzter Schritt musste nun die Platine noch in eine Konsolenanwendung integriert werden, siehe Listing 12.

Listing 12
Integration in eine Konsolenanwendung.

public static class Program { public static void Main(string[] args) { var eindeutige_Stichwörter_Ermitteln = new Eindeutige_Stichwörter_ermitteln(); eindeutige_Stichwörter_Ermitteln.Result += stichwörter => { foreach (var stichwort in stichwörter) { Console.WriteLine(stichwort); } }; eindeutige_Stichwörter_Ermitteln.Process( new Tuple<string, string>( args[0], args[1])); } }

Ich habe mich entschieden, den Pfad und die Suchmaske aus den Parametern der Kommandozeile zu entnehmen. Hier erfolgt keine Prüfung, ob die Parameter vorliegen. In einer realen Anwendung würde man sicherlich prüfen, ob das args-Array passend gefüllt ist.

An den Result-Event der Platine wird eine Lambda-Expression gebunden, welche die ermittelten Stichwörter mit Console.Write­­Line auf der Konsole ausgibt. Hier stellt sich natürlich die Frage, ob es tatsächlich die Aufgabe der Anwendung sein sollte, sich um die Konsolenausgabe zu kümmern. Zumindest die Main-Methode sollte sich damit nicht unmittelbar befassen. Daher habe ich die Konsolenausgabe in das Bauteil Auf_Konsole_aus­geben ausgelagert, siehe Listing 13. Damit reduziert sich die Main-Methode auf den Code in Listing 14.

Listing 13
Konsolenausgabe auslagern.

public class Auf_Konsole_ausgeben { public void Process( IEnumerable<string> zeilen) { foreach (var zeile in zeilen) { Console.WriteLine(zeile); } } }

Listing 14
Das Hauptprogramm.

public static class Program { public static void Main(string[] args) { var eindeutige_Stichwörter_Ermitteln = new Eindeutige_Stichwörter_ermitteln(); var auf_Konsole_ausgeben = new Auf_Konsole_ausgeben(); eindeutige_Stichwörter_Ermitteln.Result += auf_Konsole_ausgeben.Process; eindeutige_Stichwörter_Ermitteln.Process( new Tuple<string, string>( args[0], args[1])); } }

Fazit

Die Aufgabe hat in der Vorbereitung mehr Zeit in Anspruch genommen, als ich vermutet hatte. Das lag daran, dass die Softwarehersteller sich dazu entschlossen haben, die Stichwörter nicht gleich in den JPEG-Dateien abzulegen. Die Herausforderung, die Hierarchie in den Stichworten darzustellen, konnte ich gar nicht meistern, da diese Information beim Export aus dem Elements Organizer verloren geht. Doch keine Sorge, es wird eine weitere Iteration des Programms geben. Schauen Sie sich einfach die neue Aufgabe in diesem Heft an!

[ml]

[1] NUnit, www.nunit.org [2] ReSharper, www.jetbrains.com/resharper

Aufgabe 3
Bilddateien parallel verarbeiten
Alle anpacken!

Sie sollen aus vielen JPEG-Dateien die Stichworte extrahieren. Eine ideale Aufgabenstellung für Parallelverarbeitung. Aber läuft das Programm damit auch wirklich schneller?

Versuch macht klug. Daher soll es in der Aufgabe für diesen Monat darum gehen herauszufinden, ob die Parallelverarbeitung von Dateien das Programm beschleunigen kann. Natürlich soll dabei Flow-Design zum Einsatz kommen. Denn Flow-Design hat im Bereich der Parallelisierung einen entscheidenden Vorteil : Ein Entwurf, der zunächst ohne den Aspekt der Parallelisierung erstellt wurde, kann leicht um Parallelverarbeitung ergänzt werden. Das gilt natürlich nur, wenn überhaupt einzelne Schritte im Flow parallel ausführbar sind, ohne dass dadurch die Semantik verändert wird. Bei der Parallelisierung von Flows lassen sich zwei Fälle unterscheiden:

 Es existieren im Flow bereits parallele Datenflüsse, die allerdings bislang sequenziell ausgeführt werden.

 Es werden Aufzählungen von Daten verarbeitet, sodass sich eventuell mehrere Instanzen parallel um die Verarbeitung kümmern können.

Abbildung 1 zeigt den ersten Fall vor und nach der Parallelisierung.

Damit die beiden Pfade auf jeweils eigenen Threads parallel ausgeführt werden, muss jeweils zu Beginn und Ende des Pfades eine zusätzliche Funktionseinheit für den Threadwechsel sorgen. Die Funktionseinheit Asynchronize sorgt dafür, dass auf einem neuen Thread weitergearbeitet wird. Am Ende des Teil-Flows sorgt Synchronize dafür, dass der Flow auf den ursprünglichen Thread zurückkehrt. Dies ist vor allem in Verbindung mit Windows Forms und WPF wichtig, weil ein Zugriff auf die Controls meist nur aus dem UI-Thread heraus gestattet ist. Ihre erste Aufgabe für diesen Monat besteht darin, die beiden Funktionseinheiten Asynchronize und Synchronize zu implementieren. Das ist mithilfe der Klassen Thread und SynchronizationContext eine überschaubare Herausforderung.


[Abb. 1]

Parallele Pfade.

In Abbildung 2 sehen Sie den zweiten Fall für eine mögliche Parallelisierung.

Die Funktionseinheit A erhält mehrere Elemente vom Typ X und verarbeitet diese zu mehreren Elementen vom Typ Y. Der Stern zeigt an, dass statt eines einzelnen Datums eine Aufzählung von Daten fließt. Ziel ist es, die Bearbeitung der Aufzählung von mehreren parallel arbeitenden Instanzen der Funktionseinheit A ausführen zu lassen. Dazu verteilt der Scatter-Baustein die anstehenden Daten auf mehrere parallel arbeitende Funktionseinheiten. Die Ergebnisse sammelt Gather am Ende wieder ein und fasst sie zu einer Aufzählung zusammen. Um die Aufgabe nicht zu kompliziert zu machen, genügt es, Scatter und Gather mit einer fixen Anzahl von Ein- bzw. Ausgängen zu versehen. Beginnen Sie ruhig mit nur zwei Ein- bzw. Ausgängen.

Natürlich müssen die Daten sich für eine Parallelverarbeitung eignen. Müssen die Daten etwa in einer vorgegebenen Reihenfolge bearbeitet werden, wird eine Parallelisierung schwierig. Durch die parallel laufenden Threads ist nämlich die Einhaltung der Reihenfolge nicht mehr automatisch sichergestellt. Müssen die Threads zur Einhaltung der Reihenfolge aufeinander warten, bringt die Parallelisierung aus Performancesicht ohnehin nichts. Und auch bei Daten, deren Bearbeitungsreihenfolge keine Rolle spielt, müssen Sie darauf achten, dass die Daten jeweils genau einmal bearbeitet werden. [ml]


[Abb. 2]

Aufzählungen parallelisiert bearbeiten.

Lösung 3
Parallelisierung im Flow-Design
Erst trennen, dann vereinen

Mit Flow-Design lassen sich auch parallele Vorgänge modellieren. Standardbausteine erleichtern die Umsetzung. Diese Übung ergänzt die neuen Bausteine Scatter und Gather.

Inzwischen sind in der dotnetpro über 20 dojo-Lösungen erschienen. In vielen davon habe ich eine Lösung mit Flow-Design modelliert und umgesetzt. Und in zahlreichen davon stand bereits der Hinweis auf das ebclang-Projekt [1]. Wer wollte, konnte dort eine Lösung für den ersten Teil der Parallelisierungsaufgabe finden: Asynchronizer und Synchronizer sind Standardbausteine, die dort implementiert sind. Aber natürlich haben Sie Ihre übungszeit ernst genommen und beide Bausteine selbst implementiert. Oder?

Sollten Sie das ebclang-Projekt als „Spickzettel“ benutzt haben, werden Sie gleich merken, dass ich hier die Implementation aus genau diesem Projekt als Lösung der Aufgabe vorstellen werde. Das betrifft allerdings nur den ersten Teil der übung. Der zweite Teil, Scatter/Gather, befindet sich bislang nicht im ebclang-Projekt.

1 435,42 ₽
Возрастное ограничение:
0+
Объем:
356 стр. 95 иллюстраций
ISBN:
9783844259261
Издатель:
Правообладатель:
Bookwire
Формат скачивания:
epub, fb2, fb3, ios.epub, mobi, pdf, txt, zip

С этой книгой читают