Mehrschichtigkeit ist immer noch tief in den Köpfen verankert. Und wenn nicht in den Köpfen, dann im Code. Übel daran ist nicht nur, dass Mehrschichtigkeit viele Fragen weder beantwortet noch stellt, sondern dass damit Abhängigkeiten geschaffen werden. Code der einen Schicht hängt irgendwie von Code der anderen ab. Die eine Schicht braucht die andere.
Aber welche Schicht sollte welche brauchen? Darüber hat Jens Schauder in einem Blogartikel nachgedacht. Den habe ich gelesen – und dann habe ich gestutzt. Jens erkennt zurecht Abhängigkeiten als Problem – doch dann sieht er eine Lösung nur darin, dass er sie umdreht. Aus dem üblichen
wird bei ihm nur
Die Domäne soll Interfaces definieren, die UI/Persistence dann nutzen bzw. implementieren [1].
Das empfinde ich als keinen großen Schritt nach vorne. Klar, irgendwie wird jetzt die Domäne etwas mehr betont. Das ist nicht schlecht. Aber das grundsätzliche Problem der Abhängigkeiten wird nicht angegangen.
Was ist dieses grundsätzliche Problem der Abhängigkeiten? Naja, dass das Abhängige das Unabhängige braucht, um etwas leisten zu können. Wer vom Alkohol abhängig ist, der ist nicht arbeitsfähig ohne. Und wo ein UI abhängig ist von der Domain, da kann das UI nichts tun, solange es keine Domain-Schicht in der Hand hat.
Auch das hochgelobte Prinzip IoC macht da keinen großen Unterschied. Aus statischen Abhängigkeiten werden damit nur dynamische. Zur Laufzeit – also auch bei Tests – muss die Abhängigkeit erfüllt werden. Attrappen sind insofern weniger Lösung als Problem, dito DI-Container. Beide kaschieren die fundamental starre Verbindung, die Abhängigkeit selbst.
Ich sehe dafür auch keine Entsprechung in der sonstigen Technik. Wenn ich UI+Domain+Persistence mal zusammen als Application bezeichne, dann könnte die Analogie doch ein Motor sein, oder?
Eine Application besteht aus Teilen, ein Motor auch. Nur im Zusammenspiel aller Teile ergibt sich die gewünschte Funktion des Ganzen.
Jetzt aber der Unterschied, der entscheidende:
In der Application braucht und kennt UI die Domain, und bei Jens braucht und kennt Persistence ebenfalls Domain. UI wie Persistence sind unvollständig ohne Domain. Oder allgemeiner: das Abhängige ist unvollständig ohne das Unabhängige.
Im Motor hingegen… da weiß eine Zündkerze nichts vom Zylinder. Ein Zylinder weiß nichts von einer Zündkerze. Die Kurbelwelle weiß nichts von Kolben und umgekehrt.
In der materiellen Technik gibt es keine Abhängigkeiten wie in der Software.
In der materiellen Technik gibt es nur Teile, die für sich genommen eine Form und eine Funktionsweise haben. Sie folgen dem, was ich mal
Das Prinzip der gegenseitigen Nichtbeachtung
(principle of mutual oblivion (PoMO))
nennen will.
Eine Zündkerze beachtet kein anderes Teil. Ein Kolben, eine Kurbelwelle, ein Keilriemen auch nicht. Alle sind für sich geformt und ausgeprägt.
Dass man sie zusammenfassen kann zu etwas Größerem: klar. Dieses Größere definiert einen Kontext und fordert, dass die Teile zusammenpassen. Es ist die Existenzberechtigung für die Teile. Ohne das Größere müssten wir gar nicht drüber reden.
Aber das große Ganze kann eben in unterschiedlicher Weise seine Teile formen. Es kann das wie beim Motor tun. Das bedeutet, die Teile beachten einander nicht – sondern dienen dem Ganzen, das sie integriert.
Oder das Ganze kann es tun wie in der Software üblich. Das bedeutet, es macht die Teile abhängig von einander.
Konkret steckt diese Abhängigkeit in zwei unheiligen Verbindungen:
- Es wird Input mit Output verbunden: Request/Response ist der vorherrschende Kommunikationsstil zwischen den Softwareteilen.
- Es werden mehrere Funktionalitäten im selben Interfaces zusammengefasst.
Die erste unheilige Verbindung macht den Übergang von synchroner zu asynchroner und damit zu verteilter Kommunikation schwierig. Außerdem führt sie zu einem unbeschränktem Wachstum von Methoden.
Die zweite unheilige Verbindung macht die Umkonfiguration von Funktionalität schwierig – trotz allem ISP- und IoC-Aufwand, den man treiben mag.
Beim PoMO hingegen ist das alles kein Thema mehr. Denn beim PoMO gibt es keine Abhängigkeiten mehr zwischen “Kooperationspartnern”. Funktionseinheiten kennen einander nicht mehr. “Dienstleister” UI kennt keinen “Dienstleiste” Domain oder umgekehrt, dito “Dienstleister” Persistence.
Wenn die Anwendungsarchitektur dem PoMO folgt, dann sieht sie zunächst so aus:
Welch Wonne! Keine Abhängigkeiten, kein Testattrappendrama!
Aber natürlich sind die “Schichten” jetzt gänzlich unverbunden. Da entsteht noch keine Gesamtfunktionalität im Sinne der Application.
Dafür tut nun die Application etwas. Und das tut sie ganz offiziell. In der Abhängigkeitsarchitektur war das nicht so offensichtlich. Aber bei PoMO geht es nun nicht mehr anders. Insofern führt PoMO zu einer fundamentalen Separation of Concerns:
UI, Domain und Persistence gehören zum Concern (Aspekt) “Operation”. Und die Application vertritt den Aspekt “Integration”. Die Application verbindet die Teile so, dass sie zusammen etwas sinniges tun. Diese Verbindung könnte so aussehen:
Grün integriert, schwarz operiert. Teile wissen nichts davon, ob und wie sie verbunden sind. Und die integrierende Funktionalität weiß nichts davon, wie die Teile funktionieren. Sie ist ausschließlich von deren Form abhängig.
Und genau das macht den Unterschied aus zwischen einer PoMO-Architektur und dem Üblichen: Bei den üblichen Abhängigkeiten ist der Abhängige nicht nur von der Form des Unabhängigen abhängig, also dass zum Beispiel B eine Funktion mit einer Signatur wie Func<string,int> implementiert. Nein, A ist auch vom Namen der Funktion abhängig und damit von ihrer Semantik, dem, was sie leistet.
Das klingt ganz natürlich. Aber es ist eine Last. Das ist eine doppelte Abhängigkeit: eine von Form und Inhalt. Und dann obendrein noch die Abhängigkeit davon, dass diese Funktion auf einem bestimmten Interface implementiert ist und auch noch, dass sie ein Ergebnis zurückliefert. Puh… das sind ganz schön viele Abhängigkeiten, oder?
Bei PoMO jedoch, da kennt ein A kein B. Und es gibt keine Rückgabewerte; Daten fließen unidirektional. Und ob Methoden von der einen oder einer anderen Funktionseinheit angeboten werden, ist der integrierenden Funktionalität egal. Sie bringt “Angebote” einer Funktionseinheit mit “Nachfragen” anderer zusammen. Das ist alles.
Integration interessiert sich somit nur für Semantik. Das muss sie ja auch, denn sie definiert ein höheres Abstraktionsniveau.
PoMO löst damit mehrere Probleme von Abhängigkeiten. Manche dadurch, dass Abhängigkeiten abgeschafft werden. Manche dadurch, dass Abhängigkeiten fokussiert werden im Sinne von SoC und SRP.
Das scheint mir the way to go, um mal raus aus dem Mehrschichtigkeitssumpf zu kommen.
Fußnoten
[1] Am Anfang seines Artikels stellt Jens die Frage, was denn Teams überhaupt mit einem Pfeil “–>” in einem Diagramm meinen. Ob da immer Klarheit herrsche? Das kenne ich auch: Es wird bildlich entworfen, Kästchen, Kreise, Pfeile, Linien werden gemalt… Aber was bedeuten die? Zwei Entwickler, drei Meinungen ;-)
Für mich gibt es zwei zentrale Symbole für Beziehungen zwischen Funktionseinheiten:
- Pfeil, A –> B
- Lolli, A –* B
Der Lolli bezeichnet eine Abhängigkeitsbeziehung: A ist abhängig von B, d.h. A braucht B in irgendeiner Weise. A hat zur Laufzeit ein B in der Hand.
In den meisten UML-Diagrammen und auch bei Jens steht für diese Art der Beziehung allerdings der Pfeil. Der ist mir dafür aber verschwendet, weil bei ihm gar nicht recht klar wird, was die Pfeilspitze aussagt. Ein Pfeil ist anders als eine Linie asymmetrisch. Das ist schon passend zur Abhängigkeit. Abhängiges und Unabhängiges haben unterschiedliche Rollen. Doch die Pfeilspitze trägt für mich dabei zuviel Gewicht. Sie ist verschwendet bei einer simplen Asymmetrie.
Bei einem Datenfluss hingegen kommt die Pfeilspitze viel besser zum Einsatz. Da zeigt sie deutlich die Flussrichtung. Das entspricht ihrer üblichen Bedeutung von “Bewegungsrichtung”. In Diagramme stehen Pfeile für mich also dort, wo Daten fließen, z.B. von Funktionseinheit A zu Funktionseinheit B.
Solcher Datenfluss ist auch asymmetrisch; Datenproduzent und Datenkonsument haben unterschiedliche Rollen. Und Datenfluss ist unidirektional.