DRY vs KISS – Clean Code Prinzipien

Bei DRY und KISS handelt es sich um zwei Prinzipien in der Softwareentwicklung, die für besseren, korrekteren und verständlicheren Code (Clean Code) sorgen sollen. Beide setzen bereits im Kleinen an, lassen sich jedoch auch im größeren Maßstab, z.B. in einem Projekt anwenden. Es gibt jedoch Situationen, in denen sie sich gegenüberstehen und gegenseitig auszuschließen. Dieser Artikel soll sich mit diesem Dilemma beschäftigen. Eine allgemeingültige Lösung für das Problem wird er nicht liefern können, aber ich hoffe zumindest die Aufmerksamkeit dafür ein wenig zu schärfen.

Ich will zunächst kurz auf die beiden Clean Code Prinzipien eingehen, mit denen sich der Artikel befasst, und dann auf die Problemstellung, sowie mögliche Ansätze aufzeigen.

 

Die Prinzipien

DRY

DRY steht für „Don’t repeat yourself„, zu Deutsch: Wiederhole dich nicht. Dieses Clean Code Prinzip erklärt sich fast von selbst. Wiederholungen im Code sind zu vermeiden. Das ist die Essenz. Es lässt sich auch außerhalb des Codes anwenden, etwa für automatisierbare Vorgänge oder Handgriffe im Entwicklungsprozess, die sonst immer von Hand gemacht würden, aber damit soll sich dieser Artikel nicht befassen.

Einfachstes Beispiel: „Magic Numbers“ oder auch „Magic Strings“. Hartcodierte Zahlenwerte oder Strings. Oftmals finden diese an mehreren Stellen Verwendung und haben dann immer die gleiche Bedeutung. Um DRY einzuhalten, sollte ein solcher Wert in eine Konstante geschrieben werden und diese Konstante an seiner statt im Code stehen. Ändert sich dieser Wert, muss nur eine Stelle angepasst werden.

Ein anderes Beispiel ist das Extrahieren von Methoden. Ein Codeblock wiederholt sich mehrfach, oft hervorgerufen durch Copy and Pasting. „Oh, genau das habe ich doch schon mal weiter oben gemacht. Dann kopiere ich die Stelle einfach“. Besser wäre es, die Funktionalität in eine eigene Methode auszulagern und diese an den benötigten Stellen aufzurufen. Zusätzlich zum verringerten Aufwand bei Änderungen genau dieser Logik, wird der Code dadurch auch lesbarer, sofern sprechende Namen für solche Funktionen verwendet werden.

 

KISS

Diese Abkürzung steht für „Keep it simple, stupid“. Halte es einfach. Einfachheit liegt immer auch ein wenig im Auge des Betrachters, doch grundsätzlich soll dieses Clean Code Prinzip den Entwickler dazu anhalten, nach dem einfachsten, am wenigsten komplexen Lösungsweg Ausschau zu halten. Die trickreichere Möglichkeit, mit der man all sein technologisches Können unter Beweis stellen kann, ist nicht immer die Beste. Einfach soll in erster Linie heißen, dass der Code für alle beteiligten leicht zu verstehen ist. Andere Entwickler (das schließt gewissermaßen auch das eigene „zukünftige Ich“ mit ein), sollen nicht lange überlegen müssen, um den Sinn und Zweck einer Funktion zu erkennen. Das schließt tief verschachtelte Verzweigungen und Schleifen, sowie Methoden, die sich über eine große Anzahl Zeilen erstrecken, grundsätzlich aus. Eine Verletzung dieses Prinzips wird am ehesten durch frühe und regelmäßige Codereviews aufgedeckt. Denn dann tritt genau der Fall ein, dass ein anderer Entwickler den Code zum ersten Mal sieht und verstehen muss. Hat er damit Probleme, ist die betrachtete Struktur möglicherweise zu kompliziert gestaltet.

Das Problem mit diesem Prinzip ist, dass sich „einfach“ nicht so strikt definieren lässt. Jeder hat unter Umständen ein anderes Empfinden davon, was schwierig und was einfach ist.

 

Das Problem

