Warum wird meine Software zunehmend schwerer zu ändern?

geschrieben von:

Adrien Wehrung

Lokale Umgebung (8)
Lokale Umgebung (8)

Wenn Softwareprodukte auf einer industriellen Skala produziert werden, müssen viele Personen und Rollen auf organisatorischer Ebene und dutzende Komponenten auf technischer Ebene koordiniert werden. Der Bedarf nach Softwarearchitektur erscheint, um eine möglichst reibungslose und vor allem langfristig nachhaltige Entwicklung des Produktes zu ermöglichen.  In diesem Artikel veranschaulichen wir die kritische Rolle der Softwarearchitektur unter dem Blickwinkel der Kopplung: 

Welche Arten es davon gibt, wie sie sich auf das Produkt auswirken und welche Methoden es gibt, um sie unter Kontrolle zu halten.

Kopplung ist ein wichtiger Begriff in der Softwarearchitektur:

Sie festzustellen, zu verstehen und zu kontrollieren ist einer der Hauptaufgaben von Softwarearchitekten, mit der Unterstützung des ganzen Entwicklungsteams. Wird diese vernachlässigt, lassen sich Features schwieriger entwickeln, man wird langsamer, es schleichen sich mehr Bugs ein und die Stimmung der Kollegen kippt zunehmend. Das alles sind Faktoren, die ein Softwareprodukt schließlich zum Scheitern bringen können.  

Im Kontext der Softwareentwicklung bedeutet Kopplung „die Verknüpfung von verschiedenen Systemen, Anwendungen oder Softwaremodulen sowie ein Maß, das die Stärke dieser Verknüpfung bzw. der daraus resultierenden Abhängigkeit beschreibt“ (Wikipedia). Um aus dieser sehr generischen Definition konkrete Beispiele herleiten zu können, betrachten wir zuerst diese kurze, natürlich frei erfundene Horror-Geschichte: 

Eine Geschichte zu Kopplung und Kohäsion 

Alexander ist Backend-Entwickler bei einem IT-Dienstleister, der für einen großen Kunden zusammen mit drei anderen Teams eine Webseite für An- und Verkauf von Keramikkunst baut. Er arbeitet in dem Katalog-Team an einem neuen Feature, das es erlauben soll, in der Übersichtseite der Angebote nach Erstelldatum zu filtern und sortieren. 

Die zuständige Backend-API benötigt Ergänzungen und die dazugehörige Funktionalität muss bereitgestellt werden. Außerdem muss das Frontend an mehreren Komponenten angepasst werden. Frontend und Backend gehören zu zwei getrennten Teams, sodass als Erstes ein Meeting geplant wird, um die Anpassung zu besprechen. Es wird entschieden, dass Alexanders Team die API-Anpassung in einer neuen Version liefert und den Endpunkt anpasst, um die neuen Sortierungsoptionen anzubieten. Ein getrenntes Backlog Item wird beim Frontend Team erstellt, um später die neue Sortierung dem Kunden anzubieten. Dabei stellt sich heraus, dass das Verständnis vom Begriff „Erstelldatum“ bei den Teilnehmern des Meetings unterschiedlich war. Es wird nach Rücksprache mit dem Requirements-Engineer festgestellt, dass es sich um den Zeitpunkt handelt, wann das Kunststück entstanden ist und nicht wann das Angebot hochgeladen wurde. 

8
7

Alexander fängt an, die API-Spezifikation anzupassen. Zu Einheitlichkeitszwecken werden alle Spezifikationen in einem zentralen Repo gehalten und folgen einem gemeinsamen Release-Prozess, der teamübergreifend einmal pro Woche stattfindet. Leider erfordert die API-Anpassung die Nutzung eines neuen Features bei der Codegenerierung, sodass diese ein wenig angepasst werden muss. Letztendlich stellt sich heraus, dass Alexander also auch mit den anderen Teams (welche diese Codegenerierung auch nutzen) dazu Rücksprache halten muss, um ihren Prozess nicht zu brechen.

Nachdem das alles erledigt ist, beginnt Alexander, die serverseitige Implementierung anzupassen. Der Catalog-Service greift auf die Datenbank über den Database-Access-Service zu, eine Zwischenschicht, die den konkreten Zugriff abstrahiert und eine leicht bedienbare Schnittstelle mit Caching zur Verfügung stellt. Die API und Implementierung von diesem teaminternen Service muss auch angepasst werden, um das Erstelldatum in den Anfrageparametern zu inkludieren. Alexander fährt mit der Implementierung fort und stellt beim Build fest, dass viele alte Tests rot sind: Die “Exact-Match” Prüfungen beim Holen der Produktliste schlagen fehl. Das liegt daran, dass Mocks beim Catalog-Service bisher das Erstelldatum nicht berücksichtigt hatten und von Alexander ergänzt werden müssen, um die neue Funktionalität zu testen. Nachdem er alles glatt gezogen hat, ist das Feature fertig und kann zum Code-Review gehen. Mit allen Stellen zusammen, die angepasst werden mussten, änderte er das mehr als 1000 Zeilen Code über vier Repos.

