Langlebige Architekturen: Technische Schulden erkennen und beseitigen

Die wenigsten Entwicklungsteams haben heute noch die Chance, ein komplett neues System "from scratch" zu entwickeln. Meistens stehen wir vor der Herausforderung, ein bestehendes über Jahre gewachsenes System zu warten und auszubauen. Damit das auf Dauer gelingen kann, brauchen wir eine qualitativ hochwertige und flexible Architektur mit möglichst wenig technischen Schulden. Sind die technischen Schulden gering, dann finden sich die Wartungsentwickler gut im System zurecht. Sie können schnell und einfach Bugs fixen und haben keine Probleme, kostengünstig Erweiterungen zu implementieren. Wie kommen wir in dieses gelobte Land der Architekturen mit reduzierten Schulden?
Technische Schulden in Architektur und Design
Der Begriff "Technische Schulden" würde 1992 von Ward Cunningham geprägt. Technische Schulden entstehen, wenn bewusst oder unbewusst falsche oder suboptimale technische Entscheidungen getroffen werden. Diese falschen oder suboptimalen Entscheidungen führen zu einem späteren Zeitpunkt zu Mehraufwand, der Wartung und Erweiterung teurer macht. Zu dem Zeitpunkt der falschen oder suboptimalen Entscheidung hat man also technische Schulden aufgenommen, die man mit ihren Zinsen irgendwann abbezahlen muss, wenn man nicht überschuldet enden will.
Solche suboptimalen Entscheidungen schlagen sich im Design der Klassen, Pakete, Subsysteme, Schichten und Module und den Abhängigkeiten zwischen diesen nieder. Sie führen zu einer uneinheitlichen und komplexen Architektur, die für die Entwickler in der Wartungsphase schwer nachzuvollziehen und auch schwer zu erhalten ist. Gerade die Schulden, die in der Endphase eines Projektes aufgenommen werden, weil die Software endlich "fertig" werden soll, fallen in diese Kategorie und müssen schnellstmöglich abgetragen werden.
Tut man das nicht, so gerät man immer tiefer in den Strudel technischer Schulden. In Abb.1 kann man diesen Weg in die Wartungshölle verfolgen. Gehen wir davon aus, dass das System bei Auslieferung eine qualitativ hochwertige Architektur hat. Dann befindet sich das Softwaresystem in dem Korridor geringer technischer Schulden mit einem gleichbleibenden Aufwand für die Wartung (s. Abb.1).
Erweitert man das System mehr und mehr, so entstehen zwangsläufig technische Schulden (gelbe Pfeile in Abb. 1 im Korridor gleichbleibendem Aufwand). Softwareentwicklung ist ein ständiger Lernprozess, bei dem der erste Wurf einer Lösung selten der Endgültige ist. Die Überarbeitung der Architektur, der Tests und der Dokumentation (Architekturerneuerung, grüne Pfeile in Abb. 1) muss in regelmäßigen Abständen durchgeführt werden. So entsteht eine stetige Folge von Erweiterung und Refactoring.
Kann ein Team diesen Zyklus von Erweiterung und Refactoring dauerhaft verfolgen, so wird das System im Korridor geringer technischer Schulden bleiben. Darf das Wartungsteam die technischen Schulden nicht kontinuierlich reduzieren, so setzt im Laufe der Zeit zwangsläufig Architektur-Erosion ein (gelbe und rote aufsteigende Pfeile in Abb. 1).
Über kurz oder lang verlässt das Softwaresystem den Korridor guter Architekturqualität mit gleichbleibendem Aufwand für Wartung (rote Pfeile in Abb. 1). Die Architektur erodiert und es entstehen immer mehr technische Schulden. Die Ursachen dieses Erosions-Prozesses sind vielfältig: Aufgrund von Zeitdruck müssen wir Hacks in unser System einbauen, obwohl wir wissen, dass die Architektur eigentlich ganz anders aussehen müsste. Die Komplexität und der Kopplungsgrad in der Software wachsen unbemerkt, weil wir beim Entwickeln nicht darauf achten (können). Wir haben keine Zeit für Architekturdiskussionen und so fehlt uns das nötige Architekturverständnis.
Befinden wir uns erst einmal auf dem aufsteigenden Ast der technischen Schulden, so werden Wartung und Erweiterung der Software immer teurer bis zu dem Punkt, an dem jede Änderung zu einer schmerzhaften Anstrengung wird. Abb. 1 macht diesen langsamen Verfall dadurch deutlich, dass die roten Pfeile immer kürzer werden. Pro Zeiteinheit kann man bei steigenden Schulden immer weniger Funktionalität umsetzen. Änderungen, die früher einmal an einem Personentag möglich waren, dauern jetzt doppelt bis dreimal so lange. Die Software wird fehleranfällig. Um aus diesem Dilemma der technischen Schulden herauszukommen, hat man zwei Möglichkeiten:
- Erneuern: Man kann das System von innen heraus erneuern und so die Entwicklungsgeschwindigkeit und Stabilität wieder erhöhen. Auf diesem meist beschwerlichen Weg muss das System Schritt für Schritt wieder in den Korridor geringer technischer Schulden zurückgebracht werden (s. rote und gelbe absteigende Pfeile in Abb. 1).
- Ersetzen: Oder man kann das System durch eine andere Software ersetzen, die weniger technische Schulden hat. (s. Kreis in Abb. 1).
Natürlich kann es auch passieren, dass zu Beginn der Entwicklung kein fähiges Team vor Ort war und das Softwaresystem ohne Architektur oder mit einer rudimentären Architekturvorstellung entwickelt wurde. In einem solchen Fall wächst die Architektur im Laufe der Zeit ohne Plan vor sich hin. Technische Schulden werden in diesem Fall gleich zu Beginn der Entwicklung aufgenommen und kontinuierlich erhöht. Über solche Softwaresysteme kann man wohl sagen: sie sind unter schlechten Bedingungen aufgewachsen.
Diese Betrachtung von technischen Schulden ist für die meisten Projektleitern und Produktverantwortlichen heute verständlich und nachvollziehbar. Niemand will technische Schulden anhäufen und sich mit der Entwicklung langsam festfressen, bis jede Anpassung zu einer unkalkulierbaren Kostenschraube wird. Auch der Aspekt, dass es kontinuierlicher Arbeit bedarf, um die technischen Schulden über die gesamte Lebensdauer der Software gering zu halten, lässt sich gut vermitteln.
Zum Ende eines Projektes haben Projektleiter und Produktverantwortlicher die Aufgabe, dem Management zu verdeutlichen, wie wichtig die dauerhafte Pflege des gerade entwickelten Softwaresystems ist, um technische Schulden zu vermeiden. Gelingt ihnen diese Aufgabe, so sollte das Management dem Wartungsteam 10-20 Prozent des Budgets für den Abbau von technischen Schulden zugestehen. Ist dieses Budget verfügbar, so kann man davon ausgehen, dass die technischen Schulden auf den Korridor geringer Schulden begrenzt werden können und die Software lange leben wird.
Architekturanalyse und -verbesserung
Um technische Schulden zu reduzieren, ist es sinnvoll, regelmäßig zu überprüfen, ob die geplante Architektur im Sourcecode tatsächlich umgesetzt worden ist. Für solche Soll-/Ist-Vergleiche stehen heute eine Reihe guter Tools zur Verfügung: Sotograph, SonarQube, J/N/PHPDepend, Axovion Bauhaus, Structure101, Lattix, TeamScale, SoftwareDiagnostics u.v.m. In meinen Analysen arbeite ich mit dem Sotographen, von dem Sie in diesem Artikel einige Graphiken sehen werden.
Die Soll-Architektur ist der Plan für die Architektur, der auf Papier oder in den Köpfen des Architekten und Entwicklers existiert (s. Abb.2). Dieser Plan ist eine Abstraktion und Vereinfachung des Sourcecodes. Häufig wird dieser Plan bereits vor Beginn der Implementierung erstellt und im Laufe der Zeit an die Gegebenheiten angepasst. In der Soll-Architektur werden die Klassen und Pakete zu Subsystemen, Komponenten, Modulen (je nachdem, welchen Begriff man wählt) und Schichten zusammengefasst. Im weiteren Artikel bezeichne ich alle diese Elemente als Bausteine.
Die Soll-Architektur wird bei der Architekturanalyse mit dem echten Sourcecode abgeglichen. Der Sourcecode enthält die implementierte Ist-Architektur. In allen mir bekannten Fällen weicht die Ist-Architektur von der Soll-Architektur ab. Die Ursachen dafür sind vielfältig und hängen oft auch mit Architektur-Erosion und technischen Schulden (s. o.) zusammen. Bei der Architekturanalyse und -verbesserung machen wir uns gemeinsam mit den Architekten und Entwicklern auf die Suche nach einfachen Lösungen, wie die Ist-Architektur an die Soll-Architektur angeglichen werden kann. Oder aber wir diskutieren die geplante Soll-Architektur und stellen fest, dass die im Sourcecode gewählte Lösung besser ist. In diesem Fall muss der Plan – die Soll-Architektur – angepasst werden.
Neben diesem Abgleich zwischen Soll- und Ist-Architektur besteht ein wichtiger Teil der Architekturanalyse darin, dass das Entwicklungsteam oder auch das Management wissen will, ob die gewählte Architektur gut wartbar ist. Um diese Frage zu beantworten, bediene ich mich bei meinen Analysen eines Modells, das ich auf Basis von Erkenntnissen aus der kognitiven Psychologie entwickelt habe.
Kognitive Psychologie als Basis der Bewertung
Das menschliche Gehirn hat sich im Laufe der Evolution einige beeindruckende Mechanismen angeeignet, die uns beim Umgang mit komplexen Strukturen helfen. Diese Mechanismen gilt es in Softwaresystemen zu nutzen, damit Wartung und Erweiterung schnell und ohne viele Fehler von der Hand gehen. Das Ziel ist dabei, dass wir unsere Softwaresysteme auch mit sich verändernden Entwicklungsteams lange bei gleichbleibender Qualität weiterentwickeln können. Die drei Mechanismen, die unser Gehirn für komplexe Strukturen entwickelt hat, sind (s. Abb. 3): Chunking, Bildung von Hierarchien und Aufbau von Schemata. Diese Mechanismen haben direkte Abbilder in Kriterien für die Architektur.
Aufbau von Schemata und Musterkonsistenz
Der effizienteste Mechanismus, um komplexe Zusammenhänge zu strukturieren, sind sogenannte Schemata. Unter einem Schema werden Wissenseinheiten verstanden, die aus einer Kombination von abstraktem und konkretem Wissen bestehen. Ein Schema besteht auf der abstrakten Ebene aus den typischen Eigenschaften der von ihm schematisch abgebildeten Zusammenhänge. Auf der konkreten Ebene beinhaltet ein Schema eine Reihe von Exemplaren, die prototypische Ausprägungen des Schemas darstellen. Jeder von uns hat beispielsweise ein Lehrer-Schema, das abstrakte Eigenschaften von Lehrern beschreibt und als prototypische Ausprägungen Abbilder unserer eigenen Lehrer umfasst.
Haben wir für einen Zusammenhang in unserem Leben ein Schema, so können wir die Fragen und Probleme, mit denen wir uns gerade beschäftigen, sehr viel schneller verarbeiten als ohne Schemata. Schauen wir uns ein Beispiel an: Bei einem Experiment wurden Schachmeistern und Schachanfängern für ca. fünf Sekunden Spielstellungen auf einem Schachbrett gezeigt. Handelte es sich um eine sinnvolle Aufstellung der Figuren, so waren die Schachmeister in der Lage, die Positionen von mehr als zwanzig Figuren zu rekonstruieren. Sie sahen Muster von ihnen bekannten Aufstellungen und speicherten sie in ihrem Kurzzeitgedächtnis. Die schwächeren Spieler hingegen konnten nur die Position von vier oder fünf Figuren wiedergeben. Die Anfänger mussten sich die Position der Schachfiguren einzeln merken. Wurden die Figuren den Schachexperten und Schachlaien allerdings mit einer zufälligen Verteilung auf dem Schachbrett präsentiert, so waren die Schachmeister nicht mehr im Vorteil. Sie konnten keine Schemata einsetzen und sich so die für sie sinnlose Verteilung der Figuren nicht besser merken.
Die in der Softwareentwicklung vielfältig eingesetzten Entwurfsmuster nutzen die Stärke des menschlichen Gehirns, mit Schemata zu arbeiten. Haben Entwickler bereits mit einem Entwurfsmuster gearbeitet und daraus ein Schema gebildet, so können sie Programmtexte und Strukturen schneller erkennen und verstehen, die dieses Entwurfsmuster einsetzen. Der Aufbau von Schemata liefert für das Verständnis von komplexen Strukturen also entscheidende Geschwindigkeitsvorteile. Das ist auch der Grund, warum Muster in der Softwareentwicklung bereits vor Jahren Einzug gefunden haben.
Muster kann man bei der Architekturanalyse nicht messen, aber man kann sie sichtbar machen und ihre Umsetzung diskutieren. Ich untersuche einerseits die Muster auf der Architekturebene und andererseits die Muster auf der Klassenebene. Auf beiden Ebenen ist für die Entwickler und Architekten wichtig, dass es Muster gibt, dass sie im Sourcecode wiederzufinden sind und dass sie einheitlich und durchgängig eingesetzt werden. Deshalb verwende ich für diesen Bereich den Begriff "Musterkonsistenz".
Mangelnde Musterkonsistenz weist beispielweise Abb. 4 auf. In Abb. 4 sieht man den Package-Baum eines Java-Systems. Die Pfeile gehen jeweils vom übergeordneten Package zu seinen Kindern. Das System ist in vier Komponenten aufgeteilt, die im Package-Baum durch vier Farben markiert sind. An den gestrichelten Linien ist zu erkennen, dass zwei der Komponenten (orange und lila) über den Package-Baum verteilt sind. Diese Verteilung ist nicht konsistent zu dem von der Architektur vorgegebenen Muster und führt bei Entwicklern und Architekten zu Verwirrung. Das Einführen von jeweils einem Package-Root-Knoten für die orangene und die lila Komponente würde hier Abhilfe schaffen.
Auf der Klassenebene werden heute in vielen Systemen Entwurfsmuster eingesetzt. Sie leiten die Entwickler noch stärker als die Muster auf Architekturebene. In Abb. 5 sieht man ein anonymisiertes Tafelbild, das ich mit einem Team entwickelt habe, um seine Muster aufzunehmen. Auf der rechten Seite von Abb. 5 ist der Sourcecode in diese Musterkategorien eingeteilt und man sieht sehr viele grüne und einige wenige rote Beziehungen. Die roten Beziehungen gehen von unten nach oben gegen die durch die Muster entstehende Schichtung. Die geringe Anzahl der roten Beziehungen ist ein sehr gutes Ergebnis und zeugt davon, dass das Entwicklungsteam seine Muster sehr konsistent einsetzt. Spannend ist bei der Analyse noch, welchen Anteil des Sourcecodes man Mustern zuordnen kann und wie viele Muster das System schlussendlich enthält. Lassen sich 80 Prozent oder mehr des Sourcecodes Mustern zuordnen, so spreche ich davon, dass dieses System eine Mustersprache hat. Für die "richtige" Anzahl an Mustern habe ich keine exakte Zahl. Wichtig ist vielmehr, dass die vorhandenen Muster tatsächlich unterschiedliche Konzepte darstellen und nicht Varianten eines Konzeptes sind. Beispiele für solche Varianten könnten zum Beispiel zwei Muster sein, die "Service" und "Manager" heißen. Hier wäre zu klären, was den Manager von einem Service unterscheidet und in welchem Verhältnis sie zueinander stehen.
Die Untersuchung der Muster im Sourcecode ist in der Regel der spannendste Teil einer Architekturanalyse. Hier hat man die Ebene zu fassen, auf der das Entwicklungsteam wirklich arbeitet. Die Klassen, die die einzelnen Muster umsetzen, liegen oft über die Packages oder Directories verteilt. Mit einer Modellierung der Muster wie in Abb. 5 rechts kann man diese Ebene der Architektur sichtbar und analysierbar machen.
Chunking und Modularität
Damit man in der Menge der Informationen, mit denen man konfrontiert ist, zurechtkommen, muss man auswählen und Teilinformationen zu größeren Einheiten gruppieren. Dieses Bilden von höherwertigen Abstraktionen, die immer weiter zusammengefasst werden, nennt man Chunking. Dadurch, dass Teilinformationen als höherwertige Wissenseinheiten abgespeichert werden, wird das Kurzzeitgedächtnis entlastet und weitere Informationen können aufgenommen werden. Chunking kann unser Gehirn allerdings nur dann anwenden, wenn die Teilinformationen eine sinnvolle zusammenhänge Einheit bilden. Bei unzusammenhängenden Informationen gelingt uns Chunking nicht.
Entwickler wenden Chunking automatisch an, wenn sie sich unbekannte Programme erschließen müssen. Der Programmtext wird im Detail gelesen und die gelesenen Zeilen werden zu Wissenseinheiten gruppiert und so behalten. Schritt für Schritt werden die Wissenseinheiten immer weiter zusammengefasst, bis ein Verständnis des benötigten Programmtextes erreicht ist. Allerdings funktioniert auch bei Softwaresystemen das Chunking nur dann, wenn die Struktur des Softwaresystems sinnvoll zusammenhängende Einheiten darstellen. Programmeinheiten, die beliebige Operationen zusammenfassen, so dass für die Entwickler nicht erkennbar ist, warum sie zusammengehören, lassen sich nicht in Wissenseinheiten codieren. Für unsere wartbaren Softwarearchitekturen ist es also essentiell, dass sie Bausteine wie Klassen, Komponenten, Module, Schichten enthalten, die sinnvoll zusammenhängende Elemente gruppieren.
Ob die Bausteine in einer Softwarearchitektur zusammenhängende Elemente darstellen, lässt sich leider nicht messen oder mit Analysewerkzeugen überprüfen. Um bei der Analyse von Architekturen trotzdem Aussagen über die Modularität machen zu können, untersuche ich die folgenden Aspekte:
- Entwurf nach Zuständigkeit: Sind die Bausteine eines Systems modular gestaltet, so sollte man für jeden Baustein die Frage beantworten können: Was ist sein Aufgabe? Der entscheidende Punkt dabei ist, dass der Baustein wirklich eine Aufgabe hat und nicht mehrere. Diese Frage ist natürlich nur im fachlichen Kontext des jeweiligen Systems gemeinsam mit dem Entwicklerteam zu klären. Anhaltspunkte bei der Suche nach Bausteinen mit unklarer Zuständigkeit sind:
- Der Name des Bausteins – der Name sollte seine Aufgabe beschreiben. Ist der Name schwammig, so sollte man ihn sich ansehen.
- Seine Größe (s. nächster Punkt).
- Der Umfang seiner Kopplung mit anderen Bausteinen – wird ein Baustein sehr viel von allen möglichen anderen Bausteinen verwendet, so liegt die Vermutung nahe, dass er ein Sammelbecken von vielfältigen nicht unbedingt zusammenhängenden Funktionalitäten ist.
- Seine mangelnde Musterkonsistenz (s. Musterkonsistenz).
- Ausgewogene Größenverhältnisse: Bausteine, die auf einer Ebene liegen, also die Schichten, die fachlichen Module, die Packages, die Klassen oder die Methoden, sollten untereinander ausgewogene Größenverhältnisse haben. Hier lohnt es sich, die sehr großen Bausteine zu untersuchen, um festzustellen, ob sie Kandidaten für eine Zerlegung sind.
- Zusammengehörigkeit durch Kopplung untereinander: Bausteine sollten Subbausteine enthalten, die zusammengehören. Eine Klasse sollte beispielsweise Methoden enthalten, die gemeinsam ein Ganzes ergeben. Dasselbe gilt für größere Bausteine, wie Packages, Komponenten, Module und Schichten. Haben die Subbausteine mehr mit anderen Bausteinen zu tun, als mit ihren "Schwestern und Brüdern", dann stellt sich die Frage, ob sie nicht eigentlich in einen anderen Baustein gehören.
Bildung von Hierarchien und Hierarchisierung
Hierarchien spielen beim Wahrnehmen und Verstehen von komplexen Strukturen und beim Abspeichern von Wissen eine wichtige Rolle. Menschen können Wissen dann gut aufnehmen, es wiedergeben und sich darin zurechtfinden, wenn es in hierarchischen Strukturen vorliegt. Untersuchungen zum Lernen von zusammengehörenden Wortkategorien, zur Organisation von Lernmaterialien, zum Textverstehen, zur Textanalyse und zur Textwiedergabe haben gezeigt, dass Hierarchien vorteilhaft sind. Bei der Reproduktion von Begriffslisten und Texten war die Gedächtnisleistung der Versuchspersonen deutlich höher, wenn ihnen Entscheidungsbäume mit kategorialer Unterordnung angeboten wurden. Lerninhalte wurden von den Versuchspersonen mit Hilfe von hierarchischen Kapitelstrukturen oder Gedankenkarten deutlich schneller gelernt. Lag keine hierarchische Struktur vor, so bemühten sich die Versuchspersonen, den Text selbständig hierarchisch anzuordnen. Die kognitive Psychologie zieht aus diesen Untersuchungen die Konsequenz, dass hierarchisch geordnete Inhalte für Menschen leichter zu erlernen und zu verarbeiten sind und dass aus einer hierarchischen Struktur effizienter Inhalte abgerufen werden können.
Die Bildung von Hierarchien wird in Programmiersprachen bei den Enthalten-Seins-Beziehungen unterstützt: Klassen sind in Packages, Packages wiederum in Packages und schließlich in Projekten bzw. Build-Artefakten enthalten. Diese Hierarchien passen zu unseren kognitiven Mechanismen. Sind die Hierarchien an die Muster der Architektur angelehnt, so unterstützen sie uns nicht nur durch ihre hierarchische Strukturierung, sondern sogar auch noch durch Architekturmuster.
Für alle anderen Arten von Beziehungen gilt das nicht: Wir können beliebige Klassen und Interfaces in einer Sourcecode-Basis per Benutzt-Beziehung oder/und per Vererbungsbeziehung miteinander verknüpfen. Dadurch erschaffen wir verflochtene Strukturen (Zyklen), die in keiner Weise hierarchisch sind. Es bedarf einiges an Disziplin und Anstrengung, Benutzt-Beziehung und Vererbungsbeziehung hierarchisch zu verwendet. Verfolgen die Entwickler und Architekten von Anfang an dieses Ziel, so sind die Ergebnisse in der Regel nahezu zyklenfrei.
In meinen Analysen bekomme ich die ganze Bandbreite von sehr wenigen zyklischen Strukturen bis zu großen zyklischen Monstern zu Gesicht. Ähnlich wie bei den Mustern und der Modularität kann man Zyklen auf Architekturebene und auch Klassenebene untersuchen.
In Abb.6 sieht man vier technische Schichten eines kleinen Anwendungssystem (80.000 LOC). Zwischen den Schichten haben sich einige Rückreferenzen (rote Bögen) eingeschlichen, die zu Zyklen führen. Die Zyklen in Abb.6 werden nur durch 16 Klassen hervorgerufen und lassen sich in diesem Fall leicht ausbauen. Abb.6 stellt also eine gut gelungene Schichtenarchitektur dar.
Der Klassenzyklus in Abb.7 stammt von einem anderen System. Die 242 Klassen in diesem Zyklus sind über 18 Verzeichnisse verteilt. Jedes Verzeichnis ist in Abb.7 mit einer anderen Farbe vertreten.
Insgesamt hat das System, aus dem der Zyklus in Abb.7 stammt, 479 Klassen. Hier brauchen sich also über die Hälfte aller Klassen (242) direkt oder indirekt. Noch dazu hat dieser Zyklus eine starke Konzentration im Zentrum und wenige Satelliten. Eine natürliche Möglichkeit, ihn anhand von Kopplungszentren zu zerlegen, bietet sich also nicht an. Zum Glück finden sich in den meisten Systemen kleinere und weniger konzentrierte Zyklen, die man mit wenigen Refactorings zerlegen kann.
Zusammenfassung
In diesem Artikel haben Sie einen ersten Eindruck bekommen, wie technische Schulden in Architekturen entstehen und wie man sie reduzieren kann. Technische Schulden lassen sich abbauen, indem man Strukturen schafft, die unser Gehirn leicht verarbeiten kann. Hat man die Architektur in diese Richtung verbessert, so geht Wartung und Erweiterung effizienter und schneller von der Hand.