Qualität ist besser!

Nichtfunktionale Anforderungen: Struktur, Organisation und Wartung

Marco Schulz

© Shutterstock / Wright Studio

Um der Definition von Codequalität gerecht zu werden, genügt es nicht, sich einzig auf den Quelltext zu konzentrieren. Auch Struktur, Organisation und Wartung spielen eine wichtige Rolle.

Aus Erfahrung wissen die meisten von uns, wie schwierig es ist, auszudrücken, was wir unter Qualität verstehen. Warum ist das so? Es gibt viele verschiedene Ansichten zum Thema Qualität, und jede von ihnen hat ihre Bedeutung. Im Kontext eines Projekts muss die Definition von Qualität mit den Bedürfnissen und dem Budget des Projekts übereinstimmen. Der Versuch, Perfektionismus zu erreichen, kann kontraproduktiv sein, wenn ein Projekt erfolgreich beendet werden soll. Wir werden unsere Überlegungen auf der Grundlage eines 1976 von Barry W. Boehm verfassten Aufsatzes beginnen. Boehm beleuchtet die verschiedenen Aspekte der Softwarequalität und den zugehörigen Kontext. Schauen wir uns das etwas genauer an.

Wenn wir über Qualität sprechen, sollten wir uns auf drei Bereiche konzentrieren: Codestruktur, Korrektheit der Implementierung und Wartbarkeit. Viele Manager kümmern sich nur um die ersten beiden Aspekte, der Bereich Wartung wird von ihnen meist vollständig vernachlässigt. Das ist gefährlich, da das Risiko einer Investition für Unternehmen in die individuelle Entwicklung einer Anwendung nicht eingegangen wird, um diese nur für kurze Zeit in Produktion zu nutzen. Abhängig von der Komplexität der Anwendung kann der Preis für die Erstellung leicht mehrere Hunderttausend Euros erreichen. Da ist es mehr als verständlich, dass der erwartete Geschäftsnutzen solcher Aktivitäten als hoch eingeschätzt wird. Eine Lebensdauer von zehn Jahren und mehr in der Produktion ist üblich. Um die Vorteile auch künftig zu sichern, sind Anpassungen obligatorisch. Das impliziert einen starken Fokus auf die Wartung. Sauberer Code bedeutet nicht, dass sich Ihre Anwendung einfach ändern kann. Ein sehr leicht verständlicher Artikel, der dieses Thema berührt, wurde von Dan Abramov geschrieben. Bevor wir weiter darauf eingehen, wie Wartung definiert werden kann, werden wir den ersten Punkt diskutieren: die Struktur.

Einrüstarbeiten

Ein häufig unterschätzter Aspekt in Entwicklungsabteilungen ist ein fehlender Standard für Projektstrukturen. Eine klare Definition, wo Dateien abgelegt werden müssen, hilft Teammitgliedern, sogenannte Points of Interest (POI) schnell zu identifizieren. Eine solche Metastruktur für Java-Projekte wird beispielsweise vom Build-Werkzeug Maven definiert. Vor mehr als einem Jahrzehnt haben Unternehmen den Einsatz von Maven in ihren Projekten ausprobiert und das Tool an die vorhandene Verzeichnisstruktur angepasst. Das führte zu umfangreichen Wartungsarbeiten, da im Laufe der Zeit immer mehr Infrastrukturtools für die Softwareentwicklung hinzugekommen sind, wie beispielsweise SonarQube zur Codeanalyse. Solche Tools arbeiten nach dem von Maven definierten Standard. Das bedeutet, dass jede Anpassung des Standards an eigene Strukturen den Erfolg der Integration neuer Tools oder des Austauschs eines vorhandenen Tools gegen ein anderes beeinflusst.

