Wer sich vor dem Kodieren Gedanken zum Aufbau der zu entwickelnden Software macht, der erzeugt eine deutlich höhere Softwarequalität. Dies gilt nicht nur bei der Entwicklung digitaler Produkte, sondern generell.
Vor kurzem habe ich von einem Plan gesprochen, welcher einem bei der Umsetzung helfen soll und damit zu einer besseren Softwarequalität führt. Aber wie kann dieser, für ein Programm wie RomanNumerals, aussehen? Ich könnte es mit einem Flow-Chart modellieren, das wäre mir aber viel zu detailliert für einen groben Plan. Auch die objektorientierte Analyse (z. B. mit UML) scheint mir für dieses Problem nicht passend zu sein.
Vielleicht versuchen wir es eine Stufe einfacher. Wir könnten doch das Umwandeln der römischen Zahlen erst mal in einzelne Teilaufgaben zerlegen. Das wären aus meiner Sicht die folgenden Teilaufgaben.
- Römische Zahl in Ziffern zerlegen (
"XIV"
→'X', 'I', 'V'
) - Römische Ziffer in dezimale Zahl umwandeln (
'X', 'I', 'V'
→10, 1, 5
) - Subtraktionsregel anwenden (
10, 1, 5
→10, -1, 5
) - Aufsummieren (
10, -1, 5
→14
)
Wir haben also eine Kette von Schritten. Die Umsetzung der einzelnen Schritte kann nun getrennt nacheinander erfolgen. Man kann sich somit auf eine Aufgabe nach der anderen fokussieren, was letztendlich auch für Softwarequalität förderlich ist. Erst zerlegen wir die römische Zahl in Ziffern, dann wandeln wir die Ziffern in ihren Dezimalwert um. Anschließend kommt die Subtraktionsregel, welche in diesem Fall der komplizierte Teil ist. Dieser lässt sich durch Negieren der Werte, falls die folgende Zahl größer ist, dennoch einfach implementieren. Das aufsummieren ist dank LINQ als Einzeiler wieder schnell umgesetzt.
Das ganze könnte dann so modelliert werden:
Wenn wir jetzt jeden dieser Schritte als eigene Methode implementieren und die Verkettung der Aufrufe dann in der eigentlichen Umwandlungsmethode implementieren, entsteht folgender Code:
using System.Collections.Generic;
using System.Linq;
internal static class FromRomanNumerals
{
public static int Convert(string romanNumber)
{
var romanNumerals = SplitRomanNumerals(romanNumber);
var decimals = ConvertToDecimal(romanNumerals);
var negatedDecimals = NegateWhenLarger(decimals);
return Sum(negatedDecimals);
}
private static char[] SplitRomanNumerals(string romanNumber)
{
return romanNumber.ToCharArray();
}
private static int[] ConvertToDecimal(IEnumerable<char> romanNumerals)
{
var mapping = new Dictionary<char, int> {{'I', 1}, {'V', 5},
{'X', 10}, {'L', 50}, {'C', 100},
{'D', 500}, {'M', 1000}};
return romanNumerals.Select(x => mapping[x]).ToArray();
}
private static int[] NegateWhenLarger(int[] decimals)
{
var result = new int[decimals.Length];
for (var i = 0; i < decimals.Length; i++)
{
if (i < decimals.Length - 1 && decimals[i] < decimals[i + 1])
{
result[i] = -decimals[i];
}
else
{
result[i] = decimals[i];
}
}
return result;
}
private static int Sum(int[] decimals)
{
return decimals.Sum();
}
}
Die hier gezeigte Lösung ist zwar größer als meine erste Lösung. Aber dafür sind die einzelnen Aspekte sehr schön voneinander getrennt. Die einzelnen Methoden, mit Ausnahme der Methode Convert()
, sind komplett unabhängig voneinander. Jede nimmt etwas entgegen, macht damit etwas und liefert ein Ergebnis zurück. Wer davor war oder danach kommt, interessiert nicht. Lediglich die Hauptmethode Convert()
enthält Abhängigkeiten. Diese ist aber so einfach aufgebaut, dass man durch kurzes Anschauen bereits den Ablauf sieht. Jetzt sieht man auch, wieso ich nicht immer TDD einsetze. Das Zerlegen, Umwandeln oder Aufsummieren bekomme ich für dieses Problem auch ohne TDD implementiert. Die Umsetzung der Subtraktionsregel hingegen kann durchaus von TDD profitieren.
Ich habe hier eine kleine Entwurfsmethodik gezeigt, die auch dafür sorgt, dass der Code modularer wird. Durch diese Zerlegung muss nicht alles am Stück umgesetzt werden, was am Ende auch in einer besseren Softwarequalität mündet. Wie bin ich darauf gekommen? Flow Design ist hier das Stichwort. Die hier gezeigte Lösung kann auch auf GitHub heruntergeladen werden.
Anmerkung: Bei diesem Text handelt es sich um einen überarbeiteten Repost eines alten Blog-Artikels aus 2015 von mir.