Dojos für Entwickler 2. Stefan Lieser
Fett und Kursiv zu einer einzigen zusammenfassen. Damit weiterhin alle vier möglichen Kombinationen von zwei booleschen Eigenschaften abgebildet werden, könnte ein enum-Typ verwendet werden, der die vier Kombinationen explizit enthält:
T Normal,
T Fett,
T Kursiv,
T FettUndKursiv.
So wäre klar, welche Bedeutung die Eigenschaft hat. Allerdings ist es nun etwas schwieriger, den vorhandenen Wert der Eigenschaft so zu ändern, dass kursiv ergänzt wird. Denn aus Normal müsste dann Kursiv werden, während aus Fett dann FettUndKursiv werden müsste. Ob die Lösung so besser aussieht, wollte ich wissen und habe es ausprobiert. Dabei zeigte sich, dass der Umgang mit einem enum-Typ die Lösung deutlich aufwendiger machen würde, daher habe ich den enum-Typ verworfen. Eine andere Idee wäre, den Schriftstil als zusammengesetzten Typ wie in Listing 3 zu definieren.
Listing 3
Eine Klasse für den Schriftstil.
public class SchriftStil { public bool Fett { get; set; } public bool Kursiv { get; set; } }
Doch auch das würde die Sache nicht einfacher machen, allein schon weil in den Tests der Vergleich zweier Objekte dieses zusammengesetzten Typs nicht mehr einfach so funktionieren würde. Am Ende entschied ich mich daher, es bei den beiden booleschen Eigenschaften Fett und Kursiv zu belassen.
Implementation
Die Implementation ging zunächst leicht von der Hand. Text, der in Fettschrift ausgegeben werden soll, muss in Markdown in doppelte Sternchen eingefasst werden. Folglich suche ich in den eingehenden Text-Element-Objekten nach doppelten Sternchen. Bei jedem Treffer wird ein Flag, welches festhält, ob gerade Fettschrift ausgegeben werden soll, umgeschaltet. Ferner wird der bis dahin eingelesene String in ein neues TextElement-Objekt verpackt und als Output der Funktionseinheit ausgegeben.
Die Herausforderung bestand darin, auch solche Fälle korrekt zu behandeln, die nicht direkt auf der Hand liegen. So kann es beispielsweise sein, dass das öffnende und das schließende Doppelsternchen in zwei unterschiedlichen TextElement-Objekten liegen. Um auch damit korrekt umzugehen, musste ich das Flag aus der Methode in die Klasse verschieben, damit der Zustand TextElement übergreifend gehalten wird. Ohne automatisierte Tests wäre ich hier aufgeschmissen gewesen. Es passierte nämlich ab und zu, dass ein neuer Spezialfall funktionierte, dafür aber ein anderes Szenario nicht mehr korrekt lief. Durch die Tests habe ich das jeweils schnell erkennen und beheben können. Listing 4 zeigt meine Implementation.
Listing 4
Fette Markierungen erkennen.
public class Extrahiere_Fett { private bool inFett; public event Action<IEnumerable< TextElement>> Result; public void Process(IEnumerable< TextElement> textElements) { Result(ProcessElements(textElements)); } private IEnumerable<TextElement> ProcessElements(IEnumerable< TextElement> textElements) { foreach (var textElement in textElements) { foreach (var element in ProcessElement( textElement)) { yield return element; } } } private IEnumerable<TextElement> ProcessElement(TextElement textElement) { const string fettTag = "**"; var result = new TextElement {Fett = inFett}; var input = textElement.Text; while (input.Length > 0) { if (input.Contains(fettTag)) { result.Text = TextBisZumTag( input, fettTag); if (result.Text.Length > 0) { yield return result; } inFett = !inFett; result = new TextElement {Fett = inFett}; input = TextNachDemTag(input, fettTag); } else { result.Text = input; input = ""; yield return result; } } } private static string TextBisZumTag(string input, string tag) { return input.Substring(0, input.IndexOf(tag)); } private static string TextNachDemTag( string input, string tag) { return input.Remove(0, input.IndexOf(tag) + tag.Length); } }
Die Methode ProcessElement ist zwar etwas lang geraten, doch ich habe keine elegantere Variante finden können. Für meinen Geschmack ist die Methode gerade so an der Grenze der Verständlichkeit. Sollte hier einmal eine Erweiterung anstehen, müsste ich die Methode vermutlich vorher refaktorisieren.
Die Umsetzung für das Erkennen von kursiven Texten sieht fast genauso aus. Allerdings galt es dabei eine Besonderheit zu berücksichtigen: Kursive Text-ElementObjekte können vorher bereits auf fett gesetzt worden sein. Diese Eigenschaft, also fett gesetzter Text, muss beim Extrahieren der kursiven Texte erhalten bleiben. Umgekehrt gilt dies nicht, da die Erkennung der fett gesetzten Texte ja in jedem Fall vor den kursiven Texten stattfindet. Dies ergibt sich zwingend daraus, dass zuerst die Doppelsternchen aus dem Text entfernt werden müssen, bevor auf einzelne Sternchen geprüft wird. Die Reihenfolge der beiden Funktionseinheiten Extrahiere_Fett und Extrahiere_Kursiv ist im Flow also nicht beliebig. Beim Extrahieren der kursiven Texte wird die Fett-Eigenschaft aus den Eingangsdaten übernommen. Wird ein Text-Element in mehrere aufgeteilt, müssen sie alle die Fett-Eigenschaft aus den Eingangsdaten übernehmen.
Platine
Der nächste Schritt bestand darin, die drei Funktionseinheiten zu einem Fluss zusammenzustecken. Diesmal habe ich dazu nicht das Tooling aus dem ebclang-Projekt verwendet [3], sondern die Platine „zu Fuß“ in C# implementiert. Ich wollte auf diese Weise einmal überprüfen, ob mir etwas fehlt, wenn ich nicht per Tooling eine Visualisierung des Flows erhalten kann. Listing 5 zeigt die Platine.
Listing 5
Teile auf der Platine integrieren.
public class Zerlege_MarkDown_Text { private Action<string> process; public Zerlege_MarkDown_Text() { var verpacke_in_Text = new Verpacke_in_TextElement(); var extrahiere_fett = new Extrahiere_Fett(); var extrahiere_kursiv = new Extrahiere_Kursiv(); process += verpacke_in_Text.Process; verpacke_in_Text.Result += extrahiere_fett.Process; extrahiere_fett.Result += extrahiere_kursiv.Process; extrahiere_kursiv.Result += textElements => Result(textElements); } public void Process(string input) { process(input); } public event Action<IEnumerable< TextElement>> Result; }
Auch zur Platine habe ich Tests geschrieben. Ich wollte auf diese Weise sicherstellen, dass auch Kombinationen aus fett und kursiv gesetzten Texten möglich sind. Das war nämlich bei meiner Implementation anfangs nicht der Fall. Herausgefunden habe ich das durch exploratives Testen: Ich habe mir einen kleinen Testrahmen erstellt, in dem das UserControl verwendet wird. Darin habe ich dann mit verschiedenen Texten experimentiert und herausgefunden, dass nicht alle Kombinationen von fett und kursiv korrekt funktionierten. Als ich das erkannt hatte, habe ich die Integrationstests für die Platine ergänzt, um damit das Problem reproduzieren zu können. Erst als ich einen entsprechenden fehlschlagenden Test hatte, habe ich die Implementation korrigiert.
UserControl
Im letzten Schritt müssen die TextElement-Objekte noch in einem Control visualisiert werden. Ich habe dazu ein WPF-UserControl-Projekt erstellt. Das UserControl nimmt TextBlock-Controls in einem WrapPanel auf, die entsprechend den Angaben der jeweiligen TextElement-Objekte formatiert werden. Wenn die Eigenschaft Fett gesetzt ist, muss im TextBlock-Control die Font-Weight-Eigenschaft auf FontWeights.Bold gesetzt werden. Kursive Schrift wird erreicht, indem man die FontStyle-Eigenschaft auf den Wert FontStyles.Italic setzt.
Um nun das WrapPanel-Control mit entsprechenden TextBlock-Controls zu befüllen, will ich natürlich wieder eine Funktionseinheit im Flow ergänzen. Dabei sollen die Abhängigkeiten zu den WPF-Assemblies jedoch nicht auf die bestehenden Projekte durchschlagen. Daher habe ich die Funktionseinheit, die aus TextElement-Objekten WPF-TextBlock-Objekte macht, im UserControl-Projekt untergebracht. Dort ist die Abhängigkeit zu den WPF-Assemblies ohnehin unvermeidbar. Listing 6 zeigt die Tests zur Funktionseinheit TextBlöcke_erzeugen.
Listing 6
Textblöcke testen.
[TestFixture,