Clean Code: Grundlagen für verständlichen und wartbaren Code

Clean Code ist eine Entwicklungspraktik, die von Robert C. Martin präsentiert wurde und den Fokus auf die Verständlichkeit und Lesbarkeit von Code legt. In einer Zeit, in der KI-Modelle Code generieren können, ist es wichtiger denn je, dass Menschen diesen Code verstehen und bewerten können. Clean Code stellt sicher, dass Software nicht nur funktioniert, sondern auch langfristig wartbar bleibt. In diesem Artikel werden Konzepte und Techniken betrachtet, die Clean Code auf der Systemebene umsetzen.

Überblick

Im vorherigen Artikel lag der Fokus auf der Gestaltung einzelner Klassen und Module. Dieser Artikel hebt die Perspektive auf die Systemebene an: Das Zusammenspiel von Klassen und Modulen.

Es werden mehrere zentrale Aspekte betrachtet, die für die Gestaltung wartbarer und verständlicher Software entscheidend sind. Dazu gehört die Unterscheidung zwischen Objekten und Strukturen, um die Vorteile der jeweiligen Paradigmen optimal zu nutzen. Die Grenzen von Systemen und deren Schnittstellen werden analysiert, um externe Abhängigkeiten effizient zu handhaben. Der Aufbau und die Struktur von Klassen sowie das Zusammenspiel in größeren Systemen stehen ebenfalls im Fokus, um kohärente und modulare Software zu gewährleisten. Zudem werden Emergenzeffekte guten Designs beleuchtet, die durch klare Prinzipien wie Testbarkeit und Einfachheit entstehen. Abschließend wird auf den Umgang mit paralleler Verarbeitung eingegangen, einschließlich der Herausforderungen durch konkurrierende Zugriffe und Deadlocks.

Objekte und Strukturen

Objekte verbergen ihre internen Daten und bieten Methoden an, um mit ihnen zu interagieren. Strukturen hingegen legen ihre Daten offen und enthalten kein Verhalten. Diese Eigenschaften entspringen unterschiedlichen Programmierparadigmen: Die objektorientierte (OO) Programmierung basiert auf der Kapselung von Daten und Verhalten, während die prozedurale Programmierung auf offenen Daten und klar definierten Prozessen fußt. Man sollte diese Paradigmen nicht durchmischen, da dies dazu führen kann, die jeweiligen Vorteile wie Flexibilität und Wartbarkeit zu verlieren.

Unterscheide klar zwischen Objekten mit Verhalten und Strukturen mit offenen Daten.

Ein Objekt soll kein Wissen über die Interna anderer Objekte haben. Methoden sollen nur mit bekannten Objekten interagieren. Zum Beispiel sollte ein Objekt A nicht auf Methoden eines Objekts C zugreifen, indem es sich von Objekt B eine Referenz auf C holt. Dieses Prinzip minimiert Abhängigkeiten und erhöht die Wartbarkeit.

Folge der “Law of Demeter” und vermeide unnötige Abhängigkeiten zwischen Objekten.

Anstatt Daten von einem Objekt zu erfragen und selbst zu manipulieren, sollten die Methoden des Objekts die Manipulation ausführen. Dies führt zu klarerem und sichererem Code. Dieses Prinzip wird oft als “Tell, don’t ask” zusammengefasst: Ein Objekt sollte immer selbst für die Änderung seines Zustands zuständig sein.

Fordere keine Daten von Objekten an, sondern lasse sie die Arbeit selber erledigen.

Objekte mit Gettern und Settern sind oft problematisch. Gerade im Umfeld von Java Beans sind solche Objekte häufig anzutreffen. Sie sollten als DTOs oder ActiveRecords gekennzeichnet werden und nicht mit Business-Objects verwechselt werden, die ihre Daten kapseln.

Trenne klar zwischen Business-Objects und DTOs.

Objekte sollten ihre Interna verbergen und Methoden als sinnvolle Abstraktionen anbieten. Eine Methode stellt ein Verhalten oder eine Aktion des Objekts dar. Man soll dabei das “Was” betrachten, also was die Methode erreichen soll, und nicht “Wie” sie intern funktioniert. Dies erleichtert die Nutzung und reduziert Fehler.

Verstecke die Interna eines Objekts und stelle nur abstrahierte Methoden bereit.

Grenzen

