13. Aug. 2024
Hexagonale Architektur
Hexagonale Architektur
Die Hexagonale Architektur ist eine Architektur, die Alistair Cockburn im Jahr 2005 in einem Blogartikel vorstellte. Diese Architektur wird auch oft als 'Ports & Adapters' bezeichnet, wie Alistair sie offiziell genannt hat. Die Bezeichnung 'Hexagonale Architektur' hat sich aufgrund ihrer grafischen Darstellung mit Sechsecken jedoch etabliert und wird häufiger verwendet.
Der Legende nach wurde das Sechseck anstelle des herkömmlichen Rechtecks gewählt, um zu verdeutlichen, dass eine Anwendung über mehr als vier Seiten verfügen kann, welche sie mit anderen Systemen oder Adaptern verbindet. In einem Interview erwähnte Alistair jedoch, dass er den bildhaften Namen bevorzugt, während der offizielle Name dieses Musters seine Eigenschaften beschreiben sollte.
Für den Aufbau der Architektur gilt, dass die Hexagonale Architektur keine ausgehenden Abhängigkeiten aufweist. Stattdessen zeigen alle Abhängigkeiten nach innen.
Im Kern der Architektur liegen die Schnittstellen, die Geschäftslogik definieren, um mit der Außenwelt zu interagieren. Diese Schnittstellen werden als ”Ports” bezeichnet. Alle Use Cases der Geschäftslogik sind ausschließlich gemäß den Spezifikationen dieser Ports implementiert. Für die Geschäftslogik spielt es keine Rolle, welche technischen Details sich möglicherweise hinter diesen Ports verbergen.
Außerhalb des Hexagons befinden verschiedene Adapter, die mit der Anwendung interagieren. Adapter ermöglichen es, verschiedene Technologien oder Systeme nahtlos in die Anwendung zu integrieren, ohne die Kernlogik der Anwendung zu beeinträchtigen. Für einen Adapter ist somit ein Port eine Schnittstelle, die vom Adapter implementiert wird.
Bei der Beschreibung des Musters ”Ports und Adapter” wird bewusst angenommen, dass alle Ports und Adapter grundsätzlich ähnlich sind. Bei der Implementierung nach Hexagonaler Architektur treten jedoch zwei Arten von Ports und Adaptern auf, die Alistair als “primär” und “sekundär” bezeichnet hat. Alternativ kann man diese auch ”treibende” (eng. driving) und ”getriebene” (eng. driven) Ports und Adapters nennen.
Dies steht im Zusammenhang mit dem Konzept der ”primären Akteure” und ”sekundären Akteure” aus der Struktur von Anwendungsfällen. Ein ”primärer Akteur” ist ein Akteur, der die Anwendung steuert. Ein ”sekundärer Akteur” ist einer, den die Anwendung steuert, entweder um Antworten zu erhalten oder um einfach Benachrichtigungen zu senden.
Diese Beobachtungen führen dazu, dass wir uns an der Darstellung und Struktur des Systems orientieren und die ”primären Ports” und ”primären Adapter” auf der linken Seite des Hexagons darstellen und die ”sekundären Ports” und ”sekundären Adapter” auf der rechten Seite des Hexagons. Zusätzlich erfordern die sekundären Ports die Anwendung des DIP, während die primären dies nicht tun.
In der hexagonalen Architektur wird bewusst offen gelassen, was sich innerhalb des Application Hexagons befindet. Alistair gab in einem Interview auf die Frage ”What do you see inside the Application? (deutsch: Was sehen Sie innerhalb der Anwendung?)” die Antwort: ”I don’t care – not my business. (deutsch: Das ist mir egal - nicht mein Aufgabenbereich)”. Jedoch werden in der Literatur häufig Darstellungen der Hexagonalen Architektur entweder mit ”Entities” und ”Use Cases” oder mit ”Domain Model”, ”Domain Services” und ”Application Services” innerhalb des Application Hexagons verwendet.
Onion Architektur
Unter dem Begriff ”Onion Architecture” hat Jeffrey Palermo im Jahr 2008 in einer Blog-Serie ein Architekturmuster beschrieben, als Verbesserung der Schichtenarchitektur. Der Hauptgedanke besteht darin, die Kopplung zu kontrollieren.
Die Onion Architektur umfasst viele Schichten, die im Folgenden aufgeführt werden.
Domain Model:
Im Zentrum der Anwendung befindet sich das Domain Model, welches die essenzielle Geschäftslogik beinhaltet. Es repräsentiert die Kombination von Zustand und Verhalten und bildet die grundlegende Struktur. Das Domain Model ist die zentrale Darstellung der Kernlogik und Daten, die für eine spezifische Organisation oder Anwendung wesentlich sind. Es bildet somit die Grundlage für die gesamte Softwarearchitektur und beeinflusst die Struktur und Funktionalität der umgebenden Komponenten.
Um das Domain Model herum gibt es weitere Schichten mit mehr Verhalten. Dazu gehören der Domain Service und der Application Service.
Domain Service:
Früher oder später kann es vorkommen, dass man auf eine Geschäftslogik stößt, die keinem bestimmten Domänenobjekt zugeordnet werden kann oder die für mehrere Objekte von Bedeutung ist. In solchen Fällen bietet die Onion Architektur die Lösung, diese Logik als Domain Service umzusetzen. Ein Domain Service ist eine zustandslose Operation, die eine domänenspezifische Aufgabe erfüllt.
Das Quellcode-Beispiel zeigt, wie die Implementierung eines Use Case als Methode eines Domain Service aussehen kann. Dabei legt der Kunde einen Artikel in den Warenkorb und der verfügbare Lagerbestand des Artikels wird entsprechend verringert. Diese Logik erfordert Informationen aus verschiedenen Quellen wie dem Warenkorb und dem Lager. Dadurch wird sie zu einem geeigneten Kandidaten für die Implementierung als Domain Service.
@Service
public class DomainService {
public void legeArtikelInDenWarenkorb(Warenkorb warenkorb, Artikel artikel, Anzahl anzahl, Lager lager) {
warenkorb.fuegeHinzu(artikel, anzahl);
lager.reduziere(artikel.artikelID(), anzahl);
}
//...
}
Application Service:
Die Schicht des Application Services repräsentiert die Anwendungsfälle und das Verhalten der Anwendung. Die Anwendungsfälle werden als Application Service implementiert, die die Anwendungslogik enthalten. Damit koordinieren sie die Erfüllung eines Anwendungsfalls, indem sie an die Domänen- und Infrastrukturschichten delegieren. Der Application Service arbeitet auf einer abstrakteren Ebene als die Domänenobjekte und bietet mehrere Funktionalitäten an, wobei die Details der Domänenschicht verborgen bleiben. Er beschreibt, was das System tut, jedoch nicht wie es dies tut.
Die Koordination der Abrufung von Domänenobjekten aus einem Datenspeicher, die Delegation von Arbeiten an sie und die Speicherung des aktualisierten Zustands liegt in der Verantwortung des Application Services.
Außerdem übernimmt der Application Service infrastrukturelle und anwendungsspezifische Aufgaben, die erforderlich sind, um die Funktionalität des Domänenmodells zu nutzen.
Die Implementierung von einem Application Service ist in folgendem Quellcode-Beispiel zu sehen. Es handelt sich um ein Beispiel für einen Warenkorb, um einen Artikel in den Warenkorb zu legen. Der Application Service greift auf die Persistenzschicht zu, um den Datenzugriff und die Datenpersistenz zu ermöglichen. Hierbei verwendet er ein Repository, um die erforderlichen Aggregate zu laden. Anschließend ruft er die spezifische Geschäftslogik für den entsprechenden Anwendungsfall auf, indem er die Methode des Domain Services aufruft. Sobald die Geschäftslogik abgeschlossen ist, speichert der Application Service die Aggregate mithilfe des Repositories. Dadurch wird die Domänenlogik von den Details der Datenzugriffstechnologien isoliert, was zu einer besseren Trennung der Verantwortlichkeiten führt.
public class ApplicationService {
private final DomainService domainService;
private final WarenkorbRepository warenkorbRepository;
private final ArtikelRepository artikelRepository;
private final LagerRepository lagerRepository;
//...
@Transactional
public void legeArtikelInDenWarenkorb(UUID artikelId, int anzahl, UUID warenkorbID) {
Warenkorb warenkorb = getWarenkorb(warenkorbID);
Artikel artikel = getArtikel(artikelId);
Lager lager = lagerRepository.findeMit(artikelId);
domainService.legeArtikelInDenWarenkorb( warenkorb, artikel, new Anzahl(anzahl), lager);
warenkorbRepository.speichere(warenkorb);
lagerRepository.speichere(lager);
}
//...
}
Infrastructure, User Interface, Tests:
Die äußerste Schicht der Onion Architektur bilden Infrastructure, User Interface und Tests. Sie ist für die Dinge reserviert, die sich häufig ändern. Diese Dinge sollten daher bewusst vom Anwendungskern isoliert werden. Daher gehören zu der Schicht:
- Frameworks zur Anbindung an Infrastruktur (DB, Message Bus, Umsysteme, usw.)
- User Interface/Web APIsFachliche Tests gegen Funktionalität der Domäne, Tests für den Application Service und die Web APIs
Man könnte hier streiten, ob Tests überhaupt zu dieser Schicht gehören. Betrachtet man die Paketstruktur einer typischen Anwendung, zeigt sich, dass Tests oft eine eigene Struktur und Hierarchie haben, die sich über alle Schichten der Anwendung erstrecken. Dies könnte ein spannender Aspekt sein, über den man nachdenken könnte.
Zusammengefasst ist die äußerste Schicht dafür verantwortlich, mit der externen Welt zu interagieren und Ein- und Ausgabeoperationen zu implementieren. Sie enthält keine Geschäftslogik und auch keine Steuerung des Ablaufes von Anwendungsfällen. Stattdessen konzentriert sich sie auf die technischen Aspekte einer Anwendung. Das bedeutet, dass es in dieser Schicht darum geht, die Anwendung für Benutzer über ein UI bereitzustellen oder für andere Anwendungen über Webdienste, Nachrichtenendpunkte, usw. zugänglich zu machen. Zusätzlich ist diese Schicht für die technische Umsetzung der Speicherung von Informationen der Domänenobjekte und somit der Implementierung der Repositories verantwortlich.
Clean Architektur
Die Clean Architecture wurde 2012 von Robert C. Martin, in seinem Clean Coders Blog geprägt und 2017 ausführlich in seinem Buch beschrieben. Laut Robert war die Clean Architektur ein Versuch, Architekturen wie Hexagonale Architektur von Alistair Cockburn, DCI von James Coplien und Trygve Reenskaug, BCE von Ivar Jacobson zu einer einzigen umsetzbaren Idee zu integrieren und die Ideen und Vorteile zusammenfassen. Zu den Vorteilen kommen wir noch später.
In einem Interview hat Robert erklärt, dass die ursprüngliche Bezeichnung der Architektur "Screaming Architecture" war. Die Idee dahinter ist, dass unsere Architektur die Geschichte der Anwendung oder des Systems erzählen soll – nicht über die genutzten Frameworks.
Nun werfen wir einen Blick darauf, wie die Clean Architektur aufgebaut ist und wie die Komponenten miteinander interagieren:
Entities:
Die Entities befindet sich in der innersten Schicht und umfassen unternehmensweite kritische Geschäftsregeln. Ein Entity kann ein Objekt mit Methoden sein oder als eine Reihe von Datenstrukturen und Funktionen umgesetzt werden. Innerhalb eines Unternehmens können diese Entities von mehreren Anwendungen genutzt werden. In einer einzelnen Anwendung bilden die Entities die grundlegenden Geschäftsobjekte ab.
Die Entities sind am wenigsten wahrscheinlich von externen Änderungen betroffen. Zum Beispiel bleiben sie unbeeinflusst, wenn eine Datenbank ausgetauscht wird.
Use Cases:
Die Schicht der Use Cases enthält anwendungsspezifische Geschäftslogik. Sie kapselt und implementiert alle Anwendungsfälle des Systems. Diese Anwendungsfälle orchestrieren die Aufrufe von den Entities.
Wenn es zu Veränderungen in der Geschäftslogik kommt, ist es wahrscheinlich, dass auch diese Schicht betroffen sein wird. Allerdings haben Änderungen in externen Systemen keinen Einfluss auf diese Schicht.
Interface Adapters:
Die Schicht der Interface Adapters besteht aus Adaptern, die Daten von dem Format, das den Use Cases und Entities am besten passt, in das Format umwandeln, das für externe Komponenten wie die Datenbank oder das UI am besten geeignet ist. Die Daten werden meist durch einfache Datenstrukturen repräsentiert.
Des Weiteren befinden sich in dieser Schicht die folgenden Komponenten:
- die Presenter, Views und Controller einer MVC-Architektur
- Datenbank
Allgemein lässt sich sagen, dass diese Schicht jegliche Adapter beinhaltet, die erforderlich sind, um Daten von einer externen Darstellung, in das interne Format zu konvertieren, das von den Use Cases und Entities verwendet wird.
Frameworks and Drivers:
Die Schicht der Frameworks and Drivers bildet die äußerste Ebene und besteht in der Regel aus den technischen Details, die für die Integration externer Systeme erforderlich sind, wie zum Beispiel Datenbank und Web-Frameworks. In dieser Schicht schreibt man in der Regel nicht viel Code, außer dem sogenannten ”Glue-Code”, der die einzelnen Komponenten verbindet.
In den kommenden Teilen werden wir uns genauer mit den Unterschieden und Gemeinsamkeiten dieser Architekturen befassen.