Ein weiterer zu betrachtender Aspekt ist die unternehmensweit definierte Metaarchitektur. Wenn möglich, sollte jedes Projekt der gleichen Metaarchitektur folgen. Das reduziert die Zeit, die ein neuer Entwickler benötigt, um einem bestehenden Team beizutreten und produktiv zu werden. Eine solche Metaarchitektur muss für Adaptionen offen sein, was in zwei einfachen Schritten erreicht werden kann:

  1. Kümmern Sie sich nicht um zu viele Details.
  2. Folgen Sie dem KISS-Prinzip (Keep it simple, stupid!).

Ein klassisches Muster, das gegen das KISS-Prinzip verstößt, besteht darin, dass Standards stark angepasst werden. Ein sehr gutes Beispiel für die fatalen Auswirkungen einer starken Anpassung beschreibt George Schlossnagle [1]. In Kapitel 21 seines Buchs erklärt er die Probleme, die für ein Team entstanden, als der ursprüngliche PHP-Kern angepasst wurde, anstatt dem empfohlenen Weg zu folgen und Erweiterungen zu nutzen. Das führte dazu, dass jedes Update der PHP-Version manuell bearbeitet werden musste, um die eigenen Entwicklungen in den neuen Kern aufzunehmen. In diesem Zusammenhang bilden die besprochenen Maßnahmen hinsichtlich Struktur, Architektur und KISS bereits drei Qualitätsziele, die einfach umzusetzen sind.

Das auf GitHub gepostete Open-Source-Projekt TP-CORE befasst sich mit den oben genannten Punkten: Struktur, Architektur und KISS. Dort wird gezeigt, wie Sie den beschriebenen Ansatz in die Praxis umsetzen können. Diese kleine Java-Bibliothek hält die Maven-Konvention mit ihrer Verzeichnisstruktur streng ein. Das vereinfacht unter anderem auch die Komplexität der zu schreibenden Build-Logik. Zur schnellen Kompatibilitätserkennung werden Releases durch semantische Versionierung definiert. Als Architektur wurde das Schichtenmodell gewählt, das wir nachfolgend in den wichtigsten Punkten erläutern. Eine ausführliche Beschreibung findet sich auf der Webseite.

Die wichtigsten Architekturentscheidungen basieren auf folgenden Überlegungen: Jede Ebene wird durch ein eigenes Paket definiert und die Dateien folgen ebenfalls einer strengen Regel. Es wird kein spezielles Prä- oder Postfix verwendet. Die Logger-Funktionalität wird beispielsweise von einer Schnittstelle namens Logger und der entsprechenden Implementierung LogbackLogger deklariert. Die APIs können im Paket business und in den Implementierungsklassen im Paket application gefunden werden. Namen wie ILogger und LoggerImpl sollten vermieden werden. Stellen Sie sich ein Projekt vor, das vor zehn Jahren gestartet wurde und dessen LoggerImpl auf Log4j basiert. Jetzt entsteht eine neue Anforderung und das Loglevel muss zur Laufzeit aktualisiert werden. Um diese Herausforderung zu lösen, könnte die Log4j-Bibliothek durch Logback ersetzt werden. Jetzt ist es verständlich, warum es eine gute Idee ist, die Implementierungsklasse wie die Schnittstelle zu benennen und das mit den Implementierungsdetails zu kombinieren – es erleichtert die Wartung erheblich! Gleiche Konventionen finden Sie auch im Java-Standard-API. Das Interface List wird von einer ArrayList implementiert. Offensichtlich ist die Schnittstelle nicht als IList und die Implementierung nicht als ListImpl benannt.

Zusammenfassend wurde hier ein vollständiger Regelsatz definiert, der auch messbar ist und unser Verständnis der strukturellen Qualität beschreibt. Erfahrungsgemäß sollte diese Beschreibung kurz sein. Wenn andere Menschen Ihre Absichten leicht nachvollziehen können, akzeptieren sie eine Empfehlung eher, da sie unter den genannten Umständen einleuchtend ist. Darüber hinaus erkennt der Architekt Regelverstöße weitaus schneller.

Erfolgsmesslatte