Externe APIs sind oft umfangreich. Adapter helfen dabei, die API an den eigenen Bedarf anzupassen. Die Verwendung eines Adapters hält die externe Schnittstelle aus dem eigenen Code fern. Zudem erleichtert der Adapter bei Bedarf den Austausch der externen Komponente.

Nutze Adapter, um externe APIs an deinen Bedarf anzupassen.

Durch die Definition von Interfaces können externe APIs flexibel integriert werden. Dieses Vorgehen folgt dem Prinzip der “Abhängigkeitsumkehr”. Man behält dadurch die Hoheit über die Schnittstelle, schützt den eigenen Code und erleichtert das Auswechseln der Abhängigkeit. Später wird der externe Code mittels Adapter angebunden.

Definiere Interfaces, bevor du externen Code einbindest.

Um externe APIs zu verstehen, kann man sie mit UnitTests ansprechen. Dies führt einerseits zu einer lebenden Dokumentation. Andererseits schützt man sich so vor Änderungen der Schnittstelle.

Erstelle Tests, um externe APIs zu dokumentieren und deren Verhalten zu sichern.

Dadurch, dass Adapter externe APIs kapseln, können Änderungen an der API zentral im Adapter behandelt werden. Dies hält den Wartungsaufwand klein.

Behandle API-Änderungen zentral in den Adaptern.

Externe Objekte sollten nicht direkt im eigenen Code genutzt werden. Stattdessen sollten externe Strukturen und DTOs beim Übergang in den eigenen Code gemapped werden. Dies dient ebenfalls dem Schutz vor Änderungen und erleichtert die Wartung.

Mappe externe Objekte im Adapter in eigene.

Klassen

Klassen sollten nach einem einheitlichen Schema strukturiert sein, um die Lesbarkeit zu erhöhen: Zuerst statische Variablen, dann Instanzvariablen und zuletzt Methoden. Code Formatter und IDEs können dabei unterstützen.

Strukturiere Klassen nach einer klaren Konvention.

Jede Klasse sollte genau eine Verantwortung haben. Man kann auch sagen, dass eine Klasse sich nur aus einem Grund ändern soll. Dies reduziert die Komplexität und erhöht die Wiederverwendbarkeit. Eine Ausnahme bilden Querschnittsaspekte wie z.B. Logging. Allerdings ändern sich derartige Klassen üblicherweise nur aus inneren Gründen.

Halte dich an das Single Responsibility Principle.

Funktionen innerhalb einer Klasse sollten dieselben Variablen nutzen. Lassen sich Variablen und Methoden einer Klasse in zwei getrennte Gruppen aufteilen, liegt der Verdacht nahe, dass die Klasse mehr als eine Zuständigkeit hat. Wenn dies der Fall ist, ist eine Aufspaltung sinnvoll.

Stelle sicher, dass alle Funktionen einer Klasse kohärent zusammenarbeiten.

Klassen sollten von Abstraktionen abhängen, nicht von Details. Dieses Prinzip bedeutet, dass der Code so gestaltet werden sollte, dass er mit allgemeinen Schnittstellen arbeitet, anstatt direkt von konkreten Implementierungen abzuhängen. Dies wird in der Praxis durch den Einsatz von Interfaces und Konstruktor-Injektionen erreicht. Durch die Abstraktion von Abhängigkeiten über Interfaces wird der Code flexibler, da konkrete Implementierungen leicht ausgetauscht werden können. Gleichzeitig erleichtert dies das Testen, da Testdoubles oder Mock-Objekte für die Abstraktionen verwendet werden können.

Hänge Klassen an Abstraktionen, nicht an Details.

Kleine Klassen mit wenigen Verantwortlichkeiten sind einfacher zu testen und zu warten. Je kleiner die Menge der möglichen Zustände und Zustandswechsel ist, desto weniger Testfälle müssen abgedeckt werden.

Halte Klassen klein und fokussiert, um die Testbarkeit zu erhöhen.

Systeme

Die Konstruktion von Objekten sollte getrennt von deren Nutzung erfolgen. Große Anwendungen bestehen üblicherweise aus komplexen Graphen von Abhängigkeiten. Diese Strukturen sollten unbedingt von den einzelnen Klassen ferngehalten werden, um eine einfache Umstrukturierung zu ermöglichen. Dies kann manuell über Factories oder automatisch über Dependency Injection (DI) erfolgen.

Trenne die Konstruktion und Nutzung von Objekten.

