Domain Driven Design Grundlagen
Domain-Driven Design: Software nach der Domäne
DDD richtet Software-Architektur an der Geschäftsdomäne aus. Lernen Sie die Grundkonzepte für komplexe Anwendungen.
Kernkonzepte
Domain-Driven Design
├── Strategisches Design
│ ├── Bounded Contexts
│ ├── Ubiquitous Language
│ └── Context Maps
│
└── Taktisches Design
├── Entities
├── Value Objects
├── Aggregates
├── Repositories
└── Domain Services
Ubiquitous Language
# Gemeinsame Sprache zwischen Entwicklern und Domain-Experten
❌ Entwickler-Sprache:
"Der UserEntity wird in die OrderTable inserted"
✅ Ubiquitous Language:
"Der Kunde platziert eine Bestellung"
# Im Code spiegeln:
class Customer { } // Nicht: UserEntity
class Order { } // Nicht: OrderRecord
customer.placeOrder(items) // Nicht: user.insert(data)
Entities vs. Value Objects
| Entity | Value Object |
|---|---|
| Hat Identität (ID) | Keine Identität |
| Mutable (veränderbar) | Immutable (unveränderlich) |
| Gleichheit durch ID | Gleichheit durch Werte |
| Beispiel: User, Order | Beispiel: Address, Money |
// Entity - hat ID, Gleichheit durch ID
class Customer {
constructor(
public readonly id: CustomerId,
private name: string,
private email: Email
) {}
equals(other: Customer): boolean {
return this.id.equals(other.id);
}
changeName(newName: string): void {
// Validierung
if (newName.length < 2) throw new Error('Name too short');
this.name = newName;
}
}
// Value Object - immutable, Gleichheit durch Werte
class Money {
constructor(
public readonly amount: number,
public readonly currency: string
) {
if (amount < 0) throw new Error('Amount cannot be negative');
}
equals(other: Money): boolean {
return this.amount === other.amount
&& this.currency === other.currency;
}
add(other: Money): Money {
if (this.currency !== other.currency) {
throw new Error('Currency mismatch');
}
return new Money(this.amount + other.amount, this.currency);
}
}
class Address {
constructor(
public readonly street: string,
public readonly city: string,
public readonly postalCode: string,
public readonly country: string
) {}
equals(other: Address): boolean {
return this.street === other.street
&& this.city === other.city
&& this.postalCode === other.postalCode
&& this.country === other.country;
}
}
Aggregates
// Aggregate = Cluster von Entities/Value Objects
// Eine Entity ist der Aggregate Root
/*
Order Aggregate:
┌─────────────────────────────────────┐
│ Order (Aggregate Root) │
│ ├── OrderId │
│ ├── Customer (Reference) │
│ ├── OrderItems[] │
│ │ ├── ProductId │
│ │ ├── Quantity │
│ │ └── Price (Money) │
│ └── ShippingAddress (Value Object) │
└─────────────────────────────────────┘
Regeln:
- Zugriff nur über Aggregate Root
- Transaktionsgrenzen = Aggregate-Grenzen
- Referenz auf andere Aggregates nur per ID
*/
class Order {
private items: OrderItem[] = [];
constructor(
public readonly id: OrderId,
private customerId: CustomerId, // Referenz per ID!
private shippingAddress: Address
) {}
addItem(productId: ProductId, quantity: number, price: Money): void {
// Geschäftsregel: Max 20 Items pro Bestellung
if (this.items.length >= 20) {
throw new Error('Maximum items reached');
}
this.items.push(new OrderItem(productId, quantity, price));
}
removeItem(productId: ProductId): void {
this.items = this.items.filter(i => !i.productId.equals(productId));
}
get total(): Money {
return this.items.reduce(
(sum, item) => sum.add(item.subtotal),
new Money(0, 'EUR')
);
}
}
Repositories
// Repository = Persistenz-Abstraktion für Aggregates
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
findByCustomer(customerId: CustomerId): Promise<Order[]>;
}
class PostgresOrderRepository implements OrderRepository {
async findById(id: OrderId): Promise<Order | null> {
const row = await this.db.query(
'SELECT * FROM orders WHERE id = $1',
[id.value]
);
if (!row) return null;
return this.toDomain(row);
}
async save(order: Order): Promise<void> {
// Upsert Order + OrderItems in Transaction
await this.db.transaction(async (tx) => {
await tx.query(
'INSERT INTO orders ... ON CONFLICT UPDATE ...',
this.toRow(order)
);
// Items speichern...
});
}
private toDomain(row: any): Order {
// Mapping von DB zu Domain
}
}
Bounded Contexts
/*
E-Commerce System
┌─────────────────┐ ┌─────────────────┐
│ Sales Context │ │ Shipping Context│
│ │ │ │
│ Order │ │ Shipment │
│ Customer │ │ Delivery │
│ Product │ │ Address │
└────────┬────────┘ └────────┬────────┘
│ │
└──────────────────────┘
│
▼
Context Map (Integration)
- Gleiche Begriffe, andere Bedeutung!
- "Order" in Sales ≠ "Order" in Shipping
- Jeder Context hat eigene Modelle
*/
Domain Services
// Domain Service = Logik die nicht zu einer Entity gehört
class PricingService {
calculatePrice(
product: Product,
customer: Customer,
quantity: number
): Money {
let price = product.basePrice.multiply(quantity);
// Mengenrabatt
if (quantity > 10) {
price = price.multiply(0.9);
}
// Kundenrabatt
if (customer.isPremium) {
price = price.multiply(0.95);
}
return price;
}
}
// Application Service orchestriert
class OrderApplicationService {
constructor(
private orderRepo: OrderRepository,
private pricingService: PricingService
) {}
async placeOrder(command: PlaceOrderCommand): Promise<OrderId> {
const price = this.pricingService.calculatePrice(
command.product,
command.customer,
command.quantity
);
const order = new Order(/*...*/);
order.addItem(command.productId, command.quantity, price);
await this.orderRepo.save(order);
return order.id;
}
}
💡 Wann DDD?
DDD lohnt sich bei komplexer Business-Logik. Für einfache CRUD-Apps ist es Overkill.