Der schwierigste Teil ist es, sauberen Code über einen langen Zeitraum zu erhalten und vor Erosion zu schützen. Einige Ratschläge sind an sich nicht schlecht, aber im Kontext Ihres Projekts möglicherweise nicht so nützlich. Daher habe ich nachfolgend einige Grundregeln zusammengestellt, die in der Praxis meist schon angewendet werden.

Meiner Meinung nach wäre die wichtigste Regel, immer die Compilerwarnung zu aktivieren, egal welche Programmiersprache Sie verwenden. Alle Compilerwarnungen müssen behoben werden, wenn eine Version vorbereitet wird. Organisationen wie die NASA, die sich mit kritischer Software befassen, wenden diese Regel in ihren Projekten strikt an, was zu äußerst erfolgreichen Resultaten führt.

Codierungskonventionen zu Klassenbenennung, Zeilenlänge und API-Dokumentation wie Javadoc können von Tools wie Checkstyle einfach definiert und eingehalten werden. Dieser Prozess kann während des Builds vollautomatisch ausgeführt werden. Aber Achtung: Selbst wenn die Codeprüfungen ohne Warnungen bestanden werden, bedeutet das nicht, dass alles optimal funktioniert. Javadoc ist beispielsweise problematisch. Mit einem automatisierten Checkstyle kann sichergestellt werden, dass die API-Dokumentation vorhanden ist, obwohl wir keine Ahnung von der Qualität der Beschreibungen haben.

An dieser Stelle sollte es nicht erforderlich sein, die Vorteile von Tests zu erörtern. Lassen Sie mich lieber einen Überblick über die Testabdeckung geben. Der Industriestandard von 85 Prozent Testabdeckung sollte eingehalten werden, da eine Abdeckung von weniger als 85 Prozent die komplexen Teile Ihrer Anwendung nicht erreicht. Eine hundertprozentige Abdeckung belastet lediglich Ihr Budget, ohne dass sich daraus weitere Vorteile ergeben. Ein Paradebeispiel dafür ist das TP-CORE-Projekt, dessen Testabdeckung meist zwischen 92 und 95 Prozent liegt. Das wurde getan, um zu verdeutlichen, ab wann sich mit vertretbarem Aufwand keine aussagekräftigen Tests mehr schreiben lassen. Auch Mock-Objekte würden keine weitere Aussagekraft bei den Tests bringen.

Wie bereits erläutert, enthält die Businessschicht nur Schnittstellen, die das API definieren. Diese Ebene ist ausdrücklich von der Code Coverage ausgeschlossen. Ein anderes Paket heißt internal und enthält versteckte Implementierungen wie den SAX DocumentHandler. Aufgrund der Abhängigkeiten, an die der DocumentHandler gebunden ist, ist es selbst mit Mocks sehr schwierig, diese Klasse direkt zu testen. Das ist an sich auch völlig unproblematisch, da ihr Zweck nur der interne Gebrauch ist. Darüber hinaus wird die Klasse von der Implementierung des DocumentHandler implizit getestet. Um eine höhere Abdeckung zu erreichen, könnte es auch eine Option sein, alle internen Implementierungen von Überprüfungen auszuschließen. Es ist jedoch immer eine gute Idee, die implizite Abdeckung dieser Klassen zu beobachten, um Aspekte zu erkennen, die sonst möglicherweise nicht erkannt werden.

Neben den Unit-Tests auf niedriger Ebene sollten auch automatisierte Abnahmetests durchgeführt werden. Wenn Sie diese Punkte genau beachten, können Sie eine Vielzahl von Problemen vermeiden. Aber vertrauen Sie vollautomatischen Prüfungen niemals blind: Regelmäßig wiederholte manuelle Codeinspektionen sind immer obligatorisch, insbesondere bei der Arbeit mit externen Anbietern. In unserem Vortrag auf der JCON 2019 haben wir gezeigt, wie einfach die Testabdeckung gefälscht werden kann. Um andere Schwachstellen zu erkennen, können Sie zusätzliche Checker wie etwa SpotBugs ausführen. Tests zeigen nicht an, dass eine Anwendung fehlerfrei ist, sie zeigen jedoch ein definiertes Verhalten für implementierte Funktionen an.