Standards und Paradigmen sollten verwendet werden, wenn sie Mehrwert bieten. Dogmatik führt zu unnötiger Komplexität. Ein Beispiel dafür ist die stringente Anwendung des DRY-Prinzips (Deduplikation), die zu einer Verletzung des Single Responsibility Principles führen kann. Wenn ein deduplizierter Code-Baustein von mehreren Verantwortlichkeiten abhängt, wird dieser schwer wartbar und erhöht die Komplexität statt sie zu reduzieren.

Nutze Standards und Paradigmen nur, wenn sie wirklich helfen.

Schreibt man als erstes die Tests entlang der Anforderungen, führt dies dazu, dass auch nur so viel produktiver Code geschrieben wird, wie benötigt wird. Es wird kein unnötiger Code erstellt. Architekturen sollten einfach gehalten werden und mit den Anforderungen wachsen. Test Driven Development hilft, dies umzusetzen.

Halte Architekturen einfach und passe sie bei Bedarf an.

Domain-Specific Languages und Fluent-APIs erleichtern die Entwicklung, da sie Anforderungen in der Fachsprache abbilden. Man kann somit in abgegrenzten Bereichen der Domäne Code schreiben, der quasi wie Prosa aussieht. Dies macht es sehr einfach, den Code zu verstehen und ggf. anzupassen.

Nutze DSLs, um Code nah an den Anforderungen zu schreiben.

Querschnittsaspekte wie Logging oder Sicherheit sollten mittels AOP (Aspekt orientierte Programmierung) und Annotationen ausgelagert werden. Der Code kann somit auf die Fachlichkeit fokussiert werden und bleibt verständlicher. Zusätzlich fördert dieses Vorgehen die Einhaltung des Single Responsibility Principle.

Lagere Querschnittsaspekte aus, um den Code schlank zu halten.

Emergenz

Die vier Regeln einfachen Designs bieten einen klaren Rahmen für wartbaren Code:

  1. Alles wird getestet: Jeder Codepfad sollte durch Tests abgesichert sein. Dies stellt sicher, dass die Software korrekt funktioniert und spätere Änderungen keine unerwünschten Nebenwirkungen haben.

  2. Keine Duplikate: Duplikate im Code sollten konsequent vermieden werden. Redundanz erhöht die Wartungskosten und das Risiko von Inkonsistenzen. Stattdessen sollte Wiederverwendbarkeit durch Abstraktionen gefördert werden.

  3. Ausdruck des Denkens: Der Code sollte klar und verständlich die Absicht des Entwicklers ausdrücken. Gut gewählte Namen und eine saubere Struktur helfen, den Code leichter lesbar und nachvollziehbar zu machen.

  4. Minimale Klassen: Weniger und kleinere Klassen führen zu einer einfacheren Struktur. Jede Klasse sollte eine klar abgegrenzte Verantwortung haben, um die Komplexität gering zu halten.

Diese Regeln führen zu einem Design, das einfacher zu verstehen und leichter zu warten ist.

Halte dich an die Regeln für einfaches Design, um wartbaren Code zu schreiben.

Schreibt man im Kleinen ausdrucksstarken Code ohne Duplikate, erhält man Code, der klar und wartbar ist. Hält man sich zusätzlich im Großen an das Single Responsibility Principle, führt dies von selbst zu einer guten Gesamtstruktur. Automatisch entstehen kleine und fokussierte Klassen, die leicht zu verstehen und effektiv nutzbar sind. Dies zeigt, dass die konsequente Einhaltung dieser Prinzipien nicht nur einzelne Code-Bereiche verbessert, sondern auch die gesamte Architektur positiv beeinflusst.

Halte dich an SRP, DRY und klare Benamung

Regelmäßiges Refactoring hilft, die Regeln umzusetzen und den Code schlank zu halten. Bei jeder Bearbeitung des Codes sollte man nach Stellen Ausschau halten, die optimiert werden können. Man sollte sich an die Regel der Pfadfinder halten: “Verlasse jeden Ort besser, als du ihn vorgefunden hast.”

Refaktoriere regelmäßig, um Designprobleme zu beheben.

Parallelität

Parallelität bringt Overhead und komplexe Bugs mit sich, die oft schwer zu reproduzieren sind. Fehler in parallelen Programmen treten häufig sporadisch auf und lassen sich unter bestimmten Bedingungen gar nicht nachstellen. Dies erschwert die Analyse und Behebung erheblich. Zudem erfordert korrekte Parallelität oft grundlegende Designänderungen, um die Stabilität und Zuverlässigkeit des Systems zu gewährleisten.

