元数据卡
- 前置知识:第12章(六边形与整洁架构);第13章(单体与微服务迁移)
- 预计时间:50 分钟
- 完成标志:能为一组微服务划分 Bounded Context 并运用战术模式建模
你的进度
工匠之都的工坊系统越做越大。锻造坊管一个铁件叫 "精密锻件",组装坊管它叫 "传动组件骨架",质检坊管它叫 "待检品第 3 类"。
三个工坊,三种叫法。但它们在讨论的明明是同一件东西。开会时互相听不懂,数据同步时字段对不上,代码里充满了"如果是从组装坊传来的,name 字段对应的是 component.skeleton_id"这样的翻译逻辑。
更可怕的是:你想加一个新的"淬火处理"功能,发现代码分散在六个服务的 12 个类里——没人能说清楚"淬火到底归谁管"。 你的任务
DDD(Domain-Driven Design,领域驱动设计)是 Eric Evans 提出的应对复杂业务建模的方法论。它不教你写代码时用哪些技术——它教你怎么理解你的业务领域,怎么在代码里表达出这个理解,怎么划定清晰的范围防止概念混乱。DDD 分为战略设计和战术设计,这一章两个都讲。
本章分层
- 必读:Bounded Context、Ubiquitous Language、Entity vs Value Object
- 选读:Aggregate 设计、Domain Events
- 进阶:Anti-Corruption Layer 的实现
本章不会要求你掌握
- DDD 与 CQRS 事件溯源的结合(第15章已覆盖)
- Event Storming 工作坊的具体操作
破局 · 溯源
先看一个反例。你有一个 Workshop 类,里面有 getName()、getAddress()、getEquipment()、listParts()、addWorker()、calculateProductivity()。这是"一个工坊"的模型——看起来合理,但出了什么问题?
问题:Workshop 承担了太多职责。calculateProductivity() 关心的是产能管理,addWorker() 关心的是人力资源,listParts() 关心的是库存——三个不同的概念塞进一个类。领域模型膨胀成了"上帝对象"。这不是 DDD 想看到的。
为什么 DDD 存在
传统的面向对象建模通常"根据现实世界的名词"建类。一个 Workshop 有一个名字、一个地址、一批设备和工人——于是你建一个 Workshop 类,把所有属性堆进去。
但当系统变大,同一个名词在不同的上下文中含义不同:
- 在质检上下文中,"一个工坊"指的是"需要定期巡检的工作区域"
- 在人力调度上下文中,"一个工坊"指的是"一组具有特定技能的工人"
- 在财务上下文中,"一个工坊"指的是"一个成本中心"
这叫概念爆炸——一个词对应多个概念。DDD 的核心回答是: 把这些上下文明确划定边界,每个边界内只用一套统一的概念和术语。
第一层:战略设计——Bounded Context 与 Ubiquitous Language
Bounded Context(限界上下文) 是 DDD 最重要的概念。它是"一个概念在哪组含义下有效"的边界。
在锻造坊系统里:
+---------------------------+ +---------------------------+
| 锻造上下文 | | 组装上下文 |
| ForgeContext | | AssemblyContext |
| | | |
| Part = "原始零件" | | Part = "待装配部件" |
| Order = "锻造委托单" | | Order = "组装任务单" |
| Worker = "炉前工" | | Worker = "装配工" |
+---------------------------+ +---------------------------+
\ /
v v
+------------------------------------+
| 全厂上下文 |
| FactoryContext |
| |
| Part = "备件档案" |
| Order = "生产批次" |
| Worker = "雇员" |
+------------------------------------+每个 Bounded Context 内有自己的Ubiquitous Language(通用语言)——这个上下文内的团队(业务人员 + 开发人员)使用的统一术语。
// 锻造上下文中的 Part
// 这里的 Part 只讨论"毛坯件"概念
package com.forge.boundedcontext;
public class Part {
private PartId id;
private Material material; // 原料类型
private ForgingSpec specification; // 锻造规格
private HeatTreatStatus heatStatus;
public void forge(ForgePlan plan) {
// 毛坯件锻造逻辑
if (material.getHardness() > plan.getMaxHardness()) {
throw new ForgingException("材料硬度超出锻造能力");
}
this.heatStatus = HeatTreatStatus.PREHEATED;
this.specification = plan.getResultSpec();
}
}// 组装上下文中的 Part
// 这里的 Part 讨论的是"备好的零件,可以拼装"
package com.assembly.boundedcontext;
public class Part {
private PartId id;
private AssemblySchema schema; // 组装图纸位置
private FitTolerance tolerance; // 配合公差
private Alignment alignment;
public void installInto(Assembly assembly, SlotPosition position) {
// 零件安装逻辑
if (!alignment.canFit(position, tolerance)) {
throw new AssemblyException("零件无法安装到指定位置");
}
assembly.addPart(this, position);
}
}同一个 PartId 在两个上下文中的含义、属性和行为完全不同。它们不该共享一个 Part 类。
Context Map(上下文地图) 记录了 Bounded Context 之间的关系:
| 关系类型 | 含义 |
|---|---|
| Partnership | 两个团队协调变更 |
| Shared Kernel | 共享部分核心模型 |
| Customer-Supplier | 上游决定,下游适配 |
| Conformist | 下游完全跟随上游模型 |
| Anti-Corruption Layer | 下游翻译上游的模型 |
| Separate Ways | 两个上下文完全独立 |
第二层:战术模式——Entity、Value Object、Aggregate
战略设计划清了界限,战术设计给出了界限内的建模工具。
Entity(实体)——有唯一标识的对象。它的 identity 重要,属性可以变。
// Entity: 有 ID 的对象
public class Forge {
private final ForgeId id; // 身份标识——不可变
private String name;
private Temperature currentTemp;
private ForgeStatus status;
public Forge(ForgeId id, String name) {
this.id = id;
this.name = name;
this.status = ForgeStatus.COLD;
this.currentTemp = Temperature.roomTemp();
}
public void heatUp(Temperature target) {
if (target.isAbove(maxOperatingTemp())) {
throw new IllegalStateException("目标温度超过安全上限");
}
this.currentTemp = target;
this.status = ForgeStatus.HEATING;
}
// 没有 setId()——ID 永不改变
public ForgeId getId() { return id; }
}Value Object(值对象)——没有唯一标识,只靠属性值区分。两个值相等即认为相同。不可变。
// Value Object: 没有 ID,不可变
public final class Temperature {
private final double celsius;
private Temperature(double celsius) {
this.celsius = celsius;
}
public static Temperature ofCelsius(double value) {
return new Temperature(value);
}
public static Temperature ofFahrenheit(double value) {
return new Temperature((value - 32) * 5.0 / 9.0);
}
public static Temperature roomTemp() {
return new Temperature(25.0);
}
public boolean isAbove(Temperature other) {
return this.celsius > other.celsius;
}
public double getCelsius() { return celsius; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Temperature that)) return false;
return Double.compare(celsius, that.celsius) == 0;
}
@Override
public int hashCode() {
return Double.hashCode(celsius);
}
}判断一个概念应该是 Entity 还是 Value Object: "如果两个对象属性相同,它们应该被视为同一个对象吗?" 是 -> Value Object。否 -> Entity。
- 两个 Temperature(1000°C) 在系统中就是同一个温度值。Value Object。
- 两个 Forge("1号炉") — 它们是不同的炉子,即使名字相同。Entity。
Aggregate(聚合)——一组对象的组合,作为一个数据变更的原子单位。聚合由一个根实体(Aggregate Root)作为唯一入口。
// Aggregate Root: 一个工单聚合
public class WorkOrder {
private final WorkOrderId id;
private List<ForgingStep> steps;
private WorkOrderStatus status;
// 聚合根是唯一外部可访问的入口
// steps 不能从外部直接修改
public void addStep(ForgingStep step) {
if (status != WorkOrderStatus.DRAFT) {
throw new IllegalStateException("只能修改草稿状态的工单");
}
this.steps.add(step);
}
public void submit() {
if (steps.isEmpty()) {
throw new IllegalStateException("工单必须包含至少一个锻造步骤");
}
this.status = WorkOrderStatus.SUBMITTED;
}
public WorkOrderId getId() { return id; }
// 不暴露 List<ForgingStep> ——聚合内部封装
public List<ForgingStep> getSteps() {
return Collections.unmodifiableList(steps);
}
}聚合的设计原则:
- 一致性边界 — 聚合内所有操作满足事务一致性。需要 "要么都成功,要么都不改" 的数据放在同一个聚合内。
- 小聚合 — 聚合越大,并发冲突越多,性能越差。一个聚合通常不超过 10-15 个实体。
- 通过根访问 — 外部只能通过聚合根操作内部对象,不能绕过根直接修改内部。
Repository(仓储)——聚合的持久化接口。每个聚合对应一个 Repository。
public interface WorkOrderRepository {
WorkOrder findById(WorkOrderId id);
void save(WorkOrder workOrder);
void delete(WorkOrderId id);
}Repository 只操作 Aggregate Root,不暴露底层存储细节。
Domain Event(领域事件)——领域内发生的重要事件,其他 Bounded Context 可以订阅。
// 领域事件
public record WorkOrderSubmittedEvent(
WorkOrderId workOrderId,
List<ForgingStep> steps,
Instant occurredAt
) {}
// 在聚合中发布事件
public class WorkOrder {
private final List<DomainEvent> events = new ArrayList<>();
public void submit() {
if (steps.isEmpty()) throw new IllegalStateException(...);
this.status = WorkOrderStatus.SUBMITTED;
this.events.add(new WorkOrderSubmittedEvent(
id, steps, Instant.now()));
}
public List<DomainEvent> popEvents() {
var result = List.copyOf(events);
events.clear();
return result;
}
}第三层:DDD 与六边形架构的关系
第 12 章讲的六边形架构和 DDD 是天然的互补关系:
| 六边形架构关注 | DDD 关注 |
|---|---|
| 技术边界: 核心 vs 外部 | 业务边界: 每个 Bounded Context |
| Ports 适配外部世界 | Repository 是持久化端口 |
| 端口作为抽象接口 | Aggregate 是端口操作的对象 |
| Adapter 层实现外部 I/O | Infrastructure 层实现持久化 |
在代码结构上,DDD 的战术模式完美映射到六边形的核心层:
core/ ← 六边形核心 (无框架依赖)
domain/ ← DDD 领域层
model/
WorkOrder.java ← Aggregate Root
ForgingStep.java ← Value Object
WorkOrderId.java ← Value Object
event/
WorkOrderSubmittedEvent.java ← Domain Event
service/
SchedulingService.java ← Domain Service
port/
WorkOrderRepository.java ← Repository (Port)
adapter/ ← 六边形适配器层
db/
JpaWorkOrderRepository.java ← Repository 的 JPA 实现
event/
KafkaEventPublisher.java ← 事件的 Kafka 发布
bootstrap/ ← 依赖注入组装
BeanConfig.java六边形架构给了你"核心对外部一无所知"的干净边界。DDD 给了你"核心内怎么建模"的战术工具箱。两个一起用,你的核心是: 纯净的业务模型 + 适配所有外部世界的端口。
第四层:Anti-Corruption Layer(防腐层)
两个系统之间的术语冲突不可避免,尤其是当你的系统需要对接一个"老系统"(legacy system)时。老系统说 "CUST" 和 "ORD",你的系统说 "Customer" 和 "WorkOrder"。直接对接,你被老系统的混乱模型污染。
Anti-Corruption Layer (ACL) 是一个翻译层——位于你的 Bounded Context 和外部系统之间。它把外部系统的模型翻译成你的 Ubiquitous Language。
你的系统 (Bounded Context) 外部老系统
+-------------------+ +----------------+
| WorkOrder | | ORD |
| Customer | ← ACL → | CUST |
| ForgingPlan | | PLAN_TABLE |
+-------------------+ +----------------+// ACL: 把老系统的 ORD 翻译成 WorkOrder
@Component
public class LegacyOrderTranslator {
private final LegacyApiClient legacyClient;
public WorkOrder toWorkOrder(String legacyOrderId) {
LegacyOrder legacyOrder = legacyClient.fetchOrder(legacyOrderId);
// 翻译: 老系统的数据 → 你的领域模型
return new WorkOrder(
new WorkOrderId(legacyOrder.getOrderNumber()),
translateItems(legacyOrder.getLineItems()),
translateCustomer(legacyOrder.getCustomerCode())
);
}
private Customer translateCustomer(String custCode) {
LegacyCustomer legacy = legacyClient.fetchCustomer(custCode);
return new Customer(
new CustomerId(legacy.getCustId()),
legacy.getCustName(),
legacy.getCustAddress()
);
}
private List<ForgingStep> translateItems(List<LegacyLineItem> items) {
return items.stream()
.map(item -> new ForgingStep(
new PartId(item.getPartCode()),
Temperature.ofCelsius(item.getRequiredTemp())
))
.toList();
}
}防腐层的典型用途:
- 对接第三方 API — 供应商 A 的订单模型翻译成你的统一模型
- 对接老系统 — COBOL 系统的 "CUSTOMER_MASTER" 表翻译成你的 Customer 模型
- Bounded Context 之间 — 锻造上下文的 Part 翻译成组装上下文的 Part
ACL 放在六边形架构的适配器层,它是一个特殊的出站适配器。
常见陷阱
陷阱一:把数据库表当做聚合。 "用户表就是 User 聚合"——错了。聚合是业务概念,不是数据概念。一个聚合可能包含多张表的数据,一张表的数据也可能跨越多个聚合。
陷阱二:Bounded Context 切得太碎。 一个上下文只有一个 Entity。导致上下文之间相互调用极其频繁。上下文应该有足够的业务自治性,通常一个微服务对应 1-3 个 Bounded Context 就够了。
陷阱三:Ubiquitous Language 只写在代码里,不在团队沟通中使用。 你和业务开会时说 "订单",代码里写 "Order"——但团队内部说 "那个单子"、"这个单"。口头用语和代码术语不一致,最终代码里的概念会慢慢跑偏。
陷阱四:DDD 是从技术出发的。 "我们要用微服务了,必须上 DDD"——这是本末倒置。DDD 从业务建模出发,它的输出是清晰的概念边界。微服务只是实现这种边界的部署形式之一。模块化单体同样可以用 DDD。
通关挑战
- 热身:在你的项目里找出一个"一个词多个含义"的场景(如 status、type、state),画出它属于几个不同的 Bounded Context。
- 挑战:为一个中型子系统做 DDD 建模。画出 Context Map(至少 3 个 Bounded Context),每个上下文内列出 Ubiquitous Language 的核心术语。识别 Entity 和 Value Object,画出一个聚合的组成。
- 挑战:为你的项目里对接的一个老系统或第三方 API 实现 Anti-Corruption Layer。写一个 Translator 类,把外部模型的全部数据转换成你的领域模型。
验收标准
- 你能区分 Bounded Context 和 Subdomain 的区别
- 你能为一个业务场景划分至少 3 个 Bounded Context
- 你能识别一个聚合根和它的内部实体/值对象
- 你能解释 DDD 中的 Repository 和六边形架构中的 Port 的关系
- 你能写一个最简单的 Anti-Corruption Layer
常见卡点
- 不知道 Entity 和 Value Object 怎么分。 试这个判断: "如果两个对象所有属性值相同,但你认为它们是不同的——那是 Entity。如果完全相同就是同一个——那是 Value Object。"
- 不知道聚合切多大。 一个简单的判断: 如果两件事不必在同一个事务中完成,它们就不该在同一个聚合里。
现在不需要理解
- Event Storming 工作坊的完整流程
- DDD 与 CQRS / Event Sourcing 的组合模式(第15章已覆盖)
- Domain Service 与 Application Service 的区别辨析
旅人笔记
DDD 不是代码技巧,是思维工具。战略设计让你看清业务的全貌地图——哪里有一致语言,哪里需要翻译层。战术设计给你在边界内建模的手艺——Entity 负责身份,Value Object 负责数据,Aggregate 负责一致性,Repository 负责持久化,Domain Event 负责上下文间通信。第 12 章的六边形架构提供了技术上的内外边界,DDD 提供了业务上的概念边界——两章合在一起,你的系统从技术到概念的每一层都有清晰的分界线。
下一站预告
第六卷从软件构造的静态检查出发,走过设计模式、架构演进、微服务、事件驱动、AI 系统模式,再到 API 治理和领域驱动设计——你从写代码走到了设计系统。工匠之都的训练到此完成。前方的路通往 Vol 7: 分布式系统——林将军的远征军需要你。带上你的全套工具,去解决更大的问题。