5 Prinzipien agiler Entwicklung für höhere Softwarequalität

von Alexander Gunkel und Christoph Hochholzer

Die Qualität von Software ist viel diskutiert und auch Gegenstand von Standards und Normierungen. ISO 9126 etwa nennt neben Funktionalität, Effizienz und Zuverlässigkeit auch Änderbarkeit und Übertragbarkeit als Qualitätsmerkmale. Änderbarkeit, Verständlichkeit und Übertragbarkeit sind Merkmale, die gerade aus Sicht agiler Entwicklung besondere bedeutend sind. Agile Entwicklung betont schließlich die Notwendigkeit von Anpassungen und Weiterentwicklungen, die bei der ursprünglichen Konzeption noch nicht abzusehen waren. Es ist ihr zufolge legitim und wünschenswert, dass der Kunde im Laufe der Softwareentwicklung seine Wünsche konkretisiert und neue Wünsche anbringt. Dafür muss auch die Software flexibel bleiben. Ein Source Code, der zwar die Funktionalität mit hoher Zuverlässigkeit und Effizienz bereitstellt, aber hohe Hürden für spätere Änderungen aufstellt, ist hier ungeeignet. Welche Folgen das haben kann, sah man zuletzt bei der Bundesagentur für Arbeit, die ein 60 Mio. Euro teures Softwareprojekt kurzerhand zu den Akten legte, weil neue Anforderungen nicht mehr umsetzbar waren.

Dass die Auswirkungen so gravierend sind, mag selten sein. Dass der vorhandene Source Code Veränderungen und Erweiterungen massiv erschwert, ist leider Entwickleralltag. Gerade größere Softwareprojekte leiden unter Mängeln in der Softwarequalität (Einer Studie der CAST Research Labs zufolge baute eine durchschnittliche Business-Applikation im Schnitt technische Schulden in Höhe von einer Million US-Dollar auf). Unflexibler Source Code nervt aber nicht nur uns Entwickler, er verteuert gewünschte Änderungen und provoziert Bugs in weiterentwickelten Anwendungen – zum Leidwesen aller Stakeholder. Misslich sind auch die Auswirkungen auf die betriebsinterne Organisation: Unübersichtlicher Code erschwert die Einarbeitung neuer Entwickler und macht es schwer, nötige Aufwände der Weiterentwicklung realistisch einzuschätzen. Die Leitfrage unserer SOLID-Reihe war also: Wie lässt sich der Quellcode so gestalten, dass er spätere Änderungen nicht erschwert, sondern erleichtert? Genau das ist die Absicht von SOLID, einer Sammlung von fünf Grundprinzipien agiler Softwareentwicklung, die ursprünglich von Robert Martin zusammengestellt wurden.

1. Single Responsibility Principle

Jede Softwareeinheit (jede Klasse und jede Funktion) sollte genau eine Aufgabe haben. Eine Repository-Klasse ist für die Kommunikation mit der Datenbank bzgl. einer bestimmten Art von Objekten zuständig, eine Controller-Klasse koordiniert das Zusammenspiel von View, Model und Repository, übernimmt aber nicht ihre Aufgaben. Wendet man dieses Prinzip konsequent an, resultieren kleine, kompakte und leicht verständliche Klassen. Was die Verletzung bewirkt, erkennt man leicht an alten TYPO3-PiBase-Plugins. Dort werden nicht selten Aufgaben von Controller, Repository, Logger, Caching und View in einer einzigen Klasse bearbeitet, die dann schnell eine Länge von mehreren Tausend Zeilen erlangt. Überschaubar ist deren Komplexität dann nur noch für diejenigen, die mit dem Code seit Jahren vertraut sind. Und entsprechend zeitaufwendig ist die Einarbeitung neuer Kollegen in ein Projekt!

2. Open-Closed Principle

