战术设计解决的是"一个团队怎么写代码"的问题。战略设计解决的是"多个团队怎么一起写代码"的问题。
前面两篇文章,我们走了两条路。
把三层架构那个2000行的Service拆开,用四层架构+充血模型,让业务规则各归其位。发现类爆炸了------一个功能从1个类变成10个类,项目从50个类膨胀到152个。然后用桥接模式、视图模式、充血模型,把不必要的类砍掉了一半。
两篇文章解决的都是 "一个团队、一个项目" 内部的问题。
现在假设一个更大的场景:你的系统已经大到需要多个团队并行开发了。
订单团队、商品团队、库存团队、支付团队、用户团队......每个团队都有自己的代码库、自己的发布节奏、自己的领域专家。
这时候,前面两篇文章里的所有技巧------充血模型、桥接模式、视图模式------都还在用,但已经不够了。
因为问题变了。
从前的问题是:"一个Service里逻辑太乱怎么办?"
现在的问题是:"订单上下文里的'商品'和商品上下文里的'商品',是同一个东西吗?"
这不是代码层面的问题,这是认知层面的问题。
DDD把前一种问题交给战术设计 (前两篇讲的就是这个),把后一种问题交给战略设计。
读完这篇文章,你将获得:
✅ 理解限界上下文 是什么------为什么"商品"在订单里和库存里不是同一个概念
✅ 学会用事件风暴 划分限界上下文------把大系统切成多个小模块
✅ 掌握上下文映射 的三种核心模式------多个上下文之间怎么协作
✅ 看清战略设计 和战术设计的分工------什么时候该用哪个
一、一个让你头疼的真实场景
你所在的电商系统已经运行了三年。现在有五个团队并行开发:
- 订单团队:负责下单、订单状态管理
- 商品团队:负责商品上架、分类、详情
- 库存团队:负责库存扣减、补货
- 支付团队:负责支付流程、退款
- 用户团队:负责注册、登录、积分
某天,订单团队要给"订单详情页"加一个字段:productCategory(商品分类)。
订单团队的开发说:"很简单,我在OrderItem里加一个category字段就行了。"
商品团队的开发看到了,说:"等等!'分类'在我们这边有三级结构,你那个category是个字符串,跟我们的Category实体根本不是一回事。你不能这么用。"
订单团队说:"我只是想展示一下分类名称而已,不需要你们那三级结构。"
商品团队说:"那你至少应该调用我们的API来获取分类名称,而不是自己存一份。"
两个团队吵起来了。
谁对?
都没错。
订单团队想要的是"展示用"的分类名称------一个字符串就够了。商品团队维护的是"业务用"的分类体系------有层级、有属性、有规则。
它们说的"分类",在不同的上下文里有不同的含义。
这个"上下文",就是DDD战略设计的核心概念------限界上下文(Bounded Context)。
二、限界上下文:给"同一个词"画一个边界
2.1 什么是限界上下文?
回到刚才的例子。
在订单上下文 里,"商品"是什么?是一个OrderItem------有商品ID、商品名称、单价、数量、小计金额。它是一个交易凭证,记录的是"下单那一刻"的商品快照。订单上下文不关心商品的库存、不关心商品的分类层级、不关心商品的上下架状态------它只关心"这个商品当时卖多少钱、买了多少个"。
在商品上下文 里,"商品"是什么?是一个Product------有SKU、有分类、有详情描述、有规格参数、有上下架状态。它是一个信息实体,记录的是"商品本身是什么"。商品上下文不关心某个商品被谁买过、买了几次------它只关心"这个商品现在是什么状态、有什么属性"。
在库存上下文 里,"商品"又是什么?是一个InventoryItem------有商品ID、有可用库存、有锁定库存、有在途库存。它是一个数量凭证,记录的是"这个商品还有多少能卖"。库存上下文不关心商品的分类、不关心商品的价格------它只关心"数量"。
同一个词"商品",在三个上下文里有三种不同的含义、三种不同的字段、三种不同的行为。
如果你强行把它们塞进同一个Product类里,结果就是:
java
// 一个试图满足所有人的"上帝类"
public class Product {
// 商品上下文需要的
private String name;
private String description;
private Category category;
private List<Spec> specs;
// 订单上下文需要的
private BigDecimal priceAtOrder; // 订单里的价格是快照!
private Integer quantityAtOrder; // 订单里的数量是快照!
// 库存上下文需要的
private Integer availableStock;
private Integer lockedStock;
private Integer inboundStock;
// 还有更多......
}
这个类会变得越来越臃肿,越来越难以理解。改一个字段,不知道会影响哪个上下文。这就是大泥球 (Big Ball of Mud)------所有东西混在一起,谁也说不清边界在哪里。
限界上下文就是用来解决这个问题的。 它给每个上下文画了一个清晰的边界,边界内使用一套独立的模型。订单上下文有自己的OrderItem,商品上下文有自己的Product,库存上下文有自己的InventoryItem。它们可以共享同一个数据库物理表(通过不同的映射),也可以使用完全独立的数据库------但代码层面,它们是彻底分开的。
2.2 怎么划分限界上下文?
限界上下文不是拍脑袋定的。DDD社区有一套成熟的方法------事件风暴 (Event Storming)。
事件风暴怎么做?
简单来说,就是拉上领域专家、产品经理、开发人员,围在一面巨大的白板前,用便利贴把业务流程中的事件(Event)全部贴出来。
比如一个"用户下单"的流程:
- 用户提交订单 → 订单已创建
- 系统扣减库存 → 库存已扣减
- 系统发起支付 → 支付已发起
- 用户完成支付 → 支付已完成
- 系统更新订单状态 → 订单已支付
- 系统增加用户积分 → 积分已增加
每一个"已 "字结尾的句子,就是一个领域事件。
然后,把这些事件按照业务阶段分组。比如:
- "订单已创建""订单已支付""订单已发货""订单已完成" → 订单上下文
- "库存已扣减""库存已恢复""库存已补货" → 库存上下文
- "支付已发起""支付已完成""退款已完成" → 支付上下文
分完组之后,再给每个组起一个名字------这就是限界上下文 。
划分的核心理念 :限界上下文应该与子域(Subdomain)一一对应。子域是业务层面的划分(订单域、库存域、支付域),限界上下文是解决方案层面的划分(订单上下文、库存上下文、支付上下文)。
简单说:子域回答"业务上分几块",限界上下文回答"代码上怎么切"。
2.3 限界上下文的三种类型
| 类型 | 含义 | 例子 | 投入策略 |
|---|---|---|---|
| 核心域 | 公司最核心的竞争力,最需要投入精力的部分 | 电商的订单履约、推荐算法 | 最高优先级,最精细建模 |
| 支撑子域 | 为核心域服务的定制开发部分 | 内部的权限管理、配置中心 | 适度投入,够用就好 |
| 通用子域 | 行业通用的、可以买现成的部分 | 支付网关对接、短信发送 | 最小投入,能用就行 |
认清这三种类型,你就知道精力该花在哪里。核心域值得你用充血模型、用事件溯源、用最精细的设计。通用子域,调个API就行了,别在上面过度设计。
三、上下文映射:限界上下文之间怎么"说话"?
划分完限界上下文之后,下一个问题来了:它们之间怎么协作?
订单上下文需要知道库存够不够,库存上下文需要知道订单有没有支付,支付上下文需要知道订单金额是多少......它们虽然被切开了,但终究还是一个系统,免不了交互 。
描述这些交互关系的方式,叫做上下文映射 (Context Mapping)。
上下文映射就是一张"系统间的关系地图"------告诉你谁依赖谁、怎么依赖、依赖到什么程度。
3.1 三种最常用的映射模式
DDD定义了九种上下文映射模式。但日常开发中,90%的场景只用得到三种 。
模式一:防腐层(ACL------Anti-Corruption Layer)
场景:你的上下文要依赖一个"外部"上下文------可能是遗留系统,可能是第三方服务,可能是另一个团队维护的系统。这个外部系统的模型很"脏",跟你自己的纯净模型不匹配。
做法 :在你们两个上下文之间,建一个翻译层。外部系统的"脏模型"进来,被翻译成你内部的"干净模型"。
java
// 文件: infrastructure/acl/ExternalProductTranslator.java
// 这是防腐层------把外部系统的模型翻译成内部模型
@Component
public class ExternalProductTranslator {
// 外部系统的商品模型(可能是XML、可能是旧版的DTO)
public InternalProduct toInternal(LegacyProduct external) {
// 翻译逻辑:把外部乱七八糟的字段映射到内部干净的模型
return new InternalProduct(
external.getProductId(),
external.getProductName(), // 外部叫ProductName,内部叫name
parseCategory(external.getCategoryCode()) // 外部的分类代码转成内部的对象
);
}
}
防腐层的价值 :当外部系统发生变化时(比如遗留系统升级、第三方API改版),你只需要改防腐层,内部的领域模型完全不受影响。
防腐层就像两个国家之间的翻译官------对方说什么语言不重要,翻译官会帮你转成你听得懂的话。
模式二:开放主机服务(OHS------Open Host Service)
场景:你的上下文是"核心域",有很多下游上下文需要调用你。你不想让每个下游都用不同的方式调用------那样你会疯掉。
做法 :你定义一套公开的、有文档的、稳定的API ,让所有下游统一通过这套API来调用你。
java
// 文件: interfaces/openapi/ProductOpenApi.java
// 这是开放主机服务------所有下游统一通过这里调用商品上下文
@RestController
@RequestMapping("/open-api/v1/products")
public class ProductOpenApi {
@Autowired private ProductAppService productService;
// 所有下游统一用这个接口查商品信息
@GetMapping("/{productId}")
public ProductOpenDto getProduct(@PathVariable Long productId) {
return productService.getProduct(productId);
}
}
开放主机服务的价值:你只需要维护一套API,所有下游都用同样的方式调用。新增下游不需要你额外适配。
开放主机服务就像一个机场------所有航班都用同样的跑道起降,而不是每个航空公司修一条自己的跑道。
模式三:防腐层 + 开放主机服务(最经典的组合)
在实际项目中,这两个模式通常是成对出现的。
- 上游上下文 提供开放主机服务(OHS)------定义一套稳定的公开API
- 下游上下文 实现防腐层(ACL)------调用上游API,并把结果翻译成自己的模型
java
// 上游:订单上下文提供开放主机服务
// 文件: order/interfaces/openapi/OrderOpenApi.java
@RestController
@RequestMapping("/open-api/v1/orders")
public class OrderOpenApi {
@GetMapping("/{orderId}/status")
public OrderStatusDto getOrderStatus(@PathVariable Long orderId) {
// 返回订单状态
}
}
// 下游:库存上下文实现防腐层
// 文件: inventory/infrastructure/acl/OrderStatusTranslator.java
@Component
public class OrderStatusTranslator {
@Autowired private OrderOpenApiClient client; // 调用上游的OHS
public InventoryOrderStatus toInternal(OrderStatusDto external) {
// 把订单上下文的"PAID"翻译成库存上下文能理解的"已支付可发货"
return InventoryOrderStatus.fromExternal(external);
}
}
这样做的效果:
- 上游变了 → 只改OHS的接口定义,下游的ACL跟着适配
- 下游变了 → 只改自己的ACL翻译逻辑,上游完全不知道
- 双方各自独立演进,互不干扰
3.2 一张表看懂三种模式
| 模式 | 谁用 | 解决什么问题 | 一句话解释 |
|---|---|---|---|
| 防腐层(ACL) | 下游上下文 | 防止上游的"脏模型"污染自己的纯净模型 | 翻译官 |
| 开放主机服务(OHS) | 上游上下文 | 让所有下游用统一的方式调用自己 | 公共机场 |
| ACL + OHS | 上下游配合 | 上游稳定输出,下游安全接入 | 标准接口 + 本地翻译 |
四、战术设计 vs 战略设计:两张地图
到这里,我们可以把两篇文章的内容串起来了。
战术设计 (前两篇)解决的是"一个限界上下文内部怎么写代码":
| 战术设计工具 | 解决什么问题 |
|---|---|
| 四层架构 | 把Service拆成应用层和领域层 |
| 充血模型 | 把逻辑从Service下沉到实体 |
| 值对象 | 封装细粒度的业务规则 |
| 聚合根 | 划定事务一致性边界 |
| 仓储模式 | 隔离领域层和基础设施层 |
战略设计 (这一篇)解决的是"多个限界上下文之间怎么协作":
| 战略设计工具 | 解决什么问题 |
|---|---|
| 限界上下文 | 给"同一个词"画语义边界 |
| 子域分类 | 识别核心域 vs 支撑域 vs 通用域 |
| 上下文映射 | 定义上下文之间的协作关系 |
| 防腐层 | 保护下游上下文不被上游污染 |
| 开放主机服务 | 让上游上下文稳定输出 |
五、战略设计怎么落地?------一个完整示例
回到开头的例子:订单团队和商品团队为"分类"吵架。
用战略设计重新看这个问题:
第一步:识别限界上下文
- 订单上下文:关注"交易快照"------下单那一刻的商品信息(名称、价格、数量)
- 商品上下文:关注"商品本身"------分类体系、规格参数、上下架状态
第二步:定义上下文映射
订单上下文需要展示商品分类名称 → 它是下游 ,商品上下文是上游。
- 商品上下文提供开放主机服务 :
/open-api/v1/products/{id}/category - 订单上下文实现防腐层:调用这个API,把返回的分类信息转成自己需要的格式
第三步:各自独立演进
- 商品上下文改了分类结构(从两级变成三级)→ 只需要改OHS的返回格式,订单上下文的ACL跟着适配
- 订单上下文想展示更多商品信息(比如增加"品牌")→ 只需要在ACL里加一个字段,商品上下文完全不知道
代码长这样:
java
// === 上游:商品上下文 ===
// 文件: product/interfaces/openapi/ProductOpenApi.java
@RestController
@RequestMapping("/open-api/v1/products")
public class ProductOpenApi {
@GetMapping("/{id}/category")
public CategoryDto getCategory(@PathVariable Long id) {
// 商品上下文自己的Category模型
Category category = productService.getCategory(id);
// 转成公开的DTO返回
return new CategoryDto(category.getId(), category.getName());
}
}
// === 下游:订单上下文 ===
// 文件: order/infrastructure/acl/ProductCategoryTranslator.java
@Component
public class ProductCategoryTranslator {
@Autowired private ProductOpenApiClient client;
// 把商品上下文的CategoryDto转成订单上下文能理解的格式
public OrderCategory toInternal(CategoryDto external) {
// 订单上下文只需要分类名称,不需要id和其他信息
return new OrderCategory(external.getName());
}
}
// 文件: order/domain/order/OrderItem.java
public class OrderItem {
private Long productId;
private String productName;
private BigDecimal price;
private Integer quantity;
private OrderCategory category; // 订单上下文自己的分类模型
// 订单上下文的业务逻辑------跟商品上下文完全无关
public Money calculateSubtotal() {
return new Money(price.multiply(BigDecimal.valueOf(quantity)));
}
}
结果:
- 两个团队各干各的,互不干扰
- "分类"这个词在两个上下文里有不同的含义和不同的代码实现------各说各话,但各不越界
- 谁都不用为对方的改动买单
六、什么时候需要战略设计?
不是所有项目都需要战略设计。判断标准很简单:
| 项目规模 | 需要什么 | 原因 |
|---|---|---|
| 1个团队,1个代码库 | 战术设计就够了 | 大家都在同一个上下文里,用同一套语言 |
| 2-3个团队,1个代码库 | 开始考虑战略设计 | 不同团队对同一概念的理解开始出现分歧 |
| 3个以上团队,多个代码库 | 必须做战略设计 | 没有限界上下文,协作会变成灾难 |
战术设计解决"怎么把代码写清楚"。战略设计解决"怎么把系统切明白"。
七、核心总结:一张表讲透战略设计
如果你没时间读完全文,读完这张表就够了。
| 概念 | 是什么 | 解决什么问题 | 一句话解释 |
|---|---|---|---|
| 限界上下文 | 语义和语境的边界 | "商品"在订单里和库存里不是同一个东西 | 给同一个词画边界 |
| 核心域 | 公司最核心的竞争力 | 知道精力该花在哪里 | 最重要的事只有一件 |
| 防腐层(ACL) | 下游翻译上游的模型 | 防止上游的"脏模型"污染自己 | 翻译官 |
| 开放主机服务(OHS) | 上游提供统一的公开API | 让所有下游用同样的方式调用 | 公共机场 |
| 上下文映射 | 描述上下文之间的关系 | 知道谁依赖谁、怎么依赖 | 系统间的关系地图 |
最后说一个可能得罪人的判断
没有战略设计的DDD,是假DDD。
很多团队学了充血模型、学了值对象、学了仓储模式,就觉得自己在做DDD了。但打开项目一看------所有的代码都在一个模块里,所有的"上下文"都混在一起,订单的Product和商品的Product是同一个类。
这叫 "战术DDD" ------只用了一半。
真正的DDD,是从战略设计开始的。先划分限界上下文,再在每一个上下文内部做战术设计。战略设计决定"切几刀",战术设计决定"每一刀怎么切" 。
如果你正在做一个需要多个团队协作的大型系统,先花两周做事件风暴,把限界上下文画出来。这比你先写三个月代码再回头重构,要划算得多。