Der Reviewer validiert die Änderungen, nun bleibt Alexander nur noch sicherzustellen, dass die Änderungen ausgeliefert werden. Dafür muss er Absprache mit dem Operations-Team halten, denn es ist hier aus Implementierungsgründen im Database-Access-Service wichtig, dass dieser vor dem Catalog-Service ausgeliefert und dass der Cache auch teilweise invalidiert wird, um die Zugriffe der angepassten API korrekt zu verdrahten.

Der ganze Prozess hat zwischen Absprachen, Entwicklungsaufwand und Wartezeiten mehrere Wochen gedauert. Nach ein paar Wochen mehr hat das Frontend-Team seine Anpassung der Übersichtseite auch ausgeliefert, und damit ist der angestrebte Mehrwert erreicht: Die Kunden können die Produkte nach Erstelldatum filtern und sortieren.

11

Was lehrt uns diese Geschichte zum Thema Kopplung?

Fachlich hatten am Ende viele der benötigten Änderungen nichts mehr mit der Sortierung nach Erstelldatum zu tun. Um die Funktionalität zu implementieren, musste Alexander über ein tiefes Wissen über viele Aspekte des Produkts verfügen und alle möglichen Seiteneffekte im Kopf haben. Dies ist ein Indikator von enger Kopplung. In der Folge betrachten wir zwei Arten von Kopplung, die hier besonders prägnant sind. 

Als Erstes ist die technische Kopplung zu erkennen, worum es in der Definition ging.

Diese zeigt sich in unterschiedlichen Gesichtern: 

  • Ein Baustein (Service, Module, Klasse) ruft einen anderen auf
  • Zwei Services publizieren auf die gleiche Queue oder schreiben in die gleiche Datenbank, oder nutzen eine gemeinsame Library
  • Ein Baustein wird von einem anderen erzeugt oder konfiguriert (z.B. Factory Pattern)
  • Eine Klasse erbt von einer anderen
    Mehrere Services teilen sich eine Laufzeitumgebung
  • Ein automatischer Test prüft eine konkrete Implementierung

 

Viele dieser Punkte lassen sich in Alexanders Geschichte wiederfinden. Achtung: Dass Kopplung existiert, muss nicht zwangsweise etwas Schlechtes bedeuten, sondern nur, dass irgendetwas in dem System passiert. Wären alle Bausteine komplett unabhängig voneinander, oder würden die unterschiedlichen Teile des Systems nicht miteinander kommunizieren, dann würde das besagte System nichts tun. Sowie zwei Zahnräder nichts bewegen können, wenn sie nicht aneinander gekoppelt sind, gilt auch bei Softwaresystemen: 0 Kopplung = 0 Aktivität. 

Kohäsion: Ein Schlüsselkonzept für bessere Softwarearchitektur

Dieses Problem umzugehen ist viel Arbeit und braucht die Einführung eines neuen Konzeptes: Kohäsion oder „wie gut eine Programmeinheit eine logische Aufgabe oder Einheit abbildet“ (Wikipedia). Es handelt sich um das „gute Pendant“ zur Kopplung, das möglichst ausgeprägt sein sollte.

Was zusammengehört, sollte im Code nah aneinander sein. Dies gilt für alle diversen Arten von Kohäsion: 

  • Alle Teile, die zusammen eine wohldefinierte Aufgabe lösen → sollten nicht über diverse Repos verteilt werden 
  • Fachlich zusammenhängende Konzepte → sollten in einem Service/Module zusammen erfasst werden 
  • Prozedural aufeinanderfolgende Schritte → können hintereinander in einer Funktion ausgeführt werden
  • Zeitlich aneinander gebundene Operationen → sollten aus einem gemeinsamen Ort gesteuert werden

 

Purple Did you know interesting fact instagram story (1)

Auf der anderen Seite sollte alles andere, was nicht zusammengehört, auch nicht zueinander gekoppelt sein: Das ist das Prinzip der Trennung der Zuständigkeiten (Engl. Separation of concerns). Gute Architektur erreicht eine geringe Kopplung und eine hohe Kohäsion durch eine saubere Trennung der Zuständigkeiten. So wird sichergestellt, dass die Seiteneffekte von neuen Features und Anpassungen an bestimmten Teilen des Systems deutlich besser kontrolliert werden können. Dafür muss ein geringerer Anteil der Codebasis angefasst werden und weniger Gesamtwissen ist notwendig, um die Implementierung durchzuführen. Dies führt dazu, dass diese Anpassungen schneller fertig sein können und Bugs einfacher zu lokalisieren sind, in Summe, dass die Wartbarkeit steigt. Ein schöner Nebeneffekt einer solchen Architektur ist die Testbarkeit. Sind Services, Module und Klassen in sich kohäsiv, aber aneinander wenig gekoppelt, dann ist die Implementierung von automatischen Tests deutlich vereinfacht und diese sind generell auch robuster. 

 

