Zum Inhalt springen

Clean-Architecture-Diagramm: Schichten, Dependency Rule und ein durchgespieltes Beispiel

Robert C. Martins Clean Architecture als Diagramm zum Nachbauen: die vier Schichten, die Dependency Rule und ein durchgespieltes PlaceOrder-Beispiel mit Code und Ordnern.

· Gründer & Entwickler

Veröffentlicht am

Vier UML-Packages nebeneinander: frameworks and drivers, interface adapters, use cases und entities. Das Web framework ruft OrderController auf, der ruft PlaceOrder auf, der Order erzeugt. OrderPresenter und PostgresOrderRepository implementieren Interfaces der Use-Case-Schicht.

Die meisten Entwickler kennen das Bild der Clean Architecture: vier farbige Ringe mit Pfeilen, die zur Mitte zeigen. Deutlich weniger können sagen, was bei einem echten Feature in welchen Ring gehört, in welche Richtung die Imports zeigen sollen oder wo das SQL liegt.

Die Lücke zeigt sich im Code-Review: ein domain-Ordner, der das ORM importiert, ein Use Case, der einen HTTP-Statuscode zurückgibt, fünf Mapper zwischen einem Formular und einer Tabelle.

Dieser Beitrag macht aus Robert C. Martins Kreisen ein Komponentendiagramm für einen Use Case, das Aufgeben einer Bestellung: die Schichten, die Dependency Rule, Code und Ordner, Grenzübergänge, Clean im Vergleich zu Hexagonal und Onion, Fehler, wann es übertrieben ist und wie man es zeichnet.

Die vier Schichten

Martin beschrieb die Clean Architecture 2012 in dem Blogbeitrag The Clean Architecture und baute sie 2017 in seinem Buch Clean Architecture aus. Er stellte sie als Zusammenführung früherer Ideen dar, darunter Hexagonal und Onion Architecture. Von der Mitte nach außen:

Schicht Was sie enthält Im Beispiel Ändert sich, wenn
Entities Unternehmensweite Geschäftsregeln, über mehrere Anwendungen wiederverwendbar Order und die Regel, dass eine Bestellung mindestens eine Position braucht Sich eine Geschäftsregel ändert
Use Cases Anwendungsspezifische Geschäftsregeln, je ein Ablauf PlaceOrder, seine zwei Interfaces, Request- und Response-Modelle Sich der Ablauf ändert
Interface Adapters Controller, Presenter, Gateways und das gesamte SQL OrderController, OrderPresenter, PostgresOrderRepository Sich die API oder das Schema ändert
Frameworks and drivers Werkzeuge plus Glue-Code: Web-Framework, Datenbank, Treiber Das HTTP-Framework, PostgreSQL, pg, main.ts Du ein Werkzeug austauschst

Die Kreise sind schematisch; Martin sagt, dass du vielleicht mehr als vier brauchst. Die Regel, die sie verbindet, ändert sich nie.

Die Dependency Rule

In Martins Worten: „Source-Code-Abhängigkeiten dürfen nur nach innen zeigen.“ Code in einem inneren Kreis nennt nie eine Funktion, Klasse oder Variable, die in einem äußeren deklariert ist, und nutzt auch dessen Datenformate nicht. Für dieses Beispiel heißt das:

  • entities importiert nichts aus den anderen Schichten.
  • use-cases importiert nur entities: kein Web-Framework, kein ORM, kein Treiber.
  • adapters importieren nach innen, dazu die Bibliothek, die sie adaptieren.
  • Nur main.ts sieht alles.

Die Regel betrifft den Source Code, nicht Aufrufe zur Laufzeit: Zur Laufzeit ruft PlaceOrder nach außen, zum Presenter und zur Datenbank. Martin löst das mit dem Dependency-Inversion-Prinzip: Der Use Case ruft ein Interface auf, das ihm gehört, und eine äußere Klasse implementiert es. Der Aufruf geht nach außen; die Abhängigkeit zeigt nach innen.

