Warum gewöhnliches CRUD Ihr Projekt tötet und wie DDD das beheben kann
\n\nJeder Entwickler ist mindestens einmal auf die Situation gestoßen, in der ein zunächst einfaches Projekt zu einem „Sumpf“ aus Services, Controllern und über den gesamten Code verteilter Geschäftslogik wird. Sie beginnen, UserService zu schreiben, dann OrderService, und nach einem halben Jahr befindet sich im OrderService die Logik zum Senden von E-Mails, zur Berechnung von Rabatten und zur Überprüfung des Benutzerguthabens. Kommt Ihnen das bekannt vor?
Das Problem ist, dass wir zu früh an Datenbanken, Frameworks und REST-APIs denken. Wir vergessen das Wesentliche – die Domäne (domain). Domain-Driven Design (DDD) bietet einen radikal anderen Ansatz: Zuerst modellieren wir die Geschäftsprozesse in der Sprache der Experten, erst dann den Code.
\n\nIn diesem Artikel werden wir untersuchen, wie man Domain-Driven Design in TypeScript anhand realer Beispiele anwendet. Sie werden sehen, wie man eine saubere Architektur aufbaut, in der Geschäftsregeln getrennt von der Infrastruktur leben und der Code leicht zu testen und zu ändern ist.
\n\nGrundlagen von DDD: Sprache, Kontexte und Modell
\n\nUbiquitäre Sprache (Ubiquitous Language)
\nDas erste Prinzip von DDD ist eine einheitliche Sprache, die sowohl für Entwickler als auch für Fachexperten verständlich ist. Wenn ein Manager sagt „Der Kunde hat das Abonnement aktiviert“, dann sollte es im Code eine Methode activateSubscription() geben, nicht setStatus(1). TypeScript mit seiner strengen Typisierung ist dafür ideal geeignet – die Namen von Klassen, Methoden und Variablen werden zur Dokumentation.
Begrenzter Kontext (Bounded Context)
\nEin großes System wird in unabhängige Kontexte unterteilt: „Zahlungen“, „Benutzerverwaltung“, „Lieferung“. Jeder Kontext hat sein eigenes Modell und seine eigene Sprache. TypeScript-Module und strikte Typen helfen, diese Kontexte auf Importebene strikt zu trennen.
\n\nAufbau des Domänenmodells in TypeScript
\n\nValue Object (Wertobjekt)
\nDies ist eine unveränderliche Entität, die durch ihre Attribute und nicht durch eine ID definiert wird. Beispiele: Email, Money, Coordinates.
\n\n// value-objects/Email.ts\nexport class Email {\n private readonly value: string;\n\n constructor(email: string) {\n if (!this.isValid(email)) {\n throw new Error('Invalid email address');\n }\n this.value = email;\n }\n\n private isValid(email: string): boolean {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);\n }\n\n public getValue(): string {\n return this.value;\n }\n\n public equals(other: Email): boolean {\n return this.value === other.value;\n }\n}\n\n\nBeachten Sie: Email prüft selbst seine Gültigkeit. Kein Service muss wissen, wie eine korrekte E-Mail aussieht. Das ist reines Domain-Driven Design in TypeScript – die Geschäftsregel lebt innerhalb des Objekts.
Entity (Entität)
\nIm Gegensatz zu Value Objects hat eine Entität eine ID und kann sich im Laufe der Zeit ändern. Beispiele: User, Order.
\n\n// entities/User.ts\nimport { Email } from '../value-objects/Email';\nimport { UserId } from '../value-objects/UserId';\n\nexport class User {\n private readonly id: UserId;\n private email: Email;\n private isActive: boolean;\n\n constructor(id: UserId, email: Email) {\n this.id = id;\n this.email = email;\n this.isActive = true;\n }\n\n public getId(): UserId {\n return this.id;\n }\n\n public changeEmail(newEmail: Email): void {\n if (!this.isActive) {\n throw new Error('Cannot change email for inactive user');\n }\n this.email = newEmail;\n }\n\n public deactivate(): void {\n this.isActive = false;\n }\n}\n\n\nAggregate (Aggregat)
\nEin Aggregat ist eine Gruppe von Entitäten und Value Objects, die als eine Einheit behandelt werden. Ein Aggregat hat eine Wurzel (Aggregate Root), über die der Zugriff auf die inneren Objekte erfolgt.
\n\nBeispiel: eine Bestellung (Order)