Die Einhaltung dieser zwei recht einfachen Regeln kann bereits eine große Wirkung auf ein Projekt haben. Wenn man es nur beherzigt, Wiederholungen durch das Extrahieren von Methoden zu vermeiden, wird der Code auch in der Regel übersichtlicher und somit einfacher verständlich. Anstelle des sich wiederholenden Codeblocks steht dort nun der Name einer Methode, der beschreibt, was an dieser Stelle passiert. Der Leser kann die Funktion somit schneller erfassen, und sie lässt sich an beliebig vielen Stellen einsetzen.

Wenn sich KISS und DRY aber derart komplementieren, was ist dann der Sinn davon, sie gegeneinander zu stellen? Nun ja, wenn man die Welt einfacher Beispiele verlässt, kommt es vor, dass man sich für eines von beidem entscheiden muss, um es mal dramatisch zu formulieren. Ich bin in eine solche Situation geraten, wodurch mir dieses „Kräftefeld“ erst deutlich wurde. Bei dieser Betrachtung sind ein paar Dinge zu beachten. Zum einen war das Projekt als Ganzes betrachtet fernab davon „Clean Code“ zu sein. In einem von Grund auf nach modernem Kenntnisstand, einheitlich designten Umfeld, wäre diese Situation vielleicht gar nicht erst entstanden. Zum Anderen ist es sehr schwierig, den Sachverhalt vollumfänglich darzustellen, ohne eine komplette Skizzierung des Projekts und der Umstände zu liefern, was selbstredend nicht im Umfang dieses Postings möglich ist. Aber ich werde mir Mühe geben, das zugrunde liegende Problem verständlich zu machen.
Eine Gruppe von Winforms-Controls, die Teil unseres Projekts war, sollte für ein anderes Softwareprojekt zur Verfügung stehen. Diese Views repräsentieren ein Modell, das zentraler Bestandteil des Programms ist. Der User erhält in verschiedenen Ansichten alle Infos zu dem Modell und kann viele davon auch ändern. Jeder View hat ein ViewModel und eine Reihe von Events, bzw. Messages, über die er jeweils nur mit seinem Parent kommuniziert. Die gesamte Logik, zum Aufbau der ViewModels und zum Verändern des zu Grunde liegenden Models befindet sich außerhalb der Views. Diese wissen nichts davon.

Soweit so gut. Durch diesen Umstand war es zumindest schon mal relativ einfach, die reinen Controls aus dem Projekt zu extrahieren und ohne jegliche Abhängigkeit auf unsere Software wiederzuverwenden. Mit den Controls wurden natürlich auch deren ViewModels mit extrahiert. Auch hier kein Problem. Dieser Part war einfach. Die Controls waren vollständig von uns entkoppelt und konnten im anderen Projekt verwendet werden.

Doch was ist mit der Logik? Das „Basismodell“, welches unsere Software verwendet ist ein anderes, als das, was Projekt B zur Verfügung steht. Es enthält letzten Endes die gleichen Daten, sonst könnte man die Views ja schon rein logisch nicht wiederverwenden. Außerdem verwendet unsere Software eine Kommando-Infrastruktur, die der anderen einfach nicht zur Verfügung steht. Wie gesagt, das Modell ist zentraler Bestandteil des Programms. Hinzu kommt, dass unsere Anwendung in Verbindung mit diesen Views ein paar Funktionen zusätzlich bietet, die für die externe Anwendung nicht verfügbar sind. Unser Problem ist also die Schnittstelle zwischen den ViewModels und dem Datenmodell. Obwohl sehr ähnlich, müssen die beiden Datenmodelle unterschiedlich auf die ViewModels gemapped werden. Alle Änderungen, die vom User in den Views angestoßen werden, müssen unterschiedlich, wenn auch hier sehr ähnlich, auf das Datenmodell angewendet werden.