Plane Parallelität von Anfang an sorgfältig ein.

Konkurrierender Zugriff auf Daten führt oftmals zu schwer analysierbaren Bugs. In der funktionalen Programmierung werden Datenstrukturen verwendet, die nicht veränderbar (immutable) sind. Dadurch wird das Problem konkurrierenden Zugriffs von vornherein verhindert. Dieses Prinzip lässt sich auch auf die objektorientierte Programmierung übertragen. Dabei arbeitet man auf Kopien der Daten und führt diese am Ende der parallelen Verarbeitung zusammen. Dies reduziert Abhängigkeiten zwischen Threads und vermeidet Fehler.

Teile so wenige Daten wie möglich zwischen Threads.

Deadlocks treten bei parallelisierten Programmen auf, wenn mehrere Threads gleichzeitig denselben Satz von Ressourcen sperren wollen und jeder bereits einen Teil dieser Ressourcen gesperrt hat. Jeder Thread wartet auf die Freigabe durch die anderen, wodurch ein Zyklus von Abhängigkeiten entsteht, der zu einem vollständigen Stillstand führt. Diese lassen sich durch mehrere Strategien vermeiden. Zum Beispiel können Timeout-Mechanismen dafür sorgen, dass ein Thread nach einer bestimmten Wartezeit seine Ressourcen freigibt, wodurch Blockaden beendet werden. Ressourcen-sperr-Reihenfolgen können sicherstellen, dass alle Threads Ressourcen in derselben Reihenfolge sperren. Dies verhindert zyklische Abhängigkeiten. Alternativ kann die Anzahl der verfügbaren Ressourcen erhöht werden, um die Wahrscheinlichkeit von Deadlocks zu minimieren. Die hier genannten Ansätze sind nur Beispiele; es gibt viele weitere Strategien, die je nach System und Anforderungen eingesetzt werden können.

Nutze Mechanismen, um Deadlocks zu verhindern.

Parallele Systeme sind sehr abhängig vom Lastzustand und den Plattformen, auf denen sie laufen. Eine höhere Last erhöht die Wahrscheinlichkeit für Ressourcen-Engpässe, was zu unerwarteten Problemen führen kann. Zudem behandelt jede Plattform die Koordination von Threads anders, was plattformspezifische Fehlerquellen schaffen kann. Sie sollten daher immer auf verschiedenen Plattformen und unter hoher Auslastung getestet werden.

Teste parallelen Code intensiv und plattformübergreifend.

Verwende Thread-Safe Collections, Atomic Primitives und Executors, um Threads zu koordinieren und Fehler zu vermeiden. Dies sind Beispiele aus dem Java-Ökosystem. In nahezu jedem Sprach-Ökosystem stehen ebenfalls Bibliotheken zur Verfügung, die Parallelität effektiv unterstützen. Es ist ratsam, diese Bibliotheken zu nutzen, um zuverlässige parallele Anwendungen zu erstellen.

Nutze geeignete Bibliotheken, um Threads sicher zu handhaben.

Zusammenfassung

Clean Code ermöglicht wartbare und verständliche Software, indem es klare Strukturen, saubere Abstraktionen und Prinzipien wie SRP und DIP in den Vordergrund stellt. Diese Prinzipien verbessern die Qualität des Codes und erleichtern die Zusammenarbeit im Team. Durch gemeinsame Konventionen und leicht verständlichen Code wird der Austausch unter Entwicklern vereinfacht. Dies steigert sowohl die Effizienz als auch die Motivation innerhalb eines Projekts. Es sorgt dafür, dass Software nicht nur fehlerfrei funktioniert, sondern auch langfristig leicht zu pflegen ist. Zusätzlich trägt der gezielte Umgang mit Parallelität dazu bei, die Stabilität und Performance von Software zu gewährleisten.

Ausblick

Der nächste Artikel widmet sich langfristigen Strategien zur Pflege von Software. Refactorings und Techniken, um Software flexibel und wartbar zu halten, stehen im Fokus.

Abschluss

Clean Code ist eine essenzielle Grundlage für jeden Softwareentwickler. Das Buch “Clean Code” von Robert C. Martin ist eine sinnvolle Investition, um die hier beschriebenen Konzepte und Praktiken detailliert kennenzulernen und anhand von Beispielen zu verstehen.