DDD(Domain-Driven Design,领域驱动设计)是 Eric Evans 2003 年的著作,影响深远但被滥用严重。"我们在做 DDD"经常意味着"我们建了一堆 Entity / VO / Service / Repository,但业务还是一团乱"。这篇文章把 DDD 的真正核心思想讲透 —— 战略层(限界上下文、统一语言)比战术层(Entity / VO)重要十倍。
DDD 解决什么
复杂业务系统的核心痛点:
- 需求方说"订单状态"和开发理解的不一样,沟通成本巨大。
- 系统大了之后,谁也说不清"当用户下单时,到底发生了什么"。
- 不同业务模块逻辑互相纠缠,改一处影响一片。
DDD 给出的解法:让代码长得像业务,让模块边界清晰。
战略设计:限界上下文(Bounded Context)
DDD 最重要的概念。"限界上下文"是一个明确边界内的领域模型 —— 在这个边界内,术语有明确含义,模型自洽。
例子:电商系统里"订单"在不同上下文意思不同:
- 销售上下文:订单 = 客户下的单(总价、商品、用户)。
- 履约上下文:订单 = 要发的货(物流地址、SKU 列表、包裹)。
- 财务上下文:订单 = 一笔收入(税务、发票、对账)。
朴素做法:一个 Order 类承载所有字段 —— 几十个字段,谁都不能完整修改。DDD 做法:三个上下文各有自己的 Order 模型,通过 ID 关联。每个上下文里 Order 都是简单清晰的。
统一语言(Ubiquitous Language)
每个限界上下文里,业务方和开发用同一套术语。"订单"在销售上下文意味着什么 —— 业务、产品、开发、测试都用同一个定义。
代码里这意味着:类名、方法名、字段名要用业务术语,不要用"万能名词"。
// 不好:技术术语
class OrderManager {
process(o) { ... }
update(o, status) { ... }
}
// 好:业务术语
class SalesOrder {
place() { ... }
cancel(reason: CancellationReason) { ... }
fulfillBy(warehouse: Warehouse) { ... }
}
上下文映射(Context Map)
多个限界上下文之间怎么协作?9 种关系:
- 共享内核(Shared Kernel):两个上下文共享一小段领域模型。慎用。
- 客户/供应商:上游为下游提供服务,下游提需求,上游有义务满足。
- 遵奉者(Conformist):下游完全按上游模型,自己不再翻译。
- 防腐层(ACL):下游把上游模型翻译成自己的领域语言,防"污染"。最常用。
- 发布语言(Published Language):上游公开一个标准格式(如标准 JSON Schema),下游按这个对接。
- 分道扬镳(Separate Ways):两个上下文没共同业务关系,完全独立。
- 大泥球(Big Ball of Mud):上下文边界不清晰,混在一起。这是要避免的反模式。
战术设计:核心构件
Entity(实体)
有唯一标识、可变状态的对象。如 User、Order、Product。两个 ID 相同的 Entity 即使其他字段不同也是同一个。
class User {
constructor(private id: UserId, private email: Email) {}
changeEmail(newEmail: Email) {
if (this.email.equals(newEmail)) return;
this.email = newEmail;
// 发领域事件
DomainEvents.publish(new UserEmailChanged(this.id));
}
}
Value Object(值对象)
没有标识,只看值。Email、Address、Money 都是。不可变,改一个字段返回新实例。
class Money {
constructor(readonly amount: number, readonly currency: string) {}
add(other: Money): Money {
if (this.currency !== other.currency) throw new Error('currency mismatch');
return new Money(this.amount + other.amount, this.currency);
}
equals(other: Money) {
return this.amount === other.amount && this.currency === other.currency;
}
}
值对象很重要 —— 它把业务约束封装到类型里。Money 不会"悄悄加错币种,因为 add 会抛异常。
Aggregate(聚合)
一组紧密相关的对象,有一个"聚合根"(Aggregate Root)作为外部访问入口。其他对象都通过 root 访问。
class Order {
// 聚合根
private items: OrderItem[] = [];
constructor(private id: OrderId, private userId: UserId) {}
addItem(product: Product, quantity: number) {
if (this.isFinalized()) throw new Error('已下单,不能改');
this.items.push(new OrderItem(product, quantity));
}
total(): Money {
return this.items.reduce((sum, i) => sum.add(i.subtotal()), Money.zero('CNY'));
}
}
// 外部代码:不能直接拿到 OrderItem 改它,必须通过 Order
const order = repo.findOrder(id);
order.addItem(product, 2);
repo.save(order);
聚合的核心价值:封装业务一致性。Order 内部的所有变更都保证"不变量"(如总价 = 各项之和)。
Repository(仓储)
把聚合根的持久化封装起来,业务代码看不到 SQL / ORM 细节。
interface OrderRepository {
findById(id: OrderId): Order | null;
save(order: Order): void;
findPendingByUser(userId: UserId): Order[];
}
// 实现可以是 MySQL、MongoDB、内存(测试)
class MySQLOrderRepository implements OrderRepository { ... }
Domain Service(领域服务)
"不属于任何单个 Entity 或 VO"的业务逻辑。例:转账涉及两个账户,放哪个 Entity 都别扭,放领域服务。
class TransferService {
transfer(from: Account, to: Account, amount: Money) {
from.withdraw(amount);
to.deposit(amount);
}
}
Application Service(应用服务)
编排领域对象完成用例,不包含业务逻辑本身。它是"用例"的入口。
class PlaceOrderUseCase {
constructor(
private orderRepo: OrderRepository,
private productRepo: ProductRepository,
private paymentService: PaymentService,
) {}
async execute(cmd: PlaceOrderCommand) {
const products = await Promise.all(cmd.items.map(i => this.productRepo.find(i.productId)));
const order = Order.create(cmd.userId, products);
await this.paymentService.charge(order.total());
await this.orderRepo.save(order);
}
}
领域事件(Domain Event)
业务里发生的有意义的事件,用于触发后续动作。
class OrderPaid {
constructor(readonly orderId: OrderId, readonly paidAt: Date) {}
}
// Order 内部
this.status = 'paid';
DomainEvents.publish(new OrderPaid(this.id, new Date()));
// 其他模块订阅(解耦)
on(OrderPaid, async (event) => {
await inventoryService.deduct(event.orderId);
await emailService.sendReceipt(event.orderId);
await pointsService.addPoints(event.orderId);
});
领域事件让模块解耦 —— "下单"业务不需要知道有哪些下游关心它。这是从单体演化到事件驱动架构的基础。
常见反模式
反模式 1:贫血模型。Entity 只有 getter/setter,业务逻辑全堆在 Service 里。这不是 DDD,是"面向对象编程的反面"。Martin Fowler 专门撰文批评过。
反模式 2:聚合太大。一个 Order 聚合包含 Customer、Address、Items、Shipments、Payments... 加载时性能差,事务粒度大。规则:聚合要小,跨聚合用 ID 引用 + 最终一致。
反模式 3:CRUD 套 DDD 外壳。增删改查的简单管理后台用 DDD 是过度设计,Active Record 模式更合适。DDD 给"真正有业务复杂度"的系统用。
什么时候用 DDD
- 业务复杂(几十个子领域、术语多)。
- 领域专家深度参与开发(否则建不出统一语言)。
- 系统长期演化(几年到几十年)。
- 多团队协作。
什么时候不用
- CRUD 管理后台。
- 原型 / MVP。
- 团队没人懂 DDD,且没培训计划。
写在最后
DDD 不是"用 Entity / VO / Repository 这套架构",而是"让代码长得像业务"的思想。战略层(限界上下文 + 统一语言)比战术层(代码结构)更重要 —— 你可以不用 Aggregate,但不能没有明确的领域边界和清晰的术语。这是从程序员到架构师的核心能力跃迁。
一图看懂
DDD 限界上下文 + 聚合一图看懂:
—— 别看了 · 2026