Inhalt

Wir gehen der Frage nach, was eine Softwarearchitektur eigentlich ist, warum wir Entscheidungen in Detailfragen so lange wie möglich aufschieben sollten und wie das Ziehen von Grenzen uns dabei helfen kann.

Dieser Post ist der dritte Teil einer Reihe von Blogposts, welche das Buch „Clean Architecture“ von Robert C. Martin zusammenfasst und in den Kontext der App-Entwicklung einordnet.

Was ist Softwarearchitektur?

Im vorherigen Teil ging es um Komponenten, den Bausteinen aus denen ein System zusammengesetzt wird.

Die Aufteilung eines Systems in diese Komponenten, deren Zusammensetzung und die Art und Weise der Kommunikation zwischen ihnen, wird als Softwarearchitektur bezeichnet. Ziel einer Softwarearchitektur ist es Entwicklung, Deployment, Betrieb und Wartung des Softwaresystems zu vereinfachen. Für „Uncle Bob“ besteht die Erleichterung darin, sich so viele Möglichkeiten so lange wie möglich offen zu halten. Was heißt das genau?

Entwicklung

Eine Softwarearchitektur muss die Entwicklung der Software einfach machen. Für einen allein arbeitender App-Entwickler bringt beispielsweise die Aufteilung der App in Komponenten erstmal einen ziemlichen Overhead mit sich. Bei einer großen App, an der verschiedene Teams an verschiedenen Features gleichzeitig arbeiten, ist es dagegen unerlässlich, dass es voneinander isolierte, unabhängig deploybare Komponenten gibt, damit sich die einzelnen Teams nicht ständig in die Quere kommen.

Deployment

Ziel einer guten Softwarearchitektur ist das Deployment (deutsch: Softwareverteilung) in nur einem Schritt. Die derzeitig so beliebten Micro-Services für Websysteme sind beispielsweise auch sehr gut von großen Teams entwickelbar, da jeder Service unabhängig von den anderen ist. Allerdings kann das Deployment von all den Mini-Services viel Zeit erfordern und bringt potentiell mehr Fehler mit sich als wenige, aber dafür größere Services. Eine Softwarearchitektur muss also auch diesen Umstand berücksichtigen.

Betrieb

In vielen Systemen ist der Einfluss der Softwarearchitektur auf den Betrieb des Systems eher gering. In vielen Websystemen genügt es die Hardware etwas aufzustocken, wenn der Betrieb nicht rund läuft. Es gibt aber auch sehr große Systeme mit vielen Nutzern. Diese Systeme müssen auch horizontal skalieren können, d.h. mehrere Instanzen einzelner Services müssen gleichzeitig laufen können. Hierfür ist die Aufteilung des Systems in Komponenten unerlässlich. Für uns als App-Entwickler sind solche Hochlast-Szenarien jedoch zu vernachlässigen. Uns gibt meist das mobile Betriebssystem die Grenzen vor, in denen wir uns austoben dürfen, sodass die Komponentenbildung für die Ausführung von Apps keine große Rolle spielt.

Wartung

Die Wartung eines Softwaresystems über viele Jahre hinweg ist meist kostspieliger als die ursprüngliche Entwicklung. In einer Software ohne gute Architektur führen neue Features oft zum Zerfall der ursprünglich angedachten Struktur, da hier und da Ergänzung an bestehenden Modulen vorgenommen werden – ein klarer Verstoß gegen das Open-Closed-Prinzip. Unerwartete Seiteneffekte erzeugen zudem neue Bugs. Eine gescheite Architektur ist daher das Mittel der Wahl um die Wartungskosten im Zaum zu halten, auch wenn die initiale Entwicklung dadurch erstmal aufwändiger wird.

Und was hatte es nun mit dem „Möglichkeiten offenhalten“ auf sich?

Möglichkeiten offen halten

Software heißt Software, weil sie die schnelle und einfache Änderung des Verhaltens von Hardware ermöglichen soll. Um Software „soft“ zu halten, müssen wir uns unabhängig von Details machen. Wir wollen uns nicht festlegen, wenn es nicht absolut notwendig ist, sondern uns alle Möglichkeiten offen lassen, um schnell und agil auf neue Anforderungen reagieren zu können. Wir wollen nur die grundlegende Geschäftslogik unserer Software in Code gießen, komplett losgelöst von Details wie Input/Output, Datenbanken, Servern, Frameworks, Protokollen usw. Wir wollen unabhängig sein!