Eine Abhängigkeit zeigt tatsächlich nach außen: Der Adapter importiert den Datenbanktreiber. Das ist Absicht. Martin legt das gesamte SQL in die Adapter-Schicht, und kein Code weiter innen sollte „irgendetwas über die Datenbank wissen“.

Erzwinge die Regel in der CI mit dem Check onionArchitecture() in ArchUnit auf der JVM, mit dependency-cruiser für TypeScript oder mit dem Layers-Contract von import-linter für Python.

Durchgespieltes Beispiel: eine Bestellung aufgeben

Ein POST /orders-Request nimmt diesen Weg:

  1. Das Web-Framework routet ihn an OrderController.
  2. Der Controller parst den Body in einen PlaceOrderRequest, ein einfaches Objekt der Use-Case-Schicht, erzeugt einen OrderPresenter und ruft PlaceOrder.execute() auf.
  3. PlaceOrder lässt Order.place() die Geschäftsregeln anwenden, speichert über OrderRepository und übergibt eine PlaceOrderResponse an PlaceOrderOutput.
  4. OrderPresenter implementiert dieses Interface und baut ein View-Model: Status 201, einen Location-Header und einen JSON-Body.
  5. Der Controller gibt das View-Model zurück, und das Framework schreibt es.

Die Use-Case-Schicht mit beiden Interfaces, die ihr gehören:

// src/orders/use-cases/place-order.ts
import { Order } from '../entities/order';

export interface OrderRepository {             // implemented by an adapter
  nextId(): Promise<string>;
  save(order: Order): Promise<void>;
}
export interface PlaceOrderOutput {            // implemented by the presenter
  present(response: PlaceOrderResponse): void;
}
export type PlaceOrderRequest = {
  customerId: string;
  lines: { sku: string; quantity: number }[];
};
export type PlaceOrderResponse = { orderId: string; itemCount: number };

export class PlaceOrder {
  constructor(private readonly orders: OrderRepository) {}

  async execute(request: PlaceOrderRequest, output: PlaceOrderOutput): Promise<void> {
    const order = Order.place(await this.orders.nextId(), request.customerId, request.lines);
    await this.orders.save(order);
    output.present({ orderId: order.id, itemCount: order.itemCount() });
  }
}

Sie importiert nur die Entity. Order.place() wirft einen Fehler, wenn eine Bestellung keine Positionen oder eine Menge unter eins hat, und die Entity-Datei importiert nichts. Der Adapter importiert nach innen, dazu den Treiber:

// src/orders/adapters/postgres/postgres-order-repository.ts
import { randomUUID } from 'node:crypto';
import type { Pool } from 'pg';                         // adapters may import the driver
import type { Order } from '../../entities/order';
import type { OrderRepository } from '../../use-cases/place-order';

export class PostgresOrderRepository implements OrderRepository {
  constructor(private readonly pool: Pool) {}

  async nextId(): Promise<string> { return randomUUID(); }

  async save(order: Order): Promise<void> {
    await this.pool.query('INSERT INTO orders (id, customer_id, lines) VALUES ($1, $2, $3)',
      [order.id, order.customerId, JSON.stringify(order.lines)]);
  }
}

Allein die Composition Root kennt jede konkrete Klasse:

// src/main.ts
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
const controller = new OrderController(new PlaceOrder(new PostgresOrderRepository(pool)));

app.post('/orders', async (req, res) => {
  const view = await controller.post(req.body);        // the presenter's view model
  res.status(view.status).set(view.headers).json(view.body);
});

Ein Unit-Test von PlaceOrder braucht nur ein In-Memory-Repository und einen Presenter, der seine Eingabe aufzeichnet: kein Server, keine Datenbank.

Ordnerstruktur

src/
  orders/
    entities/
      order.ts                        Order and its rules
    use-cases/
      place-order.ts                  PlaceOrder, both interfaces, request and response models
    adapters/
      http/
        order-controller.ts
        order-presenter.ts
      postgres/
        postgres-order-repository.ts
  main.ts                             composition root: builds and wires everything

