Read in English · Leer en español · Lire en français
Abschnitt 5.6 von 5.8 · Mittelstufe · 25 Min. Lesezeit
Hexagonale, Onion- und Clean Architecture
Videolektion
5 Min. · auf Englisch gesprochen, mit UntertitelnNur anhören (Audio)
Transkript lesen
Diese Lektion behandelt eine Idee mit drei Namen: hexagonale, Onion- und Clean Architecture. Sie hält Ihre Geschäftsregeln unabhängig von Datenbanken, Frameworks und externen Diensten, sodass Sie sie schnell testen und die Technologie um sie herum ersetzen können.
Denken Sie an einen Reiseadapter. Ihr Laptop-Ladegerät hat eine Steckerform, und jedes Land hat seine eigene Steckdose. Sie bauen den Laptop nicht für jedes Land um. Sie haben kleine Adapter dabei, die auf der einen Seite in die Steckdose und auf der anderen zu Ihrem Stecker passen. Diese Stile bauen Software auf dieselbe Weise. Die Geschäftsregeln haben eigene Standardsteckdosen, die Ports heißen. Datenbanken, Websites, Zahlungsanbieter und Testwerkzeuge werden über Adapter angeschlossen, sodass sich jedes davon austauschen lässt, ohne die Regeln anzufassen.
Diese Stile entstanden aus Frust über Schichten, bei denen die Geschäftsschicht von der Persistenz abhängt, sodass Regeln von Tabellen geprägt werden und das Testen einer Regel das Starten einer Datenbank bedeutet. Alistair Cockburns Antwort war 2005 die hexagonale Architektur, auch Ports and Adapters genannt. Jeffrey Palermos Onion-Architektur folgte 2008. Und Robert Martin führte diese Ideen als Clean Architecture zusammen, 2012 in einem Blogbeitrag und 2017 in einem Buch. Alle drei kehren die Abhängigkeit um. Der Geschäftscode deklariert die Schnittstellen, die er braucht, und die Infrastruktur implementiert sie.
Hier ist das Hexagon. Im Inneren sitzt die Anwendung: die Domäne und ihre Use Cases. Die Form selbst hat keine Bedeutung; entscheidend ist innen gegenüber außen. Ein Port ist eine Schnittstelle, die der Anwendung gehört. Links liegen treibende Ports wie Place Order und Track Order: die eigene API der Anwendung. Treibende Adapter rufen sie auf: ein REST-Controller, die API einer Mobil-App, ein Test-Harness oder ein Batch-Skript. Jeder macht aus seiner Eingabe einen Aufruf mit Domänenwerten. Rechts liegen getriebene Ports wie Orders und Payments. Die Anwendung ruft sie auf, wenn sie etwas von der Außenwelt braucht. Getriebene Adapter implementieren sie: ein PostgreSQL-Repository oder ein In-Memory-Repository, ein echter Kartenanbieter oder ein Fake. Zur Laufzeit ruft die Anwendung den Adapter auf, aber im Code hängt der Adapter von der Anwendung ab.
Clean Architecture zeichnet dieselbe Idee als Ringe. Im Zentrum stehen die Entitäten: unternehmensweite Regeln wie Order und Money. Um sie herum die Use Cases: anwendungsspezifische Regeln wie Place Order. Dann die Interface Adapters: Controller, Presenter und Repository-Implementierungen. Und am Rand Frameworks und Treiber: das Web-Framework, die Datenbank, die Benutzeroberfläche. Die Abhängigkeitsregel hält alles zusammen: Quellcode-Abhängigkeiten zeigen nur nach innen. Nichts in einem inneren Ring darf etwas benennen, das in einem äußeren deklariert ist.
Darum ist das wichtig. Die Essenslieferplattform muss wegen einer Preisänderung innerhalb von sechs Wochen zu einem neuen Kartenanbieter wechseln. In einer Version des Systems ruft der Checkout das Software Development Kit des alten Anbieters direkt auf, an dreiundzwanzig Stellen. Seine Fehlertypen werden an elf Stellen abgefangen, und seine Objekte sind in der Datenbank gespeichert. Jede Aufrufstelle muss sich ändern, die gespeicherten Daten müssen migriert und die Zahlungstests neu geschrieben werden. Das sind fünf bis sechs Wochen, mit einer riskanten Umstellung auf einen Schlag. In der anderen Version implementiert eine Adapterklasse von etwa zweihundert Zeilen einen Payment-Gateway-Port. Der Kern kennt nur charge, refund und seinen eigenen Ergebnistyp. Also schreibt das Team einen zweiten Adapter, lässt die vorhandenen Tests des Ports dagegen laufen und verlagert dann ein Prozent der Zahlungen, dann zehn, dann alle. Zwei Wochen, bei jedem Schritt umkehrbar. Der Port kostete zu Beginn etwa drei zusätzliche Tage. Sein Wert hängt davon ab, wie wahrscheinlich und teuer die Änderung ist. Und er muss die Sprache der Domäne sprechen, nicht die API eines Anbieters spiegeln.
Nutzen Sie diese Struktur, wenn die Geschäftslogik umfangreich ist und die aktuellen Frameworks überdauern wird und wenn schnelle, isolierte Tests der Geschäftsregeln wichtig sind. Sie lohnt sich auch, wenn Sie externe Dienste integrieren, die sich wahrscheinlich ändern. Seien Sie vorsichtig bei Prototypen, kurzlebigen Werkzeugen und Anwendungen, die überwiegend Formularfelder in Tabellen schieben. Dort fügen zusätzliche Schnittstellen und Mapping-Code Dateien hinzu, ohne Schutz hinzuzufügen. Und wie bei Schichten gilt: Setzen Sie die Regel automatisch durch, mit import-linter, mit der fertigen Onion-Architecture-Regel von ArchUnit oder mit einem Domänenmodul, das mit einer Framework-Abhängigkeit gar nicht erst kompiliert.
Der vollständige Abschnitt baut den Use Case Place Order in Python, mit einem echten Adapter und einem Test Double, und vergleicht die drei Stile nebeneinander. Als Nächstes, in Abschnitt fünf Punkt sieben, finden wir mit Domain-Driven Design gute Grenzen. Bis gleich.
Was Sie wissen müssen
- Hexagonale Architektur (Ports and Adapters, Cockburn, 2005), Onion-Architektur (Palermo, 2008) und Clean Architecture (Martin, Blog 2012, Buch 2017) teilen eine Idee: Geschäftslogik im Zentrum, unabhängig von Frameworks und Infrastruktur, mit nach innen zeigenden Abhängigkeiten.
- Ein Port ist eine Schnittstelle, die der Anwendung gehört; ein Adapter übersetzt zwischen einem Port und einer Technologie. Treibende Adapter rufen die Anwendung auf; getriebene Adapter implementieren Schnittstellen, die die Anwendung aufruft.
- Die Abhängigkeitsregel besagt, dass innerer Code nie etwas aus einem äußeren Ring benennen darf; die Abhängigkeitsumkehr lässt den Kontrollfluss nach außen gehen, während Quellcode-Abhängigkeiten nach innen zeigen.
- Der Hauptnutzen sind schnelle, isolierte Tests der Geschäftsregeln und die Möglichkeit, Infrastruktur wie Zahlungsanbieter durch einen neuen Adapter zu ersetzen.
- Die Kosten sind zusätzliche Schnittstellen, Mapping-Code und Indirektion, die sich für einfache CRUD-Anwendungen oder kurzlebige Werkzeuge nicht lohnen.
- Setzen Sie die Regel automatisch durch, etwa mit import-linter, der onionArchitecture-Regel von ArchUnit oder getrennten Build-Modulen.
Eine Idee, drei Namen
Hexagonale Architektur, Onion-Architektur und Clean Architecture sind drei Formulierungen einer Idee: Die Geschäftslogik kommt ins Zentrum, wird unabhängig von Benutzeroberflächen, Datenbanken, Frameworks und externen Diensten gemacht, und alle Quellcode-Abhängigkeiten zeigen auf sie. Sie beschreiben die innere Struktur einer Anwendung und lassen sich daher mit jedem Deployment-Stil kombinieren: einem einzelnen Monolithen, einem modularen Monolithen, dessen Module jeweils so gebaut sind, oder einem Microservice.
Sie entstanden aus Frust über die klassische Schichtenarchitektur. Im Schichtenstil aus Abschnitt 5.2 hängt die Geschäftsschicht von der Persistenzschicht ab, die von der Datenbank abhängt, sodass Geschäftsregeln am Ende von Tabellen und objektrelationalem Mapping geprägt sind und das Testen einer Regel das Starten einer Datenbank bedeutet. Die Stilfamilie in diesem Abschnitt kehrt diese Abhängigkeit mit dem Prinzip der Abhängigkeitsumkehr um: Der Geschäftscode deklariert die Schnittstellen, die er braucht, und der Infrastrukturcode implementiert sie. Das Prinzip selbst behandelt Abschnitt 3.5; dieser Abschnitt zeigt die darauf aufbauenden Architekturen.
Hexagonale Architektur: Ports and Adapters
Alistair Cockburn beschrieb die Idee zuerst im Wiki des Portland Pattern Repository und verfasste 2005 einen Artikel darüber unter dem anschaulicheren Namen Ports and Adapters. Seine erklärte Absicht war, eine Anwendung gleichermaßen von Benutzern, Programmen, automatisierten Tests oder Batch-Skripten steuern zu lassen
und sie isoliert von ihren späteren Datenbanken und Geräten zu entwickeln und zu testen. 2024 veröffentlichten er und Juan Manuel Garrido de Paz ein kurzes Buch, Hexagonal Architecture Explained, das das Muster neu darlegt.
Ein Port ist ein zweckgerichtetes Gespräch zwischen der Anwendung und der Außenwelt, ausgedrückt als Schnittstelle: „eine Bestellung aufgeben“, „Bestellungen speichern“, „eine Karte belasten“. Ein Adapter ist der Code, der zwischen einem Port und einer bestimmten Technologie übersetzt: ein HTTP-Controller, ein SQL-Repository, das SDK eines Zahlungsanbieters, ein Kommandozeilenskript, ein In-Memory-Fake. Ein Port kann viele Adapter haben. Die Anwendung im Hexagon kennt nur ihre Ports.
Das Hexagon hat als Form keine Bedeutung; Cockburn wählte es, um dem Bild der Schichten von oben nach unten zu entkommen und Platz zu lassen, auf jeder Seite mehrere Ports zu zeichnen. Entscheidend sind die Grenze zwischen innen und außen und die Asymmetrie zwischen zwei Arten von Ports.
Treibende und getriebene Ports
Treibende und getriebene Seite
Treibende (primäre) Ports und Adapter sitzen im üblichen Bild links. Ein treibender Adapter nimmt eine Anfrage eines primären Akteurs entgegen (der Browser eines Nutzers, eine Mobil-App, ein anderes System, ein Test, ein geplanter Job) und ruft einen treibenden Port auf, der die eigene API der Anwendung ist: in der Praxis ein Use Case wie PlaceOrder. Der Adapter macht aus HTTP, einer Nachricht oder einem Kommandozeilenargument einen Aufruf mit Domänenwerten und aus dem Ergebnis wieder eine Antwort.
Getriebene (sekundäre) Ports und Adapter sitzen rechts. Die Anwendung ruft sie auf, wenn sie etwas von der Außenwelt braucht: eine Bestellung speichern, eine Karte belasten, eine Benachrichtigung senden, die Uhrzeit lesen. Der Port ist eine Schnittstelle, die der Anwendung gehört und in ihren Begriffen geschrieben ist (OrderRepository.save(order)), und ein getriebener Adapter implementiert sie für eine Technologie (PostgreSQL, die API eines Kartenanbieters, ein SMS-Gateway). Hier geschieht die Abhängigkeitsumkehr: Zur Laufzeit ruft die Anwendung den Adapter auf, aber im Quellcode hängt der Adapter von der Schnittstelle der Anwendung ab, nicht umgekehrt.
Cockburns Originaltext spricht von primären und sekundären Akteuren; die Begriffe treibend und getrieben sind heute verbreiteter, und er verwendet sie selbst. Die Unterscheidung ist wichtig für das Testen: Treibende Ports werden von Test-Harnesses angesprochen, die als Nutzer auftreten, und getriebene Ports werden durch Test Doubles ersetzt. Sie ist auch für Änderungen wichtig: Neue Kanäle (ein Sprachassistent, eine Partner-API) kommen als neue treibende Adapter hinzu, neue Infrastruktur als neue getriebene Adapter.
Irgendjemand muss die Adapter erzeugen und in die Anwendung einstecken. Das geschieht in einem einzigen Composition Root, typischerweise dem Startcode des Programms, dem einzigen Teil des Systems, der jeden Adapter kennt. Ein Dependency-Injection-Framework kann diese Arbeit übernehmen, aber einfache Konstruktoraufrufe in main sind oft klarer.
Onion-Architektur
Jeffrey Palermo beschrieb die Onion-Architektur in einer Reihe von Blogbeiträgen ab Juli 2008, gedacht für langlebige Geschäftsanwendungen mit umfangreichem Verhalten (für kleine Websites riet er davon ab). Er zeichnete konzentrische Ringe: das Domänenmodell im Zentrum, Domain Services darum, Application Services um diese und einen äußeren Ring mit Benutzeroberfläche, Infrastruktur (einschließlich der Datenbank) und Tests. Seine zentralen Grundsätze sind, dass die Anwendung um ein unabhängiges Objektmodell herum gebaut wird, dass innere Schichten Schnittstellen definieren, die äußere Schichten implementieren, dass alle Kopplung zum Zentrum hin verläuft und dass der Anwendungskern getrennt von der Infrastruktur kompiliert und ausgeführt werden kann.
Im Vergleich zur hexagonalen Architektur fügt Onion benannte Ringe innerhalb der Anwendung hinzu und macht einen Punkt explizit, den Cockburns Bild offenlässt: Die Datenbank ist nicht das Fundament des Systems, sondern ein externer Dienst am Rand. Repository-Schnittstellen liegen im Kern; ihre Implementierungen liegen im Infrastrukturring.
Clean Architecture und die Abhängigkeitsregel
Robert C. Martin veröffentlichte „The Clean Architecture“ im August 2012 in seinem Blog als Synthese aus hexagonaler Architektur, Onion-Architektur und anderen ähnlichen Vorschlägen und baute es 2017 zum Buch Clean Architecture aus. Sein Bild hat vier Ringe. Entitäten enthalten unternehmensweite Geschäftsregeln. Use Cases enthalten anwendungsspezifische Regeln, die Entitäten orchestrieren. Interface Adapters wandeln Daten zwischen der Form, die die Use Cases brauchen, und der Form, die externe Instanzen brauchen: Controller, Presenter und Gateways. Frameworks und Treiber bilden den äußeren Ring: das Web-Framework, die Datenbank, die UI.
Die Abhängigkeitsregel hält alles zusammen: Quellcode-Abhängigkeiten zeigen nur nach innen, und nichts in einem inneren Ring darf etwas benennen, das in einem äußeren Ring deklariert ist, sei es eine Klasse, eine Funktion oder ein Datenformat. Muss die Steuerung nach außen fließen (ein Use Case speichert eine Bestellung), deklariert der innere Ring eine Schnittstelle, und der äußere Ring implementiert sie. Daten, die eine Grenze überqueren, sollten einfache Strukturen in den Begriffen des inneren Rings sein, nie eine Datenbankzeile oder ein HTTP-Request-Objekt. Martin argumentiert außerdem, dass die oberste Struktur den Zweck des Systems (Bestellungen, Zahlungen, Lieferungen) zeigen sollte statt seines Frameworks, eine Idee, die er Screaming Architecture nennt.
Die Ringe der Clean Architecture
Hexagonale, Onion- und Clean Architecture im Vergleich
| Aspekt | Hexagonal (Ports and Adapters) | Onion | Clean |
|---|---|---|---|
| Urheber und Datum | Alistair Cockburn; 2005 als Artikel veröffentlicht | Jeffrey Palermo; Blogreihe ab Juli 2008 | Robert C. Martin; Blogbeitrag August 2012, Buch 2017 |
| Bild | Ein Hexagon mit einem Innen (Anwendung) und einem Außen (Adapter), links treibend, rechts getrieben | Konzentrische Ringe: Domänenmodell, Domain Services, Application Services, dann UI, Infrastruktur und Tests | Konzentrische Ringe: Entitäten, Use Cases, Interface Adapters, Frameworks und Treiber |
| Zentrale Regel | Die Anwendung spricht mit der Welt nur über Ports; Adapter übersetzen | Alle Kopplung verläuft zum Zentrum hin; der Kern definiert Schnittstellen, die der äußere Ring implementiert | Die Abhängigkeitsregel: Quellcode-Abhängigkeiten zeigen nur nach innen |
| Vorgegebene innere Struktur | Keine: nur innen gegenüber außen | Ja: benannte innere Ringe für die Domäne | Ja: Entitäten und Use Cases, dazu Hinweise zu Presentern und zu Daten, die Grenzen überqueren |
| Hauptschwerpunkt | Symmetrie: Nutzer, Tests, Skripte und andere Programme können die Anwendung gleichermaßen steuern | Das Domänenmodell statt der Datenbank ins Zentrum einer Geschäftsanwendung stellen | Unabhängigkeit von Frameworks, UI, Datenbank und externen Instanzen; testbare Geschäftsregeln |
| Typisches Vokabular | Port, Adapter, treibend (primär), getrieben (sekundär), Akteur | Domänenmodell, Domain Services, Application Services, Infrastruktur | Entität, Use Case (Interactor), Gateway, Presenter, Controller |
Ein Use Case mit einem Port und zwei Adaptern
Das folgende Python-Beispiel implementiert den Use Case PlaceOrder der Essenslieferplattform auf hexagonale Weise. Das erste Snippet ist das Innere: eine kleine Domänenklasse, zwei getriebene Ports, geschrieben als typing.Protocol-Klassen, und der Use Case, der als treibender Port dient. Das zweite Snippet ist das Äußere: ein PostgreSQL-Adapter und ein In-Memory-Test-Double, die denselben Port implementieren. Das dritte zeigt, warum das wichtig ist: ein schneller Unit-Test und der Composition Root, in dem die Produktionsadapter eingesteckt werden.
from dataclasses import dataclass
from decimal import Decimal
from typing import Protocol
@dataclass
class Order:
id: str
total: Decimal
status: str = "new"
class OrderRepository(Protocol): # driven port
def save(self, order: Order) -> None: ...
class PaymentGateway(Protocol): # driven port
def charge(self, order_id: str, amount: Decimal) -> bool: ...
class PlaceOrder: # driving port + use case
def __init__(self, orders: OrderRepository, payments: PaymentGateway):
self.orders, self.payments = orders, payments
def __call__(self, order_id: str, total: Decimal) -> Order:
if total <= 0:
raise ValueError("empty basket")
order = Order(order_id, total)
order.status = "paid" if self.payments.charge(order_id, total) else "payment-failed"
self.orders.save(order)
return orderclass PostgresOrderRepository: # real driven adapter
def __init__(self, conn):
self.conn = conn
def save(self, order):
with self.conn.cursor() as cur:
cur.execute(
"INSERT INTO orders (id, total, status) VALUES (%s, %s, %s) "
"ON CONFLICT (id) DO UPDATE SET status = EXCLUDED.status",
(order.id, order.total, order.status))
self.conn.commit()
class InMemoryOrderRepository: # test double, same port
def __init__(self):
self.saved = {}
def save(self, order):
self.saved[order.id] = orderclass DecliningPayments:
def charge(self, order_id, amount): return False
def test_failed_payment_is_recorded():
orders = InMemoryOrderRepository()
place = PlaceOrder(orders, DecliningPayments())
place("A-17", Decimal("23.50"))
assert orders.saved["A-17"].status == "payment-failed"
# composition root (main.py): the only place that knows every adapter
# place_order = PlaceOrder(PostgresOrderRepository(conn), CardProviderGateway(key))
# app.add_route("/orders", make_rest_controller(place_order)) # driving adapterBeachten Sie, was der Use Case nicht enthält: kein SQL, keine HTTP-Statuscodes, keine anbieterspezifischen Exception-Typen, keine Framework-Decorators. Der Test läuft in Millisekunden und deckt einen Fall ab (eine abgelehnte Zahlung), der sich gegen die Sandbox eines echten Anbieters nur umständlich herstellen ließe. Der PostgreSQL-Adapter braucht eigene Tests, doch das sind fokussierte Integrationstests einer Klasse gegen eine echte Datenbank, keine Tests von Geschäftsregeln. Das ist der Testbarkeitsnutzen, der in Abschnitt 2.6 beschrieben ist: Steuerbarkeit und Beobachtbarkeit durch injizierte Abhängigkeiten.
Ausgearbeitetes Beispiel
Ausgearbeitetes Beispiel: den Zahlungsanbieter wechseln
Die Essenslieferplattform muss wegen einer Preisänderung innerhalb von sechs Wochen von einem Kartenanbieter zu einem anderen wechseln. Vergleichen Sie den Aufwand in zwei Versionen des Systems: einer, in der der Checkout-Code das SDK des alten Anbieters an 23 Stellen direkt aufruft, und einer, die mit einem getriebenen Port PaymentGateway und einem Adapter gebaut ist.
| Direkte Aufrufe: die Auswirkungen ermitteln | Die Suche findet das SDK im Checkout, in den Erstattungen, den Abonnements und drei Admin-Ansichten. Die eigenen Exception-Typen des SDK werden an 11 Stellen abgefangen, und sein Charge-Objekt ist in einer Datenbankspalte gespeichert. |
|---|---|
| Direkte Aufrufe: die Änderung | Jede Aufrufstelle ändert sich, dazu kommt eine Datenmigration für die gespeicherten Objekte. Jeder Test, der Zahlungen berührte, wurde gegen die Sandbox des alten SDK geschrieben und muss neu geschrieben werden. Schätzung: 5 bis 6 Wochen, mit einer riskanten Umstellung auf einen Schlag. |
| Ports and Adapters: die Auswirkungen ermitteln | Eine Adapterklasse (etwa 200 Zeilen) implementiert PaymentGateway. Der Kern kennt nur charge, refund und den eigenen Typ PaymentResult des Kerns. |
| Ports and Adapters: die Änderung | Einen zweiten Adapter für den neuen Anbieter schreiben; die vorhandenen Contract-Tests des Ports dagegen laufen lassen; den Adapter je Bestellung mit einem Feature-Flag wählen, sodass 1 %, dann 10 %, dann 100 % der Zahlungen den neuen nutzen. Schätzung: 2 Wochen, bei jedem Schritt umkehrbar. |
| Was der Port gekostet hat | Port und Adapter zu Beginn zu entwerfen kostete etwa 3 zusätzliche Tage, und jedes neue Zahlungsfeature braucht eine kleine Änderung am Port wie auch am Adapter. |
Der Wert eines Ports ist proportional zur Wahrscheinlichkeit und zu den Kosten der Änderung, die er isoliert. Zahlungsanbieter, Message Broker und APIs von Drittanbietern ändern sich oft; Ihre eigene zentrale Datenbank-Engine ändert sich vielleicht nie, und sie in aufwendige Ports zu verpacken, ist schwerer zu rechtfertigen.
Der Port muss die Sprache der Domäne sprechen (charge, PaymentResult), nicht die des alten Anbieters. Ein Port, der die API eines einzelnen Anbieters spiegelt, schützt nichts.
Merkmale der Ports-and-Adapters-Familie
| Merkmal | Bewertung | Begründung |
|---|---|---|
| Testbarkeit | Hoch | Geschäftsregeln lassen sich im Speicher testen, schnell und deterministisch, indem man Test Doubles in die getriebenen Ports einsteckt. |
| Änderbarkeit der Infrastruktur | Hoch | Eine Datenbank, einen Zahlungsanbieter oder ein Web-Framework zu ersetzen heißt, einen neuen Adapter zu schreiben; der Kern ändert sich nicht. |
| Änderbarkeit der Geschäftsregeln | Mittel bis hoch | Die Regeln liegen an einer Stelle, frei von Framework-Code, doch ein Feature durchquert weiterhin Port, Use Case und Adapter. |
| Einfachheit | Niedrig bis mittel | Mehr Typen, Schnittstellen und Mapping-Code als in einer schlichten Schichtenanwendung; anfangs schwieriger für Neue im Team. |
| Entwicklungsgeschwindigkeit (früh) | Mittel | Die zusätzliche Struktur bremst die ersten Features einer einfachen CRUD-Anwendung; sie zahlt sich aus, wenn die Domäne wächst. |
| Gesamtkosten | Günstig bis mittel | Keine zusätzliche Infrastruktur; die Kosten sind die Entwicklungszeit für das Schreiben und Pflegen von Ports, Adaptern und Mapping-Code. |
| Performance | Mittel bis hoch | Schnittstellenaufrufe kosten fast nichts; der echte Overhead ist das Objekt-Mapping zwischen Schichten und der Verlust mancher ORM-Annehmlichkeiten. |
| Skalierbarkeit, Deploybarkeit, Fehlertoleranz | Nicht durch diesen Stil bestimmt | Diese hängen vom Deployment-Stil ab (Monolith oder Services), in dem das Hexagon sitzt. |
Kosten, Kritik und wann man sie einsetzt
Der Nutzen ist real, die Kosten sind es auch. Jeder Port ist eine zusätzliche Schnittstelle; jede Grenze braucht unter Umständen Mapping-Code, um etwa zwischen einer ORM-Entität, einem Domänenobjekt und einer API-Antwort umzuwandeln. Für eine Anwendung, die überwiegend Formularfelder in Tabellen schiebt, mit wenig Logik dazwischen, fügt diese Struktur Dateien hinzu, ohne Schutz hinzuzufügen, und die Konventionen eines Frameworks (ein Active-Record-Modell, eine generierte Admin-Oberfläche) liefern womöglich schneller ein besseres Ergebnis. Manche erfahrenen Praktiker argumentieren, dass strenge Varianten der Clean Architecture zu viel Indirektion erzeugen und die meisten Teams ihre Datenbank nie austauschen; andere entgegnen, dass der Hauptnutzen nicht im Austausch von Technologie liegt, sondern in schnellen, verlässlichen Tests und in Geschäftscode, den man lesen kann, ohne das Framework zu kennen. Beide haben in unterschiedlichen Kontexten recht.
Setzen Sie sie ein, wenn die Geschäftslogik umfangreich ist und die aktuellen Frameworks überdauern wird; wenn das System mit externen Diensten integriert ist, die sich wahrscheinlich ändern; wenn schnelle, isolierte Tests der Geschäftsregeln wichtig sind; und als innere Struktur der Module eines modularen Monolithen oder von Services, die mit Domain-Driven Design gebaut sind (Abschnitt 5.7). Seien Sie vorsichtig bei einfachen CRUD-Anwendungen, Prototypen und kurzlebigen Werkzeugen, wo die Standardstruktur des Frameworks meist ausreicht. Ein pragmatischer Mittelweg ist verbreitet: Ports für externe Dienste und alles, was sich wahrscheinlich ändert, und anderswo die Annehmlichkeiten des Frameworks.
Die Abhängigkeitsregel durchsetzen
Wie bei Schichten erodiert die Regel, wenn keine Maschine sie prüft. In Python kann import-linter die Ringe als layers-Vertrag deklarieren (Adapter über Anwendung über Domäne) und forbidden-Verträge hinzufügen, die verhindern, dass die Domänen- und Anwendungspakete Web-Frameworks, Datenbanktreiber oder SDKs importieren. In Java hat ArchUnit eine fertige Regel onionArchitecture(), in der Sie die Pakete für Domänenmodell, Domain Service, Application Service und Adapter benennen, und sie lässt einen Test fehlschlagen, wenn eine Abhängigkeit nach außen zeigt. Den Code in getrennte Build-Module aufzuteilen (ein Domänenmodul ganz ohne Framework-Abhängigkeiten) macht Abhängigkeiten nach außen unkompilierbar, und das ist die stärkste Prüfung von allen. Diese Prüfungen sind Beispiele für die Fitness Functions, die Kapitel 11 behandelt.
Eine Paketstruktur, die die Regel sichtbar macht, hilft beim Review: zum Beispiel orders/domain, orders/application (Use Cases und Ports) und orders/adapters/{rest,postgres,payments}. Zuerst nach Geschäftsbereich und dann nach Ring zu gliedern, hält die Struktur an den Domänengrenzen der Abschnitte 5.3 und 5.7 ausgerichtet.
Prüfen Sie Ihr Verständnis
Ist ein REST-Controller ein treibender oder ein getriebener Adapter? Und ein SQL-Repository?
Ein REST-Controller ist ein treibender Adapter: Er nimmt Anfragen entgegen und ruft die Anwendung auf. Ein SQL-Repository ist ein getriebener Adapter: Es implementiert eine Schnittstelle, die die Anwendung aufruft.
Darf in der Clean Architecture ein Use Case eine ORM-Entität an einen Controller zurückgeben?
Nein. Das ORM gehört zum äußeren Ring, also hat ein Use Case, der es benennt, eine Abhängigkeit nach außen. Er sollte eine einfache Struktur oder ein Domänenobjekt zurückgeben, das in einem inneren Ring definiert ist.
Wo werden Adapter erzeugt und mit der Anwendung verbunden?
In einem Composition Root, meist dem Startcode, der einzigen Stelle, die jeden Adapter kennt.
Was fügt die Onion-Architektur dem hexagonalen Bild hinzu?
Benannte Ringe innerhalb der Anwendung (Domänenmodell, Domain Services, Application Services) und die ausdrückliche Aussage, dass die Datenbank ein externes Anliegen am Rand ist, nicht das Fundament.
Quellen und weiterführende Literatur
Haben Sie eine Frage zu diesem Abschnitt? ArchitectBot, der KI-Lernassistent des Kurses, antwortet zuerst aus dem Kurs.
Fragen: „Bei unserer Django-App sind die Geschäftsregeln über Models, Views und Celery-Tasks verstreut. Wie könnten wir Ports and Adapters schrittweise einführen, ohne alles neu zu schreiben?“