Start / Seminare / Domain-Driven Design in der Praxis
Modul
Ausblick und Anti-Patterns
6 Kapitel in diesem Modul-Video · Laufzeit
Transkript
Der gesprochene Text dieses Moduls zum Mitlesen, Überfliegen und Durchsuchen. Ein Klick auf einen Zeitstempel springt an die Stelle im Video.
DDD — Ausblick und Anti-Patterns
0:00 Zum Abschluss treten wir einen Schritt zurück. Die Muster, die wir gebaut haben — Entity, Value Object, Aggregat, Repository, Domain Event —, sind erstaunlich schnell gelernt. Schwer ist etwas anderes: sie nicht mechanisch anzuwenden. In diesem Modul ergänzen wir die letzten Bausteine, die noch fehlen, und schauen uns dann die vier häufigsten Fallen an, in die Teams mit DDD tappen.
0:23 Drei davon sind Entwurfsfehler. Die vierte ist keine Entscheidung, sondern eine Haltung — und sie ist die teuerste, weil man ihr von außen nichts ansieht. Der Code sieht dann perfekt nach DDD aus und trägt trotzdem keine Erkenntnis.
Ausblick und Anti-Patterns
0:37 Sechs Kapitel, und sie zerfallen in zwei Hälften. Die erste ergänzt, was noch fehlt: die Frage, wann ein Objekt gültig sein muss, die Factory für aufwendige Erzeugung und die beiden Arten von Service, die sich einen Namen teilen und auf verschiedenen Seiten der Grenze liegen. Die zweite Hälfte ist der ehrliche Teil — anämische Modelle und fette Entities, die Fallen beim Repository und zum Schluss der mechanische Umgang mit den Mustern.
1:01 Dieses Modul ordnet ein, was in den sieben Modulen davor entstanden ist.
Immer gültig halten
1:06 Fangen wir mit einer Frage an, über die in Teams erstaunlich selten explizit entschieden wird — und deren Antwort dann trotzdem jeden Tag Folgen hat: Wann muss ein Objekt eigentlich gültig sein? Über sie wird in Teams erstaunlich selten explizit entschieden, und trotzdem hat die Antwort jeden Tag Folgen — in jeder Methode, die ein Objekt entgegennimmt.
1:25 Zwei Haltungen stehen sich gegenüber. Entweder ein Objekt ist zu jedem Zeitpunkt gültig — dann wacht es selbst darüber. Oder es darf zwischenzeitlich ungültig sein und wird später geprüft, meist vor dem Speichern. Die zweite Haltung ist verbreitet, weil sie sich gut anfühlt: alle Prüfungen an einer Stelle, übersichtlich, leicht zu finden.
1:45 Trotzdem ist die erste in fast allen Fällen die bessere. Der Grund ist Verlässlichkeit: Wenn jedes Objekt in Ihrer Hand gültig ist, müssen Sie nie fragen, ob dieses hier vielleicht gerade nicht. Und diese Frage stellt man sonst hundertmal am Tag. Die Tabelle ist fair — sie nennt für beide Haltungen einen echten Vorteil und einen echten Preis. Rechts: alle Prüfungen an einer Stelle, aber man muss ans Prüfen denken.
2:10 Links: jedes Objekt in der Hand ist gültig, dafür verteilen sich die Prüfungen im Modell. Und dann kommt die Fußzeile mit der Bewertung, und die ist eindeutig: Der Preis rechts ist der teurere. Denn Vergessen fällt erst beim Speichern auf — wenn überhaupt. Fehler, die spät und weit entfernt von ihrer Ursache auffallen, kosten ein Vielfaches. Verteilte Prüfungen sind lästig; vergessene Prüfungen sind teuer.
2:35 Ein Fach fasst höchstens fünfzig Karten, und diese Regel steht dort, wo das Fach verändert wird. Wer zu viel auffüllen will, scheitert sofort — nicht später, nicht beim Speichern, sondern in dem Moment, in dem der falsche Zustand entstehen würde. Die Fußzeile enthält eine Unterscheidung, die im Alltag oft verwischt: Eine verletzte Invariante ist ein Fehler im Programm, kein Eingabefehler.
2:58 Wenn ein Benutzer eine Zahl zu groß eintippt, ist das keine verletzte Invariante — das gehört an den Rand der Domäne, in die Eingabeprüfung. Wenn Ihr Code es trotzdem versucht, ist etwas kaputt. Der erste Punkt ist der Alltagsgewinn: Wer ein Objekt entgegennimmt, muss es nicht erst prüfen. Sie glauben nicht, wie viel defensiver Code allein aus dem Misstrauen entsteht, dass ein übergebenes Objekt vielleicht unvollständig ist.
3:24 Der zweite Punkt spart Merkarbeit — es gibt keine Reihenfolge zu behalten. Der dritte spart Wiederholung: dieselbe Prüfung nicht an drei Stellen eines Vorgangs. Und der vierte grenzt sauber ab, weil dieser Punkt am häufigsten missverstanden wird: Fachliche Eingabeprüfung bleibt unberührt. Beides existiert nebeneinander — die eine am Rand der Domäne, die andere im Kern.
3:46 Der erste Punkt ist genau der Fehler, den die spätere Prüfung produziert: eine Prüfmethode, deren Aufruf an einer Stelle fehlt. Der zweite ist die Verwechslung von eben — Eingabefehler als verletzte Invariante behandeln, was dazu führt, dass der Benutzer eine technische Fehlermeldung sieht, statt einen Hinweis am Formularfeld.
4:05 Der dritte ist tückisch, weil die Prüfung dann zu spät kommt: Man prüft an einer Stelle, an der das Objekt schon ungültig entstanden ist. Und der vierte ist der gefährlichste Umgang mit Ausnahmen überhaupt — abfangen und weiterarbeiten. Eine verletzte Invariante heißt, dass Ihre Annahmen nicht mehr stimmen. Dann weiterzumachen ist keine Robustheit, sondern Blindflug.
Factories
4:27 Ein Baustein, den wir bisher nur erwähnt haben — und einer, bei dem die wichtigste Aussage lautet: Meistens brauchen Sie ihn nicht. Schauen wir uns an, wann er sich tatsächlich lohnt. Denn die wichtigste Aussage zu diesem Baustein lautet: Meistens brauchen Sie ihn nicht. Umso genauer lohnt der Blick auf die Fälle, in denen er trägt.
4:46 Eine Factory erzeugt ein vollständiges Aggregat. Sie lohnt sich, wenn dafür Angaben aus mehreren Quellen zusammenkommen müssen — dann hat das Erzeugen mit dem späteren Gebrauch der Entity nichts mehr zu tun. Das ist der Kern des Kriteriums: Es geht nicht darum, ob die Erzeugung kompliziert aussieht, sondern ob sie eine eigene Aufgabe geworden ist.
5:06 Wenn ein Konstruktor Daten aus drei Repositories, einer Konfiguration und einer Berechnung zusammenführt, dann trägt die Entity plötzlich Wissen, das mit ihrem eigentlichen Zweck nichts zu tun hat. Dieses Wissen zieht man heraus. Das Bild im ersten Punkt trifft es gut: Ein Motor baut sich nicht selbst, dafür gibt es die Fertigung. Niemand erwartet, dass ein Bauteil sein eigener Hersteller ist.
5:29 Aber — und das ist der dritte Punkt — die meisten Objekte sind keine Motoren. Ist die Erzeugung einfach, genügt der Konstruktor, und eine Factory wäre nur Ballast: eine Klasse mehr, ein Umweg mehr, ohne jeden Gewinn. Und der vierte Punkt zieht die Parallele, die Ihnen die Granularität klärt: Wie Repositories arbeiten Factories auf ganzen Aggregaten, nicht auf Teilen.
5:51 Es gibt keine Factory für ein Fach. Der erste Punkt ist das Muster-Abarbeiten, das im letzten Kapitel dieses Moduls noch ausführlicher vorkommt: für jede Entity eine Factory, weil es zum Muster gehört. Der zweite ist eine Verschiebung mit Folgen — Fachregeln in der Factory unterbringen, die zur Entity gehören. Dann gilt die Regel nur beim Erzeugen und nicht mehr beim Ändern.
6:14 Der dritte ist die leere Hülle: eine Factory, die nur den Konstruktor weiterreicht, also eine Indirektion ohne Inhalt. Und der vierte ist der Grenzbruch, den wir aus Modul 4 kennen — Teile eines Aggregats einzeln erzeugen und dabei die Wurzel umgehen.
Domain Services und Anwendungsservices
6:29 Jetzt zu zwei Bausteinen, die sich einen Namen teilen und deshalb ständig verwechselt werden. Beide heißen Service, beide sind zustandslos — und sie liegen auf verschiedenen Seiten der Grenze. Beide heißen Service, beide sind zustandslos — und ausgerechnet bei der Frage, ob sie Fachlogik tragen dürfen, sind sie genau entgegengesetzt.
6:49 Ein Domain Service trägt Fachwissen, das zu keiner Entity und zu keinem Value Object passt, und liegt innerhalb der Domänenschicht. Ein Anwendungsservice liegt außerhalb und vermittelt nur — das kennen wir aus Modul 3. Das Unglück liegt allein im Namen. Weil beide „Service" heißen, landen sie in Projekten gern im selben Ordner, und ab da weiß niemand mehr, welcher Fachlogik tragen darf und welcher nicht.
7:12 Wenn Sie eine Sache aus diesem Kapitel mitnehmen, dann diese: Die Lage entscheidet über die Erlaubnis, und die Erlaubnis ist bei den beiden genau entgegengesetzt. Vier Zeilen, und die mittleren beiden sind die operativen. Trägt Fachwissen: beim Domain Service ja, beim Anwendungsservice nein. Spricht mit der Außenwelt: genau umgekehrt.
7:33 Damit haben Sie eine Kreuzprüfung — ein Service, der Fachwissen trägt und mit der Außenwelt spricht, ist beides zugleich und damit falsch geschnitten. Das Beispiel für den Domain Service ist gut gewählt: Karten gleichmäßig auf mehrere Automaten verteilen. Und die Fußzeile räumt eine falsche Fährte weg: Zustandslos sind beide.
7:52 Daran erkennen Sie sie also nicht — der Unterschied liegt allein in der Zuständigkeit. Das Bild im zweiten Punkt ist eines der besten dieses Seminars: Ein Fahrzeug wartet sich nicht selbst, dafür gibt es die Werkstatt. Es gibt fachliche Vorgänge, die zur Domäne gehören, aber zu keiner einzelnen Entity — und sie in eine hineinzuzwängen, macht sie zur fetten Entity.
8:14 Bei uns ist es das Verteilen von Karten auf mehrere Automaten: Kein einzelner Automat kann das entscheiden, denn er kennt die anderen nicht. Und der letzte Punkt ist die Abgrenzung, die Sie am häufigsten brauchen werden: Wer nur weiterreicht und nichts fachlich entscheidet, ist kein Domain Service, sondern ein Anwendungsservice.
8:33 Sie ordnen drei fachliche Vorgänge zu — Entity, Domain Service oder Anwendungsservice —, jeden begründet mit der Frage, ob Fachwissen im Spiel ist und zu wem es gehört. Zwei Fragen, in dieser Reihenfolge; die zweite stellt sich nur, wenn die erste mit Ja beantwortet ist. Der Hinweis nennt den lehrreichen Fall: der Vorgang, der mehrere Aggregate gleichzeitig betrifft.
8:53 Der ist deshalb schwierig, weil er nach Domain Service aussieht und manchmal doch ein Anwendungsservice ist — nämlich dann, wenn er die Aggregate nur nacheinander aufruft, ohne selbst etwas zu entscheiden.
Anämisches Modell und fette Entities
9:06 Damit zum ersten der großen Anti-Patterns — beziehungsweise zu zweien, denn sie gehören zusammen. Es sind die beiden Enden desselben Spektrums, und beide entstehen aus nachvollziehbaren Gründen. Beide entstehen aus nachvollziehbaren Gründen, und beide führen dazu, dass die Invarianten am Ende von außen gebrochen werden können.
9:25 Beim anämischen Modell halten Entities nur Daten, alle Logik liegt in Services. Bei fetten Entities ist es umgekehrt: Sie bekommen Aufgaben, die fachlich nicht zu ihnen gehören. Und dann der Satz, der mir an dieser Folie am besten gefällt: Die Mitte zu finden bleibt Arbeit. Es gibt keine Regel, die Ihnen das abnimmt, keine Metrik, kein Werkzeug.
9:46 Es bleibt eine Entscheidung je Vorgang, und sie verlangt, dass Sie die Domäne verstanden haben. Das ist unbequem, aber ehrlich — und es ist der Grund, warum Modellieren eine Fähigkeit ist und keine Technik. Das ist der Kartenautomat, wie er in sehr vielen Projekten aussieht: zwei öffentliche Felder und ein Service daneben, der die Arbeit macht.
10:06 Und jetzt kommt die Beobachtung der Fußzeile, die den Zusammenhang aufdeckt: Die öffentlichen Felder sind kein Versehen — ohne sie ginge die Trennung gar nicht. Wenn die Logik außerhalb liegt, muss sie an die Daten herankommen, also müssen die Felder offen sein. Das eine bedingt das andere. Deshalb ist die Kapselung hier nur noch auf dem Papier vorhanden, und deshalb kann kein Fach mehr eine Invariante halten. Es ist ein Datensatz mit Methoden daneben.
10:33 Vier Zeichen im Code, die Sie in einem Review schnell prüfen können. Alle Felder öffentlich und Logik in Services — anämisch. Eine Entity mit fachfremder Aufgabe, oder eine, die selbst die Datenbank fragt — fett. Und die letzte Zeile ist der gemeinsame Nenner: Invarianten lassen sich von außen brechen. Das passiert bei beiden Extremen, aus verschiedenen Richtungen.
10:55 Die Fußzeile erinnert an unser eigenes Beispiel und ist eine schöne Pointe: Der Waschautomat, der den Saldo der Zentrale bucht, war genau so ein Fall. Wir haben in Modul 7 eine fette Entity gebaut, erkannt und wieder abgebaut. Alle vier Punkte nennen die Begründungen, mit denen diese Fehler tatsächlich entstehen — und die klingen alle vernünftig.
11:16 Fachlogik in Services, weil sie dort leichter zu testen scheint; das stimmt sogar für den ersten Test und rächt sich ab dem zwanzigsten. Felder öffentlich machen, damit ein Service damit arbeiten kann — die Kapselung fällt für eine konkrete Bequemlichkeit. Einer Entity eine Aufgabe geben, weil sie den Zustand gerade zur Hand hat: Nähe verführt, das kennen wir aus Modul 7.
11:38 Und die Isolation aufgeben und aus dem Kern nach außen greifen, weil es sonst umständlich wäre.
Fallen beim Repository
11:44 Das zweite große Anti-Pattern betrifft die Repositories, und es entsteht aus einem legitimen Wunsch: Geschwindigkeit. Der Wunsch ist berechtigt — die übliche Lösung dafür macht Entities kaputt. Der Wunsch dahinter ist berechtigt — niemand sollte das ganze Aggregat laden, um einen Namen anzuzeigen. Nur die übliche Lösung macht Entities kaputt.
12:06 Aus Geschwindigkeitsgründen liegt es nahe, Methoden zu bauen, die eine Entity nur teilweise füllen — nur die Stammdaten, ohne die Fächer, weil die Liste ja nur Namen anzeigen soll. Und der zweite Satz nennt die Folge, die man dabei übersieht: Damit kann das Objekt seine Invarianten nicht mehr halten. Ein Kartenautomat ohne Fächer ist kein gültiger Kartenautomat — er trägt zwar den Namen der Klasse, erfüllt aber ihren Vertrag nicht.
12:30 Sie haben dann ein Objekt, das aussieht wie eine Entity und sich in bestimmten Situationen wie eine verhält und in anderen nicht. Der zweite Punkt ist der praktisch schmerzhafteste: Der Aufrufer sieht dem Objekt nicht an, welche Teile fehlen. Es gibt keine Markierung, keinen Typunterschied — nur Wissen, das irgendwo in einem Methodennamen steckt oder im Kopf des Entwicklers, der es gebaut hat.
12:53 Der dritte Punkt beschreibt die Folge: Der Zugriff auf ein nicht geladenes Feld scheitert irgendwo weit entfernt, meistens in der Oberfläche, meistens beim Kunden. Und der vierte Punkt ist mir wichtig, weil er das Anliegen nicht abtut: Das Ziel bleibt richtig. Wer wenig braucht, soll nicht alles laden. Nur der Weg dorthin ist ein anderer.
13:13 Und das ist der Weg: Der Rückgabetyp sagt, was man bekommt. Oben eine Methode, die einen vollständigen Kartenautomaten liefert — eine echte Entity mit haltenden Invarianten. Unten zwei Methoden, die etwas anderes liefern: eine Übersicht, eine Liste von Kennungen. Das sind keine Aggregate, sondern reine Daten, und sie heißen auch so.
13:34 Niemand kann sie mit einer Entity verwechseln. Die Fußzeile nennt den entscheidenden Gewinn: Der Übersetzer verhindert jetzt, dass jemand auf ein fehlendes Fach zugreift. Aus einer Disziplinfrage wird eine Typfrage — und Typfragen beantwortet der Compiler zuverlässiger als jedes Review. Der erste Punkt ist die Variante, die auf Disziplin der Aufrufer hofft — und Disziplin skaliert nicht über Teams und Jahre.
14:00 Der zweite ist der Zwischenzustand: Felder nachträglich füllen und das Objekt in der Zwischenzeit ungültig lassen, was die Haltung aus Kapitel 1 aufgibt. Der dritte und vierte zeigen, dass man auch übertreiben kann. Für jede Abfrage eine eigene Datenstruktur zu bauen, auch wo die Entity passt, ist unnötiger Aufwand — die Entity ist der Normalfall, die Datenstruktur die Ausnahme.
14:22 Und Anzeigestrukturen mit Fachlogik anzureichern, verlagert Modellwissen an einen Ort, an dem es niemand vermutet.
Der mechanische Umgang mit DDD
14:29 Und jetzt das letzte Anti-Pattern — das wichtigste dieses Seminars. Es ist keine Fehlentscheidung, die man in einer Codestelle zeigen könnte, sondern eine Haltung. Genau deshalb ist es so schwer zu erkennen. Genau deshalb ist es so schwer zu erkennen: Man sieht es keiner einzelnen Codestelle an, sondern nur dem Ganzen — und auch dem erst auf den zweiten Blick.
14:50 Die häufigste Falle ist keine einzelne Fehlentscheidung, sondern eine Haltung: zu jedem neuen Begriff reflexhaft Entity, Repository, Factory und Service anzulegen. Man kennt das aus Projekten, in denen die Ordnerstruktur perfekt aussieht und niemand sagen kann, welche Fachregel wo steht. Und dann kommt der Satz, den ich für den wichtigsten dieses Seminars halte: Der Code ist das Ergebnis des Verstehens, nicht sein Ersatz.
15:15 Die Muster halten fest, was Sie über die Domäne gelernt haben. Haben Sie nichts gelernt, halten sie nichts fest — sie sehen nur so aus, als täten sie es. Der erste Punkt ist die direkte Verletzung von YAGNI: Klassen, die niemand braucht. Der zweite beschreibt das eigentliche Problem — der Entwurf sieht nach DDD aus und trägt keine Erkenntnis über die Domäne.
15:36 Das ist besonders tückisch, weil es nach außen wie Qualität wirkt und in Reviews durchgeht. Der dritte Punkt ist in unserer Zeit besonders aktuell: Wer generiert, überspringt das Nachdenken, für das die Muster gedacht sind. Werkzeuge, die aus einem Datenmodell komplette DDD-Strukturen erzeugen, liefern Ihnen die Form ohne den Inhalt.
15:56 Und der vierte sagt es unmissverständlich: Modellieren heißt, die Domäne zu lernen. Das lässt sich nicht abkürzen. Fünf Schritte für das, was nach dem Seminar kommt — denn ein Modell ist nie fertig. Neue Anforderungen zuerst einer bestehenden Subdomäne zuordnen; passt keine, ist das ein Signal für eine neue. Der dritte Schritt erinnert an Modul 6: Der neue Kontext bekommt eine eigene Sicht auf gemeinsame Begriffe, statt sie mitzubenutzen.
16:23 Schritt vier hält die Context Map aktuell, und Schritt fünf ist die Zurückhaltung, die wir schon kennen — physisch stärker trennen erst, wenn der Code es verlangt. Die Fußzeile bringt die Haltung auf den Punkt: Grenzen zu verschieben ist erlaubt. Sie stillschweigend zu verschieben nicht. Der dritte Punkt ist der beste Selbsttest, den dieses Seminar zu bieten hat: Muster anwenden, ohne die Frage beantworten zu können, die sie lösen.
16:48 Wenn Sie nicht sagen können, welches Problem Ihre Factory löst, brauchen Sie sie nicht. Der zweite Punkt betrifft generierte Entwürfe, die man für ein Modell hält. Und der vierte schließt den Kreis zu Modul 1, wo alles anfing: DDD auf ein Projekt anwenden, dessen Schwierigkeit gar nicht fachlich ist. Nach acht Modulen ist das vielleicht die wichtigste Warnung — der Wert dieses Ansatzes hängt vollständig daran, dass er auf das richtige Problem trifft.
17:14 Die Abschlussaufgabe führt alles zusammen: Klopfen Sie ein bestehendes Domänenmodell gegen die Anti-Patterns dieses Moduls ab. Für jedes muss feststehen, ob es vorliegt — mit einer Codestelle als Beleg oder einer Begründung, warum nicht. Auch das Nicht-Vorliegen zu begründen, gehört dazu; das schützt vor dem flüchtigen Blick.
17:32 Und der Hinweis liefert den besten Einstieg, den ich kenne: Fangen Sie bei der Frage an, welche Klasse in Ihrem Modell die meisten öffentlichen Felder hat. Diese eine Frage bringt Sie in den meisten Codebasen innerhalb von zwei Minuten zum interessantesten Befund.
Was du mitnimmst
17:48 Vier Punkte zum Schluss — und einer davon gilt über dieses Modul hinaus. Entities und Value Objects sind zu jedem Zeitpunkt gültig; Verstöße sind Fehler im Programm, keine Eingabefehler. Domain Service innerhalb der Domäne, Anwendungsservice außerhalb — gleicher Name, entgegengesetzte Erlaubnis. Repositories liefern vollständige Aggregate oder ehrlich benannte Daten, niemals halb gefüllte Entities.
18:12 Und der letzte Satz ist der, den ich Ihnen aus dem ganzen Seminar mitgeben möchte: Die Muster ersetzen das Verstehen der Domäne nicht, sie halten es fest. Wer die Domäne versteht, schreibt am Ende oft weniger Code — das haben wir am Rückgeld des Kartenautomaten gesehen.
Lieber mit Trainer? Dieses Modul ist Teil unserer Team-Schulungen — mit Übungen, Ihrem eigenen Code und Fragen, die ein Video nicht beantwortet. Mehr erfahren →