Je länger wir mit den Details warten, desto besser können wir ein Werkzeug für die Detailarbeit auswählen. Warum direkt zu Beginn eines Projektes ein Framework auswählen, wenn wir die Anforderungen daran noch gar nicht richtig definieren können? Warum sich früh für eine Datenbank-Technologie entscheiden, nur um dann nach Monaten doch auf eine andere DB zu wechseln? Warum nicht erstmal die High-Level-Logik implementieren und später die notwendigen Detail-Entscheidungen treffen? Es mag erstmal ungewohnt sein, diese Dinge als Details zu bezeichnen. Aber tatsächlich sollte der Sinn und Zweck eines Software-Systems im Mittelpunkt der Entwicklung stehen. Und der Sinn eines Systems besteht selten darin, Zeichenketten aus einem HTML-Formular in eine MySQL-Datenbank zu speichern, sondern beispielsweise darin ein ToDo in eine ToDo-Liste aufzunehmen.

Du magst dich jetzt vielleicht fragen, wie Du ein System hochziehen sollst, ohne dich um UI-Frameworks, Datenbanken und Protokolle zu scheren? Die Antwort lautet: Durch Grenzziehung.

Grenzziehung

Grenzen separieren Software-Elemente von einander und verhindern, dass die Elemente auf der einen Seite von denen auf der anderen Seite wissen. Grenzen sollten gezogen werden, um wichtige Dinge von unwichtigen Dingen zu trennen. Wie oben beschrieben sind die Business-Regeln das wichtigste in einem Softwaresystem. Für die Businesslogik ist das UI jedoch erstmal irrelevant, d.h. hier sollte eine Grenze gezogen werden. Ebenso ist dem UI egal, welche Datenbank-Technologie eingesetzt wird. Daher wird auch hier eine Grenze gezogen. Genauso muss die Businesslogik sich nicht um die Datenbank kümmern, z.B. in Form von SQL-Queries. Eine Business-Regel könnte beispielsweise lauten: „Speichere ein ToDo in der ToDo-Liste“. Es besteht keine Notwendigkeit, dass das Business-Logik-Modul selbst auf der Datenbank operiert. Die Businesslogik soll sich um das große Ganze kümmern. Wir bauen unsere Anwendung um die Business-Logik herum, z.B. durch ein Modul, dass die Speicherung von ToDos in einer Datenbank übernimmt. Wie genau das Speichern funktioniert verstecken wir vor der Business-Logik, indem wir ein Interface verwenden.

Grenzlinie zwischen Business-Logik und Datenbank

Zwischen Business-Logik und Datenbank verläuft die Grenze. Auf der Seite der Business-Logik wird ein Interface zum Zugriff auf die Datenbank definiert, die Datenbank-Seite implementiert dieses Interface.

Auf diese Weise haben wir die Business-Logik von der konkreten Datenbank losgelöst. Die Businesslogik weiß nur, dass es die Möglichkeit gibt Daten zu lesen und zu schreiben. Wie genau das funktioniert wissen nur die Klassen und Module hinter der blickdichten Datenbank-Grenze.

Die Grenze überschreiten

Eine Grenze kann man natürlich auch überschreiten (zumindest in nicht-totalitären Systemen). Immerhin muss irgendeine Art von Datenaustausch zwischen den Systemen stattfinden. Je nach Art der Grenze variiert auch die Übergabe:

  • In monolithischen Systemen (die im gleichen Speicherbereich arbeiten) besteht keine physische Grenzen zwischen den beiden Seiten. Um die Grenze zu überschreiten bedarf es nur eines Methodenaufrufs. Es liegt also beim Entwickler die Grenzen zu respektieren. Im Beispiel oben heißt das, dass wir selbst darauf achten müssen nur über das Database Interface auf die Database zuzugreifen. In diesem Falle machen wir uns die Polymorphie zu nutze, welche uns zur Compile-Time eine Grenze vorgaukelt. Denn zur Laufzeit versteckt sich hinter dem Database Interface natürlich eine Instanz von Database Access.
  • Lokale Prozesse und Services arbeiten in verschiedenen Speicherbereichen oder sogar auf verschiedenen Rechnern. Es besteht also auch eine physische Grenze. Hierbei bedarf eines komplexeren Kommunikationssystems, um über die Grenzen hinweg zu kommunizieren. Es können beispielsweise Sockets, Message-Queues oder Protokolle wie HTTP verwendet werden.

