Onion Architecture
Ringe mit Abhängigkeiten nur nach innen
Die Idee und ihr Nutzen
Eine Versicherung prüft bei jedem Schaden, ob der Vertrag den Schaden abdeckt. Diese Prüfung übernimmt eine fachliche Regel, die Deckungsregel: Sie vergleicht Vertragsbeginn, versichertes Risiko und Deckungsgrenze mit dem Schaden. Die Deckungsregel soll dieselbe Entscheidung treffen, egal ob die Vertragsangaben aus der Vertragsdatenbank, aus Testdaten oder später aus einem anderen Programm kommen. Deshalb teilt das Entwicklungsteam den Programmcode in Bereiche auf, die als ineinanderliegende Ringe gezeichnet werden. Fachliche Regeln wie die Deckungsregel liegen innen, Technik wie Datenbankzugriff und Bildschirmoberfläche liegt außen, und Code darf nur Code aus dem eigenen oder einem weiter innen liegenden Ring aufrufen. Die Deckungsregel enthält deshalb keinen einzigen Datenbankbefehl. Diese Bauweise heißt Onion Architecture, auf Deutsch Zwiebelarchitektur.
Wie funktioniert es?
Für Schaden 4711 soll geprüft werden, ob der Vertrag den Schaden abdeckt. Zuerst übergibt die Schadenanwendung den Fall an den Anwendungsdienst „Deckung prüfen“, der den Ablauf der Prüfung steuert. Die Schadenanwendung ist die Bildschirmoberfläche im äußeren Ring. Der Anwendungsdienst startet die Deckungsregel. Die Deckungsregel braucht zum Vertrag von Schaden 4711 den Vertragsbeginn, das versicherte Risiko und die Deckungsgrenze und ruft dafür VertragRepository.finde(vertragsnummer) auf. Repository heißt Ablage; der Aufruf bedeutet „finde in der Vertragsablage den Vertrag mit dieser Vertragsnummer“. Im laufenden Betrieb nimmt der Datenbankzugriff diesen Aufruf entgegen. Der Datenbankzugriff liest die Angaben aus der Vertragsdatenbank und gibt sie der Deckungsregel genau in der Form zurück, die die Deckungsregel erwartet, zum Beispiel die Deckungsgrenze in Euro. Die Deckungsregel vergleicht die Angaben mit dem Schaden, entscheidet „gedeckt“ und meldet das Ergebnis an den Anwendungsdienst. Der Anwendungsdienst gibt das Ergebnis an die Schadenanwendung weiter, und die Schadenanwendung zeigt „Deckung bestätigt“. Automatische Tests sind Prüfprogramme, die ohne menschliches Zutun laufen. In automatischen Tests nimmt statt des Datenbankzugriffs ein Testspeicher denselben Aufruf entgegen. Der Testspeicher liefert vorbereitete Verträge aus dem Arbeitsspeicher, und die Deckungsregel wird ohne Datenbank in Sekundenbruchteilen geprüft.
Ganz innen liegt das Fachmodell (englisch Domain Model). Es enthält die fachlichen Gegenstände mit ihren Regeln, etwa Schaden, Vertrag und Zahlung. Um das Fachmodell herum liegen die Fachdienste (Domain Services) mit den Regeln, die mehrere fachliche Gegenstände zusammen betreffen. Die Deckungsregel ist ein solcher Fachdienst, weil sie Vertrag und Schaden vergleicht. Um die Fachdienste herum liegen die Anwendungsdienste (Application Services). Ein Anwendungsdienst steuert einen vollständigen Arbeitsvorgang wie „Schaden prüfen und freigeben“. Der Anwendungsdienst ruft die Fachdienste in der richtigen Reihenfolge auf, prüft Berechtigungen und speichert zusammengehörende Änderungen gemeinsam, etwa die Freigabe eines Schadens und die zugehörige Zahlung. Im äußeren Ring liegen die Bildschirmoberfläche, der Datenbankzugriff, die Anbindung fremder Programme und die automatischen Tests. Verwendet ein Programmteil einen anderen Programmteil, spricht man von einer Abhängigkeit. Weil Code nur Code aus dem eigenen oder einem weiter innen liegenden Ring verwenden darf, zeigen alle Abhängigkeiten nach innen, und Fachmodell und Fachdienste kennen weder eine Datenbank noch eine Bildschirmoberfläche.
Die Deckungsregel darf deshalb den Datenbankzugriff nicht direkt verwenden. Stattdessen legt das Team im Ring der Fachdienste fest, welche Angaben die Deckungsregel braucht: Der Aufruf VertragRepository.finde(vertragsnummer) gibt zu einer Vertragsnummer Vertragsbeginn, Risiko und Deckungsgrenze zurück. Eine solche vereinbarte Form eines Aufrufs, also sein Name, die übergebenen Angaben und die Art der Antwort, heißt Schnittstelle. Wie die Antwort entsteht, legt die Schnittstelle nicht fest. Die Deckungsregel ruft die Schnittstelle VertragRepository auf. Ein Programmteil im äußeren Ring bietet die Schnittstelle an und heißt deshalb Umsetzung der Schnittstelle. Im laufenden Betrieb ist das der Datenbankzugriff, der die Datenbankbefehle enthält. Ob die Deckungsregel den Datenbankzugriff oder den Testspeicher als Umsetzung bekommt, wird beim Start der Anwendung festgelegt.
Findet eine Umsetzung den Vertrag nicht oder liefert ihre Quelle veraltete Daten, muss die Umsetzung das ausdrücklich melden, statt leere Angaben zurückzugeben. Später soll eine dritte Umsetzung die Vertragsangaben beim Bestandsdienst abrufen, einem anderen Programm, das alle Verträge verwaltet. Diese dritte Umsetzung muss die Antwort des Bestandsdienstes in genau die Angaben übersetzen, die die Deckungsregel erwartet. Liefert der Bestandsdienst die Deckungsgrenze in Cent, rechnet die dritte Umsetzung den Betrag in Euro um. Nur wenn die Angaben dieselbe fachliche Bedeutung haben wie vorher, bleibt die Deckungsregel unverändert. Meint der Bestandsdienst mit der Deckungsgrenze zum Beispiel die Summe aller Schäden eines Jahres statt der Grenze für einen einzelnen Schaden, muss das Team auch die Deckungsregel ändern.
Onion Architecture: die Deckungsregel fragt nur eine Schnittstelle
Wann ist es sinnvoll und wann nicht?
Die Onion Architecture lohnt sich
- Die Anwendung hat umfangreiche fachliche Regeln und bleibt viele Jahre im Einsatz. Die Regeln entwickeln sich mit den Versicherungsbedingungen weiter, Technik wie Datenbank und Bildschirmoberfläche wechselt in dieser Zeit öfter. Ohne die Ringe stehen Datenbankbefehle mitten in den Regeln, und jeder Wechsel der Datenbank zwingt das Team, auch den Code der Regeln zu ändern. Mit den Ringen tauscht das Team nur den Datenbankzugriff im äußeren Ring aus.
- Das Team will die fachlichen Regeln bei jeder Änderung automatisch testen. Ruft die Deckungsregel die Datenbank direkt auf, enthält der Code der Deckungsregel Datenbankbefehle, Tabellennamen und Verbindungsangaben. Dann lässt sich die Deckungsregel nur prüfen, wenn eine echte Datenbank mit passenden Testverträgen bereitsteht. Jeder Test wartet dann auf die Datenbank, und das Team muss Testverträge in der Datenbank pflegen. Deshalb testet das Team die Deckungsregel seltener, als es sollte. Mit den Ringen bekommt die Deckungsregel im Test den Testspeicher als Umsetzung von
VertragRepositoryund wird ohne Datenbank geprüft. - Die Datenquelle wird voraussichtlich wechseln. Bei der Versicherung ist das der geplante Wechsel zum Bestandsdienst. Ohne die Ringe müssten Entwickler dafür den Code der Deckungsregel ändern, obwohl sich an der fachlichen Regel nichts geändert hat. Jede solche Änderung kann die Deckungsregel versehentlich verfälschen. Mit den Ringen schreibt das Team nur eine neue Umsetzung von
VertragRepositoryim äußeren Ring. - Datenbankbefehle sollen nur in einem Programmteil stehen. Ohne die Ringe stehen sie in vielen Regeln, und eine Änderung an einem Tabellennamen muss in jeder dieser Regeln nachgezogen werden. Mit den Ringen stehen sie nur im Datenbankzugriff, und eine Änderung an Tabellennamen oder Verbindungsangaben kann die Deckungsregel nicht versehentlich verfälschen.
- Fachleute sollen den Code der Deckungsregel lesen können. Ohne die Ringe stehen Datenbankbefehle zwischen den Vergleichen der Deckungsregel, und Fachleute müssen diese Befehle verstehen, um die Regel nachzuvollziehen. Mit den Ringen stehen in der Deckungsregel keine Datenbankbefehle.
- Das Team arbeitet nach Domain Driven Design. Bei dieser Arbeitsweise bilden Fachleute und Entwickler die fachlichen Begriffe gemeinsam im Code ab. Ohne die Ringe stünden diese Begriffe im selben Code wie Tabellennamen und Datenbankbefehle. Mit den Ringen stehen fachliche Begriffe wie Schaden und Vertrag im Fachmodell und Regeln wie die Deckungsregel bei den Fachdiensten, getrennt von der Technik.
Die Onion Architecture ist nicht das Richtige
- Die Anwendung erfasst und speichert im Wesentlichen Daten und zeigt sie an. Sie hat kaum fachliche Regeln, die von der Datenbank getrennt werden müssten. Dann ordnet das Team den Code als Layered Architecture, also in übereinanderliegenden Schichten: Der Code für die fachlichen Abläufe ruft den Datenbankzugriff direkt auf. Das Team spart so Schnittstellen wie
VertragRepositoryund den Code, der Daten zwischen den Ringen umwandelt, etwa eine Zeile der Vertragsdatenbank in einen Vertrag mit Deckungsgrenze in Euro. Die wenigen Regeln hängen dafür von der Datenbank ab. - Die Anwendung wird nur kurz gebraucht. Ein Beispiel ist ein Prototyp, mit dem das Team eine Idee erprobt und den es danach nicht weiterverwendet. In dieser Zeit wechselt die Datenquelle nicht, und automatische Tests der Regeln bei jeder Änderung braucht der Prototyp nicht. Die Regeln im Prototyp rufen die Datenbank deshalb direkt auf. Das Team spart Schnittstellen wie
VertragRepositoryund ihre Umsetzungen, die nur bei einem Wechsel der Datenquelle oder für solche Tests nötig sind. - Das Programm überträgt hauptsächlich Daten zwischen zwei technischen Systemen. Ein Beispiel ist ein Programm, das Dateien in eine Datenbank einliest. Dann gibt es keinen fachlichen Inhalt, der in den inneren Ring gehören würde. Das Team schreibt das Programm ohne Ringe als Ablauf aus Lesen, Umwandeln und Speichern. Das Team spart Schnittstellen und ihre Umsetzungen, die hier keine fachliche Regel von der Technik trennen würden.
- Das Team hat seine Anwendung schon nach der Hexagonal Architecture oder der Clean Architecture gebaut. Beide Bauweisen verfolgen dasselbe Ziel wie die Onion Architecture: Die Technik hängt vom fachlichen Code ab und nicht umgekehrt. Ein Umstieg auf die Onion Architecture brächte deshalb keine bessere Trennung, sondern nur andere Namen und eine andere Einteilung der inneren Bereiche. Das Team bleibt bei seiner Bauweise und muss weder neue Begriffe lernen noch vorhandenen Code umbenennen.
Nachteile und typische Fehler
Es entstehen mehr Schnittstellen und mehr Code, der Daten zwischen den Ringen umwandelt, als bei einer Anwendung, in der die Regeln die Datenbank direkt aufrufen. Welche Regeln zu den Fachdiensten und welche Schritte zu den Anwendungsdiensten gehören, ist nicht immer eindeutig. Ob zum Beispiel die Grenze, bis zu der ein Sachbearbeiter einen Schaden allein freigeben darf, als fachliche Regel zu den Fachdiensten gehört oder als Berechtigung zum Anwendungsdienst, legen Teams unterschiedlich aus. Dass Code nie Code aus einem weiter außen liegenden Ring verwendet, erzwingt die Bauweise nicht von selbst. Das Team braucht dafür ein Prüfwerkzeug, das die erlaubten Abhängigkeiten jedes Mal kontrolliert, wenn das Programm aus dem Code erzeugt wird. Neue Teammitglieder brauchen außerdem Zeit, um zu verstehen, in welchem Teil des Codes eine Schnittstelle umgesetzt ist und wo beim Start die Umsetzung festgelegt wird.
Typische Fehler und woran man sie erkennt:
- Technik im inneren Ring: Ein Entwickler schreibt unter Zeitdruck eine Datenbankabfrage direkt in die Deckungsregel. Die Deckungsregel lässt sich danach nur noch mit Datenbank testen, und mit der Zeit geht die Trennung von Regeln und Technik verloren. Erkennbar ist das an Tests der Deckungsregel, die plötzlich eine Datenbank brauchen, oder an Meldungen des Prüfwerkzeugs über verbotene Abhängigkeiten.
- Falsche Übersetzung: Der Bestandsdienst liefert eine Deckungsgrenze von 10.000 Euro als 1.000.000 Cent. Die Umsetzung für den Bestandsdienst gibt die Zahl 1.000.000 unverändert weiter, und die Deckungsregel hält sie für 1.000.000 Euro. Ein Schaden über 50.000 Euro gilt dann als gedeckt, obwohl er die Deckungsgrenze von 10.000 Euro überschreitet. Erkennbar ist das an Deckungsgrenzen, die hundertmal so hoch erscheinen wie im Vertrag.
- Verschwiegener Fehler: Der Datenbankzugriff findet den Vertrag zu einem Schaden nicht und gibt leere Angaben zurück, statt den Fall zu melden. Die Deckungsregel findet in den leeren Angaben kein versichertes Risiko und entscheidet „nicht gedeckt“. Erkennbar ist das an Ablehnungen mit der Begründung „kein Risiko versichert“ bei Verträgen, die tatsächlich bestehen.
- Regeln im Anwendungsdienst: Ein Entwickler baut einen neuen Vergleich mit der Deckungsgrenze in den Anwendungsdienst ein statt in die Deckungsregel, weil das dort schneller geht. Die Deckungsregel ist dann über zwei Ringe verteilt, und die Tests der Deckungsregel prüfen diesen Vergleich nicht mit. Erkennbar ist das an Anwendungsdiensten, die Beträge und Daten vergleichen, statt nur den Ablauf zu steuern.
- Datenbank in der Schnittstelle:
VertragRepositoryverlangt Tabellennamen oder gibt Datenbankzeilen zurück. Der innere Ring hängt dann doch von der Datenbank ab. Erkennbar ist das, wenn ein Umbau der Vertragsdatenbank eine Änderung an der Schnittstelle erzwingt.
Weiterführend
Verwandte Ansätze
- Repository: beschreibt die Schnittstelle für den Datenzugriff, die bei der Onion Architecture im inneren Ring festgelegt und im äußeren Ring umgesetzt wird.
Werkzeuge und Umsetzungen
Die Onion Architecture ist eine Bauweise für Programmcode und kein fertiges Produkt. Die folgenden Werkzeuge prüfen automatisch, ob der Code die Regeln der Bauweise einhält:
- ArchUnit: prüft Java-Code und enthält eine fertige Regel für die Onion Architecture.
- jMolecules: kennzeichnet im Java-Code, zu welchem Ring ein Baustein gehört. Zusammen mit Prüfwerkzeugen wie ArchUnit lassen sich so die erlaubten Abhängigkeiten kontrollieren.
- NetArchTest: prüft Abhängigkeitsregeln in Code für die Plattform .NET.
Quellen
- Jeffrey Palermo: The Onion Architecture: part 1: Beitrag von 2008, in dem die Bauweise benannt wurde. Er beschreibt das Fachmodell in der Mitte, Schnittstellen im Inneren mit Umsetzungen am Rand und die Regel, dass alle Abhängigkeiten nach innen zeigen.
- ArchUnit: User Guide: Dokumentation eines Prüfwerkzeugs für Java mit einer fertigen Regel für die Onion Architecture, die Fachmodell, Fachdienste, Anwendungsdienste und Anbindungen unterscheidet.