Be Software Architecture Wise
Be Wise Apps

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 Untertiteln
Nur 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

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

Hexagonale Architektur: Treibende Adapter links rufen treibende Ports der Anwendung auf; die Anwendung ruft rechts getriebene Ports auf, die getriebene Adapter implementierenTreibende Seite (primär)Getrieb. Seite (sekundär)AnwendungDomäne + Use CasesPlaceOrderTrackOrdertreibende PortsBestell.Zahlungengetrieb. PortsREST-ControllerMobil-App-APITest-HarnessBatch-SkriptPostgreSQL-RepoIn-Memory-RepoKartenanbieterFake-ZahlungenDurchgezogen: Adapter ruft Port auf. Gestrichelt: Adapter implementiert Port. Alle Abhängigkeiten zeigen nach innen.
Treibende Adapter rufen die Anwendung über treibende Ports auf; die Anwendung ruft getriebene Ports auf, die getriebene Adapter implementieren, sodass Datenbank und Zahlungsanbieter austauschbare Plug-ins sind.

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

Clean Architecture: vier konzentrische Ringe, Entitäten im Zentrum, dann Use Cases, Interface Adapters und außen Frameworks und Treiber; Quellcode-Abhängigkeiten zeigen nur nach innenEntitätenUse CasesInterface AdaptersFrameworks und TreiberÄußerer Ring: Web-Framework, ORM,Datenbank, Message Broker, UIInterface Adapters: Controller,Presenter, Repository-ImplementierungenUse Cases: anwendungsspezifischeRegeln, z. B. PlaceOrderEntitäten: unternehmensweite Regeln,z. B. Order, MoneyDie Abhängigkeitsregel: Quellcode-Abhängigkeiten nur nach innen
Jeder Ring darf nur von Ringen in seinem Inneren abhängen; Entitäten hängen von nichts ab, und Frameworks und Treiber sitzen am Rand, wo sie sich ersetzen lassen.

Hexagonale, Onion- und Clean Architecture im Vergleich

AspektHexagonal (Ports and Adapters)OnionClean
Urheber und DatumAlistair Cockburn; 2005 als Artikel veröffentlichtJeffrey Palermo; Blogreihe ab Juli 2008Robert C. Martin; Blogbeitrag August 2012, Buch 2017
BildEin Hexagon mit einem Innen (Anwendung) und einem Außen (Adapter), links treibend, rechts getriebenKonzentrische Ringe: Domänenmodell, Domain Services, Application Services, dann UI, Infrastruktur und TestsKonzentrische Ringe: Entitäten, Use Cases, Interface Adapters, Frameworks und Treiber
Zentrale RegelDie Anwendung spricht mit der Welt nur über Ports; Adapter übersetzenAlle Kopplung verläuft zum Zentrum hin; der Kern definiert Schnittstellen, die der äußere Ring implementiertDie Abhängigkeitsregel: Quellcode-Abhängigkeiten zeigen nur nach innen
Vorgegebene innere StrukturKeine: nur innen gegenüber außenJa: benannte innere Ringe für die DomäneJa: Entitäten und Use Cases, dazu Hinweise zu Presentern und zu Daten, die Grenzen überqueren
HauptschwerpunktSymmetrie: Nutzer, Tests, Skripte und andere Programme können die Anwendung gleichermaßen steuernDas Domänenmodell statt der Datenbank ins Zentrum einer Geschäftsanwendung stellenUnabhängigkeit von Frameworks, UI, Datenbank und externen Instanzen; testbare Geschäftsregeln
Typisches VokabularPort, Adapter, treibend (primär), getrieben (sekundär), AkteurDomänenmodell, Domain Services, Application Services, InfrastrukturEntitä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 order
Im Hexagon: eine Domänenregel, zwei getriebene Ports und der Use Case PlaceOrder. Nichts hier importiert ein Web-Framework, einen Datenbanktreiber oder ein Zahlungs-SDK.
class 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] = order
Außerhalb des Hexagons: ein echter Adapter und ein Test Double für denselben Port. Der Use Case kann sie nicht unterscheiden.
class 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 adapter
Ein Unit-Test treibt den Use Case direkt an, ohne Webserver, Datenbank oder Netzwerk; die Verdrahtung für die Produktion geschieht an einer Stelle beim Start.

Beachten 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 ermittelnDie 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 ÄnderungJede 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 ermittelnEine Adapterklasse (etwa 200 Zeilen) implementiert PaymentGateway. Der Kern kennt nur charge, refund und den eigenen Typ PaymentResult des Kerns.
Ports and Adapters: die ÄnderungEinen 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 hatPort 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

MerkmalBewertungBegründung
TestbarkeitHochGeschäftsregeln lassen sich im Speicher testen, schnell und deterministisch, indem man Test Doubles in die getriebenen Ports einsteckt.
Änderbarkeit der InfrastrukturHochEine Datenbank, einen Zahlungsanbieter oder ein Web-Framework zu ersetzen heißt, einen neuen Adapter zu schreiben; der Kern ändert sich nicht.
Änderbarkeit der GeschäftsregelnMittel bis hochDie Regeln liegen an einer Stelle, frei von Framework-Code, doch ein Feature durchquert weiterhin Port, Use Case und Adapter.
EinfachheitNiedrig bis mittelMehr Typen, Schnittstellen und Mapping-Code als in einer schlichten Schichtenanwendung; anfangs schwieriger für Neue im Team.
Entwicklungsgeschwindigkeit (früh)MittelDie zusätzliche Struktur bremst die ersten Features einer einfachen CRUD-Anwendung; sie zahlt sich aus, wenn die Domäne wächst.
GesamtkostenGünstig bis mittelKeine zusätzliche Infrastruktur; die Kosten sind die Entwicklungszeit für das Schreiben und Pflegen von Ports, Adaptern und Mapping-Code.
PerformanceMittel bis hochSchnittstellenaufrufe kosten fast nichts; der echte Overhead ist das Objekt-Mapping zwischen Schichten und der Verlust mancher ORM-Annehmlichkeiten.
Skalierbarkeit, Deploybarkeit, FehlertoleranzNicht durch diesen Stil bestimmtDiese 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.

Weiter · 5.7Domain-Driven DesignGrenzen in der Fachdomäne finden: Ubiquitous Language, Core-, Supporting- und generische Subdomänen, Bounded Contexts, Beziehungen in der Context Map, Event Storming, Aggregate und eine Context Map der Essenslieferplattform.Weiter →

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?“