Die Verwendung von boolschen Ausdrücken ist ein Faktor, der die Lesbarkeit und das fehlerfreie Verständis eines Algorithmus maßgeblich beeinflusst. Schon im Beitrag Zauberei mit Wahrheiten wurden einige Probleme von komplizierter Logik angesprochen. Hier nun ein paar neue Beispiele für Code Smells, die sich um Wahrheitswerte ranken.
Folgender kurzer Code zeigt ein typisches Konstrukt, das sich im Laufe der Zeit, wie Grünspan auf der Kirchturmspitze, im Sourcecode bildet.
if (amount == null) { if (value.isEmpty())) { amount = new Amount(value); } } else { amount.add(value); }
Im then-Zweig finden noch weitere Prüfungen statt, während der else-Zweig keine Prüfungen enthält. Die Prüfung auf einen leeren Betrag ist vermutlich später eingefügt worden und der Entwickler als vorsichtiger Mensch, hat das bestehende Konstrukt nicht verändert. Durch die Negation des ersten Ausdrucks, können then– und else-Zweig getauscht werden und die das innere if-Konstrukt, als else-if-Zweig nach oben gezogen werden.
if (amount != null) { amount.add(value); } else if (!value.isEmpty()) { amount = new Amount(value); }
Wer sich die korrigierte Version anschaut, wird sie sicherlich schneller verstehen und sofort auf die nicht uninteressante Frage kommen, warum nur im else-Zweig auf einen leeren Betrag geprüft wird. In diesem Fall war es der Gedanke, die teure Konstruktion des Summe Objektes zu vermeiden. Ohne Insider-Information können wir für diesen Fall eine andere Form wählen. Es wird überhaupt nur etwas getan, wenn der Betrag nicht leer ist. Falls noch kein Summe Objekt existiert, erzeugen wir ein leeres Objekt und addieren daraufhin den Betrag.
if (!value.isEmpty()) { if (amount == null) { amount = new Amount(); } amount.add(value); }
Die Arbeit eines Software-Entwicklers ist aber immer ein Insider-Job. Soviel sei verraten, das Konstrukt wird verwendet um eine Liste von Beträgen aufzusummieren. Außerdem ist immer mindestens ein Betrag nicht leer. Daher hier die letzte Version. Wir erzeugen zu Beginn ein leeres Summe Objekt und addieren alle Beträge. In einer ersten Version dieses Beitrags, hatte ich noch ein Auge zugedrückt, aber sollte die Klasse Amount nicht selber entscheiden, was sie mit leeren Beträgen anstellt?
Amount amount = new Amount(); ... amount.add(value);
Eine lange Reihe von Ant-Pattern ranken sich um den Umstand, dass Programmierer in Eile und ohne tieferes Verständnis fremden Code bearbeiten müssen,
Ein weiteres fiktives Beispiel für die Häufung boolescher Bedingungen, wie Abfälle in einem Vorklärbecken, folgt. Ich schreibe gerne das Wort fiktiv, da ich mittlerweile festgestellt habe, egal wir blöd mein Beispiel ist, irgendwo da draußen gibt es produktiven Code, der genauso ausschaut.
Hier wird die Statusmeldung eines Drittsystems überprüft. Im Laufe der Zeit ändern sich die Statusmeldungen, neue werden hinzugefügt und alte fallen weg. Da die Statusmeldungen in keiner Systematik vorliegen, es keinen bekannten Status Quo gibt, können die Programmierer nur Änderungen in Code gießen.
private final static String STATUS_VALUE = "VALUE"; private final static String STATUS_MANAGER = "MANAGER"; String typ = null; if (!message.contains(STATUS_VALUE) && !message.contains(STATUS_MANAGER)) { if (message.equals("XXX-AAA)) { typ = "Typ A"; } else if (message.startsWith("YYY-)) { typ = message.substring("YYY-.length() + 1); } else { typ = message; } setTyp(typ); } else if (string.contains(STATUS_VALUE)) { setValue(string.split(":")[0]); } else if (string.contains(STATUS_MANAGER)) { setManager(message.split(":")[1]); }
Mit ein wenig Code Forensik und bewaffnet mit Kaiser’s Razor entdecken wir schnell, dass die Prüfungen umgestellt werden können, Auch das Prüfen auf zwei Teilzeichenketten, vor der Gleichheit mit einer Zeichenkette, ohne diese Teilzeichenketten, schaut verdächtig aus. Eine korrigierte Variante mit optimierter Reihenfolge sieht daher wie folgt aus.
private final static String STATUS_VALUE = "VALUE"; private final static String STATUS_MANAGER = "MANAGER"; if (string.contains(STATUS_VALUE)) { setValue(string.split(":")[0]); } else if (string.contains(STATUS_MANAGER)) { setManager(message.split(":")[1]); } else if (message.equals("XXX-AAA)) { setTyp("Typ A"); } else if (message.startsWith("YYY-)) { setTyp(message.substring("YYY-.length() + 1)); } else { setTyp(message); }
Oft verwendet, wenn es schnell gehen muss, ist der Brute-Force Ansatz bei logischen Verknüpfungen. Man spart viel Zeit, wenn man einfach alle Kombinationen bearbeitet. Hier das beliebte, Prüfen ob ein Wert in einem Intervall liegt. Das Problem scheint immer einfache Lösungen zu bescheren, wenn beide Intervallgrenzen definiert sind. Sobald eine oder beide Grenzen optional sind, ergeben sich eine Vielzahl komplizierter Lösungen.
boolean between(Date date, Date start, Date end) { boolean result = false; if (start == null && end == null) { result = true; } else if (start != null && end == null) { result = date.after(from) || date.equals(from) ? true : false; } else if (end != null && start == null) { result = date.before(end) || date.equals(end) ? true : false; } else if (end != null && start != null) { boolean between = date.before(end) && date.after(start) ? true : false; boolean exaxtStart = date.equals(start) ? true : false; boolean exactEnd = date.equals(end) ? true : false; result = between || exaxtStart || exactEnd ? true : false; } return result; }
Um ordentlichen Legacy Code darzustellen, wird hier auf der Date Klasse operiert, statt auf einer Klasse der neueren Date and Time Library. Auch habe ich die unsägliche Art verwendet, boolesche Konstanten über Ausdrücke zuzuweisen, wo doch der Ausdruck schon ausreicht.
Es gibt zwei elegantere Varianten zu der obigen Methode und es liegt am Datentyp, welche bevorzugt wird. Die erste sehr kurze Variante prüft ob der obere Grenzwert null oder größer ist und ob der untere Grenzwert null oder kleiner ist.
boolean between(Date date, Date start, Date end) { return (end == null || date.before(end)) && (start == null || date.after(start)); }
Die zweite Variante wird verwendet, wenn es sich um ein offenes Intervall handelt, also beide Grenzwerte außerhalb der erlaubten Werte liegen, oder Prüfungen auf kleiner-gleich und größer-gleich existieren. Dann kann man fehlende Grenzwerte durch den gesuchten Wert, oder einen kleineren, bzw. größeren ersetzen. Hier mal mit einem Währungsbetrags vorgestellt
boolean between(CurrencyValue current, CurrencyValue min, CurrencyValue max) { CurrencyValue currentMin = min != null ? min : current; CurrencyValue currentMax = max != null ? max : current; return current.smallerOrEquals(max) && current.biggerOrEquals(min); }
Was lernt man nun aus all diesen komplizierten Code Fragmenten, sind tatsächlich so viele Software Entwickler und Programmierer nicht in der Lage qualitativ hochwertigen Source Code zu schreiben? Sicherlich kann man an den Fähigkeiten des einen oder anderen Mitarbeiters zweifeln, aber hier handelt es sich in der Regel um Source Code, der schon viele Jahre seine Arbeit verrichtet, der schon hunderte Male angesehen, geprüft, geändert und getestet wurde. Nicht von einem Entwickler, sondern vielen.
Einen großen Beitrag zur schlechten Code Qualität liefert das Management. Häufig wird der Qualität und der Nachhaltigkeit keinerlei Beachtung geschenkt, es wird auf schnelle Feature-Umsetzung gedrängt und Programmierfehler bestraft. Entwickler, die schon lange in solchen Organisationen tätig sind oder dort ihr Arbeitsleben beginnen, sind anfällig für Anti-Pattern, die sie tagtäglich in ihrem Umfeld erleben. Das Repertoire an Lösungen, die in ihren Projekten möglich sind, ist sehr begrenzt, stagniert möglicherweise und andere, innovative Wege sind versperrt.
Es existiert kein böser Fluch, der die Entwickler daran hindert, anders und besser zu arbeiten und kein Zauber ist vonnöten, damit sich die Dornenhecke um die Entwicklungsabteilung in einen Duft von Rosen verwandelt. Wertschätzung der Arbeit der Entwickler, Teilhabe an den technischen Entscheidungen und Anerkennung bewirken mehr, als Zauberfolianten voller Prozessbeschreibungen und Arbeitsanweisungen.