2 Clean Architecture
Was ist Clean Architecture?
Allgemeine Beschreibung der Clean Architecture in eigenen Worten
Hintergrund
Seit Anbeginn der Menschheit wurden Technologien eingesetzt, um Ziele effektiv und effizient zu erreichen. Waffen zum Jagen von Tieren wurden aus Stein und Holz gewonnen und diese immer weiter verbessert. Doch die Entwicklungsfortschritte in den letzten Jahrhundert sind zeitlich gesehen enorm und verdichten sich immer weiter. Nicht zuletzt durch die Synchronisation der Uhren gegen Ende des 19. Jahrhunderts, die Geschwindigkeitsrevolution der Gesellschaft durch neue Transportmittel wie der Eisenbahn sowie die Erfindung des Internets 1969 kommt immer mehr ein GefĂŒhl der Beschleunigung der modernen Geschichte auf.
Besonders in der Informatik sind die Entwicklungen immens und als Entwickler:in ist es teils sehr schwierig, mit den neuen Technologien mitzuhalten und vom Zug des Fortschritts nicht abgehĂ€ngt zu werden. Dies mag zum Teil daran liegen, dass die Kosten zum Erstellen von Prototypen, aber auch zum Ausliefern eines finalen Produkts extrem gering sind, wenn man die Informatik mit anderen Disziplinen vergleicht. Zwar mĂŒssen Entwickler:innen und Serverinfrastruktur bezahlt werden â doch im Vergleich zum Bau eines Hauses, bei dem Hunderttausende Euro allein fĂŒr Baukosten aufkommen können, scheinen die Kosten fĂŒr eine Anwendung in der Informatik vergleichsweise gering.
Um beim Hausbau zu bleiben: als Informatiker:innen genieĂen wir einen weiteren, bedeutenden Vorteil. Wir können kostengĂŒnstig Prototypen entwickeln, die eventuell sogar zu einem ausgereiften Produkt weiterentwickelt werden. Als Architekt sind sicherlich Prototypen auch hilfreich, doch diese werden im Modellformat Ă la DIN-A4-Blatt angefertigt und nicht anhand eines âechtenâ Hauses mit Rohstoffen, die auch beim richtigen Hausbau verwendet werden.
Privilegien und Herausforderungen im IT-Sektor
Ein weiteres Privileg der Informatiker:innen ist die Anpassbarkeit unserer Software. Beim Hausbau muss bereits beim Entwurf einer Wohnung entschieden werden, an welchen Stellen Steckdosen in der Wand angebracht werden sollen, damit die Stromleitungen entsprechend geplant werden können. Im Nachhinein wĂ€re es nur noch mit (gröĂerem) Aufwand möglich, die Position der Steckdosen anzupassen.
In der Software-Branche hingegen können wir â bei geeigneter Architektur â unser Produkt in der Art gestalten, dass es auch noch nachtrĂ€glich einfach anpassbar und erweiterbar ist. In Hinblick auf den vorher angefĂŒhrten, rasenden Technologiewandel ist dies enorm wichtig, da Technologien stĂ€ndig aussterben und neue entstehen. In den seltensten FĂ€llen basiert heutzutage ein Programm nur noch auf einer Technologie. Stattdessen treffen wir eine Entscheidung und wĂ€hlen aus einer Myriade von Optionen die fĂŒr uns am besten geeignetste heraus â zumindest die geeignetste nach unserem Empfinden zu einem bestimmten Zeitpunkt. Ein Jahr spĂ€ter hĂ€tte diese Wahl ganz anders ausfallen können. Im schlimmsten Fall ist bis dahin eine Technologie vom Aussterben bedroht, auf die wir vor einem Jahr gesetzt haben. Wenn unsere Architektur nicht gut genug durchdacht ist, könnte dies das Ende unserer gesamten Lösung bedeuten, da der Aufwand zu groĂ ist, die bisherige Codebasis zu migrieren.
Was ist nun Clean Architecture?
Clean Architecture bezeichnet ein Architektur-Prinzip, das eine nachhaltige Architektur von Programmcode ermöglichen soll. Nachhaltig in diesem Sinne bedeutet, dass nachtrĂ€glich Entscheidungen von Technologiewahlen revidiert werden können, ohne eine Unmenge von Ănderungen im Code nach sich zu ziehen. Dazu wird der Code in Schichten (Layers) aufgeteilt, die jeweils fĂŒr sich stehen und eine ganz bestimmte Aufgabe ĂŒbernehmen (âSeparation of Concernsâ).
Insbesondere beim Zwiebelmodell (Onion Architecture) wird bildlich klar, was Clean Architecture auszeichnet. Im Kern ist der zentrale, langlebige Code angesiedelt, der sich möglichst wenig Ă€ndert und unabhĂ€ngig von den Ă€uĂeren Schichten kompiliert werden kann. Dies wird nur dadurch erzielt, dass der DomĂ€nen-Code in der innersten Schicht keine AbhĂ€ngigkeit zu einer Ă€uĂeren Schicht hat. Stattdessen werden im Kern hĂ€ufig Interfaces definiert, die dann von Ă€uĂeren Schichten implementiert werden. ĂuĂere Schichten dĂŒrfen also AbhĂ€ngigkeiten zu inneren Schichten haben, jedoch dĂŒrfen innere Schichten nicht von Ă€uĂeren abhĂ€ngen. Dies beschreibt die Dependency Rule: AbhĂ€ngigkeitspfeile zeigen immer von auĂen nach innen.
Dieser Aufbau hat einen groĂen Vorteil: der Kern ist â abgesehen von der verwendeten Programmiersprache â komplett unabhĂ€ngig von jeglichen Technologien, das heiĂt zum Beispiel unabhĂ€ngig von einem konkreten UI oder einer konkreten Datenbank. Dies impliziert, dass Ă€uĂere Schichten einfach ausgetauscht werden können, ohne die Implementierung der DomĂ€ne bzw. die Business-Logik zu gefĂ€hrden. Die inneren Schichten sind im besten Fall komplett von der AuĂenwelt abgeschirmt. ĂuĂere Schichten beeinflussen innere Schichten in keinster Weise.
Ein weiterer Vorteil von Clean Architecture ist die UnabhĂ€ngigkeit von âschwerenâ Frameworks, die oftmals eine bestimmte Arbeitsweise verlangen und uns limitieren. Mit Clean Architecture programmieren wir nicht mehr fĂŒr die Frameworks, sondern können sie gezielt eher in der Funktionsweise von Libraries verwenden. DarĂŒber hinaus ist durch âSeparation of Concernsâ und die âDependency Ruleâ eine einfache Testbarkeit unseres Codes gewĂ€hrleistet: Business-Regeln stehen alleine fĂŒr sich und können also auch eigenstĂ€ndig getestet werden, unabhĂ€ngig von UI, Datenbank oder sonstigen externen Komponenten. ĂuĂere Schichten können sich zwar auf innere beziehen, diese AbhĂ€ngigkeiten können aber einfach âgemocktâ werden, sodass auch hier das Testen einfacher fĂ€llt (im Vergleich zu einer Architektur, die den Code nicht in Schichten aufteilt und die âDependency Ruleâ nicht einhĂ€lt).
Clean Architecture - TLDR
Clean-Architecture bezeichnet ein Architektur-Prinzip, bei dem Code Ă€hnlich wie bei einer Zwiebel in Schichten aufgeteilt wird. Technologie-unabhĂ€ngiger Code wird im Kern angesiedelt und weist keinerlei AbhĂ€ngigkeiten zu Ă€uĂeren Schichten auf (âDependency Ruleâ). Durch diese Trennung können Ă€uĂere Schichten mit kurzlebigerem (da technologie-abhĂ€ngigerem) Programmcode ausgewechselt werden, ohne die Business Logik zu gefĂ€hrden. Gerade im schnelllebigen IT-Sektor ist dies von groĂer Bedeutung, um beim Absterben von Technologien mit moderatem Aufwand auf andere umzusteigen zu können. Auf die Zwiebel bezogen: wenn Ă€uĂere Schichten faulen, kann der Kern noch wohlauf sein.
Eine schöne Ăbersicht zu Clean Architecture von Robert C. Martin ist hier zu finden.
Analyse der Dependency Rule
Eine Klasse, die die Dependency Rule einhÀlt und eine Klasse, die die Dependency Rule verletzt; jeweils UML der Klasse und Analyse der AbhÀngigkeiten in beide Richtungen (d.h., von wem hÀngt die Klasse ab und wer hÀngt von der Klasse ab) in Bezug auf die Dependency Rule
In den folgenden UML-Diagrammen werden zur besseren Ăbersichtlichkeit die fĂŒr die Analyse der Dependency Rule unnötigen Details ausgelassen, beispielsweise Methoden von anderen Klassen. AbhĂ€ngigkeiten werden ausschlieĂlich ausgehend von der zentralen Klasse eingezeichnet, die gerade diskutiert wird.
1. Positiv-Beispiel
Die Klasse Game
befindet sich im Application-Layer. GemÀà der Dependency Rule darf sie also nur AbhÀngigkeiten zu inneren Schichten (Domain) haben. Wie im UML-Diagramm zu sehen, hÀngt die Klasse von anderen Klassen in ihrer eigenen Schicht (z.B. von GamePlayer
) als auch von Klassen aus der Domain-Schicht (z.B. von Deck
) ab. Es gibt jedoch keinen AbhÀngigkeitspfeil, der von Game
in eine Klasse aus der Plugin-CLI-Schicht zeigt. Stattdessen wird Game
von der Klasse Dominion
in ihrer main()
-Methode verwendet.
2. Positiv-Beispiel
Die von den Dozenten vorgegeben Projektstruktur mit vorpopulierten pom.xml
-Dateien fĂŒr einzelne Maven-Module schlieĂt ein Verletzen der Dependency Rule aus. Es ist schlichtweg nicht möglich, in einer inneren Schicht eine Klasse einer Ă€uĂeren Schicht zu verwenden, da innere Schichten in dieser Projektstruktur nichts von Ă€uĂeren wissen und dadurch auch Symbole, d.h. zum Beispiel Klassennamen, nicht aufgelöst werden können. Es kommt dann bereits beim Kompilieren zu Fehlern. Statt eines Negativ-Beispiels soll hier deshalb ein weiteres Positiv-Beispiel vorgestellt werden.
Die abstrakte Klasse Move
aus dem DomĂ€nen-Kern ist ausschlieĂlich von Klassen innerhalb ihrer eigenen Schicht abhĂ€ngig (z.B. von der abstrakten Klasse Player
). PlayerMove
im Application-Layer erbt von Move
und ĂŒberschreibt die mit protected
versehenen Methoden. AbhĂ€ngigkeitspfeile zeigen auch in diesem Beispiel stets von auĂen nach innen, niemals von innen nach auĂen.
Analyse der Schichten
Jeweils eine Klasse zu zwei unterschiedlichen Schichten der Clean-Architecture: jeweils UML der Klasse (ggf. auch zusammenspielenden Klassen), Beschreibung der Aufgabe, Einordnung in die Clean-Architecture (mit BegrĂŒndung).
Action
in der Domain-Schicht
Die Klasse Action
innerhalb der Domain stellt eine Art âContainerâ fĂŒr mehrere Anweisungen einer Karte zur VerfĂŒgung. Diese Anweisungen werden von dem Interface Instruction
modelliert, wobei dieses im Wesentlichen nur eine execute(...)
-Methode umfasst. Beim Spielen einer Karte fĂŒhrt der/die Spieler:in wĂ€hrend der Aktionsphase die Anweisungen âvon oben nach untenâ (wie auf der Karte aufgeschrieben) aus. Diese Reihenfolge wird durch eine einfache Liste von Instruction
s innerhalb der Klasse Action
modelliert.
Die Klasse Action
befindet sich in der DomĂ€nen-Schicht, da sie zur DomĂ€ne des Spiels Dominion zĂ€hlt: der Begriff âAnweisungâ (Instruction) wird durchgehend in der Spielanleitung verwendet, auĂerdem wird von âAktionskartenâ geredet. Dementsprechend liegt es nahe, eine Klasse Action
einzufĂŒhren, die die Instruction
s einer Karte zu einer Aktion bĂŒndelt, die dann ausgefĂŒhrt werden kann. Dass eine Aktion aus Anweisungen besteht ist eine Invariante, die fĂŒr jede Aktion berĂŒcksichtigt werden muss, auch dies begrĂŒndet die Position von Action
im Domain-Code. Dass die Anweisungen âvon oben nach untenâ ausgefĂŒhrt werden, ist durch die Reihenfolge der Anweisungen in der Liste implizit gegeben. Die Klasse ÂŽAction wird vom
CardPool` verwendet, um die Karten im Spiel aufzubauen.
CardFormatter
in der Plugin-CLI-Schicht
Der CardFormatter
befindet sich in der Plugin-CLI-Schicht und ist dafĂŒr zustĂ€ndig Karten fĂŒr den/die Benutzer:in ansprechend in der Konsole mithilfe von Unicode-Zeichen darzustellen, zum Beispiel so:
âââââââ KUPFER âââââââ
â â
â đ°đ°đ° â
â đ°1 đ° â
â đ°đ°đ° â
â â
â â
â â
â â
â â
â â
â â
â 0đ° ââ Geld ââââââââ
Um den Body der Karte zu ârendernâ (im Fall der Geldkarte die â1â umgeben von GeldsĂ€cken), nutzt der CardFormatter
eine konkrete Implementierung der abstrakten Klasse CardBodyFormatter
, je nachdem von welchem Typ die Karte bei getFormatted(Card card)
ist. In diesem Beispiel wird demnach der MoneyCardFormatter
zum Einsatz kommen.
Die Plugin-CLI-Schicht wurde als Ort fĂŒr den CardFormatter
gewĂ€hlt, da dieser lediglich fĂŒr die Anzeige von Karten, jedoch fĂŒr keinerlei Anwendungslogik zustĂ€ndig ist. Normalerweise greift diese Schicht nur auf den Adapter zu. Im Rahmen dieses Projekts wurde jedoch auf einen Adapter verzichtet, da dieser zur jetzigen Zeit keinen groĂen Mehrwert liefern und den Code nur unnötig aufblĂ€hen wĂŒrde. Stattdessen ruft der CardFormatter
fĂŒr die Ausgabe direkt Methoden der Karte auf, zum Beispiel card.getName()
oder card.getMoney()
.
Trotz des herausfordernden Codes, Karten mit Unicode in einem Grid anzuordnen und auszugeben, ist die emotionale Bindung an diesen Code sehr gering: der CardFormatter
könnte jederzeit ausgetauscht werden, beispielsweise mit einem ârichtigenâ UI fĂŒr eine Web-Applikation. Die Business Logik und der Kern unserer Anwendung wĂ€re dadurch nicht betroffen; die innersten Schichten bekĂ€men von diesen Ănderungen in der Tat ĂŒberhaupt nichts mit.