Gruppiere erst nach Feature, dann nach Schicht. Martins Screaming-Architecture-Argument: Die Ordner auf oberster Ebene sollen sagen, was das System tut (orders, payments), nicht welches Framework es nutzt. Simon Browns Kapitel im selben Buch trennt Organisation von Kapselung: Ordner organisieren Code, während Zugriffsregeln oder ein Build-Check ihn durchsetzen.

Grenzen überschreiten

Jeder Grenzübergang trägt einen Typ, der einer Schicht gehört:

Übergang Was wandert Typ gehört Mechanismus
Framework → Controller HTTP-Request Framework Glue-Code in main.ts
Controller → Use Case PlaceOrderRequest Use Cases Direkter Aufruf
Use Case → Presenter PlaceOrderResponse Use Cases Interface PlaceOrderOutput
Use Case → Datenbank Order Entities Interface OrderRepository
Presenter → Framework View-Model Adapters Vom Controller zurückgegeben

Drei Gewohnheiten halten diese Übergänge sauber:

  • Einfache Request- und Response-Modelle. Martin ist deutlich: „Wir wollen nicht schummeln und Entities oder Datenbankzeilen weiterreichen.“ Der Presenter bekommt PlaceOrderResponse, nie Order, sodass das Umbenennen eines Entity-Felds die API unberührt lässt. Repositories sind eine häufige Ausnahme: Viele Codebasen lassen sie Entities akzeptieren, was trotzdem nach innen zeigt. Strengere Teams mappen im Adapter auf einen Persistenz-Record.
  • Ein Interface an jedem Aufruf nach außen, im Besitz der inneren Schicht und benannt nach dem, was der Use Case braucht, nicht nach der Technologie.
  • Eine Composition Root. Mark Seemann definiert die Composition Root als „einen (vorzugsweise) eindeutigen Ort in einer Anwendung, an dem Module zusammengesetzt werden“, nah am Einstiegspunkt. Martin nennt main „das ultimative Detail“. Alles andere erhält Abhängigkeiten über Konstruktoren.

Martins Abbildung enthält vor dem Interactor außerdem ein Interface Use Case Input Port. Mit einem Controller und einer Implementierung ist der direkte Aufruf der Klasse eine gängige Vereinfachung.

Clean vs. Hexagonal vs. Onion Architecture

Hexagonal (Ports and Adapters) Onion Clean
Grundlagentext Alistair Cockburn, Artikel von 2005 Jeffrey Palermo, Blogserie von 2008 Robert C. Martin, Blogbeitrag 2012, Buch 2017
Bild Anwendung im Sechseck, Adapter außen Ringe um das Domänenmodell Vier konzentrische Kreise
Struktur innen Nicht vorgeschrieben Domänenmodell, Domain Services, Application Services Entities, Use Cases
Repository-Implementierung Sekundärer (getriebener) Adapter Infrastructure, äußerer Ring Interface Adapters
Kernregel Code innen darf nicht nach außen lecken; Kommunikation über Ports Jede Kopplung zeigt zur Mitte Source-Code-Abhängigkeiten zeigen nach innen

Alle drei stellen die Geschäftsregeln in die Mitte und lassen die Ränder von ihnen abhängen. Sie unterscheiden sich darin, wie viel Struktur sie innen vorschreiben: Cockburns Originalartikel trennt nur innen von außen, Palermos Onion Architecture benennt Ringe um das Domänenmodell, und die Clean Architecture ergänzt Presenter sowie Request- und Response-Modelle. Das Hexagonal-Architecture-Diagramm geht ein ähnliches Bestellbeispiel in den Begriffen von Ports and Adapters durch.

Häufige Fehler

  • Framework-Typen in Entities. ORM-Decorators oder Serialisierungs-Attribute auf Order koppeln die innerste Schicht an die äußerste. Mappe im Adapter.
  • Ein Mapper für alles. Sechs Kopien derselben Felder für ein Formular, das eine Zeile speichert. Mappe nur dort, wo die Formen sich unterscheiden.
  • Schichten ohne Verhalten. Entities aus Gettern und Settern, das Anemic Domain Model, das Martin Fowler 2003 beschrieb, und Use Cases, die nur save() aufrufen.
  • Interfaces überall. Sie gehören an Grenzen, nicht auf jede Klasse.
  • HTTP im Use Case. Statuscodes gehören in den Presenter.