Einen Schritt weiter geht das Open-Closed Principle: Softwareeinheiten sollen offen bleiben für Erweiterungen, aber abgeschlossen sein gegenüber Änderungen. Da mag man zunächst einwenden, dass wir die Änderbarkeit von Software doch gerade zum Qualitätsmerkmal erhoben hatten. Doch dieses Prinzip schließt Änderungen nicht aus, sondern definiert, wie sie zu koordinieren sind. Die Erweiterung von Funktionalität soll ohne Eingriffe in bestehenden Source Code möglich sein. Das ist selbstverständlich im Umgang mit Frameworks (ein Mindestmaß an Professionalität vorausgesetzt) und Gegenstand vieler Entwurfsmuster, bspw. des Oberserver-Patterns.

Ein Vorteil des OCP liegt in der geringeren Fehleranfälligkeit. Denn mit jedem Feature, für das bestehender Quellcode geändert werden musste, können sich neue Bugs einschleichen und Funktionalität, die zuvor entwickelt wurde, kann verloren gehen. Hauptsächlich verbessert es aber die Wiederverwendbarkeit des Codes, der an verschiedenen Stellen genutzt werden kann, ohne dass hierfür Änderungen notwendig sind.

3. Liskov Substitution Principle

Das LSP mutet zunächst sehr technisch an: Eine Softwareeinheit P, die Objekte einer Basisklasse T verwendet, muss auch mit Objekten einer von T abgeleiteten Klasse S korrekt funktionieren, ohne dass dazu das Programm P angepasst werden muss. Dabei geht es um gute und schlechte Vererbung. Ein bekanntes Beispiel: Quadrate sind per definitionem Rechtecke. Also liegt es nahe, eine Klasse Quadrat von einer Klasse Rechteck abzuleiten, um die Methoden nicht neu definieren zu müssen (der Flächeninhalt und der Umfang werden ja in beiden Fällen gleich berechnet). Bei genauerem Hinschauen verhalten sich Quadrate aber oft nicht wie Rechtecke: Setzt man bei dem Quadrat die Höhe, so wird die Breite automatisch mitgesetzt. Ein Softwareeinheit könnte sich nun aber darauf verlassen, dass ihr übergebene Rechtecke ihre Breite nicht ändern, nur weil die Höhe neu gesetzt wird. Also müssen wir entweder darauf verzichten, die Klasse Quadrat von der Klasse Rechteck abzuleiten, oder die Softwareeinheit um Abfragen und Bedingungen erweitern, die auf die Unterschiede der verschiedenen Arten von Rechtecken Rücksicht nimmt. Da letzteres jedoch gegen das OCP verstößt und den resultierenden Code komplexer und schwerer verständlich macht, tun wir gut daran, auf die Ableitung zu verzichten.

4. Dependency Inversion Principle

Module höherer Ebenen sollen nicht von Modulen niedrigerer Ebenen abhängen. Beide sollen von Abstraktionen abhängen. Abstraktionen sollten nicht von Details abhängen; Details sollten von Abstraktionen abhängen.

Schließlich soll ein Programm, das auf eine Datenbank zurückgreift, nicht von ihrer konkreten Implementierung abhängig sein. Sonst hätten wir das Caching von TYPO3 niemals von MySQL auf Redis umstellen können. Und eine Loggingfunktionalität soll offen lassen, ob letztlich in eine Datei, den Syslog oder in FirePHP geloggt wird. In aller Regel liegt die Lösung darin, keine Abhängigkeiten in Form von Type-Hints auf konkrete Klassen einzuführen, sondern immer auf Interfaces zu type-hinten, die dann von konkreten Klassen implementiert werden. Allgemein anerkannte Interfaces wie psr-3 für Logger und psr-6 für Caching-Frameowrks ermöglichen die leichte Verwendung weiterer Module.

5. Interface Segregation Principle

Interfaces ermöglichen klar definierte Abhängigkeit, die die Art der konkreten Umsetzung offenlassen. Dabei möchte man natürlich nur von dem abhängig sein, was man tatsächlich benötigt. Zu große Interfaces sollen daher auf mehrere Interfaces aufgeteilt werden. Dadurch vermeidet man, dass Klassen Methoden implementieren müssen, die bei ihnen keinen Sinn ergeben (und die dann in aller Regel nichts tun oder lediglich Exceptions werfen).