Die Auswirkungen organisatorischer Kopplung auf Softwareentwicklungsprojekte: Eine kritische Perspektive

 

Das ist aber nicht die einzige Art von Kopplung, die in Alexanders Geschichte zu erkennen ist. In seinem Projekt herrscht ein hoher Grad an organisatorischer Kopplung. Diese wird häufig vernachlässigt, obwohl sie mindestens genauso wichtig ist wie die technische Kopplung. Unter dem lobenswerten Ziel, viel zu vereinheitlichen, sind organisatorische Hürden geschaffen worden, die Prozesse aneinander festbinden, welche eigentlich unabhängig funktionieren könnten. Von dem Requirements-Engineering über das API-Design und die Datenbank-Anpassung bis zum produktiven Release musste Alexander die Entwicklung ständig halten und sich um die Koordination mit den anderen Teams Gedanken machen. Dadurch ist viel Zeit vergangen, bis die Endkunden von der neuen Sortierungsmöglichkeit überhaupt profitieren konnten, was die Feedbackschleife verlangsamt: Sollten, wie das immer zu erwarten ist, neue Iterationen von diesem Feature gewünscht sein, dann können sie erst später angefangen werden. 

Diese Art von Kopplung zu bekämpfen, ist nicht leicht. Auf der positiven Seite können hier ähnliche Techniken angewendet werden, wie die, die sich zur Kontrolle der technischen Kopplung eignen. Services und Klassen können so geschnitten werden, dass sie eine gute Trennung der Verantwortlichkeiten und eine hohe Kohäsion ergeben. Nach den gleichen Prinzipien können auch Teams und ihre Aufgabenbereiche geschnitten werden, um sicherzustellen, dass sie über genug Autonomie verfügen, um ihren Teil des Systems zu entwickeln.

Es existieren viele Methoden, um auf diese Aufteilung hinzuarbeiten, die bekannteste davon ist sicherlich das „Domain Driven Design“: Die unterschiedlichen Fachdomänen werden identifiziert und viele Szenarien werden aufgestellt, um die wichtigen Zusammenhänge herauszufinden, wodurch sich Bausteine des Systems herauskristallisieren und sich die Teamgrenzen an dieser Aufteilung orientieren. Eine der Konsequenzen einer solchen Aufteilung ist häufig die Abschaffung von klassischen Silos (Entwicklung, Testing und Operations sind stark voneinander getrennt): Dieser Ansatz führt zu Teams, die sich von Anfang bis Ende um ihren fachlichen Teil des Systems kümmern können und Kompetenzen in diversen Bereichen der Entwicklung (Frontend bis Backend) brauchen, aber auch im Requirements-Engineering bis hin zu Operations. Wird dieser Ansatz richtig realisiert, dann entstehen nur Abhängigkeiten zwischen den Teams, wenn die Fachdomäne es erfordert, was den Koordinationsaufwand mindert und die Geschwindigkeit damit erhört.

Ein positiver Nebeneffekt besteht darin, dass die Expertise der Entwicklungsteams über ihr Fachbereich erhöht ist, was wie eine hilfreiche Feedbackschleife für das technische Design und die Vision wirkt: je mehr das Team die Fachdomäne versteht, desto besser kann es sich in das Produkt involvieren, desto bessere Implementierungen können erzeugt sowie sinnvolle Vorschläge gemacht werden und desto stärker wächst die Team-Expertise über die Fachdomäne. 

BlogMaterial_Fachdomaenebesserverstehen

Wie Kopplung und Kohäsion die Softwareentwicklung beeinflussen: Was wir durch Alexanders Erfahrung lernen können

6

Zusammengefasst bringt uns Alexanders Beispiel einige wichtige Einblicke zu Kopplung, Kohäsion und ihr Zusammenspiel. Durch das Anwenden von Softwarearchitekturmethoden kann das Kopplungsproblem zeitgleich auf mehreren Ebenen angegangen werden: Die technische und organisatorische Ebene gehen hier Hand in Hand. Als Erstes muss herausgefunden werden, was zusammengehören muss. Daraus ergeben sich nach und nach immer bessere Unterdomäne sowie Service- und Teamgrenzen. Abgesehen von seinem Aufwand, der mit dem Alter des Produktes steigt, erzeugt der Entkopplungsprozess Kosten: Einige Ideen oder Prozesse können sich an unterschiedliche Stellen des Systems doppeln und das Risiko besteht, dass sie auseinandergehen. Diese Kosten sind (wenn gut abgewogen, zum Beispiel mit Einsatz von architektonischen Querschnittkonzepten), ein vorteilhafter Abtausch gegen die Alternative, sprich eine überflüssige unerwünschte Kopplung mit allen erwähnten Problemen. Die dadurch entstehenden kohäsiven Komponenten sind einfacher zu warten, Anpassungen sind weniger fehleranfällig und die Qualität ist durch die bessere Testbarkeit einfacher hochzuhalten. Am Ende des Tages ist genau diese schnellere Entwicklung besserer Software das, was Produkte erfolgreich macht.