Code-Duplikate

Lesezeit: 14 Minuten

Als Code-Duplikate, auf Wikipedia Quelltextklone, bezeichnet man Redundanzen im Quellcode. Es kann sich dabei um identische oder sehr ähnliche Abschnitte handeln. Code-Duplikate werden überwiegend negativ wahrgenommen, können aber auch Vorteile haben.

Ursachen

Code-Duplikate entstehen meistens durch Copy-Paste-Programmierung. Dabei dient ein bestehender Code-Abschnitt als Vorlage, wird kopiert und an anderer Stelle eingefügt, und manchmal im Anschluss auf die individuellen Bedürfnisse angepasst.

Vorlage im Internet

Im Internet gefundener Code, etwa aus einer Antwort auf Stackoverflow, kann als Vorlage dienen, wenn der Lösungsweg vorher nicht bekannt war oder nach einer besseren, etwa performanteren, Lösung gesucht wird. Dabei entsteht per se noch kein Duplikat im eigenen Code, allerdings birgt diese Form der Copy-Paste-Programmierung die Gefahr, dass man sich mit den Details der Lösung nicht intensiv befasst und sich dadurch Sicherheitslücken oder Fehler einschleichen.

Vorlage für ein neues Projekt

Gerade im Zeitalter von Microservices ist es populär, ein bestehendes Quellcode-Projekt zu kopieren, den überflüssigen Code zu entfernen, und die entsprechenden Details wie den Projektnamen anzupassen, um die Basis für einen neuen Service zu schaffen. Hierbei entsteht projektübergreifende Code-Duplizierung.

Vorlage für eine neue Technologie

Ein ähnliches Szenario wie das zuvor beschriebene ist die Einführung eines neuen Frameworks. Man stelle sich zum Beispiel vor, dass die bestehende synchrone HTTP-Kommunikation zwischen mehreren Services durch eine asynchrone Kommunikation auf Basis von Nachrichten mit Apache Kafka abgelöst werden soll. Ein Team hat diese Umstellung bereits erfolgreich vollzogen. Andere Teams nehmen sich den neuen Code dieses Teams als Vorlage, um die Anbindung ebenfalls umzusetzen. Dabei wird sämtlicher relevante Code kopiert, im eigenen Projekt eingefügt und nach den eigenen Bedürfnissen angepasst. Auch hier kommt es zu projektübergreifender Code-Duplizierung.

Vorlage im eigenen Projekt

Vor einiger Zeit habe ich in einem Projekt mitgearbeitet, in dem eine Unterstützung für Artikel mit gemischten Steuersätzen umgesetzt werden sollte, etwa wenn einem Buch auf Papier (ermäßigter Steuersatz) ein E-Book (Regelsteuersatz) beiliegt, und beide als ein Artikel verkauft werden. Diese Erweiterung hatte Auswirkungen auf diverse Module: Einkauf, Lieferung, Faktur, Gutschrift etc. Die Umsetzung erfolgte in Form von Copy-Paste-Programmierung, so dass sich am Ende über ein Dutzend sehr ähnlicher, langer Code-Abschnitte im Quellcode wiederfanden.

Code-Duplizierung als Antipattern

Code-Duplikate haben viele Nachteile, die man gut am letzten Beispiel, der Vorlage im eigenen Projekt, aufführen kann. Wird die Definition eines Artikels mit gemischten Steuersätzen erweitert oder ein Fehler in der Umsetzung entdeckt, so muss die Anpassung an einem Dutzend verschiedener Code-Abschnitte vorgenommen werden. Kommt es dabei zu einem Fehler, so muss die Korrektur ebenfalls an einem Dutzend verschiedener Code-Abschnitte vorgenommen werden. Wird dabei ein Code-Abschnitt übersehen, kommt es ebenfalls zu einem Fehler. Dient dieser nicht angepasste Code-Abschnitt als Vorlage für eine weitere Anwendung dieses Codes, so vervielfältigt sich der Fehler damit. Geht man davon aus, dass diese Software eine Laufzeit von mindestens zehn Jahre haben soll, so können die einzelnen Umsetzungen der ehemals gemeinsamen Vorlage im Detail sehr stark voneinander abweichen. Im schlimmsten Fall ist nicht mal mehr erkennbar, welche Detailabweichungen fachlich begründet und legitim sind, und welche es eigentlich nicht geben sollte. Es entsteht ein ziemliches Chaos.

