Fragen? Jetzt Anruf vereinbaren!

Clean Architecture

Haben unsere Schichten etwa Staub angesetzt?

Architekturstile wie Clean Architecture von Robert “Uncle Bob” Martin oder die Hexagonale Architektur von Alistair Cockburn stoßen in den letzten Jahren auf immer mehr Interesse. Sie zielen darauf ab, den fachlichen Kern einer Anwendung unabhängig von Frameworks, Datenbanken und sonstiger Infrastuktur entwickeln und testen zu können. Neben verbesserter Test- und Wartbarkeit erlaubt dieser Ansatz auch, Infrastruktur-Komponenten erst spät festzulegen und auch später noch leicht durch andere zu ersetzen. Der Artikel erläutert die Nachteile traditioneller Drei-Schichten-Architekturen und die Verbesserungen, die sich mit Clean Architecture erzielen lassen.

Die traditionelle Drei-Schichten-Architektur

Mit “Schichten” sind im Folgenden immer logische Schichten (engl. layer) gemeint, nicht die typischen physikalischen Schichten (engl. tier) Client, Server und Datenbank. In diesem logischen Sinn sieht eine Drei-Schichten-Architektur, wie sie sicher noch in einer Vielzahl von Systemen zum Einsatz kommt, folgendermaßen aus (links sind nur die Schichten und ihre Abhängigkeiten dargestellt, rechts exemplarische Klassen in diesen Schichten):

Die traditionelle Drei-Schichten-Architektur

Die traditionelle Drei-Schichten-Architektur

Die Präsentationsschicht (Presentation oder User Interface Layer) ist verantwortlich für die Darstellung der Anwendung gegenüber dem Anwender. Sie interpretiert dessen Eingaben und ruft die zugehörige Funktionalität in der Business-Logik-Schicht auf (oft Business Logic oder auch Domain Layer genannt). In der Regel ist es für diese Funktionalität erforderlich, Daten, die in einer Datenbank persistent gespeichert sind, auszuwerten oder zu ändern. Zu diesem Zweck ruft die Business-Logik-Schicht eine Datenzugriffsschicht (Data Access Layer) auf, die die nötige Kommunikation mit der Datenbank abwickelt.

Das grundlegende Prinzip von Schichtenarchitekturen ist, dass die Komponenten einer Schicht nur von Komponenten in darunter liegenden Schichten abhängig sein dürfen (häufig sogar nur von der unmittelbar darunter liegenden Schicht), während sie von den darüber liegenden Schichten überhaupt nichts wissen. Die höheren Schichten können daher problemlos ausgetauscht oder geändert werden, ohne dass die weiter unten liegenden Schichten angepasst werden müssen.

Die Schichtung an sich ist nicht problematisch, aber die traditionelle Ausprägung der Drei-Schichten-Architektur bringt folgende Nachteile mit sich:

Entkopplung durch Dependency Inversion

In der oben beschriebenen “Reinform” wird die Drei-Schichten-Architektur aufgrund der vielen Nachteile bei neuen Systemen kaum noch angewendet, und auch ältere Systeme werden vor anstehenden Änderungen zumindest in den betroffenen Teilen mittels Refactoring oft so weit umgebaut, dass die Business-Logik nicht mehr direkt mit der Datenbank gekoppelt ist. Dazu wendet man zunächst das Dependency Inversion Principle (DIP) an, indem man ein Interface extrahiert, so dass die Business-Logik zukünftig nur noch von diesem Interface abhängt, nicht mehr von der konkreten Implementierung, die immer gleich die Datenbank aufruft:

Entkopplung der Business-Logik von der Datenbank

Entkopplung der Business-Logik von der Datenbank

In Unit Tests für die entsprechenden Teile der Business-Logik kann man nun das Interface durch ein Test-Double (z.B. ein Mock-Objekt) implementieren, das exakt die für den Test notwendigen Antworten liefert, ohne dazu eine Datenbank aufzurufen. Natürlich laufen solche Tests viel schneller als Tests mit Datenbank und sind auch viel besser beherrschbar, weil man alles unter Kontrolle hat.

Was ist mit der Abhängigkeit auf Architektur-Ebene?

Durch die Anwendung des DIP haben wir erreicht, dass MyService nicht mehr direkt von MyDataAccessObject abhängt, was die Testbarkeit enorm verbessert hat. Aus Architektursicht (linke Seite des obigen Diagramms) hat sich aber nichts geändert: Die fachliche Business-Logik hängt immer noch von der technischen Komponente für den Datenzugriff ab. Wird das neue Interface als reiner Implmentierungs-Kunstgriff zur Verbesserung der Testbarkeit erstellt, aber nach wie vor an den technischen Erfordernissen des Datenzugriffs ausgerichtet und in der Datenzugriffsschicht angesiedelt, bleibt auch das Problem erhalten, dass die Business-Logik immer wieder an technisch motivierte Änderungen in der Datenzugriffsschicht angepasst werden muss.