Wann es übertrieben ist

Palermo schrieb, die Onion Architecture „ist für kleine Websites nicht geeignet“, und Martins Kapitel über partielle Grenzen beginnt mit „Vollwertige Architekturgrenzen sind teuer.“

Situation Besser geeignet
CRUD-Service mit wenigen Regeln Eine dünne Schichten-Anwendung; die wenigen Regeln aus den Controllern heraushalten
Prototyp oder kurzlebiges Tool Keine Schichten
Eine veränderliche Abhängigkeit, etwa ein Zahlungsanbieter Ein Interface und ein Adapter dafür
Langlebiges Produkt mit echten Regeln und mehreren Einstiegspunkten Alle vier Schichten

Dieses Kapitel beschreibt auch günstigere partielle Grenzen; stufe eine hoch, wenn eine zweite Implementierung oder ein Test sie braucht.

Das Clean-Architecture-Diagramm

Vier UML-Packages nebeneinander: frameworks and drivers, interface adapters, use cases und entities. Das Web framework ruft OrderController auf, der ruft PlaceOrder auf, der Order erzeugt. OrderPresenter und PostgresOrderRepository implementieren Interfaces der Use-Case-Schicht.

20 Komponenten · 9 Verbindungen

In Archboard öffnen
Diagramminhalt
  • Frameworks and drivers
  • Interface adapters
  • Use cases
  • Entities
  • Web framework
  • PostgreSQL
  • OrderPresenter
  • OrderController
  • PostgresOrderRepository
  • PlaceOrderOutput
  • PlaceOrder
  • OrderRepository
  • Order
  • POST /orders
  • execute(request)
  • Order.place()
  • reads view model
  • present(response)
  • save(order)
  • implements
  • SQL via pg driver
Ein Use Case, PlaceOrder, durch die vier Schichten der Clean Architecture verfolgt, die äußerste links. Kein Pfeil zeigt nach außen: Presenter und Postgres-Repository implementieren Interfaces der Use-Case-Schicht, der Kontrollfluss geht also nach außen, die Abhängigkeiten nach innen.

So liest du es:

  • Vier UML-Packages, das äußerste links. Nach rechts zu gehen heißt, nach innen zu gehen.
  • Durchgezogene Pfeile sind Aufrufe und Imports. Drei überqueren eine Grenze in Richtung der Entities. Die anderen drei bleiben innerhalb einer Schicht: present(response) und save(order) rufen die zwei Interfaces auf, die der Use-Case-Schicht gehören.
  • Gestrichelte Pfeile mit hohlen Dreiecken bedeuten implementiert. Presenter und Repository zeigen nach innen auf diese Interfaces, während der Kontrollfluss nach außen läuft.
  • Die gepunktete Linie ist SQL über den Treiber, und sie beginnt in einem Adapter.
  • Die Notizen und der Markdown-Block sagen, wo main.ts liegt, was die Entity durchsetzt und was jeder Linienstil bedeutet.

Clean-Architecture-Diagramme klar zeichnen

  • Zeichne einen Use Case, nicht die Ringe. Verfolge ein echtes Feature mit echten Klassennamen.
  • Lege jedes Interface in die Schicht, der es gehört. OrderRepository steht neben PlaceOrder, nicht neben seiner Implementierung.
  • Nutze zwei Pfeilstile: durchgezogen für Aufrufe, gestrichelt mit hohlem Dreieck für implementiert, wie bei der UML-Realisierung. Das UML-Klassendiagramm-Beispiel behandelt die Notation.
  • Gib „nach innen“ eine Richtung auf der Seite. Ein Pfeil zurück ist eine Verletzung oder ein Laufzeitaufruf, der beschriftet werden muss.
  • Beschrifte Pfeile mit der Methode und dem Modell, das die Grenze überquert.
  • Lass weg: DTO-Felder, Framework-Interna und den DI-Container.