SCM-Suites wie GitLab oder Microsoft Azure unterstützen seit einiger Zeit Pull-Anfragen, die vor langer Zeit auf GitHub eingeführt wurden. Diese Workflows sind per se nichts Neues. IBM Synergy verwendete eine ähnliche Strategie. Ein Build Manager war dafür verantwortlich, die Änderungen der Entwickler in der Codebasis zusammenzuführen. Das führte dazu, dass alle vom Entwickler durchgeführten Revisionen vom Build Manager in das Repository aufgenommen wurden. Der Build Manager verfügte nicht über ausreichend fundierte Kenntnisse, um die Implementierungsqualität beurteilen zu können. Es wurde schnell zur üblichen Praxis, die Commits durchzuwinken und lediglich darauf zu achten, dass der Build nicht kaputt geht und die Kompilierung immer ein Artefakt erzeugt.

Unternehmen haben als neue Strategie Pull-Request entdeckt, um sie in ihren Projekten einzusetzen. Den dahinterstehenden Dictatorship-Workflow, wie er zu Recht in Open-Source-Projekten angewendet wird, hinterfragen sie hingegen nicht auf Sinnhaftigkeit. Denn das sagt implizit, dass dem eigenen Entwicklungsteam weder Vertrauen entgegengebracht noch Kompetenz zugetraut wird. Heutzutage treffen Manager häufig die Entscheidung, Pull-Requests als Qualitätsschranke zu verwenden. Nach meiner Erfahrung verlangsamt das die Produktivität, da es einige Zeit dauert, bis die Änderungen in der Codebasis verfügbar sind. Das Verständnis von Branch- und Merge-Mechanismen hilft Ihnen bei der Entscheidung für die Nutzung eines einfachen Branch-Modells, wie beispielsweise Release Branch Lines. Auf diesen Branches arbeiten dann Tools wie SonarQube, um das geforderte Qualitätsziel zu sichern. Wenn ein Projekt einen orchestrierten Build mit einer definierten Reihenfolge für die Erstellung von Artefakten benötigt, haben Sie einen starken Hinweis auf eine notwendige Refaktorierung.

Die Kopplung zwischen Klassen und Modulen wird oft unterschätzt. Es ist sehr schwierig, eine automatisierte Visualisierung für die Bindungen von Modulen zu haben. Sie werden sehr schnell feststellen, welchen Effekt es hat, wenn eine starke Kopplung aufgrund zunehmender Komplexität Ihre Build-Logik unnötig verkompliziert.

Wiederholungstäter

Seien Sie versichert, nichts ist so sicher wie die Änderung! Es ist eine Herausforderung, eine Anwendung für Anpassungen offen zu halten. Einige der vorherigen Empfehlungen haben implizite Auswirkungen auf die zukünftige Wartbarkeit. Eine gute Source-Qualität vereinfacht das Unterfangen durchaus. Es gibt jedoch keine Garantien. Im schlimmsten Fall ist das Ende des Produktlebenszyklus (EOL) erreicht, wenn obligatorische Verbesserungen oder Änderungen beispielsweise wegen einer erodierten Codebasis nicht mehr realisiert werden können. Das tritt weit häufiger auf, als man annimmt.

Wie bereits erwähnt, bringt eine lose Kopplung zahlreiche Vorteile für die Wartung und Wiederverwendung mit sich. Dieses Ziel zu erreichen ist nicht so schwierig, wie es auf den ersten Blick erscheinen mag. Versuchen Sie zunächst, die Verwendung von Drittanbieterbibliotheken so weit wie möglich zu vermeiden. Nur um zu überprüfen, ob ein String leer oder null ist, ist es nicht notwendig, sich von einer externen Bibliothek abhängig zu machen. Diese wenigen Zeilen sind schnell selbst formuliert. Ein zweiter wichtiger Punkt, der in Bezug auf externe Bibliotheken berücksichtigt werden muss: Nur eine Bibliothek zur Lösung eines Problems. Wenn beispielsweise im Projekt JSON verwendet wird, entscheiden Sie sich für eine Implementierung und integrieren Sie nicht verschiedene Artefakte mit gleicher Funktionalität. Das wirkt sich auch stark auf die Sicherheit aus: Ein Artefakt von Drittanbietern, dessen Verwendung wir vermeiden können, kann keine Sicherheitslücken verursachen.