Um dieses Problem zu lösen, muss man noch etwas mehr umdenken: Nicht die Datenzugriffsschicht sollte eine Low-Level-Schnittstelle zum Zugriff auf die Datenbank anbieten, sondern die Business-Logik sollte eine fachlich motivierte High-Level-Schnittstelle vorgeben, die ein der Fachlichkeit angemessener Persistenz-Mechanismus umzusetzen hat! Die fachlichen Probleme, die eine typische Enterprise-Anwendung löst, haben in der Regel nichts damit zu tun, dass bestimmte Datensätze in einer Datenbank-Tabelle manipuliert oder gefunden werden müssen, entsprechend wird man auch nichts derartiges in so einer Schnittstelle vorfinden. Eine Business-Logik-Komponente für das Mahnwesen wird aber z.B. den Bedarf haben, unbezahlte Rechnungen zu finden, die bis zu einem bestimmten Datum fällig gewesen wären, und dafür vielleicht eine Methode wie Collection<Invoice> findUnpaidInvoicesDueBy(Date dueDate) fordern, die genau diese Rechnungen zurückliefern soll.

Uncle Bob bezeichnet eine solche Schnittstelle als Gateway bzw. Gateway-Interface. Eric Evans hat für das analoge Konstrukt in Domain-Driven Design den Begriff Repository geprägt. Die Idee ist ähnlich, der Begriff Gateway betont allerdings den Zugriff über ein Netzwerk, während der Begriff Repository diesen Aspekt herunterspielt. Da hier primär Clean Architecture beschrieben werden soll, bleiben wir bei Gateway:

Umkehrung der Abhängigkeit zwischen Business-Logik und Datenzugriff

Umkehrung der Abhängigkeit zwischen Business-Logik und Datenzugriff

Wie man sieht, geht die Abhängigkeit auf Architektur-Ebene jetzt vom Datenzugriff zur Business-Logik, womit die Business-Logik von technischen Details des Datenzugriffs unabhängig wird!

Da die von der Gateway-Implementierung geforderten Fähigkeiten fachlich definiert sind, können im Gateway-Interface keine technischen Details auftauchen, die nur für einen bestimmten Persistenzmechanismus zutreffen. Entsprechend einfach sollte es möglich sein, neben einer Implementierung für – sagen wir – Oracle auch eine Implementierung für PostgreSQL oder auch nicht-relationale Datenbanken wie MongoDB zu erstellen. Damit wird es vergleichsweise einfach möglich, die Datenbank-Technologie (oder auch nur den OR-Mapper, wenn ein solcher zur Implementierung verwendet wird) später zu wechseln oder mehrere Datenbanken für die gleiche Anwendung zu unterstützen (etwa für unterschiedliche Kundeninstallationen).

Mit dieser Architektur ist es sogar möglich, die Business-Logik (oder einen signifikaten Teil davon, z.B. für ein Minimum Viable Product) bereits zu implementieren und zu testen, wenn man noch gar keine Datenbank am Laufen hat und sich noch nicht einmal auf eine bestimmte Datenbank festgelegt hat! Das erlaubt es, diese technische Entscheidung zu vertagen, bis man genügend über die tatsächlichen Anforderungen an ein erfolgreiches Produkt weiß, um die Vor- und Nachteile bestimmter Datenbank-Technologien im Hinblick auf diese Anforderungen zu bewerten.

Ist für die GUI auch etwas zu beachten?

Wie in der letzten Abbildung zu sehen, sind sowohl GUI- als auch Datenzugriffs-Komponenten nun abhängig von der Business-Logik, während die Business-Logik von beidem unabhängig ist. Um das auch für die GUI zu garantieren, empfiehlt Clean Architecture, zunächst die Business-Logik nicht nur unabhängig von der Datenbank, sondern auch unabhängig von der GUI und zugehörigen Frameworks zu entwickeln, rein auf Basis von fachlich definierten Use Cases, die von Tests angetrieben werden (TDD - Test Driven Development).

Die Use-Case-Implementierung gibt dazu eine Schnittstelle vor (inkl. der mitzugebenden Daten), über die die GUI (oder ein Unit Test) den Use Case auslösen kann. Der Use Case ruft seinerseits entsprechende fachliche Logik auf und liefert das Ergebnis in einem fachlich angemessenen Format zurück. Dem Anwender geeignet Rückmeldung zu geben ist dann wieder Aufgabe der GUI und nicht Teil der Business-Logik.

Dieses Vorgehen stellt sicher, dass die Business-Logik nicht vom verwendeten GUI-Framework abhängig ist, so dass dieses Framework sich wieder relativ leicht austauschen lässt, ohne die Business-Logik ändern zu müssen. Das erleichtert auch ungemein, parallel mehrere GUI-Clients zu unterstützen (z.B. neben einem Web-Client auch native Clients für Android und iOS).