Egal ob virtuelle oder physische Grenze: Es dürfen nur so viele Daten wie nötig hin- und hergereicht werden. Zudem dürfen keine Interna ausgetauscht werden. Wenn bei einem Aufruf des Database Interface beispielsweise Datenbank-Zeilen ausgespuckt werden, ist die Business-Logik nicht unabhängig von der Datenbank-Implementierung. Falls wir eines Tages auf eine NoSQL-Datenbank umsteigen, die das Konzept von Zeilen gar nicht verwendet, müssen wir auch alle Business-Regeln anpassen. Der Datenaustausch zwischen den zwei Grenzseiten sollte daher durch einfache Modelle erfolgen, die so strukturiert sind, dass die Daten bestmöglich für die Seite mit dem höheren Level abgebildet sind. Diese Modelle werden Request-Model und Response-Model genannt.

Level

Gerade haben wir bereits über Komponenten mit hohem bzw. niedrigem Level gesprochen. Was ist damit gemeint?

Je weiter eine Komponenten, ein Modul, eine Klasse oder eine Methode vom Input/Output (IO) entfernt ist, desto höher ist ihr Level.

Beispiele für LL-Komponenten sind:

  • Die GUI: Sie verarbeitet Eingaben des Nutzers und zeigt Ausgaben an.
  • Ein Repository-Modul schreibt und liest Daten in einer Datenbank.
  • Ein Network-Manager sendet und empfängt Daten im Netzwerk.

Wichtig ist zudem, dass HL-Module nicht von LL-Modulen abhängen. Dies erreichen wir durch Dependency Inversion. Im oberen Beispiel haben wir ein Database Interface in der Business-Logic Komponente definiert, welches von der Database Komponente implementiert wird. Dadurch haben wir eine Umkehrung der Abhängigkeiten erreicht. Obwohl die Business-Logik sich die Datenbank zu nutze machen möchte, der Kontrollfluss also von HL zu LL geht, zeigt die Quellcode-Abhängigkeit von LL zu HL.

Die Low-Level Komponente ist von der High-Level Komponente abhängig und nicht umgekehrt.

Ziel unserer Architektur sollte es sein, die Businesslogik so weit wie möglich vom IO zu distanzieren, sodass sie das höchste Level im System aufweist. Nur so ist das wahrhaft wichtige unserer Anwendung – die Realisierung der Business-Regeln – komplett losgelöst von austauschbaren Implementierungsdetails wie Datenbank, UI, Protokollen, Frameworks etc.

Zusammenfassung

  • Softwarearchitektur definiert das Zusammenspiel von verschiedenen Komponenten in einem Softwaresystem, um die Entwicklung, das Deployment, den Betrieb und die Wartung so einfach wie möglich zu machen.
  • Der zentrale Aspekt dabei ist es, sich alle Möglichkeiten so lange wie möglich offen zu halten, indem Grenzen zwischen wichtigen Aspekten (z.B. der Business-Logik) und unwichtigen Aspekten (z.B. welche Datenbank genau verwendet wird) gezogen werden.
  • Es gibt High-Level und Low-Level Komponenten. Je weiter eine Komponente von Input- oder Output-Kanälen entfernt ist, desto höher das Level.
  • Beim Anlegen von Grenzen ist darauf zu achten, dass High-Level-Komponenten nicht von Komponenten mit niedrigerem Level abhängig sind, indem auf Dependency Inversion zurückgegriffen wird.

Wie immer bedanke ich mich herzlich für Dein Interesse. Im nächsten Blogpost wird sich dann endlich alles um die Clean Architecture drehen.

Beitragsbild von Raphaël Biscaldi auf Unsplash


Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.