Neben der Fehleranfälligkeit wird durch diese Doppelungen aber auch der für Anpassungen erforderliche Aufwand massiv in die Höhe getrieben und die Code-Base als Ganzes wächst.

Don’t Repeat Yourself

Der Grundsatz DRY (Don’t Repeat Yourself) ist vielen ein Begriff und er fasst Code-Duplikate auf den ersten Blick eindeutig als Antipattern auf. DRY wurde von Andy Hunts und Dave Thomas in ihrem Buch The pragmatic Programmer vorgeschlagen. Das Prinzip besagt, dass jedes Stück Wissen in einem System eindeutig und einzigartig repräsentiert sein sollte. Hunts und Thomas sprechen dabei von knowledge und intent, also Wissen und Absicht. Dieses Detail ist für mich entscheidend, denn damit ist mitnichten jegliche Form von Code-Duplizierung negativ, viel mehr geht es darum, das, was man in einer Software als (Domänen-)Wissen versteht, eindeutig an einer einzigen Stelle zu definieren. Die Definition eines Artikels mit gemischten Steuersätzen gehört für mich eindeutig dazu, eine generische bedingte Anwendung einer Funktion auf alle Elemente einer komplizierten Struktur aber zum Beispiel nicht, so dass ich es völlig legitim finde, wenn der dazugehörige Code in einem Projekt an verschiedenen Stellen in ggf. leicht abgewandelter Form auftaucht. Der Unterschied ist hier auch ganz klar: Wenn sich das Wissen ändert, müssen alle Duplikate gleichermaßen angepasst werden. Eine Anpassung, die das Beispiel der Struktur betrifft, muss aber nicht zwingend für alle Fälle gelten, da diese sich unabhängig voneinander aus unterschiedlichen Gründen ändern können, denn sie liegen ja eben gerade nicht dem gleichen Domänenwissen zugrunde, sondern sind in unterschiedlichen Kontexten entstanden. Man kann hier auch von zufälligen Duplikaten sprechen.

Rule of Three

Ein Grundsatz, den man im Kontext von DRY gut berücksichtigen kann, ist die sogenannte Rule of Three, die von Don Roberts geprägt und von Martin Fowler populär gemacht worden ist: Wenn du einen Code-Abschnitt ein zweites Mal brauchst, solltest du ihn kopieren. Erst wenn du ihn ein drittes Mal brauchst, solltest du ihn in eine Funktion auslagern.

Avoid Hasty Abstractions

AHA (Avoid Hasty Abstractions) ist ein Pendant zu DRY. Es weist darauf hin, dass vorzeitige Optimierungen mindestens genauso gefährlich sind wie unbewusste Code-Duplikate. Wenn man DRY ins Extreme treibt, findet man plötzlich überall Stellen im Code, die man zentralisieren bzw. generalisieren kann.

Arbeitet man mit Streams, so befindet man sich oft in der Situation, dass man im Stream erst filtern und das Ergebnis dann mappen möchte. Um bei den Artikel zu bleiben, möchte ich vielleicht alle Artikel mit ermäßigtem Steuersatz in einem Stream filtern und anschließend auf den Namen mappen, weil mich eben die Namen aller Artikel mit ermäßigtem Steuersatz interessieren. Da es sich jedes Mal um die gleichen beiden Aufrufe handelt, könnte ich dazu eine Funktion filterAndMap() schreiben. Dabei fällt mir auf, dass es eigentlich immer um den ermäßigten Steuersatz geht und ich nehme diese konkreten Filter direkt in die Funktion auf. Der Funktionsname repräsentiert nun aber nicht mehr das, was in dieser Funktion abgebildet wird. Ein anderer Entwickler, der mit dem Code nicht vertraut ist, wird nicht darauf kommen, dass hier immer auf die gleichen Eigenschaften gefiltert wird. Ich sollte also den Namen anpassen auf filterErmaessigterSteuersatzAndMap() – liest sich sehr sperrig, oder? Am Ende komme ich hoffentlich auf den Gedanken, dass filter() und map() kompakt genug sind und eine weitere „Optimierung“ nicht erforderlich ist.

