Start / Seminare / Domain-Driven Design in der Praxis
Modul
Domain Events
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 — Domain Events
0:00 Am Ende des letzten Moduls stand eine offene Frage: Wie erfahren zwei getrennte Kontexte voneinander, ohne sich fest aneinanderzubinden? Die Antwort heißt Domain Event, und sie ist im Kern erstaunlich schlicht — ein Ereignis sagt, was geschehen ist, und nicht, wer daraufhin was zu tun hat. Dieses Modul zeigt bewusst zwei Umsetzungen: die verbreitete, bei der die Entity ihr Ereignis sofort auslöst, und die tragfähige, bei der sie es sich nur merkt.
0:25 Der Unterschied wirkt zunächst wie eine Feinheit. Er entscheidet darüber, ob Ihr Kern isoliert bleibt und ob bei einem Abbruch die richtigen Dinge nicht passieren.
Domain Events
0:35 Der Weg durch dieses Modul ist eine kleine Dramaturgie. Erst die neue Anforderung mit einem dritten Kontext, dann der erste Versuch — der naheliegende Weg, der beide Kontexte aneinanderbindet. Daraus entsteht der Bedarf für das Muster. Danach klären wir, was ein Domain Event überhaupt ist, wie man eines entwirft, wer es auslöst und wer darauf reagiert.
0:56 Und zum Schluss die Trennung, um die es eigentlich geht: Erzeugen gehört der Entity, Zustellen der Infrastruktur. Bleiben Sie besonders beim letzten Kapitel wach — dort steckt der Unterschied zwischen funktioniert und trägt.
Die neue Anforderung und der dritte Kontext
1:09 Der Waschsalon wächst. Es kommt ein drittes Teilsystem dazu, und diesmal erkennen wir schon an der Beschreibung, dass es ein eigener Kontext sein muss — weil ein Begriff auftaucht, den es bisher nicht gab. Und wir erkennen es diesmal schon an der Beschreibung, bevor wir eine Zeile Code sehen — weil ein Begriff auftaucht, den es bisher nicht gab.
1:29 Ein neues Teilsystem soll die Automaten verwalten: Es führt die Umsätze aller Waschautomaten zusammen und übernimmt das Bargeld aus den Kartenautomaten. Das ist eine eigene Subdomäne — Sie erkennen es an der Aufgabe, die weder das eine noch das andere Gerät je hatte. Die Automaten wissen, was an ihnen passiert; die Filialverwaltung weiß, was in der Filiale insgesamt passiert. Das sind zwei verschiedene Fragen, gestellt von verschiedenen Leuten.
1:55 Und nach der Regel aus dem letzten Modul heißt eine eigene Subdomäne im Normalfall: ein eigener Bounded Context. Zwei Anforderungen: Jede Belastung soll im Saldo ankommen, und das Bargeld wird bei Leerungen abgeholt. Und jetzt der Punkt, auf den es hier ankommt — beide Male taucht derselbe Begriff auf: die Zentrale. Der vierte Punkt macht daraus eine Faustregel, die ich Ihnen mitgeben möchte: Taucht ein Begriff im Gespräch zweimal auf, gehört er ins Modell.
2:22 Das ist eine der praktisch nützlichsten Beobachtungen aus dem Umgang mit Fachexperten. Wiederholung ist ein Signal. Wenn Menschen dasselbe Wort unabhängig voneinander benutzen, beschreiben sie etwas, das in ihrer Welt existiert — und das dann auch im Modell existieren sollte. Die Zentrale ist ein Aggregat mit genau einer Entity — und das ist völlig in Ordnung, wie wir in Modul 4 gesehen haben.
2:45 Sie führt zwei Bestände getrennt: Saldo aus Kartenumsätzen, Bargeld aus Leerungen. Und die Fußzeile ist wichtiger, als sie aussieht: Diese Trennung ist eine fachliche Entscheidung, keine technische. Man könnte beides addieren, die Summe wäre auch richtig — aber fachlich sind das zwei verschiedene Dinge. Das eine ist Geld, das im Tresor liegt, das andere sind Umsätze auf Karten. Wer sie zusammenwirft, kann eine Frage nicht mehr beantworten, die irgendwann bestimmt gestellt wird.
3:15 Die Karte aus dem letzten Modul, jetzt mit drei Kontexten. Der Shared Kernel umfasst weiterhin nur den Geldbetrag — er heißt überall dasselbe, und deshalb darf er geteilt werden. Neu ist die Beziehung „Konformist": Die Filialverwaltung fügt sich dem Modell der Automaten, statt eine eigene Übersetzung aufzubauen. Das ist eine legitime Entscheidung, wenn man dem anderen Modell vertraut — man spart sich die Schutzschicht.
3:39 Und ganz unten steht die entspannteste Beziehung: Kartenautomat und Waschautomat wissen nach wie vor nichts voneinander. Die Fußzeile nennt die Richtung, auf die es gleich ankommt: Die Automaten wissen nichts von der Filialverwaltung.
Der erste Versuch und seine Kosten
3:53 Jetzt zum Handwerk: Die Buchung muss irgendwo ausgelöst werden. Es gibt zwei naheliegende Wege, beide funktionieren — und beide kosten dasselbe, nämlich die Richtung der Abhängigkeit. Beide funktionieren tadellos, und beide kosten dasselbe: die Richtung der Abhängigkeit, die wir uns bisher sorgfältig erhalten haben. Der erste Reflex ist, die Buchung dort hinzuschreiben, wo die Belastung passiert — im Anwendungsservice oder gleich in der Methode des Waschautomaten.
4:22 Das ist nicht dumm, sondern naheliegend: Dort liegen die Daten, dort ist der Moment. Und der letzte Satz nennt den Preis: Beides bindet die Kontexte aneinander. Bemerkenswert ist, dass die Kosten nicht sofort spürbar sind. Der Code läuft, der Test ist grün, das Feature ist fertig. Sichtbar wird es erst, wenn der vierte Kontext dazukommt oder die Zentrale sich ändert — und dann ist es teuer.
4:46 Zwei Wege, zwei Preise. Die Buchung im Anwendungsservice erzeugt eine beidseitige Abhängigkeit, und der Zusatz ist der praktisch schmerzhafte Teil: Jede weitere Aufrufstelle muss es wiederholen. Sie haben also eine Regel, die an Aufrufstellen hängt statt am Modell — und irgendwann wird eine vergessen. Die zweite Variante reicht die Zentrale als Parameter in die Methode; das sieht sauberer aus, weil es explizit ist, ändert aber nichts an der Abhängigkeit und gibt dem Automaten obendrein eine fremde Aufgabe.
5:14 Die Fußzeile bringt es fachlich auf den Punkt: Ein Waschautomat bucht keine Salden. In der echten Welt tut er das nicht, also sollte er es im Modell auch nicht tun. Bisher gilt eine klare Richtung: Nur die Filialverwaltung weiß von den Automaten, nicht umgekehrt. Diese Einseitigkeit ist wertvoll — sie bedeutet, dass sich die Automaten weiterentwickeln lassen, ohne die Verwaltung zu fragen.
5:37 Mit der Buchung im Automaten wüssten beide voneinander, und ab da schlägt jede Änderung an der Zentrale auf die Automaten durch. Der letzte Punkt ist der, den ich hervorheben möchte: Die Richtung der Abhängigkeit ist eine Entwurfsentscheidung, kein Zufall. Sie fällt in der Praxis meistens unbeabsichtigt, an einem Dienstagnachmittag, in einer Zeile Code — und hält dann jahrelang.
6:00 Der erste Punkt beschreibt die Versuchung mit ihrer eigentlichen Ursache: Man legt eine fremde Verantwortung in eine Entity, weil der Zustand gerade greifbar ist. Nähe verführt. Der zweite ist die Folge der verstreuten Regel — eine Aufrufstelle wird vergessen, und der Saldo stimmt nicht mehr, was niemand bemerkt, bis jemand nachrechnet.
6:19 Der dritte ist ein Dokumentationsfehler mit Langzeitwirkung: die Abhängigkeitsrichtung umkehren, ohne die Context Map anzupassen. Und der vierte ist die Haltung, gegen die dieses Modul geschrieben ist — beidseitige Bindung als Kleinigkeit abtun. Sie wächst mit jedem Kontext, und irgendwann hängt alles an allem.
Was ein Domain Event ist
6:38 Also brauchen wir etwas anderes: eine Möglichkeit, dass der Automat mitteilt, was passiert ist, ohne zu wissen, wer zuhört. Genau das leistet ein Domain Event — und die Betonung liegt auf dem Wort Domain. Und die Betonung liegt auf dem Wort Domain — denn mit den Ereignissen aus der Oberflächenprogrammierung hat das erstaunlich wenig zu tun.
6:58 Ein Domain Event beschreibt etwas, das in der Domäne geschehen ist und für sie Bedeutung hat. Der zweite Satz zieht die Grenze, auf die es ankommt: Es ist kein Systemereignis. Der Knopfdruck interessiert die Domäne nicht, der ausgelöste Waschgang schon. Das ist deshalb wichtig, weil die meisten Entwickler den Begriff Event aus der Oberflächenprogrammierung kennen — dort ist alles ein Event, der Klick, das Scrollen, der Fokuswechsel.
7:23 Domain Events sind etwas anderes. Sie sind so selten wie fachlich bedeutsame Vorgänge, und das sind in einem durchschnittlichen System vielleicht ein Dutzend. Drei Beispiele, und die Fußzeile liefert die Prüffrage, die ich mir merken würde: Die Frage lautet nie „was ist passiert", sondern „interessiert es den Fachbereich".
7:42 Ein Knopfdruck ist zweifellos etwas, das passiert ist — aber kein Filialleiter der Welt möchte darüber informiert werden. Ein abgelaufener Zeitgeber ebenso wenig. Der gestartete Waschgang mit der Belastung dagegen ist genau das, worüber die Zentrale Buch führt. Wenn Sie unsicher sind, ob etwas ein Domain Event ist, stellen Sie sich vor, Sie erzählen es dem Fachexperten. Zuckt er mit den Schultern, ist es keins.
8:07 Der erste Punkt nennt den Kernnutzen: Sie ersetzen eine beidseitige Bindung durch eine einseitige. Der Automat kennt den Empfänger nicht — er weiß nicht einmal, ob es einen gibt. Der dritte Punkt ist der oft übersehene: Auch innerhalb eines Kontexts nehmen Ereignisse einer Entity fremde Aufgaben ab. Domain Events sind also kein reines Kontextgrenzen-Werkzeug.
8:28 Und der vierte Punkt fasst die Haltung in einem Satz zusammen, der mir gut gefällt: Der Automat sagt, was geschehen ist — wer reagiert, geht ihn nichts an. Genauso arbeitet eine Registrierkasse, die einen Bon druckt, ohne zu wissen, wer ihn liest. Der erste Punkt ist der Flutungsfehler, den ich eben angesprochen habe: Oberflächenereignisse zu Domain Events erklären.
8:50 Dann haben Sie dreihundert Ereignisklassen, und keine davon bedeutet etwas. Der zweite ist Übereifer — Ereignisse einführen, wo ein direkter Aufruf im selben Kontext genügt; die Entkopplung kostet Nachvollziehbarkeit, und die zahlt man nur, wenn man sie braucht. Der dritte ist ein sprachlicher, aber inhaltlich schwerer Fehler: aus dem Ereignis eine verkappte Anweisung machen.
9:12 „Saldo erhöhen" statt „Saldo geändert" — schon weiß der Absender wieder, was der Empfänger tun soll. Und der vierte erwartet eine Antwort, die es bei Ereignissen nicht gibt.
Ein Domain Event entwerfen
9:23 Wie sieht so ein Ereignis nun konkret aus? Es gibt fünf Regeln, und eine davon wird häufiger verletzt als alle anderen zusammen — meist aus reiner Bequemlichkeit. Der Entwurf entscheidet darüber, wie eng die Kontexte aneinanderhängen — und zwar an einer Stelle, an der es später niemandem mehr auffällt. Ein Ereignis trägt genau die Angaben, die der Empfänger zum Reagieren braucht — nicht mehr.
9:46 Und dann der Satz, der erklärt, warum das so streng gehandhabt wird: Alles darüber hinaus bindet die Kontexte aneinander, und zwar an einer Stelle, an der es niemandem auffällt. Genau das ist das Tückische. Ein Feld zu viel im Ereignis sieht nach Großzügigkeit aus, ist aber eine Zusage: Der Empfänger darf sich darauf verlassen, dass es da ist.
10:06 Ab da können Sie es nicht mehr entfernen, ohne den anderen Kontext zu brechen — und Sie merken es erst, wenn Sie es versuchen. Regel eins ist sprachlich: Vergangenheitsform, und dabei genau werden. „Belastet" ist besser als „geändert", wenn es tatsächlich um eine Belastung geht. Regel zwei ist die Sparsamkeit. Regel drei ist die entscheidende — ausschließlich einfache Datentypen, nie Entities oder Value Objects.
10:30 Regel vier und fünf sind eine Fallunterscheidung: Kennt der Empfänger den Absender ohnehin, genügt die Kennung; kennt er ihn nicht, müssen die geänderten Werte selbst mitgehen. Und die Fußzeile sagt, wo es in der Praxis schiefgeht: Regel drei wird am häufigsten verletzt, meist aus Bequemlichkeit, denn ein fertiges Objekt mitzugeben ist schlicht weniger Tipparbeit.
10:53 Zwei Zahlen, sonst nichts — eine Automatenkennung und ein Betrag in Cent. Kein Datum, kein Programm, kein Kundenname, obwohl man das alles hätte mitgeben können. Und jetzt die Fußzeile, die das schönste Beispiel dieses Moduls enthält: Es steht kein Geldbetrag im Ereignis, sondern eine schlichte Zahl. Warum nicht der Geldbetrag, den doch alle drei Kontexte teilen? Weil die Filialverwaltung sonst dauerhaft am Shared Kernel festhinge.
11:18 Solange nur eine Zahl übertragen wird, ist die Verbindung so dünn wie möglich — und der Empfänger baut sich daraus seinen eigenen Geldbetrag, wenn er einen braucht. Der erste Punkt ist der ehrlichste Satz auf dieser Folie: Man legt die ganze Entity ins Ereignis, weil man dann nichts vergisst. Genau diese Absicherung schafft die Bindung, die man vermeiden wollte.
11:40 Der zweite ist der sprachliche — Befehlsform statt Tatsachenform, also die Absicht statt des Geschehenen melden. Der dritte ist die Vorratshaltung: Angaben mitschicken „für später". Jede davon ist eine Bindung, und „später" kommt selten. Und der vierte ist der umgekehrte Fehler: eine Kennung mitschicken, obwohl der Empfänger den Absender gar nicht kennt und mit der Nummer nichts anfangen kann.
12:03 Sie entwerfen für einen fachlichen Vorgang ein Domain Event, das minimal und dennoch ausreichend ist. Jedes Feld muss mit dem Bedarf des Empfängers begründet sein, und keines darf einen Domänentyp tragen. Der Hinweis beschreibt eine Methode, die ich sehr empfehlen kann, weil sie das Raten beendet: Streichen Sie testweise jedes Feld einzeln und prüfen Sie, was der Empfänger dann nicht mehr kann.
12:25 Fällt Ihnen nichts ein, war das Feld überflüssig. Das ist dieselbe Denkweise wie beim Entfernen von Code — nicht fragen, ob man es braucht, sondern zeigen, was ohne es nicht mehr geht.
Auslösen, zustellen, behandeln
12:36 Das Ereignis steht. Jetzt die Frage, wo es entsteht und wo darauf reagiert wird — und warum das keine Geschmacksfrage ist, sondern die Abhängigkeitsrichtung im Code sichtbar macht. Es ist eine dieser Entscheidungen, bei denen der Ablageort einer Datei tatsächlich Architektur ist und nicht bloß Ordnung. Das Ereignis entsteht im Kontext des Waschautomaten, behandelt wird es in der Filialverwaltung.
12:59 Und der zweite Satz nennt den Grund: Diese Aufteilung bildet die Richtung der Abhängigkeit im Code ab. Der Behandler kennt beide Seiten — er kennt das Ereignis und die Zentrale. Deshalb muss er auf der abhängigen Seite liegen, also in der Filialverwaltung. Läge er beim Automaten, wäre die Abhängigkeit wieder umgedreht, und der ganze Aufwand mit dem Ereignis wäre vergeblich gewesen.
13:22 Das ist eine dieser Entscheidungen, bei denen der Dateiablageort tatsächlich Architektur ist. Der Kommentar in der ersten Zeile ist wichtiger als der Code: Er liegt in der Filialverwaltung, nicht beim Automaten. Der Behandler selbst ist unspektakulär — Zentrale holen, buchen, speichern. Genau das ist die Botschaft der Fußzeile: Behandler sind meist so kurz, weil sie die Arbeit an Domänenklassen weiterreichen.
13:46 Beachten Sie außerdem, was hier passiert: Aus der Zahl im Ereignis wird wieder ein Geldbetrag, aber erst hier, im Empfängerkontext. Ein Behandler, der anfängt zu rechnen und Fachregeln zu prüfen, ist ein Warnzeichen — dann ist Fachlogik aus dem Modell in die Infrastruktur gerutscht. Drei Lagen, drei Zustellwege: über den Arbeitsspeicher, über einen Nachrichtenbus, oder gespeichert und später gelesen, wenn der Verlauf gebraucht wird.
14:12 Der eigentliche Wert der Tabelle steht aber in der Fußzeile, und sie ist beruhigend: Die Zustellung ist unabhängig vom Begriff — derselbe Entwurf trägt alle drei Fälle. Sie müssen sich also heute nicht festlegen. Ein Ereignis, das mit einfachen Datentypen entworfen ist, funktioniert im selben Prozess genauso wie über eine Warteschlange.
14:31 Deshalb ist der Entwurf nach den fünf Regeln so lohnend: Er hält die Optionen offen, die Sie in drei Jahren brauchen könnten. Der erste Punkt ist der Ablagefehler, der die Abhängigkeit umdreht — er sieht harmlos aus und macht die Entkopplung zunichte. Der zweite ist der Behandler, der rechnet, statt zu delegieren. Der dritte zieht die Zustellungstechnik in den Entwurf des Ereignisses hinein — dann tragen Ihre Ereignisse Nachrichten- Header und Serialisierungsdetails mit sich herum und sind an eine Infrastruktur gebunden.
15:01 Und der vierte ist die Annahme, die in verteilten Systemen am teuersten ist: dass Ereignisse genau einmal ankommen. Sie kommen mindestens einmal an, manchmal zweimal — also müssen Behandler damit umgehen können, dass sie dieselbe Nachricht erneut sehen.
Erzeugen und Zustellen trennen
15:15 Und jetzt zum wichtigsten Kapitel dieses Moduls. Bisher haben wir offengelassen, wann das Ereignis eigentlich hinausgeht. Die naheliegende Antwort — sofort — ist die falsche, und die Gründe dafür sind es wert, genau angesehen zu werden. Die naheliegende Antwort — sofort — ist die falsche, und die Gründe dafür sind es wert, in Ruhe angesehen zu werden.
15:37 Löst die Entity das Ereignis selbst aus, passieren zwei Dinge gleichzeitig, und beide sind unerwünscht. Erstens greift sie aus dem Kern nach außen — sie braucht etwas, das zustellt, und damit ist die Isolation aus Modul 1 dahin. Zweitens geschieht die Zustellung, bevor der Vorgang abgeschlossen ist. Die Lösung steht im letzten Satz und ist eine dieser Ideen, die im Rückblick offensichtlich wirken: beides trennen. Die Entity merkt sich, was geschehen ist.
16:04 Zugestellt wird später, von jemand anderem. Das ist derselbe Gedanke wie bei der Session — nicht melden, sondern sammeln und gebündelt erledigen. Vier Gründe, und der zweite ist der praktisch gefährlichste: Scheitert danach eine Prüfung, ist das Ereignis längst verarbeitet. Der Waschgang wurde abgebrochen, aber der Saldo ist gebucht — und Sie bekommen ihn nicht zurück, weil der andere Kontext schon reagiert hat.
16:29 Der dritte Grund ist die Mengenfrage: Mehrere Änderungen im selben Vorgang erzeugen mehrere Ereignisse statt einem, und der Empfänger sieht Zwischenzustände, die nie relevant waren. Und der vierte ist der Alltagsschmerz: Im Test braucht es Hilfskonstruktionen, die nur für den Test existieren — eine Registrierung, ein Abfangobjekt, ein Aufräumen danach.
16:49 Jetzt füllt sich die Basisklasse, die in Modul 4 fast leer war — das war von Anfang an so geplant. Sie bekommt eine Sammlung, einen lesenden Zugriff darauf und zwei Methoden. Achten Sie auf die Sichtbarkeiten, denn darin steckt die Aussage: Das Merken ist geschützt, also kann nur die Entity selbst ein Ereignis hinzufügen.
17:08 Von außen lässt sich lesen und leeren, aber nichts hineinlegen. Und das ist genau richtig — nur die Entity weiß, was fachlich geschehen ist. Wer von außen Ereignisse in fremde Aggregate legen könnte, hätte einen Weg gefunden, Tatsachen zu behaupten, die nie passiert sind. Vergleichen Sie das mit der Version aus dem letzten Modul: Es ist dieselbe Methode, nur um eine Zeile ergänzt.
17:31 Die Entity rechnet, schreibt ihren Zustand fort — und merkt sich, was geschehen ist. Kein Aufruf nach außen, keine Abhängigkeit zu irgendeiner Infrastruktur. Das ist der Grund, warum diese Bauweise sich mit der Isolation aus Modul 1 verträgt. Und die Fußzeile nennt den schönsten Nebeneffekt: Der Test liest die Ereignisse jetzt einfach vom Automaten ab. Keine Registrierung, kein Abfangobjekt, keine Hilfskonstruktion.
17:57 Sie prüfen eine Eigenschaft des Objekts, mehr nicht. Fünf Schritte, und der dritte ist der Kern: Das Aggregat wird gespeichert, und erst danach greift die Zustellung. Vorher darf noch alles scheitern — Schritt zwei sagt es ausdrücklich, Prüfungen können den Vorgang noch abbrechen, und dann wird kein Ereignis zugestellt, weil noch keines hinausgegangen ist.
18:19 Schritt vier und fünf gehören der Infrastruktur: Sammlung lesen, jedes Ereignis weiterreichen, danach leeren. Die Fußzeile fasst die Regel in einem Satz zusammen, den man sich merken kann: Zugestellt wird nach dem Speichern, nicht davor. Vorher kann noch alles scheitern. Der erste Punkt ist die offene Sammlung, über die fremder Code Ereignisse legen kann — die Behauptung von Tatsachen, die nie geschehen sind.
18:43 Der zweite ist der Fehler, den dieses ganze Kapitel behandelt: vor dem Speichern zustellen und bei einem Abbruch nichts mehr zurücknehmen können. Der dritte ist ein Klassiker mit unangenehmer Wirkung: die Sammlung nach der Zustellung nicht leeren. Beim nächsten Speichern geht alles erneut hinaus, und der Saldo wächst, ohne dass jemand gewaschen hätte.
19:02 Und der vierte holt die Infrastruktur zurück in den Kern — Zustellcode gehört nach außen, sonst war die ganze Trennung umsonst. Die Abschlussaufgabe ist zugleich die anspruchsvollste. Sie hängen die Zustellung so auf, dass sie erst nach erfolgreichem Abschluss eines Vorgangs geschieht — und weisen das mit einem Test nach: Bei einem abgebrochenen Vorgang wurde kein Behandler aufgerufen, und die Sammlung ist danach leer.
19:26 Der Hinweis nennt den Fall, auf den es ankommt: der Abbruch nach der Änderung, nicht davor. Vorher abzubrechen ist einfach, da ist noch nichts passiert. Nach der Änderung abzubrechen ist der Fall, der in der Produktion tatsächlich vorkommt — und genau der, den die einfache Umsetzung nicht besteht.
Was du mitnimmst
19:43 Vier Sätze, die ich Ihnen mitgeben möchte. Ein Domain Event meldet eine fachliche Tatsache, keine Anweisung — daran hängt die ganze Entkopplung. Es trägt nur einfache Daten, denn Entities im Ereignis binden Kontexte aneinander, und zwar unsichtbar. Erzeugen gehört der Entity, Zustellen der Infrastruktur; diese Trennung ist der Unterschied zwischen einer Lösung, die funktioniert, und einer, die trägt.
20:06 Und zugestellt wird erst, wenn der Vorgang wirklich abgeschlossen ist. Im letzten Modul treten wir einen Schritt zurück und schauen uns an, was aus alldem in echten Projekten wird — samt der Muster, die man besser nicht baut.
Lieber mit Trainer? Dieses Modul ist Teil unserer Team-Schulungen — mit Übungen, Ihrem eigenen Code und Fragen, die ein Video nicht beantwortet. Mehr erfahren →