Clean Architecture

Der resultierende Architekturstil “Clean Architecture”, bei dem die Business-Logik als fachlicher Kern der Anwendung unabhängig von allen I/O-Details und Frameworks entwickelt und getestet wird, wird bevorzugt in Form von konzentrischen Ringen dargestellt - hier gibt es nicht mehr wie bei traditionellen Schichten-Architekturen “weiter oben” (näher an der GUI) und “weiter unten” (näher an der Datenbank), sondern “weiter innen” (näher am fachlichen Kern) und “weiter außen” (näher an den technischen Details):

Clean Architecture

Clean Architecture

Die Business-Logik ist hier noch einmal aufgeteilt in den innersten fachlichen Kern (“Entities”, gelb), der “anwendungsneutral” rein durch das geschäftliche Umfeld des Unternehmens bestimmt wird, und die konkrete Umsetzung zugehöriger Use Cases in der aktuellen Anwendung (“Use Cases”, rot) - das entspricht auch grob der in DDD üblichen Aufteilung in “Domain Model” und “Application” (jedenfalls ist das eine zweckmäßige Vereinfachung, wenn man beides zusammen einsetzen will). Der nächste Ring (“Interface Adapters”, grün) enthält Adapter, die zwischen den für Use Cases und Entities geeignetsten Datenformaten und den von den Frameworks und Treibern des äußersten Rings (“Frameworks & Drivers”, blau) benötigten Formaten konvertieren. Der äußerste Ring enthält neben technischen Frameworks in der Regel nur Glue Code, der die Frameworks des blauen mit den Interface-Adaptern des grünen Rings verbindet.

Wichtig ist dabei insb. die “Dependency Rule”, angedeutet durch die Pfeile zwischen den Ringen: Code-Abhängigkeiten dürfen nur von außen nach innen, aber nie von innen nach außen gehen. Damit insb. der fachliche Kern von technischen Belangen unbehelligt bleibt, darf er von diesen technischen Belangen auch nichts wissen!

Uncle Bob hat seine eigene Zusammenfassung dieses Architekturstils auf dem Clean Coder Blog veröffentlicht.

Wofür lässt sich Clean Architecture einsetzen?

Klar ist, dass die Entkopplung der Ringe der Clean Architecture wie oben beschrieben nicht nur eine Menge Vorteile, sondern auch einiges an Arbeit mit sich bringt. Dieser Aufwand lohnt sich, wenn die umzusetzende Fachlichkeit eher komplex ist und voraussichtlich über einen längeren Zeitraum gewartet werden muss, weil sie wichtige Geschäftsprozesse unterstützt. Für einfache CRUD-Anwendungen (die i.W. nur einigermaßen schöne Masken zur direkten Bearbeitung von Datenbank-Tabellen benötigen) lohnt sich das natürlich nicht, da tut es vielleicht auch eine Low-Code-Plattform oder Microsoft Access. Für Wegwerf-Prototypen und kurzlebige Demonstratoren lohnt sich der Aufwand natürlich auch nicht, da die Clean Architecture ihre Vorteile erst ausspielt, wenn fachliche oder technische Änderungen anstehen, die dann jeweils nur einen kleinen, wohldefinierten Bereich der Anwendung betreffen.

Diese Einsatz-Empfehlung deckt sich übrigens recht gut mit den einschlägigen Empfehlungen zum Einsatz von Domain-Driven Design, weshalb sich beides auch sehr gut ergänzt. Für Microservices und Self-Contained Systems lohnt sich diese Kombination besonders, denn DDD hat mit dem Bounded Context ein sehr gutes Konstrukt zur fachlichen Schneidung von Microservices (oder Self-Contained Systems) und Clean Architecture hilft dabei, die Fachlichkeit eines Dienstes unabhängig von anderen Diensten zu entwickeln, auch wenn er zur Laufzeit mit den anderen Diensten interagieren muss.

Eine gute (vor allem nicht absolut triviale) Beispiel-Anwendung, die die Prinzipien von Clean Architecture (und auch Domain-Driven Design) beherzigt, ist etwa Microsofts eShopOnWeb für ASP.NET Core, den es als eShopOnContainers auch in einer Microservice-Variante gibt.

Picture Credits Title: © Robert Kneschke, Adobe Stock
Siegfried Schäfler

Siegfried Schäfler

Software Architect

+49 89 5307 44-516

schaefler@4soft.de

Setzen Sie Clean Architecture ein oder haben Sie es demnächst vor? Wie sind Ihre Erfahrungen mit traditionellen Drei- oder auch Mehr-Schichten-Architekturen? Über Rückmeldungen jeder Art freue ich mich sehr.