Eine ähnlicher augenscheinlicher Optimierungskandidat ist die bedingte Anwendung einer Funktion auf alle Elemente in einer Struktur. Nehmen wir an, in einer Liste von Artikel müssen all diejenigen mit gemischten Steuersätzen, nennen wir sie Bundles, neu berechnet werden und ich brauche diesen Code-Abschnitt in zwei verschiedenen Modulen. Dann könnte ich auf die Idee kommen, dies in einer Funktion updatePreiseForBundles() unterzubringen. Irgendwann kommt ein dritter Fall hinzu, der aber von der bisherigen Umsetzung in der Form abweicht, dass tatsächlich alle Artikel aktualisiert werden müssen. Ich kann nun, um Code-Duplizierung zu vermeiden, also die bestehende Funktion refactoren. Ab sofort heißt sie updatePreise() und erhält neben der Artikelliste auch ein Flag bundlesOnly. In vielen Sprachen, inklusive Java, ist so ein Flag in Form eines Booleans aber sehr nichtssagend, von außen wird in die Funktion lediglich true oder false hineingegeben. Abhilfe kann ein enum schaffen, was nur für diese einzelne Funktion aber etwas überkonstruiert erscheint. Ander Sprachen wie Kotlin können den Aufruf durch Named Parameters expliziter machen. Es bleibt aber eine Gefahr: In der Zukunft könnten weitere Abweichungen auftreten und der genaue Inhalt der Funktion immer unverständlicher werden, man stelle sich vor, man müsse jedes Mal fünf Booleans in die Funktion reingeben, um sämtliche Randfälle abzudecken, oder es gibt diverse Überladungen der Funktion, also unterschiedliche Parametrisierungen, oder diverse sehr ähnlich benannte Funktionen, die sich zum Teil gegenseitig aufrufen, um diese Logik möglichst zentral zu kapseln.

Der Fall ist nicht rein hypothetisch. Ich habe soetwas schon häufiger in der Vergangenheit erleben müssen. Am Ende ist die eigentlich triviale Logik undurchschaubar und es besteht die Gefahr, dass neue Varianten hinzugefügt werden, die mit bestehenden indentisch sind, weil einfach der Überblick verloren gegangen ist, oder dass Parametrisierungen missinterpretiert und falsch verwendet werden. Fakt ist, der ursprüngliche Code war ziemlich simpel und eine solche Abstraktion stellt eine erhebliche Verschlimmbesserung dar, insofern: Vorsicht vor vorzeitiger Optimierung!

Vermeidung von Zentralisierung

Vermeidung von Code-Duplikaten bedeutet Zentralisierung. Diese hat in allen Lebensbereichen gleichermaßen Vorteile wie Nachteile. In Deutschland steht aus den Erfahrungen der Vergangenheit der Föderalismus als Gegenpol zur Zentralisierung. Dieser fördert nicht nur die Demokratie, sondern verhindert auch weitgehenden Machtmissbrauch. Ebenso kann eine Diversifizierung der Systemlandschaft Angriffe mit weitreichenden Folgen besser vermeiden, als wenn alle Komponenten die gleiche Sprache, das gleiche Framework und die gleiche Version der gleichen Logging-Bibliothek verwenden müssen. Gleichwohl kann dies die Zufriedenheit erhöhen und bessere Chancen bieten, geeignetes Personal zu finden. Denn welcher Entwickler freut sich nicht, wenn er nicht zwingend Java vorgesetzt bekommt, sondern auch in Teams mitarbeiten darf, die sich etwa für Kotlin oder Scala entschieden haben.

Was genau hat dies mit Duplizierung zu tun? Nun zum Beispiel, dass man bewusst in Kauf nimmt, dass Projekte in Details auseinanderwachsen. Wie ich vorhin geschrieben hatte, adressiert DRY die Duplizierung von Wissen. Es spricht aber nichts dagegen, wenn ein Team das Logging-Setup eines anderen kopiert und sich mit der Zeit Abweichungen in der Konfiguration einschleichen. Ein anderes Team mag auch die Entscheidung treffen, eine völlig andere Logging-Bibliothek einzusetzen. Wird hier auf die Vermeidung von Redundanzen gepocht, in dem zum Beispiel eine zentrale, zu verwendende Logging-Bibliothek bereitgestellt wird, so betreffen Fehler in selbiger auch alle Teams gleichermaßen und das Team, welches diese Bibliothek pflegt und bereitstellt, kann sich als Flaschenhals entpuppen, wenn es darum geht, Anforderungen der anderen Teams umzusetzen.