Zudem ist es nach einer Entscheidung für eine externe Bibliothek sehr hilfreich, diese für das gesamte Projekt zu kapseln. Das gelingt über die Verwendung von Entwurfsmustern wie Proxy, Fassade oder Wrapper und gestattet einen einfacheren Austausch, da die Codeänderungen so nicht gleich über die gesamte Codebasis verteilt werden. Sie müssen also nicht alles auf einmal ändern, wenn Sie den Anweisungen zum Benennen der Implementierungsklasse und Bereitstellen einer Schnittstelle folgen. Obwohl ein SCM auf Zusammenarbeit ausgelegt ist, gibt es Einschränkungen, wenn mehr als eine Person dieselbe Datei bearbeitet. Durch die Verwendung eines Entwurfsmusters zum Verstecken von Information können Sie Ihre Änderungen iterativ aktualisieren.

Checkliste

  1. Befolgen Sie etablierte Standards
  2. KISS – Keep it simple, stupid!
  3. Gleiche Verzeichnisstruktur für verschiedene Projekte
  4. Einfache Metaarchitektur, die weitgehend in anderen Projekten wiederverwendet werden kann
  5. Codierungsstyles definieren und befolgen
  6. Es werden keine Compilerwarnungen akzeptiert
  7. Testabdeckung um die 85 Prozent
  8. Vermeiden Sie Bibliotheken von Drittanbietern soweit möglich
  9. Verwenden Sie nicht mehr als eine Technologie für ein bestimmtes Problem (z. B. JSON)
  10. Decken Sie Fremdcode durch ein Entwurfsmuster ab
  11. Vermeiden Sie starke Objekt-Modul-Kopplungen

Fazit

Wie wir gesehen haben, ist eine nichtfunktionale Anforderung nicht so schwer zu beschreiben. Mit einer kurzen Checkliste (Kasten: „Checkliste“) können Sie die wichtigsten Aspekte für Ihr Projekt klar definieren. Es ist nicht erforderlich, alle Punkte für jeden Code-Commit im Repository zu überprüfen. Das würde höchstwahrscheinlich nur die Kosten erhöhen und führt nicht zu mehr Vorteilen. Eine vollständige Überprüfung etwa wenige Tage vor einem Release stellt in einem agilen Kontext eine effektive Lösung dar. POIs zur Qualitätssicherung sind die Überarbeitungen der Codebasis für ein Release. Das gibt Ihnen eine vergleichbare Statistik und hilft, die Zuverlässigkeit von Schätzungen zu erhöhen. Schlussendlich sollte das Hauptanliegen sein, ein hohes Maß an Automatisierung in der Infrastruktur zu erreichen; Continuous Integration z. B. ist äußerst hilfreich, befreit Sie jedoch nicht von manuellen Code Reviews.

Geschrieben von
Marco Schulz
Marco Schulz
Marco Schulz studierte an der HS Merseburg Diplominformatik. Sein persönlicher Schwerpunkt liegt in Software Architekturen, der Automatisierung des Softwareentwicklungs-Prozess und dem Softwarekonfigurationsmanagement. Seit über fünfzehn Jahren realisiert er in internationalen Projekten für nahmenhafte Unternehmen auf unterschiedlichen Plattformen umfangreiche Webapplikationen. Er ist freier Consultant, Trainer und Autor verschiedener Fachartikel. Sein persönlicher Blog lautet https://enRebaja.wordpress.com, sie erreichen ihn unter: marco.schulz@outlook.com
Kommentare

Hinterlasse einen Kommentar

avatar
4000
  Subscribe  
Benachrichtige mich zu: