Start / Seminare / Domain-Driven Design in der Praxis
Modul
UI- und Persistenzschicht
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 — UI- und Persistenzschicht
0:00 Bisher hatten wir es leicht. Ein Domänenmodell ohne Oberfläche und ohne Datenbank lässt sich beliebig sauber halten — es muss ja mit niemandem verhandeln. In diesem Modul berührt der Kern zum ersten Mal die Außenwelt, und damit stellt sich die unbequeme Frage: Wie viel Zugeständnis verträgt ein Modell, bevor es kein Modell mehr ist?
0:19 Wir schauen uns an, wer zwischen Bildschirm und Domäne vermittelt, wie eine Datenbank zu einem Modell entsteht — und nicht umgekehrt —, was ein ORM mitbringt und was es dafür verlangt. Am Ende steht eine ehrliche Bilanz: Ganz ohne Kompromiss geht es nicht.
UI- und Persistenzschicht
0:34 Die sechs Kapitel dieses Moduls arbeiten sich von außen nach innen vor. Zuerst die Anwendungsservices, die zwischen Oberfläche und Kern vermitteln. Dann der Datenbankentwurf, bei dem eine überraschende Regel gilt: Value Objects bekommen keine eigene Tabelle. Danach das ORM mit seinen zwei zentralen Bausteinen, die Frage, wer eigentlich die Identifikatoren vergibt, und wo die Abbildung zwischen Klasse und Tabelle steht.
0:58 Zum Schluss die Rechnung: Was kostet uns das Werkzeug — und warum der Handel trotzdem aufgeht.
Anwendungsservices und die Oberfläche
1:05 Fangen wir ganz außen an. Zwischen dem, was der Nutzer sieht, und dem, was fachlich gilt, liegt ein Ring, der in Projekten oft unterschätzt wird — und der genau deshalb zur Müllhalde für alles Übriggebliebene wird. Denn er trägt eine sehr klare Zuständigkeit und wird trotzdem regelmäßig überladen — meist mit genau der Fachlogik, die woanders hingehört.
1:25 Anwendungsservices liegen zwischen Oberfläche und Domänenmodell. Ihre Aufgabe lässt sich mit einem Empfangstresen vergleichen: Sie nehmen entgegen, prüfen die Formalien, leiten an die richtige Stelle weiter und geben das Ergebnis in verständlicher Form zurück. Was sie ausdrücklich nicht tun, steht im letzten Halbsatz — Fachlogik enthalten sie nicht. Der Empfang entscheidet nicht über Ihren Antrag, er organisiert nur den Weg dorthin.
1:48 Diese Trennung klingt trivial und ist im Alltag die am häufigsten verletzte Regel des ganzen Ansatzes, weil der Anwendungsservice immer der bequemste Ort für einen schnellen Zusatz ist. Diese Tabelle hat einen befreienden Effekt: Sie zeigt, dass die Architekturdiskussion und die Frameworkdiskussion zwei verschiedene sind.
2:07 Ob Sie eine Desktop-Anwendung mit ViewModels bauen oder eine Webanwendung mit Controllern — es ist derselbe Ring, nur eine andere Bauform. Und die unterste Zeile ist die eigentliche Pointe: Das Domänenmodell ist in beiden Spalten identisch. Entity und Value Object sehen gleich aus, egal was oben drauf sitzt. Genau das ist der Ertrag der Isolation.
2:27 Der Ring entscheidet über die Zuständigkeit, nicht das Framework — und wenn Ihr Team über Zuständigkeiten streitet, ist das oft die Frage, die weiterhilft. Hier haben Sie eine Faustregel, die in fast allen Zweifelsfällen trägt. Fragen Sie sich: Ergibt diese Logik nur mit Blick auf den Bildschirm Sinn? Dann gehört sie in den Service.
2:48 Gilt sie unabhängig von jeder Anzeige? Dann in den Kern. Die beiden Beispiele machen es greifbar. Wie ein Betrag dargestellt wird — mit Eurozeichen, mit Tausenderpunkt, in welcher Sprache — hängt vollständig vom Bildschirm ab und ist Sache des Anwendungsservice. Dass ein Betrag nie negativ wird, gilt auch dann, wenn ihn nie jemand ansieht. Diese Regel gehört ins Value Object, und zwar in seinen Konstruktor.
3:13 Der erste Punkt ist ehrlich formuliert: Fachregeln landen im Anwendungsservice, weil es dort schneller geht. Nicht aus Unwissen — aus Termindruck. Deshalb hilft gegen diesen Fehler kein Vortrag, sondern nur ein Code-Review. Der zweite Punkt beschreibt eine Bequemlichkeit mit Spätfolgen: Entities unverändert an die Oberfläche reichen und sie dort zurechtbiegen — plötzlich hängt Ihr Kern an einer Anzeigelogik.
3:36 Der dritte ist die schleichende Variante des ersten: Anwendungsservices als Sammelstelle für alles, was nirgends sonst passt. Und der vierte spart einen Ring ein und damit genau die Trennung, die diesen Aufwand rechtfertigt.
Die Datenbank zum Modell entwerfen
3:49 Jetzt zur anderen Seite — der Persistenz. Und gleich die Umkehrung der gewohnten Reihenfolge: Wir entwerfen die Datenbank zum Modell, nicht das Modell zur Datenbank. Was daraus folgt, überrascht viele. Denn wer das Schema zuerst baut, hat die Fachsprache schon verloren, bevor die erste Klasse entsteht. Der erste Satz ist der wichtige: Nicht alles im Modell muss gespeichert werden.
4:13 Das klingt selbstverständlich und wird trotzdem selten gefragt — im Zweifel speichert man eben alles. Und der zweite Satz sagt, wer diese Frage beantwortet: der Fachbereich. Beim Kartenautomaten ist es eindeutig. Der laufende Vorgang — also das Geld, das jemand gerade eingeworfen hat — darf bei einem Stromausfall verloren gehen; so verhält sich auch der reale Automat.
4:36 Der Bestand darf das niemals, denn das ist echtes Geld im Gerät. Zwei Felder, dieselbe Klasse, völlig unterschiedliche Anforderungen — und die Antwort steht nicht im Code. Diese vier Punkte sind eine Argumentationskette, und sie ist es wert, dass Sie ihr folgen. Eine eigene Tabelle bräuchte einen Primärschlüssel. Ein Primärschlüssel ist genau das, was wir Identität nennen.
4:59 Damit wäre der Geldbetrag aber kein Value Object mehr, sondern eine Entity mit anderem Namen — und die ganze Unterscheidung aus Modul 2 wäre über die Datenbank wieder eingerissen. Der dritte Punkt macht die Folge greifbar: Ein solcher Datensatz könnte allein weiterleben, wenn sein Automat gelöscht wird. Ein herrenloser Geldbetrag, der niemandem gehört.
5:20 Value Objects leben und sterben aber mit ihrer Entity — das ist ihre Natur. So sieht die Konsequenz in SQL aus: Der Geldbetrag bekommt keine Tabelle, sondern sechs Spalten in der Tabelle des Automaten — eine je Münz- und Scheinsorte. Nur die Entity hat einen Schlüssel. Wird der Automat gelöscht, verschwindet sein Bestand automatisch mit, ohne Fremdschlüssel und ohne Aufräumlogik.
5:43 Und jetzt achten Sie darauf, was fehlt: der laufende Vorgang. Das ist keine Nachlässigkeit, sondern die Umsetzung der fachlichen Entscheidung von eben. Er überlebt keinen Neustart, also hat er in der Tabelle nichts verloren. Das Schema erzählt damit dieselbe Geschichte wie das Modell. Die ersten beiden Punkte gehören zusammen und beschreiben denselben Reflex: Für jedes Objekt eine Tabelle, und wenn das Schema nicht aufgeht, bekommt das Value Object eben einen Schlüssel.
6:12 Das ist der Moment, in dem die Datenbank anfängt, das Modell zu diktieren — meist unbemerkt, weil es ja funktioniert. Der dritte Punkt ist die Alles-speichern-Gewohnheit, die den Kern mit toten Feldern zumüllt. Und der vierte ist der Ausgangspunkt für alle anderen: das Schema zuerst entwerfen und das Modell daran anpassen.
6:30 Danach diskutieren Sie Fachlogik in Tabellenbegriffen, und die Fachsprache ist verloren. Diese Aufgabe übt genau die Frage, die im Alltag übersprungen wird. Gehen Sie Ihr Modell Feld für Feld durch und ordnen Sie jedes einer von zwei Gruppen zu: gespeichert oder nicht. Für jedes nicht gespeicherte Feld schreiben Sie den fachlichen Grund daneben — nicht den technischen.
6:52 Der Hinweis liefert Ihnen die Prüffrage, und sie ist erstaunlich wirksam: Was passiert fachlich, wenn dieses Feld beim Neustart fehlt? Bei überraschend vielen Feldern lautet die ehrliche Antwort: nichts. Und jedes davon ist eine Spalte weniger, eine Migration weniger und ein Stück weniger Schema, das Sie pflegen müssen.
Was ein ORM mitbringt
7:11 Damit ist das Schema geklärt. Bleibt die Frage, wer die Objekte eigentlich hinein- und herausbewegt. Dafür nimmt man einen objektrelationalen Mapper — und der bringt zwei Begriffe mit, die man verstanden haben muss, weil sie später alles erklären. Ohne dieses Verständnis wirkt ein ORM wie eine Blackbox, die manchmal überraschende Dinge tut — mit ihm wird sein Verhalten vorhersagbar.
7:34 Ein ORM bildet Objekte auf Tabellen ab — so weit die geläufige Beschreibung. Das eigentlich Interessante ist die Session. Stellen Sie sich einen Assistenten vor, der Ihnen Akten bringt und dabei mitschreibt, welche Sie angefasst und was Sie darin geändert haben. Am Ende des Tages trägt er alle Änderungen gebündelt ins Archiv.
7:53 Genau das tut die Session: Sie verfolgt die geladenen Objekte und schreibt die Änderungen zurück, meist erst am Ende ihrer Lebensdauer. Dieses Verhalten erklärt fast alles, was Entwickler an ORMs überrascht — im Guten wie im Schlechten. Die Namen unterscheiden sich je nach Werkzeug — manche sagen Session, andere Kontext —, das Prinzip ist überall dasselbe.
8:14 Achten Sie vor allem auf die rechte Spalte, denn dort steckt die Aussage, die in der Praxis am meisten Ärger verhindert. Die Session ist kurzlebig: eine je Interaktion, danach weg. Die Session-Factory ist langlebig und schwergewichtig: eine je Datenbank, beim Start angelegt. Wer die beiden Lebensdauern verwechselt, baut sich entweder ein Speicherleck oder ein Performance-Problem.
8:37 Und der Begriff in der Fußzeile — Unit of Work — ist der Fachname für dieses Bündeln. Er erklärt gleich die Regeln zur ID-Vergabe. Der erste Punkt ist der wichtigste dieses Kapitels: Es gibt keinen Speicheraufruf im Modell. Änderungen werden erkannt, nicht gemeldet. Genau deshalb kann eine Entity fachlich rechnen, ohne je etwas von Persistenz zu wissen — die Isolation aus Modul 1 überlebt den Kontakt mit der Datenbank.
9:02 Der zweite Punkt ist die Transaktionssicherheit: Alle Schreibvorgänge einer Interaktion gehen als eine Einheit zur Datenbank, also entweder alle oder keiner. Der dritte spart Zugriffe, weil ein bereits geladenes Objekt nicht zweimal geholt wird. Und der vierte zieht die Grenze: Der Kern kennt weder Session noch Verbindung — das ist Sache der Repositories, unser nächstes Modul.
9:25 Die ersten beiden Punkte sind die Lebensdauer-Verwechslung, die ich eben erwähnt habe — einmal in jede Richtung. Eine Session über die gesamte Anwendungslaufzeit sammelt immer mehr Objekte an und wird träge und riskant. Die Session-Factory je Anfrage neu aufzubauen kostet dagegen dramatisch Zeit, weil sie Konfiguration und Verbindungen aufbaut.
9:44 Der dritte Punkt ist der Architekturbruch: im Kern gegen die Session programmieren. Und der vierte ist der Klassiker beim Debuggen — Sie ändern etwas, schauen in die Datenbank und sehen nichts, weil die Session noch nicht geschrieben hat. Kein Fehler, nur falsche Erwartung.
Strategien der ID-Vergabe
10:00 Jetzt wird das Bündeln praktisch relevant. Jede Entity braucht einen Identifikator — und die scheinbar banale Frage, wer ihn vergibt, entscheidet darüber, ob der Vorteil der Unit of Work erhalten bleibt. Was nach einer Formalie aussieht, hängt unmittelbar an dem Bündeln, das wir gerade kennengelernt haben — und an der Gleichheitsregel aus Modul 2.
10:21 Der Zusammenhang, um den es hier geht, ist einfach und wird trotzdem selten gesehen. Wenn die Datenbank den Schlüssel vergibt, muss das ORM beim Erzeugen jeder Entity sofort einfügen und nachfragen, welche Nummer es geworden ist. Damit ist das Bündeln dahin — statt einer Einheit am Ende gibt es viele kleine Zugriffe zwischendurch.
10:40 Wer die Datenbank fragen muss, kann nicht mehr alles am Ende auf einmal schicken. Deshalb ist die ID-Vergabe keine Formalie, sondern eine Entwurfsentscheidung mit spürbaren Folgen — und sie hängt außerdem an der Gleichheitsregel aus Modul 2. Drei Wege, und die rechte Spalte ist die ehrliche. Die Datenbank vergeben zu lassen, ist der vertrauteste Weg — und der einzige, der das Bündeln bricht.
11:03 UUID heißt: Der Client erzeugt den Schlüssel selbst und kennt ihn sofort; der Preis sind sperrige Werte und breitere Indizes. Hi/Lo ist der Kompromiss — der Client reserviert einen Block von Nummern auf Vorrat und bedient sich daraus, was eine kleine Verwaltungstabelle kostet. Die Fußzeile ist entlastend gemeint: Zwischen UUID und Hi/Lo entscheidet meist Geschmack oder Teamgewohnheit, nicht Technik.
11:26 Der wesentliche Unterschied liegt zwischen erster Zeile und den beiden anderen. Hier schließt sich der Kreis zu Modul 2. Die Wahl entscheidet, ob eine Entity schon vor dem Speichern identifizierbar ist. Und ohne Identität lässt sich eine frische Entity mit keiner anderen vergleichen — sie ist nicht einmal sich selbst in einer Menge wiederzufinden.
11:47 Genau deshalb hat unsere Basisklasse diese Prüfung auf die noch leere ID: Sie ist kein Detail, sondern die notwendige Konsequenz aus der Vergabestrategie. Wer dagegen selbst vergibt, hat dieses Problem nie. Und der letzte Punkt fasst die wirtschaftliche Seite zusammen: Wer die Datenbank je Einfügung fragt, verliert den Vorteil, für den er das ORM überhaupt eingesetzt hat.
Mapping-Strategien
12:09 Bleibt eine praktische Frage: Wo steht eigentlich, welche Spalte zu welchem Feld gehört? Es gibt drei übliche Antworten, und aus Sicht dieses Seminars ist nur eine davon tragfähig. Denn was hier entschieden wird, entscheidet zugleich darüber, ob die Isolation des Kerns hält oder still aufgegeben wird. Die Abbildung zwischen Klassen und Tabellen muss irgendwo festgehalten werden — darum kommt niemand herum. Interessant ist nur, wo.
12:35 Drei Orte kommen in Frage, und der Maßstab für die Auswahl ist keine Geschmacksfrage, sondern die Regel aus Modul 1: Der Kern darf kein Persistenzwissen tragen. Sie werden gleich sehen, dass die verbreitetste Variante genau daran scheitert — und dass sie sich trotzdem hartnäckig hält, weil sie so bequem ist. Externe Konfigurationsdateien waren der ursprüngliche Weg.
12:57 Sie halten den Kern frei, sind aber geschwätzig und — schlimmer — sie brechen beim Umbenennen unbemerkt, weil kein Übersetzer sie prüft. Annotationen an den Klassen sind knapp und angenehm zu schreiben; der Preis ist, dass Persistenzwissen in den Kern wandert. Die Entity trägt dann Tabellen- und Spaltennamen mit sich herum.
13:16 Eigene Mapping-Klassen sind der dritte Weg: Der Kern bleibt frei, und weil die Abbildung in Code steht, bleibt Umbenennen nachvollziehbar. Nur diese Zeile verträgt sich mit der Isolation, die wir uns aufgebaut haben. Der Code ist bewusst schematisch — Sie sollen kein Werkzeug lernen, sondern das Prinzip. Und das Prinzip ist: Diese Datei weiß alles über die Datenbank, und die Entity weiß nichts.
13:39 Hier stehen die Spaltennamen, hier steht die Zuordnung der Stückzahlen zu den sechs Spalten. Wichtig ist das Wort „Komponente": Es sagt dem ORM, dass der Geldbetrag kein eigenes Ding mit eigener Tabelle ist, sondern in der Tabelle der Entity liegt — genau das Einbetten, das wir vorhin im Schema gesehen haben. Und beachten Sie die letzte Zeile: Der laufende Vorgang wird bewusst nicht abgebildet. Auch das ist eine dokumentierte Entscheidung.
14:06 Der erste Punkt ist der Sündenfall in Reinform: Spaltennamen als Zeichenketten in der Entity. Der zweite ist der ehrliche — Annotationen wählen, weil sie kürzer sind, und die Isolation dabei stillschweigend aufgeben. Das ist eine legitime Entscheidung, solange sie bewusst fällt; problematisch wird sie, wenn niemand merkt, dass sie überhaupt getroffen wurde.
14:26 Der dritte Punkt bildet Felder ab, die gar nicht gespeichert werden sollen — und macht damit die Arbeit aus Kapitel 2 wieder zunichte. Und der vierte ist der Grund, warum externe Dateien so heikel sind: Die Abbildung bricht erst zur Laufzeit, oft erst beim seltenen Feld, oft erst in Produktion.
Was das ORM dem Modell abverlangt
14:43 Und jetzt der ehrliche Teil dieses Moduls. Wir haben viel dafür getan, den Kern frei zu halten — vollständig gelingt es nicht. Schauen wir uns an, was übrigbleibt, warum es übrigbleibt und ob die Rechnung trotzdem aufgeht. Und diese Ehrlichkeit gehört dazu: Ein Ansatz, der nur unter Laborbedingungen funktioniert, hilft in echten Projekten nicht weiter.
15:05 Vollständige Isolation ist nicht erreichbar, und es ist gut, das offen zu sagen statt es zu kaschieren. Der Grund liegt in der Arbeitsweise des Werkzeugs: Ein ORM erzeugt Stellvertreter-Objekte, um Daten erst bei Bedarf nachzuladen, und es baut Entities über Reflexion auf, statt Ihre Konstruktoren zu benutzen. Beides stellt Anforderungen an den Kern. Entscheidend ist der letzte Satz — klein und bewusst halten.
15:28 Der Unterschied zwischen einem sauberen Modell und einem verwässerten liegt selten in der Zahl der Zugeständnisse, sondern darin, ob jemand sie benennen kann. Der Wert dieser Tabelle liegt in der rechten Spalte: Jedes Zugeständnis hat einen nachvollziehbaren Grund, keines ist Willkür. Klassen dürfen nicht final sein, weil die Stellvertreter von ihnen erben.
15:49 Zugriffe dürfen nicht privat sein, weil der Stellvertreter sie überschreiben muss, um das Nachladen einzuschieben. Der parameterlose Konstruktor existiert, weil Objekte über Reflexion entstehen. Und die letzte Zeile ist die interessanteste: Wenn Sie den Typ nicht sorgfältig vergleichen, ist eine Entity sich selbst ungleich — einmal als echtes Objekt, einmal als Stellvertreter.
16:11 Welche Punkte konkret anfallen, hängt vom Werkzeug ab; die Ursachen sind überall dieselben. Das ist die Basisklasse aus Modul 2 — mit genau einer Änderung. Statt den Konstruktor direkt zu vergleichen, fragen wir das ORM nach dem realen Typ hinter einem möglichen Stellvertreter und fallen auf den Konstruktor zurück, wenn keiner im Spiel ist.
16:31 Mehr passiert hier nicht. Und darin liegt die eigentliche Aussage: Das gesamte Zugeständnis an die Persistenz steckt in einer einzigen Methode, an einer einzigen Stelle, mit einem Kommentar daneben, der den Grund nennt. Keine Entity im ganzen Modell muss davon etwas wissen. So sieht ein Kompromiss aus, den man verantworten kann.
16:50 Die Bilanz. Kein einziger Test aus Modul 2 muss wegen dieser Änderungen angefasst werden — das ist der objektivste Beleg dafür, dass die Fachlichkeit unberührt blieb. Die Fachregeln stehen unverändert dort, wo sie hingehören. Und das Zugeständnis ist sichtbar und benannt, statt über den Kern verstreut. Der letzte Punkt ist die Warnlampe, die Sie im Kopf behalten sollten: Wächst der Anteil an solchen Stellen, ist das ein Zeichen für ein Problem — nicht der normale Preis des Fortschritts.
17:18 Ein oder zwei benannte Kompromisse sind gesund, ein Dutzend sind ein Hinweis auf das falsche Werkzeug oder das falsche Modell. Diese Aufgabe schult den Blick, und Sie können sie sofort auf Ihr eigenes Projekt anwenden. Suchen Sie die Stellen, die allein wegen der Persistenz existieren, benennen Sie jede und ordnen Sie ihr einen Grund zu.
17:38 Für mindestens eine soll feststehen, wie sie sich vermeiden ließe. Der Hinweis nennt die typischen Fundstellen: leere Konstruktoren, Annotationen, Felder ohne fachliche Bedeutung. Was Sie dabei oft entdecken werden, ist der eigentliche Gewinn — dass manche dieser Stellen nicht das ORM verlangt hat, sondern jemand vor drei Jahren vermutet hat, es könnte sie verlangen.
Was du mitnimmst
17:59 Vier Punkte aus diesem Modul. Anwendungsservices vermitteln, sie entscheiden nicht — die Fachlogik bleibt im Kern. Value Objects werden in die Tabelle ihrer Entity eingebettet und bekommen keinen Schlüssel, weil ein Schlüssel Identität bedeuten würde. Die Wahl der ID-Vergabe entscheidet, ob gebündeltes Schreiben möglich bleibt — eine Entscheidung, die man einmal trifft und lange spürt.
18:21 Und ein Rest Persistenzwissen bleibt bestehen; benannt und klein ist er vertretbar. Im nächsten Modul geht es um Aggregate — um die Frage, welche Objekte eine gemeinsame Grenze bilden und wer diese Grenze bewacht.
Lieber mit Trainer? Dieses Modul ist Teil unserer Team-Schulungen — mit Übungen, Ihrem eigenen Code und Fragen, die ein Video nicht beantwortet. Mehr erfahren →