3. 需求分析入门
上一节我们聊了需求分析怎么从模糊的业务描述落到功能列表。但说实话,光有功能列表还不够——你拿到的很可能是一堆散落的「需求碎片」。怎么把这些碎片拼成一张可落地的架构蓝图?我个人习惯的做法,是引入领域驱动设计(DDD)的思路。
别被「领域驱动」这四个字吓到。说白了,DDD 就是一套帮我们把业务语言翻译成代码语言的方法论。我最早接触 DDD 是在一个电商中台项目里,当时团队被各种「订单」、「子订单」、「父订单」的概念绕得晕头转向。后来我们用 DDD 重新梳理了一遍,才发现很多所谓的「订单」根本不在同一个业务上下文里。
为什么需要限界上下文?
先问一个问题:一个电商系统里,「商品」这个词在不同模块里含义一样吗?
在库存模块,「商品」关心的是 SKU、库存数量、仓库位置。在营销模块,「商品」关心的是价格、促销标签、活动分组。在物流模块,「商品」关心的是重量、体积、易碎等级。你看,同一个词,在不同场景下关注的点完全不同。
这就是限界上下文要解决的问题——给每个概念划定一个清晰的边界,边界内保持概念的一致性,边界外允许概念有不同的含义。
我在一个金融项目中遇到过这样的坑:团队把「客户」这个概念放在一个全局模型里,结果风控、客服、销售三个部门对「客户」的定义完全不一样。风控要的是「黑名单客户」,客服要的是「投诉客户」,销售要的是「潜在客户」。强行用一个模型去覆盖所有场景,最后代码里全是 if-else 判断,维护成本直线飙升。
如何识别限界上下文?
识别限界上下文,我一般分三步走:
- 找业务事件——梳理业务流程中发生了哪些关键事件。比如「订单已创建」、「支付已确认」、「库存已扣减」。
- 找业务角色——谁在操作?谁在决策?比如「采购员」、「仓管员」、「客服专员」。
- 找业务规则——不同角色对同一数据的处理规则是否一致?如果不一致,大概率需要拆成不同的上下文。
举个例子,一个简单的在线教育系统,我通常会识别出这几个限界上下文:
| 限界上下文 | 核心职责 | 关键概念 |
|---|---|---|
| 课程管理 | 课程创建、章节编排、内容上传 | 课程、章节、课件 |
| 学习中心 | 学员选课、学习进度、作业提交 | 学员、选课记录、学习进度 |
| 考试系统 | 试卷生成、在线考试、自动判分 | 试卷、试题、答题卡 |
| 支付结算 | 订单生成、支付对接、退款处理 | 订单、支付单、退款单 |
你看,同样是「课程」,在课程管理上下文里它是被编辑的对象,在学习中心上下文里它是被消费的内容。分开建模,各自演进,互不干扰。
实体 vs 值对象:别再傻傻分不清
识别出限界上下文之后,下一步就是建模。建模时最基础也最容易搞混的两个概念,就是实体和值对象。
实体(Entity):有唯一标识,且会随着时间变化。比如一个「用户」,不管他改了多少次名字、换了多少次手机号,他还是那个用户——因为他的用户 ID 没变。
值对象(Value Object):没有唯一标识,只描述某个属性的值。比如「地址」,如果两个地址的省、市、区、街道完全一样,那它们就是同一个地址,不需要区分谁是谁。
我曾经在一个项目中看到有人把「金额」设计成实体,给每个金额配了一个 ID。这明显不合理——100 块钱就是 100 块钱,不需要区分「100 块钱 A」和「100 块钱 B」。金额应该是值对象,不可变,直接比较数值即可。
实战建模:一个订单系统的例子
我们拿一个简化版的订单系统来练练手。假设我们在「交易上下文」里建模:
实体:
- 订单(Order)——有订单号(唯一标识),状态会从「待支付」变成「已支付」再变成「已发货」。
- 买家(Buyer)——有用户 ID,个人信息可以修改。
值对象:
- 订单项(OrderItem)——描述买了什么商品、数量多少、单价多少。两个订单项如果商品 ID、数量、单价都一样,那它们就是相同的。
- 收货地址(ShippingAddress)——省市区街道+收件人+电话,完全一致就是同一个地址。
- 金额(Money)——金额+币种,不可变。
代码示例(Java 风格,仅做示意):
// 实体:订单
public class Order {
private OrderId orderId; // 值对象作为 ID
private Buyer buyer; // 实体引用
private List<OrderItem> items; // 值对象列表
private ShippingAddress address; // 值对象
private OrderStatus status; // 枚举,也是值对象
public void changeAddress(ShippingAddress newAddress) {
// 直接替换,不修改原对象
this.address = newAddress;
}
}
// 值对象:金额
public final class Money {
private final BigDecimal amount;
private final String currency;
public Money(BigDecimal amount, String currency) {
this.amount = amount;
this.currency = currency;
}
// 只提供 getter,不提供 setter
// 运算返回新对象
public Money add(Money other) {
// 校验币种一致
return new Money(this.amount.add(other.amount), this.currency);
}
}
注意看,Order 的 changeAddress 方法直接替换了整个地址对象,而不是修改地址内部的属性。这就是值对象不可变性的体现。
限界上下文与实体/值对象的关系
最后,我用一张图来总结这三者的关系:
这张图展示了三个限界上下文各自维护自己的实体和值对象。注意看,不同上下文之间通过虚线连接,表示它们之间存在协作关系,但各自的数据模型是独立的。这就是 DDD 的核心思想——每个上下文内部保持高内聚,上下文之间通过事件或接口进行低耦合通信。