DDD 领域驱动设计实战指南
DDD(Domain-Driven Design)是复杂业务系统的架构方法论,也是 CSDN 架构板块的高频话题。当业务逻辑复杂到"贫血模型 + 一堆 Service"写不下去时,DDD 是正解。本文从核心概念讲起,覆盖限界上下文、聚合、实体/值对象、仓储、领域服务、防腐层、落地分层与代码示例。
一、DDD 是什么
1.1 解决的问题
传统 MVC 开发的经典病:
Controller → Service(全是 if/else 和 SQL)→ Mapper贫血模型:实体只有 getter/setter,业务逻辑全堆在 Service——业务越复杂,Service 越长,谁都不敢改。
DDD 的思路:让业务逻辑回到领域模型里——先梳理业务(领域),再定技术实现。
1.2 核心思想
- 以业务为中心:技术细节(数据库、MQ)是配角;
- 统一语言:业务方和开发用同一套术语(订单、支付、库存);
- 模型驱动:代码结构反映业务结构。
1.3 DDD vs 传统分层
| 维度 | 传统(贫血) | DDD(充血) |
|---|---|---|
| 业务逻辑位置 | Service 层 | 领域模型(实体/聚合) |
| 关注点 | 数据库/接口 | 业务规则 |
| 代码结构 | 按技术分层 | 按业务模块(限界上下文) |
| 变更影响 | Service 越来越大 | 影响收敛在聚合内 |
适用:业务复杂、规则多、迭代快的中大型系统;简单 CRUD 项目用 DDD 是过度设计。
二、战略设计(划边界)
2.1 限界上下文(Bounded Context)
概念:一个业务边界,边界内使用统一的领域模型和语言。
例子(电商):
订单上下文(下单、改单、状态流转) 支付上下文(支付、退款、对账) 库存上下文(库存增减、锁定) 商品上下文(SPU/SKU、上下架)关键:同一"订单"在不同上下文含义不同——订单上下文里的订单带金额状态,物流上下文里的订单是发货单。不要全局共享一个 Order 类。
2.2 上下文映射(Context Mapping)
| 关系 | 说明 | 场景 |
|---|---|---|
| 共享内核 | 两个上下文共享部分模型 | 紧密协作 |
| 防腐层(ACL) | 隔离外部系统,翻译模型 | 对接老系统/第三方 |
| 客户方-供应方 | 上游提供能力下游消费 | 常规依赖 |
| 分离方式 | 完全独立 | 弱关联 |
2.3 防腐层(Anti-Corruption Layer)
问题:外部系统(老系统、第三方)的模型和我们的领域模型不一致,直接依赖会污染领域。
解决:加一层翻译:
我们的领域模型 ← 防腐层(适配器翻译)← 外部系统// 外部 DTO(对方系统)publicclassThirdPartyOrderDto{publicStringoid;publicStringstatusCode;}// 领域模型(我们内部)publicclassOrder{publicOrderIdid;publicOrderStatusstatus;}// 防腐层适配器:翻译@ComponentpublicclassThirdPartyOrderAdapter{publicOrdertoDomain(ThirdPartyOrderDtodto){returnOrder.rebuild(OrderId.of(dto.oid),OrderStatus.fromCode(dto.statusCode));}}三、战术设计(建模要素)
3.1 实体(Entity)
有唯一标识、有生命周期、状态会变的对象。
publicclassOrder{privateOrderIdid;// 唯一标识(值对象)privateOrderStatusstatus;// 状态会变privateMoneytotalAmount;privateList<OrderItem>items;// 业务行为(不是 getter/setter 堆砌)publicvoidpay(Moneyamount){if(status!=OrderStatus.PENDING_PAY){thrownewDomainException("订单状态不允许支付");}if(!totalAmount.equals(amount)){thrownewDomainException("支付金额不匹配");}this.status=OrderStatus.PAID;}publicvoidcancel(){if(status==OrderStatus.SHIPPED){thrownewDomainException("已发货订单不能取消");}this.status=OrderStatus.CANCELLED;}}核心:业务规则写在实体里,而不是 Service 里。
3.2 值对象(Value Object)
没有身份、由属性值定义、不可变的对象。
// 金额:值对象(不可变)publicfinalclassMoney{privatefinalBigDecimalamount;privatefinalCurrencycurrency;privateMoney(BigDecimalamount,Currencycurrency){this.amount=amount;this.currency=currency;}publicstaticMoneyof(BigDecimalamount,Currencycurrency){if(amount==null||amount.compareTo(BigDecimal.ZERO)<0){thrownewIllegalArgumentException("金额非法");}returnnewMoney(amount,currency);}publicMoneyadd(Moneyother){// 相同币种才能相加returnMoney.of(this.amount.add(other.amount),this.currency);}// 没有 setter——不可变}识别:两个值对象属性相同就相等(比较值);实体靠 id 相等(比较身份)。
3.3 聚合与聚合根(Aggregate)
聚合:一组实体和值对象的集合,作为一个整体操作;聚合根是入口。
例子(订单聚合):
Order(聚合根) ├── OrderItem(实体) ├── Money(值对象) └── OrderId(值对象)规则:
- 外部只能通过聚合根访问(不能直接操作 OrderItem);
- 事务边界 = 聚合边界(一个聚合一个事务);
- 聚合要小:一个聚合别装太多实体(订单和用户不该在一个聚合)。
publicclassOrder{// 聚合根:对外只暴露操作聚合的方法publicvoidaddItem(ProductIdproductId,Moneyprice,intcount){// 校验 + 修改内部 OrderItem}}3.4 领域服务(Domain Service)
不适合放实体里的业务(跨聚合、跨实体的流程)放领域服务。
@ServicepublicclassOrderDomainService{publicvoidplaceOrder(Orderorder,StockServicestock){// 跨聚合协调:订单聚合 + 库存聚合order.validateForPlacement();stock.lockStock(order.getItems());// 库存上下文order.markPlaced();}}3.5 仓储(Repository)
领域模型的"数据库接口"——领域层只依赖接口,不依赖 MyBatis/JPA。
// 领域层定义接口publicinterfaceOrderRepository{OrderfindById(OrderIdid);voidsave(Orderorder);}// 基础设施层实现@RepositorypublicclassOrderRepositoryImplimplementsOrderRepository{@AutowiredprivateOrderMapperorderMapper;@OverridepublicOrderfindById(OrderIdid){OrderDOdo=orderMapper.selectById(id.value());returnOrderMapper.convertToDomain(do);// 转换}}为什么这样设计:领域层不关心数据库——换存储/换实现不动业务代码。
四、落地分层架构
4.1 经典 DDD 四层
接口层(Controller、DTO、组装) ↓ 应用层(Application Service:用例编排、事务、权限) ↓ 领域层(实体、值对象、聚合、领域服务、仓储接口)★核心 ↓ 基础设施层(Mapper、Redis、MQ、外部调用)依赖规则:内层不依赖外层——领域层不 import Controller/Mapper。
4.2 代码目录
com.example.order/ ├── interfaces/ # 接口层 │ ├── OrderController.java │ └── dto/ # DTO(对外) ├── application/ # 应用层 │ ├── OrderApplicationService.java │ └── command/ # 入参命令对象 ├── domain/ # 领域层(核心) │ ├── model/ # 实体/值对象/聚合 │ ├── service/ # 领域服务 │ └── repository/ # 仓储接口 └── infrastructure/ # 基础设施层 ├── mapper/ # MyBatis ├── repository/ # 仓储实现 └── external/ # 外部调用4.3 应用层 vs 领域层(高频混淆)
| 应用层(Application) | 领域层(Domain) |
|---|---|
| 用例编排(先做什么后做什么) | 业务规则(能不能做、怎么做) |
| 事务、权限、事务边界 | 领域不变量 |
| 调领域服务/仓储 | 实体行为、领域服务 |
| 例:placeOrder 流程控制 | 例:订单状态机校验 |
判断标准:这条规则离开业务系统就不成立 → 领域层;这条规则是系统流程 → 应用层。
五、实战:订单创建 DDD 落地
5.1 领域模型
// 聚合根:OrderpublicclassOrder{privatefinalOrderIdid;privatefinalCustomerIdcustomerId;privateOrderStatusstatus;privateMoneytotalAmount;privatefinalList<OrderItem>items=newArrayList<>();privateOrder(CustomerIdcustomerId){this.id=OrderId.generate();this.customerId=customerId;this.status=OrderStatus.CREATED;this.totalAmount=Money.zero();}publicstaticOrdercreate(CustomerIdcustomerId){returnnewOrder(customerId);}publicvoidaddItem(ProductIdproductId,StringproductName,Moneyprice,intcount){// 业务规则:数量必须为正if(count<=0)thrownewDomainException("数量必须大于0");items.add(newOrderItem(productId,productName,price,count));this.totalAmount=this.totalAmount.add(price.multiply(count));}publicvoidconfirm(){if(items.isEmpty())thrownewDomainException("订单没有商品");this.status=OrderStatus.CONFIRMED;}}5.2 应用服务
@ServicepublicclassOrderApplicationService{@AutowiredprivateOrderRepositoryorderRepository;@AutowiredprivateStockDomainServicestockDomainService;@TransactionalpublicOrderIdcreateOrder(CreateOrderCommandcmd){// 1. 创建订单(领域逻辑)Orderorder=Order.create(CustomerId.of(cmd.getCustomerId()));for(ItemCmditem:cmd.getItems()){order.addItem(ProductId.of(item.getProductId()),item.getProductName(),Money.of(item.getPrice(),"CNY"),item.getCount());}order.confirm();// 2. 锁库存(另一个聚合的领域服务)stockDomainService.lock(order.getItems());// 3. 保存(仓储)orderRepository.save(order);returnorder.getId();}}5.3 接口层
@RestController@RequestMapping("/orders")publicclassOrderController{@AutowiredprivateOrderApplicationServiceorderAppService;@PostMappingpublicOrderVOcreate(@RequestBodyCreateOrderRequestreq){OrderIdid=orderAppService.createOrder(CreateOrderCommand.from(req));returnnewOrderVO(id.value());}}分层干净:Controller 只做参数转换,不碰业务。
六、DDD 常见问题与坑
| 问题 | 原因 | 解决 |
|---|---|---|
| 实体还是一堆 getter/setter | 没把规则放进去 | 把校验/状态流转写进实体 |
| Service 仍然巨长 | 应用层和领域层混了 | 流程留应用层,规则下放领域 |
| 聚合太大 | 一个聚合装整个业务 | 拆小聚合,跨聚合用领域服务 |
| 和数据库模型强耦合 | 直接暴露 DO | 仓储转换,领域与 DO 隔离 |
| 简单项目过度设计 | 滥用 DDD | 按业务复杂度取舍 |
| 事务乱 | 聚合边界不清 | 一个聚合一个事务 |
本章小结
DDD 的落地要点:战略上划清限界上下文,战术上把业务规则放回领域模型。判断做没做对:实体里有没有业务逻辑?领域层有没有依赖 Mapper?应用层是不是只剩编排?三个都是"是",DDD 就落地了。记住——DDD 解决的是业务复杂度,不是技术复杂度。
下一篇讲 Linux Shell 脚本——运维自动化的基础功。
(全文完)