Für das System rund um diese Code-Ebene siehe das C4-Modell-Diagramm-Beispiel und den Leitfaden zum Zeichnen von Softwarearchitektur-Diagrammen.

In Archboard nachbauen

  1. Melde dich mit Google oder GitHub an und erstelle ein Diagramm.
  2. Füge vier Paket-Nodes aus dem UML-Pack nebeneinander hinzu: Frameworks and drivers, Interface adapters, Use cases und Entities, mit blauen, grünen, roten und bernsteinfarbenen Akzenten.
  3. Füge in Frameworks and drivers eine Komponente aus dem UML-Pack namens Web framework und einen Postgres-Node aus dem Daten-Pack namens PostgreSQL hinzu.
  4. Füge in Interface adapters zwei Komponente-Nodes hinzu, OrderPresenter und OrderController, sowie eine Klasse namens PostgresOrderRepository.
  5. Füge in Use cases eine Klasse namens PlaceOrder zwischen zwei Schnittstelle-Nodes ein, PlaceOrderOutput und OrderRepository.
  6. Füge in Entities eine Klasse namens Order mit ihren Feldern und Methoden hinzu.
  7. Zeichne sechs durchgezogene beschriftete Kanten für die Aufrufe, zwei gestrichelte Kanten mit Dreieckspfeilspitzen für implementiert und eine gepunktete Kante von PostgresOrderRepository zu PostgreSQL.
  8. Füge einen Markdown-Block aus dem Basis-Pack mit der Regel und den Linienstilen hinzu und Text-Notizen aus dem Basis-Pack für jede Schicht.

Ein Mermaid-Flowchart deiner Schichten wird mit dem Mermaid-zu-editierbares-Diagramm-Tool zum ersten Entwurf. Free erlaubt 15 Diagramm-Nodes pro Diagramm, verfolge dort also einen Use Case pro Diagramm.

Das Beispiel nutzt 14 Diagramm-Nodes und bleibt damit im Free-Plan.

FAQ

Was sind die vier Schichten der Clean Architecture? Entities (unternehmensweite Geschäftsregeln), Use Cases (anwendungsspezifische Geschäftsregeln), Interface Adapters (Controller, Presenter und Gateways) sowie Frameworks and drivers (das Web-Framework, die Datenbank und andere Werkzeuge). Martin nennt die Kreise schematisch: Du brauchst vielleicht mehr als vier.

Was ist die Dependency Rule? Source-Code-Abhängigkeiten zeigen nur nach innen. Code in einer inneren Schicht nennt nie etwas, das in einer äußeren Schicht deklariert ist, sodass Use Cases nichts über Controller, SQL oder das Web-Framework wissen.

Was ist der Unterschied zwischen Clean Architecture und Hexagonal Architecture? Beide halten die Geschäftsregeln in der Mitte und lassen die Adapter von ihnen abhängen. Die Hexagonal Architecture trennt nur das Innere vom Äußeren, über Ports. Die Clean Architecture teilt das Innere zusätzlich in Entities und Use Cases und benennt Presenter sowie Request- und Response-Modelle.

Wohin gehört das Repository-Interface in der Clean Architecture? In die Use-Case-Schicht, neben den Use Case, der es braucht. Die Implementierung mit dem SQL gehört in die Interface-Adapters-Schicht, und die Composition Root reicht sie beim Start hinein.

Ist Clean Architecture für kleine Projekte übertrieben? Oft. Bei CRUD-Services, Prototypen und kurzlebigen Tools kosten die zusätzlichen Interfaces und Mappings mehr, als sie bringen. Ergänze Interfaces für die Abhängigkeiten, bei denen du Änderungen erwartest, und lass die Schichten wachsen, wenn echte Geschäftsregeln auftauchen.

Kostenlos starten — ein Seat, keine Kreditkarte.

Ähnliche Beiträge

← Zurück zum Blog

In einer Minute ein Canvas öffnen

Melden Sie sich mit Google oder GitHub an. Kostenlos für einen Sitzplatz. Upgraden Sie, wenn das Team dazukommt.