Clean Architecture ist ein Software-Design-Muster, das ursprünglich von Robert C. Martin (Uncle Bob) eingeführt wurde. Es basiert auf verschiedenen architektonischen Prinzipien wie der Hexagonalen und der Onion-Architektur. Clean Architecture ermöglicht es uns, mit einer Vielzahl von Problemen umzugehen und Lösungen zu finden, denen wir während der Softwareentwicklung oder des Lebenszyklus einer Anwendung begegnen können. Wir haben diese Probleme in unserem Blogbeitrag Die vielschichtigen Probleme der Softwareentwicklung diskutiert.
In diesem Beitrag werden wir Clean Architecture näher betrachten und ihre Kernprinzipien sowie ihren Einfluss auf unseren Ansatz zur Softwareentwicklung untersuchen. Während wir uns hier auf das Design und die zugrunde liegenden Konzepte von Clean Architecture konzentrieren, haben wir auch einen begleitenden Blogbeitrag, der sich mit ihren problemlösenden Aspekten befasst. Wenn Sie daran interessiert sind zu erfahren, wie Clean Architecture bei der Bewältigung häufiger Herausforderungen in der Softwareentwicklung helfen kann, empfehle ich Ihnen, unseren baldigen Artikel Wie Clean Architecture häufige Herausforderungen in der Softwareentwicklung löst zu lesen. Seien Sie gespannt!
Clean Architecture war auch das Thema eines Vortrags, den ich 2023 auf der code.talks Konferenz gehalten habe. Der Vortrag ist auf YouTube verfügbar; Sie können ihn unter diesem Link finden: https://www.youtube.com/watch?v=v3HkasWQppk
Clean Architecture
Clean Architecture ist ein Software-Design-Ansatz, der darauf abzielt, bessere Softwaresysteme in Bezug auf Wartbarkeit, Testbarkeit und Anpassbarkeit zu schaffen. Basierend auf anderen architektonischen Richtlinien wie der Hexagonalen und der Onion-Architektur strukturiert Clean Architecture Softwaresysteme so, dass die Kerngeschäftslogik von externen Abhängigkeiten isoliert wird.
Dieser Ansatz verkörpert zwei entscheidende Prinzipien:
- Entscheidungen über Technologien und Drittanbieter-Bibliotheken können und sollten bis zu einem späteren Zeitpunkt im Projekt verschoben werden, sodass der anfängliche Fokus auf der Domäne bleibt.
- Die Fähigkeiten externer Bibliotheken sollten die Domäne nicht einschränken. Stattdessen definiert die Domäne die Anforderungen, was dabei hilft, anschließend Bibliotheken zu finden, die diesen Anforderungen entsprechen. Die Verbindung zwischen der Domäne und den Bibliotheken wird dann durch sogenannte Adapter implementiert.
Clean Architecture erreicht diese Ziele durch die Trennung der Domäne von allen anderen Komponenten. Diese Trennung ermöglicht es Entwickler:innen, sich zunächst auf die Kerngeschäftslogik zu konzentrieren und sicherzustellen, dass externe Faktoren oder Implementierungsdetails die wesentliche Funktionalität des Systems nicht beeinträchtigen.
Clean Architecture organisiert Software in verschiedene Schichten, wobei die Kerngeschäftslogik im Mittelpunkt steht und von externen Belangen isoliert ist. Werfen wir einen Blick auf eine einfache Illustration:
Dieses grundlegende Diagramm veranschaulicht das Schlüsselkonzept der Clean Architecture:
- Die innerste
Domain
-Schicht enthält die Kerngeschäftslogik und Entitäten. - Die
Application
-Schicht umgibt sie und implementiert Anwendungsfälle und koordiniert den Datenfluss. - Die äußeren Schichten,
Presentation
undPersistence
, kümmern sich um externe Belange wie Benutzeroberflächen und Datenspeicherung.
Die nach innen zeigenden Pfeile repräsentieren die Abhängigkeitsregel: Äußere Schichten hängen von inneren Schichten ab, aber nicht umgekehrt. Dies stellt sicher, dass die Kerngeschäftslogik unabhängig von externen Technologien und Frameworks bleibt.
In den folgenden Abschnitten gehen wir detaillierter auf Clean Architecture ein. Lassen Sie uns zunächst einen Blick auf eine detailliertere Darstellung werfen. Sie werden feststellen, dass die Kernprinzipien zwar gleich bleiben, das detaillierte Diagramm die Schichten jedoch weiter aufschlüsselt und eine spezifischere Terminologie verwendet:
[Quelle: Robert C. Martin (Uncle Bob) - The Clean Code Blog]
Dieses erweiterte Diagramm baut auf unserer vereinfachten Version auf:
- Die
Entities
in der Mitte entsprechen unsererDomain
Schicht. - Die
Use Cases
entsprechen unsererApplication
Schicht. - Die äußeren Schichten sind weiter in spezifische Komponenten wie Controller, Presenter und Gateways unterteilt, die verschiedene Aspekte der
Presentation
- undPersistence
-Belange behandeln.
Bei der Betrachtung der einzelnen Schichten werden Sie sehen, wie diese detailliertere Struktur die Kernprinzipien der Clean Architecture unterstützt und gleichzeitig einen Rahmen für die Organisation komplexer Softwaresysteme bietet.
Grundprinzipien
Clean Architecture basiert auf mehreren Grundprinzipien, die ihre Struktur und Implementierung bestimmen:
- Domänenzentrierter Ansatz: Im Kern konzentriert sich Clean Architecture auf die Geschäftsdomäne und stellt die wesentliche Geschäftslogik und -regeln in den Mittelpunkt des Systems.
- Isolation der Domänenlogik: Ein wesentliches Merkmal ist die Trennung der Kerngeschäftslogik von externen Belangen wie Benutzeroberflächen, Datenbanken und Integrationen von Drittanbietern.
- Schichtweiser Aufbau: Wie im detaillierten Diagramm oben dargestellt, organisiert die Architektur den Code in verschiedene Schichten, jede mit einem spezifischen Zweck. Diese Schichten umfassen Entities, Use Cases, Interface Adapters und Frameworks & Drivers.
- Abhängigkeitsregel: Abhängigkeiten zeigen nur nach innen, was bedeutet, dass äußere Schichten von inneren Schichten abhängen können, aber nicht umgekehrt. Dadurch wird sichergestellt, dass der Kern unabhängig von externen Änderungen bleibt. Diese Regel wird auch als "Dependency Inversion Rule" bezeichnet.
- Verwendung von Abstraktionen: Die Architektur fördert die Verwendung von Schnittstellen zur Definition von Grenzen zwischen Schichten, was die Flexibilität und Testbarkeit erhöht.
Durch die Befolgung der Prinzipien der Clean Architecture können wir Systeme erstellen, die besser wartbar, testbar und anpassungsfähig sind.
Der domänenzentrierte Ansatz und die Isolation der Kernlogik verbessern die Wartbarkeit durch Zentralisierung der Geschäftsregeln. Der schichtweise Aufbau und die Verwendung von Abstraktionen verbessern die Testbarkeit, indem sie es ermöglichen, Komponenten isoliert zu testen. Die Dependency Inversion Rule und die Verwendung von Schnittstellen tragen zur Anpassungsfähigkeit bei, indem sie die Auswirkungen externer Änderungen auf das Kernsystem reduzieren.
In den folgenden Abschnitten werden wir jede Schicht untersuchen, um diese Prinzipien besser zu verstehen.
Aufschlüsselung der Schichten
Clean Architecture ist in vier Schichten unterteilt:
1. Entities: Diese Schicht repräsentiert die Geschäftsregeln des Unternehmens und somit die Kerngeschäftslogik des Systems und die Geschäftsdaten. Diese Schicht ist die abstrakteste und wird sich am wenigsten wahrscheinlich ändern. Die Entities-Schicht besteht aus Domänenobjekten und wird von allen anderen Belangen in der Anwendung getrennt gehalten. Diese Trennung erleichtert oft die Wartung und Testung des Systems, da viele Änderungen an der Kerngeschäftslogik vorgenommen werden können, ohne andere Teile des Systems zu beeinflussen. Es ist jedoch wichtig zu beachten, dass Änderungen an Schnittstellen, die die Anforderungen der Domänenschicht definieren, dennoch erhebliche Auswirkungen auf andere Schichten haben können. Ein weiterer wesentlicher Vorteil der Trennung der Kerngeschäftslogik und Geschäftsdaten in dieser Schicht ist, dass sie in verschiedenen Systemen wiederverwendet werden können, selbst wenn diese Systeme andere externe Technologien verwenden.
2. Use Cases: Diese Schicht enthält anwendungsspezifische Geschäftsregeln, die die Anforderungen des Systems erfüllen und den Datenfluss zu und von den Entities orchestrieren. Sie enthält die Kernanwendungslogik und beschreibt, wie das System auf verschiedene Anwendungsfälle oder Szenarien reagieren sollte, oft unter Einbeziehung von Interaktionen mit externen Akteuren.
3. Interface Adapters: Diese Schicht enthält Implementierungen von Schnittstellen, die in der Use Case-Schicht definiert sind, und Adapter, die Daten zwischen den Formaten der inneren und äußeren Schichten konvertieren. Sie umfasst Komponenten wie Controller, Presenter und Gateways, die zwischen Use Cases und der externen Umgebung, wie Benutzeroberflächen, Datenbanken und externen Diensten, vermitteln. Diese Komponenten stellen sicher, dass die Use Cases von den Besonderheiten externer Technologien isoliert bleiben.
4. Frameworks & Drivers: Diese äußerste Schicht enthält die externen Frameworks und Tools, mit denen die Anwendung interagiert, wie Datenbanken, Web-Frameworks, Benutzeroberflächen und andere externe Systeme. Sie besteht aus Adaptern und Implementierungen, die diese externen Elemente mit den inneren Schichten verbinden. Die Hauptfunktion dieser Schicht besteht darin, alle Details, wie z. B. die Infrastruktur, zu isolieren und sicherzustellen, dass Änderungen an externen Technologien nur minimale Auswirkungen auf den Kern der Anwendung haben. Sie fungiert als Grenze zwischen der externen Umgebung und den inneren Schichten der Anwendung, wie durch den blauen äußersten Kreis im Clean Architecture-Diagramm dargestellt.
Überschreiten von Grenzen
Wenn Sie das Clean Architecture-Diagramm in diesem Beitrag betrachten, haben Sie möglicherweise die Pfeile bemerkt, die von außen nach innen zeigen. Diese Pfeile repräsentieren den Kontrollfluss in Clean Architecture, der sich von den äußeren Schichten zu den inneren Schichten bewegt. Das bedeutet, dass die Komponenten höherer Ebene (innere Kreise) nicht von Komponenten niedrigerer Ebene (äußere Kreise) abhängen. So wissen beispielsweise die Entities nichts über die Use Cases, aber die Use Cases kennen und verstehen die Entities.
Dies wirft jedoch eine wichtige Frage auf: Wie kann ein Use Case mit einem Presenter oder einer Datenbank interagieren, wenn er nichts über diese Komponenten der äußeren Schicht weiß? Die Antwort liegt in zwei Schlüsselkonzepten:
- Schnittstellen: Eine Schnittstelle definiert eine Vereinbarung oder einen Vertrag zwischen den verschiedenen Schichten unseres Systems. Der Code in einer inneren Schicht kann mit dem Code in einer äußeren Schicht über Schnittstellen interagieren, die von der inneren Schicht definiert werden. Dies ist als Dependency Inversion Principle bekannt, eines der Kernprinzipien von Clean Architecture.
- Dependency Injection: Die Domäne und Use Cases kennen zur Kompilierzeit keine spezifischen Implementierungen, müssen aber zur Laufzeit mit konkreten Implementierungen arbeiten. Hier kommt Dependency Injection ins Spiel.
Um dies zu verstehen, betrachten wir das folgende Codebeispiel:
// Interface defined by the Use Case layer
interface OrderRepository {
save(order: Order): Promise<void>;
}
// Use Case
class CreateOrderUseCase {
constructor(private orderRepository: OrderRepository) {}
async execute(order: Order): Promise<void> {
await this.orderRepository.save(order);
}
}
// Concrete implementation in the Frameworks & Drivers layer
class DatabaseOrderRepository implements OrderRepository {
async save(order: Order): Promise<void> {
// Database-specific code
}
}
// Main entry point of the application
const orderRepository = new DatabaseOrderRepository();
const createOrderUseCase = new CreateOrderUseCase(orderRepository);
In diesem Beispiel hängt der CreateOrderUseCase
von der OrderRepository
-Schnittstelle ab, aber nicht von einer spezifischen Implementierung. Das konkrete DatabaseOrderRepository
wird zur Laufzeit in den Use Case injiziert.
Es ist wichtig zu verstehen, dass es einen Teil der Anwendung geben muss, der alles zusammenfügt. Dieser Teil der Software mag weniger "clean" erscheinen, da er alle Schichten kennen muss, um sie korrekt zu verbinden. Typischerweise geschieht diese Verdrahtung im Haupteinstiegspunkt der Anwendung oder in einer Konfigurationsdatei.
Durch die Verwendung von Schnittstellen und Dependency Injection wird der Grundsatz beibehalten, dass innere Schichten zur Kompilierzeit nicht von äußeren Schichten abhängen, während wir trotzdem die notwendigen Verbindungen zur Laufzeit ermöglichen. Dieser Ansatz ermöglicht die Flexibilität und Testbarkeit, die Clean Architecture anstrebt, und erlaubt uns, Implementierungen einfach zu ändern oder Abhängigkeiten für Tests zu mocken.
Ein Beispiel aus der Praxis
Ein gutes Beispiel für die Vorteile der Clean Architecture ist eine E-Commerce-Anwendung. In einem solchen System ermöglicht Clean Architecture die Isolierung der Kerngeschäftslogik - wie Produktmanagement, Auftragsabwicklung und Kundenmanagement - von externen Systemen wie Payment Gateways und Bestandsverwaltungsdiensten.
Diese Trennung bietet mehrere Vorteile:
- Flexibilität: Entwickler:innen können externe Komponenten, wie ein Payment Gateway, ändern, ohne die Kerngeschäftslogik zu beeinflussen.
- Anpassungsfähigkeit: Das System kann leicht an neue Anforderungen oder Technologien angepasst werden, indem die äußeren Schichten modifiziert werden, ohne den Kern zu beeinträchtigen.
- Testbarkeit: Kerngeschäftsregeln können unabhängig von externen Frameworks oder der Benutzeroberfläche getestet werden.
Betrachten wir ein vereinfachtes Codebeispiel zur Veranschaulichung dieser Prinzipien:
// Entities Layer
class Product {
constructor(public id: string, public name: string, public price: number) {}
}
class Order {
constructor(public id: string, public products: Product[], public total: number) {}
}
// Use Cases Layer
interface OrderRepository {
save(order: Order): Promise<void>;
}
class CreateOrderUseCase {
constructor(private orderRepository: OrderRepository) {}
async execute(products: Product[]): Promise<Order> {
const total = products.reduce((sum, product) => sum + product.price, 0);
const order = new Order(Date.now().toString(), products, total);
await this.orderRepository.save(order);
return order;
}
}
// Interface Adapters Layer
class OrderController {
constructor(private createOrderUseCase: CreateOrderUseCase) {}
async createOrder(productIds: string[]): Promise<Order> {
// In a real application, you would fetch products based on IDs here...
const products = productIds.map(id => new Product(id, `Product ${id}`, 10));
return this.createOrderUseCase.execute(products);
}
}
// Frameworks & Drivers Layer
class DatabaseOrderRepository implements OrderRepository {
async save(order: Order): Promise<void> {
console.log(`Saving order ${order.id} to database`);
// The actual database operation would go here...
}
}
// Usage
const orderRepository = new DatabaseOrderRepository();
const createOrderUseCase = new CreateOrderUseCase(orderRepository);
const orderController = new OrderController(createOrderUseCase);
// Simulated API call
orderController.createOrder(['1', '2', '3']).then(order => {
console.log(`Order created with total: ${order.total}`);
});
Der obige Code zeigt, wie Clean Architecture die Belange in einem E-Commerce-System trennt und mit den in den Diagrammen gezeigten Schichten übereinstimmt:
- Entities Layer: Die Klassen
Product
undOrder
repräsentieren die Kerngeschäftsobjekte und sind Teil der Entities-Schicht. - Use Cases Layer: Die Klasse
CreateOrderUseCase
implementiert die anwendungsspezifischen Geschäftsregeln, die der Use Cases Schicht entsprechen. - Interface Adapters Layer: Die Klasse
OrderController
fungiert als Adapter zwischen den externen Abhängigkeiten und den Use Cases, ähnlich den "Controllers" im Diagramm. - Frameworks & Drivers Layer: Die Klasse
DatabaseOrderRepository
repräsentiert eine Implementierung externer Frameworks oder Tools, die der äußersten Schicht im Diagramm entspricht.
Diese Struktur ermöglicht eine flexible, wartbare und testbare Anwendung:
- Die Kerngeschäftslogik (
Product
,Order
,CreateOrderUseCase
) bleibt von externen Belangen isoliert. - Der
OrderController
passt externe Eingaben an den Use Case an, ohne interne Details preiszugeben. - Das
DatabaseOrderRepository
behandelt Datenbankinteraktionen, ohne die Kernlogik zu beeinflussen.
Die Abhängigkeitsregel wird eingehalten: Äußere Schichten hängen von inneren Schichten ab, nicht umgekehrt. So hängt zum Beispiel CreateOrderUseCase
von der Order
-Entität ab, aber nicht vom DatabaseOrderRepository
.
Diese Trennung ermöglicht es uns, Komponenten einfach zu ändern (z.B. die Datenbankimplementierung), ohne die Kerngeschäftslogik zu beeinflussen, was die Flexibilität und Anpassungsfähigkeit der Clean Architecture demonstriert.
Vorteile der Clean Architecture
Entwickler:innen, die dem Clean Architecture-Muster folgen, können wartbarere und testbarere Systeme erstellen. Wie durch die Kreise in den Diagrammen veranschaulicht, ermöglicht die klare Trennung der Belange modularen und wiederverwendbaren Code, wodurch es einfacher wird, sich an verändernde Anforderungen anzupassen oder neue Technologien zu integrieren. Hier sind einige der wichtigsten Vorteile:
- Verbesserte Wartbarkeit: Änderungen an einer Schicht wirken sich mit geringerer Wahrscheinlichkeit auf andere Schichten aus; dadurch wird es einfacher, das System zu modifizieren, ohne es zu beschädigen. Dies wird durch die Abhängigkeitsregel sichergestellt, die durch die nach innen zeigenden Pfeile in den Diagrammen dargestellt wird.
- Verbesserte Testbarkeit: Die Trennung der Schichten ermöglicht es uns, die Kerngeschäftslogik (Entities und Use Cases) unabhängig zu testen, wodurch es einfacher wird, Unit- und Integrationstests zu schreiben, ohne auf externe Frameworks oder UI angewiesen zu sein.
- Bessere Anpassungsfähigkeit: Die äußeren Schichten (Interface Adapters und Frameworks & Drivers) können modifiziert werden, ohne die Geschäftslogik im Kern zu beeinflussen, wodurch das System anpassungsfähiger für neue Anforderungen oder Technologien wird.
- Wiederverwendbarkeit und Framework-Unabhängigkeit: Die Geschäftslogik in den Entities- und Use Cases-Schichten ist nicht von externen Frameworks abhängig (dargestellt in der äußersten blauen Schicht), was die Wiederverwendung in verschiedenen Systemen und einfachere Aktualisierungen externer Komponenten ermöglicht.
Diese Vorteile werden durch die strukturierte Schichtung und die Abhängigkeitsregel von Clean Architecture erreicht, wie in den Diagrammen dargestellt.
Fazit
Das Clean Architecture-Muster bietet einen hervorragenden Rahmen für die Erstellung von Softwaresystemen, die wartbar, testbar und hochgradig anpassungsfähig für Änderungen sind.
Durch die Befolgung dieses Musters können wir eine solide Grundlage für unsere Anwendungen schaffen und sicherstellen, dass die Kerngeschäftslogik von externen Änderungen und Abhängigkeiten unberührt bleibt. Die Trennung der Belange über die verschiedenen Schichten des Musters, wie durch die Kreise im Diagramm dargestellt, erleichtert sowohl die Wartung als auch das Testen. Sie verbessert auch die Flexibilität des Systems bei der Einführung neuer Technologien oder der Anpassung an sich ändernde Anforderungen.
Angesichts der verschiedenen Herausforderungen, denen wir während der Anwendungsentwicklung und -wartung begegnen können, werden die Stärken der Clean Architecture deutlich. Durch die Befolgung der Clean Architecture-Prinzipien können wir Probleme wie Code-Duplikation, enge Kopplung, Skalierbarkeitsprobleme und Testkomplexität effektiv bewältigen. Dies führt zu Software, die einfach zu betreiben und weiterzuentwickeln ist und widerstandsfähiger gegenüber sich ändernden Anforderungen ist.
Die Anwendung von Clean Architecture bei der Überwindung von Softwareentwicklungsherausforderungen unterstreicht die Bedeutung eines durchdachten Architekturdesigns bei der Erstellung von Softwaresystemen. Es ist entscheidend, Systeme zu entwickeln, die nicht nur kurzfristig funktional sind, sondern auch nachhaltig und anpassungsfähig für die Zukunft sind.
Mehr über die verschiedenen Probleme in der Softwareentwicklung erfahren Sie in unserem Blogbeitrag Die vielschichtigen Probleme der Softwareentwicklung. Unser zukünftiger Beitrag Wie Clean Architecture häufige Herausforderungen in der Softwareentwicklung löst erklärt, wie Clean Architecture uns hilft, diese Probleme zu lösen.
Durch die Implementierung von Clean Architecture, wie in den Schichtendiagrammen und Codebeispielen in diesem Beitrag gezeigt, können wir robuste, flexible und wartbare Softwaresysteme erstellen, die langfristig funktional und anpassungsfähig für sich ändernde Anforderungen sind.