Code-Duplikate finden sich nicht nur im originären Code des Software-Produktes wieder. Duplikate können genauso in betriebsrelevanten Skripten oder der Infrastruktur-Definition, Infrastructure as Code (IaC), auftauchen. Setzt man eine CI/CD-Umgebung wie etwa GitLab CI ein, so hat man die Möglichkeit, den generellen Pipeline-Prozess zu zentralisieren, um Code-Duplikate zu vermeiden. In einem Projekt mit mehreren Services auf Basis von Kotlin, Spring Boot und Gradle haben wir dies über die Include-Funktion von GitLab CI gemacht. Dabei werden die Abarbeitung von Tests, die statische Code-Analyse, der Build- und der Deployment-Prozess an zentraler Stelle definiert. Beliebige Projekte auf Basis dieses Stacks können diese zentrale Vorlage per Einzeiler einbinden, so dass die Pipeline eines Standard-Projektes im besten Fall nur aus einer Zeile Code besteht. Diese Zentralisierung hat aber auch Nachteile, denn Anpassungen wirken sich sofort auf sämtliche Projekte aus, die diese Zentralisierung integrieren. Auch werden hier nicht alle Bedürfnisse berücksichtigt. Manchmal repräsentiert ein Projekt gar keinen deploybaren Service, sondern nur ein Software-Artefakt, das in einem Repository veröffentlicht werden soll. Wir haben uns an dieser Stelle entschieden, die Zentralisierung als Option für Services anzubieten, für nicht deploybare Software-Artefakte aber die gemeinsamen Schritte, Tests, statische Code-Analyse und Bau des Artefaktes zu duplizieren und direkt im Projekt des Software-Artefaktes unterzubringen.

Bewusste Zentralisierung

An dieser Stelle möchte ich noch ein Beispiel nennen, wo wir uns bewusst für das Angebot einer Zentralisierung entschieden haben. Angebot bedeutet, dass es jedem Team frei steht, zu entscheiden, ob es davon Gebrauch machen möchte oder nicht. Bei einem Umzug in die AWS-Cloud haben wir nach kurzer Zeit festgestellt, dass der Standard einer VPC (Virtual Private Cloud) im CDK (Cloud Development Kit) unseren Sicherheitsansprüchen nicht genügt. Unter anderem wurde nicht explizit unterbunden, dass SSH-Verbindungen in die Cloud aufgebaut werden konnten. Wir haben daher eine Ableitung der Standard-VPC gebaut, die für die Organisation so allgemeingültig ist, dass sie allen Teams als Basis eignen sollte. Dabei haben wir bei jeder Zeile Code abgewogen, ob es sich wirklich um eine generelle Einstellung handelt, und das Ergebnis als Construct in einem internen Repository zugänglich gemacht, so dass sich nicht alle Teams mit den gleichen Problemen neu befassen müssen. Die Überlegung war hier, dass die Einstellungen unserer angepassten VPC zwar durch die Unterbringung in einem eigenen Construct implizit wurden, also versteckt waren, es sich hierbei aber um Details handelt, die die eigentliche IaC-Definition nur aufblähen und eher vom Wesentlichen ablenken, und aus unserer Sicht auch Standards des Cloud-Services hätten sein können.

Auf den Punkt gebracht

  • Domänenwissen sollte an zentraler Stelle definiert sein
  • Code-Duplikate können legitim sein, wenn sie kein Domänenwissen betreffen und das Verständnis verbessern
  • explizit ist häufig besser als implizit
  • bei der Anwendung von DRY sollte die Rule of Three beachtet und vorzeitige Optimierung vermieden werden
  • Vermeidung von Code-Duplikaten bedeutet Zentralisierung
  • Teamübergreifende Zentralisierung führt zu Kopplungen zwischen den Teams (Koordination, Abhängigkeiten, Wartezeiten, Engpässe)