Der Lösungsansatz, den wir für diese Konstellation letztlich verfolgt haben, war es, DRY hinten an zu stellen, um KISS Einhalt zu gebieten. Um die Strukturen nicht zu komplex zu machen, und nicht zu viele zusätzliche Abstraktionsschichten einzuführen, so dass der Code weiterhin lesbar und verständlich bleibt, wurde die komplette Logik in der Quelle so belassen wie sie war, inklusive lokaler ViewModels und Message-Klassen. Diese bestanden nun also zwei mal. Lokal und in dem extrahierten Projekt. DRY wurde mit Füßen getreten. Der Vorteil an dieser Stelle war, dass die Komponente so unverändert in unserem Projekt weiter verwendet werden konnte. Der Nachteil: Die lokalen ViewModels müssen auf die externen gemappt werden. Und anders herum mit den Messages, die aus den Controls kommen. Doch diese Mappingschicht hat keinerlei Intelligenz. Es ist nur Mapping. Aus Sicht von KISS also in Ordnung.

Das externe Projekt, weswegen wir die Extraktion der Controls begonnen haben, konnte nun die Controls selbst verwenden und die Logik für seine eigenen Bedürfnisse aufbauen.

Wertung

War unsere Umsetzung perfekt? Eher nicht. Wir sind einen Kompromiss eingegangen. Wir haben in Kauf genommen, uns zu wiederholen, um die Komplexität der Strukturen niedrig zu halten und nicht zu viele zusätzliche Schichten einzuführen. In einer „heilen Welt“ gäbe es ein gemeinsames Datenmodell und die verfügbare Infrastruktur würde die gleichen Schnittstellen bedienen. Dann hätten beide Projekte die exakt gleiche Bibliothek, ohne jedes weitere Zutun verwenden können. In diesem Fall musste man jedoch eine Lösung finden, mit der alle Beteiligten arbeiten können. Wir haben uns dafür entschieden KISS Vorrang zu geben.

Unbestreitbar ist, dass der Aufwand für zukünftige Anpassungen an dieser Komponente gestiegen ist. Soll eine weitere Eigenschaft des Datenmodells in Zukunft Teil des View sein, müssen mindestens zwei Stellen Anpassungen vorgenommen werden. Vielleicht hätten wir eine Lösung gefunden, wie wir diesen zusätzlichen Aufwand hätten vermeiden können. Doch unser Gefühl sagte uns, dass eine solche Lösung ungleich komplizierter wäre. Dies erhöht wiederum den Änderungsaufwand, und ein Entwickler, der nicht voll und ganz in eine solche Lösung eingewiesen worden wäre, würde mit höherer Wahrscheinlichkeit Fehler machen.

 

Fazit

Letztendlich basiert die gefällte Entscheidung einzig und allein auf Meinungen und dem Empfinden der Teammitglieder. Vielleicht hätte ein anderes Team sich für DRY und gegen KISS entschieden. Schließlich ist eine gewisse Zunahme an Komplexität normal, wenn die Komplexität der Anforderungen zunimmt, und das ist auch in Ordnung, sofern alle Beteiligten mit dem Zuwachs zurechtkommen und niemand abgehängt wird. Währenddessen birgt eine Verletzung von DRY für jedes Team, unabhängig von den Fähigkeiten einzelner Mitglieder, unabhängig von der Schwierigkeit der verwendeten Architektur und Struktur ein gewisses Risiko. Ich bin mir sicher, dass sich jeder Entwickler an einem Punkt in seiner Programmierlaufbahn des Copy& Pastings schuldig gemacht hat. Und so ziemlich jeder wird es auch erlebt haben, wie leicht es ist, bei kopiertem Code etwas zu vergessen und Fehler zu machen. Vielleicht wird das Problem sofort entdeckt und innerhalb von Sekunden behoben. Es wäre aber auch möglich, dass ein solcher Fehler größere Auswirkungen hat, die erst später jemandem auffallen.

Mein Fazit wäre daher folgendes: Es mag Konstellationen geben, in denen ein Spannungsfeld zwischen verschiedene Clean Code Prinzipien, KISS und DRY sind nur Beispiele, entsteht. Wenn keine gute Lösung gefunden wird, die beiden Regeln gerecht wird, muss man sehr genau abwägen, welcher der beiden man Vorrang gibt. Vor- und Nachteile beider Möglichkeiten wollen wohl ausgelotet sein und letztlich muss das Team sich auf eine der Möglichkeiten einigen und verpflichten. Ob die Entscheidung eine gute war, wird nur die Zeit zeigen.