1. 为什么我们需要DDD?
刚接触DDD(领域驱动设计)时,我总觉得这是个高大上的概念,直到接手了一个电商订单系统改造项目。当时系统已经迭代了三年,代码量超过10万行,新来的同事要看懂一个下单流程得花两周时间。最要命的是,产品经理说"优惠券",开发理解成"折扣码",测试以为是"促销码"——同一个业务概念,在不同角色嘴里变成了三个名字。
这就是DDD要解决的核心问题:在业务复杂度爆炸增长时,如何让系统保持可理解、可维护。传统MVC架构在这种场景下会暴露出几个典型问题:
- 业务逻辑碎片化:一个完整的优惠券使用逻辑,可能分散在Controller、Service、Dao多个层级
- 贫血模型泛滥:订单对象变成只有get/set的数据容器,业务规则以"服务+ifelse"的形式寄生在Service层
- 沟通成本剧增:需求评审会上,业务方说的"库存"和开发理解的"inventory"可能根本不是一回事
我在重构订单系统时,先用DDD的统一语言(Ubiquitous Language)方法,拉着产品、运营、测试一起开了三天workshop。大家把业务术语都写在白板上,最终确定:
- "优惠券"统一叫Coupon
- "库存"专指可销售库存(不包括预占库存)
- "订单"特指主订单(与子订单明确区分)
这个术语表后来成为我们团队的"宪法",新需求文档必须严格使用这些定义。三个月后,需求误解率下降了70%,这就是DDD的第一个魔法——用业务语言写代码。
2. DDD的战术工具箱
2.1 领域模型三剑客
在技术落地时,DDD提供了三种核心建模元素:
**实体(Entity)**就像你家的宠物狗——它有唯一ID(狗牌号),会成长变化(从幼犬到成年),但无论毛色怎么变,ID始终不变。在代码中:
public class Order extends Entity<Long> { private OrderStatus status; private List<OrderItem> items; public void cancel() { this.status = OrderStatus.CANCELLED; this.addDomainEvent(new OrderCancelledEvent(this.id)); } }**值对象(Value Object)**则像你家的家具——不需要唯一标识,完全由属性定义。换个新沙发只要款式一样,对使用者没区别:
public class Address { private final String city; private final String street; // 没有ID字段 // 所有属性final确保不可变 }**聚合根(Aggregate Root)**是更高级别的封装,比如"订单"聚合根会包含订单项、物流信息等子实体,但外部只能通过订单根来修改内部状态:
// 正确做法:通过聚合根操作 order.addItem(product, quantity); // 错误做法:直接操作子实体 order.getItems().add(new Item(...)); // 破坏了封装性2.2 分层架构的进化
传统三层架构在DDD中演进为更清晰的分工:
用户界面层 ↓ 应用层(编排业务流程) ↓ 领域层(核心业务逻辑) ↑ 基础设施层(技术实现)这个依赖关系的关键在于领域层不依赖任何其他层。比如订单支付逻辑应该纯粹用业务语言编写,不关心支付方式是支付宝还是微信。我在项目中曾犯过错误,把微信支付SDK直接引入领域服务,结果后来换支付平台时,不得不重写核心业务逻辑。
正确的做法是通过依赖倒置:
// 领域层定义接口 public interface PaymentService { PaymentResult pay(Order order, Money amount); } // 基础设施层实现 public class WechatPaymentService implements PaymentService { // 具体对接微信API }3. 从事件风暴到代码
3.1 建模实战:电商订单履约
去年双十一前,我们用DDD重构了订单履约系统。整个过程就像破案:
事件风暴工作坊:把业务方、开发、测试关在会议室8小时,用便利贴梳理出关键事件:
- "订单已创建"
- "库存已预留"
- "支付已超时"
- "订单已自动取消"
识别聚合边界:发现"订单"和"库存"应该属于不同限界上下文,因为:
- 它们有独立生命周期
- 变更频率不同(库存秒级变化,订单分钟级)
- 由不同团队维护
定义上下文映射:最终采用"发布/订阅"模式协调两个上下文:
graph LR 订单上下文 -- OrderCreated --> 消息队列 消息队列 --> 库存上下文
3.2 代码落地规范
在工程化实践中,我们制定了严格的代码规范:
目录结构:
├── order-service │ ├── application # 应用服务 │ ├── domain # 领域模型 │ │ ├── model # 聚合根/实体 │ │ └── service # 领域服务 │ └── infrastructure # 技术实现聚合根示例:
public class Order extends BaseAggregateRoot { private OrderId id; private List<OrderItem> items; private OrderStatus status; public static Order create(UserId userId, List<OrderItem> items) { // 校验业务规则 if (items.isEmpty()) { throw new BusinessException("订单项不能为空"); } Order order = new Order(); order.id = OrderId.next(); order.items = new ArrayList<>(items); order.status = OrderStatus.CREATED; order.addDomainEvent(new OrderCreatedEvent(order.id)); return order; } public void cancel() { if (!status.canCancel()) { throw new BusinessException("当前状态不可取消"); } this.status = OrderStatus.CANCELLED; addDomainEvent(new OrderCancelledEvent(id)); } }仓库实现:
public interface OrderRepository { Order findById(OrderId id); void save(Order order); } @Repository public class OrderRepositoryImpl implements OrderRepository { @Override public void save(Order order) { // 1. 保存聚合根状态 OrderPO po = convertToPO(order); orderDao.save(po); // 2. 处理领域事件 for (DomainEvent event : order.getEvents()) { eventPublisher.publish(event); } } }
4. 踩坑指南
4.1 性能陷阱
初期我们严格遵循"一个事务只修改一个聚合根",结果在订单支付流程中遇到了性能问题:
- 支付成功需要:
- 更新订单状态
- 扣减库存
- 增加用户积分
- 这三个操作涉及不同聚合根,如果完全分开会导致分布式事务问题
解决方案:
- 引入Saga模式,将大事务拆分为可补偿的子任务
- 关键代码示例:
// Saga协调器 public class PaymentSaga { public void execute(PaymentContext context) { try { orderService.confirm(context.getOrderId()); inventoryService.deduct(context.getSku(), context.getQuantity()); userService.addPoints(context.getUserId(), context.getPoints()); } catch (Exception e) { // 触发补偿操作 orderService.cancel(context.getOrderId()); inventoryService.restore(context.getSku(), context.getQuantity()); } } }
4.2 过度设计警告
有个团队在初期把每个字段都建模成值对象,导致简单查询变得复杂:
// 过度设计 public class Order { private OrderId id; private OrderNumber number; private UserId userId; private OrderStatus status; // ...其他20+值对象 } // 适度设计 public class Order { private Long id; private String orderNo; private Long userId; private String status; // 基础类型直接使用 }经验法则:
- 会被多个实体引用的概念适合做成值对象(如Money)
- 仅在当前实体使用的简单属性可直接用基础类型
- 优先保持代码可读性,必要时才引入DDD模式
5. 测试策略
DDD项目特别适合采用契约测试:
- 定义领域服务的接口契约
- 编写消费者测试(验证调用方期望)
- 编写提供者测试(验证实现方承诺)
示例测试用例:
public class OrderServiceTest { @Test public void should_emit_event_when_cancelling_order() { // 给定 Order order = Order.create(...); orderRepository.save(order); // 当 orderService.cancel(order.getId()); // 则 DomainEvent event = eventStore.findLatest(order.getId()); assertThat(event).isInstanceOf(OrderCancelledEvent.class); } }这种测试方式确保了:
- 业务意图明确表达
- 技术实现不影响测试用例
- 领域模型的行为可验证
6. 演进式设计
最后分享一个真实案例:我们有个促销系统最初设计为:
Promotion (聚合根) ├── Rule └── Reward随着业务发展,规则引擎变得复杂,我们通过四步完成演进:
- 识别出Rule逐渐成为独立子域
- 使用防腐层隔离新旧模型
- 逐步将Rule相关逻辑迁移到新限界上下文
- 最终架构变为:
促销上下文 --调用--> 规则引擎上下文 --触发--> 奖励发放上下文
这个过程就像城市改造——不是推倒重来,而是分区重建,确保业务始终正常运行。