Neulich stolperte ich ArchUnit, ein Tool, mit den man testen kann ob sich Java-Code (oder auch Kotlin-Code) an bestimmte Architekturvorgaben hält. Zum Beispiel kann es in einer Spring-Anwendung überprüfen, das keine Serviceklasse eine Controllerklasse aufruft.
Eine solche Regel klingt überflüssig, warum sollte ich von einem Service einen Controller aufrufen?
Wenn es aber doch passiert, bekommen wir eine zyklische Abhängigkeit, die eine Anwendung schwerer wartbar macht. Mit ArchUnit lässt es sich eine Überprüfung einfach in die Testsuite aufnehmen.
Ein Test auf ob in einer Service-Klasse eine Controller-Klasse aufgerufen wird lieferte in dem konkreten Projekt keine Verletzungen. Nicht überraschend, aber beruhigend.
Man kann mit ArchUnit auch Schichten nach Verzeichnissen definieren, und dann die Abhängigkeiten zwischen der Schichten überprüfen. Dies kann z.B. so aussehen:
layeredArchitecture() .layer("Controller").definedBy("..controller..") .layer("Service").definedBy("..service..") .whereLayer("Controller").mayNotBeAccessedByAnyLayer() .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller")
Hier lieferte der Test dann in unserem Projekt eine Überraschung: Die Service-Schicht hat doch einige Abhängigkeiten zur Controller-Schicht.
Die meisten dieser Abhängigkeiten hatten mit Data-Transfer-Objects (DTOs) zu tun. Die DTOs sind in unserem Projekt als Teil der Controllerschicht definiert worden. In dem Projekt gab es jedoch einige Fälle, wo Service-Methoden als Parameter DTOs annehmen.
Das führt zu der gar nicht so trivialen Frage: in welche Schicht gehören die DTOs und ihre Mapper? Und in welcher Schicht passiert die Umwandlung der DTOs in die Model-Objekte? Im Controller oder im Service?
Wenn wir die DTO’s zur Controller-Schicht dazugehörig betrachten, dann dürfe der Code der Service-Schicht und die DTOs nicht kennen. Allerdings heißt es auch, das der Controller dann die Model-Schicht kennen muss. Man kann nun argumentieren, das dann oberste Schicht auf die untere Schicht zugreift und dies nicht sein sollte.
Grund genug für mich, zum Thema DTO mehr zu recherchieren.
Dabei stieß ich auf den Blog von Mark Seeman, der folgende Frage stellte:
Is Layering Worth the Mapping?
Seine Ausgangsfrage ist: ich möchte in meinen Model und auch in meiner Datenbank ein zusätzliches Feld einfügen um es am Ende auch im User-Interface zu zeigen.
Dann muss ich nicht nur das Model, sondern auch DTO und Mapper ändern. Für klassische Java-Klassen kommen noch jeweils Getter- und Setter-Methoden dazu.
Es wäre dann doch wesentlich einfacher, wartungsfreundlicher und auch weniger fehleranfällig, überall die Entity-Klassen zu nutzen.
Diese Schlussfolgerungen fand ich hier bemerkenswert:
- Wenn ich auf DTOs verzichte, dann habe ich in Konsequenz keine richtigen Schichten mehr.
- Wenn meine Anwendung im wesentlichen Datensätze erzeugt und ediert (CRUD-Operationen), dann brauche ich dafür keine strikten Schichten. Sie bewirken nur eine künstliche Aufteilung einer eigentlich einfachen Aufgabe. Das Model ist schon die richtige Abstraktion für die Anwendung.
- Umgekehrt, wenn ich gerade Operationen ausführen möchte, die über reines Update eines Datensätze hinausgehen, dann ist es oft einfacher diese Operationen als Kommandos zu sehen, und diese Kommandos jeweils mit ihren einen Daten kommen, wie es auch das CQRS-Muster vorschlägt. Statt einem großen DTOs mit vielen Feldern gibt es dann viele kleinere Kommando-Objekte mit vergleichsweise wenigen Feldern. Für die Leseoperationen wird aus dem DTO eigentlich nur noch ein Filter, um nicht alle Felder zurückzuliefern.
DTOs wären demnach weit weniger nützlich als gedacht. Warum werden sie im Java-Umfeld fast überall benutzt? Bei Adam Bien fand ich einen wichtigen Hinweis: In früheren Versionen des J2EE Standards waren DTOs zwingend nötig, weil die normalen Entity-Beans nicht serialisierbar waren. Inzwischen ist eine Generation von J2EE-Entwicklern damit aufgewachsen und gibt an die jüngeren Kollegen diese vermeintliche „Best Practice“ weiter.