引子:为什么我们需要 DDD?
想象你接手了一个电商系统的重构项目。打开代码一看:订单、支付、库存、物流的逻辑散落在各个 Service 里,一个 OrderService 有 3000 行代码,方法名叫 processOrder、handleOrder、dealOrder,看起来都差不多但又不知道具体干啥。更可怕的是,业务规则藏在各个角落:有的在 Controller 做校验,有的在 Service 计算,有的在数据库触发器里...
这就是典型的"贫血模型 + 事务脚本"带来的问题。而 DDD(Domain-Driven Design,领域驱动设计)就是为了解决这类问题而生的。
一、DDD 的核心思想:以领域为中心
1.1 什么是领域(Domain)?
领域就是你要解决的业务问题空间。 比如电商领域、金融领域、物流领域。
DDD 的第一个核心观点:软件的复杂度来自业务本身,而不是技术。 所以我们要把精力放在理解业务上,而不是一上来就想着用什么框架、什么中间件。
举个例子:
-
❌ 传统思维:"这个功能需要一个订单表、一个订单详情表、然后写个 CRUD..."
-
✅ DDD 思维:"在我们的业务里,'订单'代表什么?它有哪些状态?什么情况下可以取消?取消后库存怎么处理?"
1.2 通用语言(Ubiquitous Language)
DDD 强调团队要建立通用语言:开发、产品、运营都用同一套术语。
比如在电商领域:
-
"订单"不叫 Order,叫 PurchaseOrder(采购订单)
-
"支付"不叫 pay(),叫 completePayment()(完成支付动作)
-
"取消"不叫 delete(),叫 cancel()(业务动作)
代码就是文档,变量名就是业务语言。 当产品经理说"订单履约",你的代码里就应该有 OrderFulfillment 这个类。
二、战略设计:划分领域边界
2.1 限界上下文(Bounded Context)
这是 DDD 最重要的概念之一。不同的上下文中,同一个词可能有不同的含义。
以"商品"为例:
-
商品上下文:商品有 SKU、SPU、规格、价格
-
库存上下文:商品只关心库存数量、仓库位置
-
营销上下文:商品关心优惠活动、促销标签
所以我们要划分限界上下文,每个上下文有自己的模型:
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ 商品上下文 │ │ 库存上下文 │ │ 订单上下文 │
│ │ │ │ │ │
│ Product │───▶│ Stock │◀───│ Order │
│ - skuId │ │ - skuId │ │ - orderId │
│ - name │ │ - quantity │ │ - items[] │
│ - price │ │ - warehouse │ │ - totalAmount │
└─────────────────┘ └─────────────────┘ └─────────────────┘
2.2 上下文映射(Context Mapping)
不同上下文之间如何交互?DDD 定义了几种模式:
1) 共享内核(Shared Kernel)
-
两个上下文共享一部分模型
-
适合紧密协作的团队
2) 客户-供应商(Customer-Supplier)
-
下游依赖上游的接口
-
比如订单上下文依赖库存上下文的接口查库存
3) 防腐层(Anti-Corruption Layer, ACL)
-
这是最常用的!当你对接外部系统或遗留系统时,用 ACL 做一层转换
举个例子,对接第三方支付:
// 防腐层:隔离外部系统
public class PaymentAdapter {
private ThirdPartyPaymentClient client;
public PaymentResult pay(Order order) {
// 将我们的领域模型转换为第三方格式
ThirdPartyRequest request = convertToThirdParty(order);
ThirdPartyResponse response = client.pay(request);
// 将第三方结果转换为我们的领域模型
return convertToDomain(response);
}
}
防腐层的价值:当第三方系统变更或更换时,只需要修改 ACL,核心领域模型不受影响。
2.3 子域划分
把整个业务领域划分为:
-
核心域(Core Domain):你的竞争力所在,比如电商的定价策略、推荐算法
-
支撑域(Supporting Domain):必须有但不是核心竞争力,比如用户管理
-
通用域(Generic Domain):可以买现成方案的,比如短信发送、文件存储
资源分配原则:核心域投入最好的人力,支撑域够用就行,通用域尽量用开源/SaaS。
三、战术设计:构建领域模型
3.1 实体(Entity)vs 值对象(Value Object)
实体:有唯一标识,生命周期中属性可变
public class Order {
private OrderId id; // 唯一标识
private Money totalAmount;
private OrderStatus status;
// 业务方法
public void cancel() {
if (status == OrderStatus.PAID) {
throw new OrderCannotCancelException("已支付订单不能取消");
}
this.status = OrderStatus.CANCELLED;
}
}
值对象:没有标识,通过属性判断相等,不可变
public class Money {
private final BigDecimal amount;
private final Currency currency;
public Money(BigDecimal amount, Currency currency) {
if (amount.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("金额不能为负");
}
this.amount = amount;
this.currency = currency;
}
public Money add(Money other) {
// 返回新对象,不修改自己
return new Money(this.amount.add(other.amount), this.currency);
}
}
为什么要区分?
-
实体关注"是谁"(身份),值对象关注"是什么"(属性)
-
值对象可以共享、缓存,实体不行
-
两个金额 100 元是相等的(值对象),但两个订单 ID 相同只能说明是同一个订单(实体)
3.2 聚合(Aggregate)与聚合根
聚合是一组相关对象的集合,有一个聚合根作为入口。
比如订单聚合:
Order (聚合根)
├── OrderItem (订单明细)
├── ShippingAddress (收货地址)
└── Payment (支付信息)
聚合的设计原则:
1) 边界内强一致性,边界外最终一致性
-
订单和订单明细必须同时保存(强一致性)
-
订单和库存可以异步更新(最终一致性)
2) 通过聚合根修改
// ❌ 错误:直接修改订单明细
orderItem.setQuantity(10);
// ✅ 正确:通过聚合根
order.updateItemQuantity(itemId, 10);
3) 聚合根持有其他实体的引用,外部只持有聚合根的 ID
public class Order {
private OrderId id;
private CustomerId customerId; // 只持有 ID,不持有整个 Customer 对象
private List<OrderItem> items; // 持有内部实体
}
聚合大小的经验法则:
-
聚合不要太大(控制在 2-3 层),否则并发冲突多、性能差
-
如果两个对象总是一起修改,放在一个聚合;否则拆分
3.3 领域服务(Domain Service)
当一个业务逻辑不属于某个实体时,用领域服务。
比如"转账"操作涉及两个账户:
public class TransferService {
public void transfer(Account from, Account to, Money amount) {
from.debit(amount); // 扣款
to.credit(amount); // 入账
// 记录转账事件
domainEventPublisher.publish(new MoneyTransferred(from.id(), to.id(), amount));
}
}
领域服务 vs 应用服务:
-
领域服务:包含业务逻辑,比如"转账规则"
-
应用服务:编排流程,比如"调用转账、发通知、记日志"
3.4 仓储(Repository)
仓储是领域对象的持久化抽象,隐藏数据库细节。
public interface OrderRepository {
Order findById(OrderId id);
void save(Order order);
List<Order> findByCustomerId(CustomerId customerId);
}
关键点:
-
接口定义在领域层,实现在基础设施
-
用领域语言(findByCustomerId),而不是 SQL 语言(selectByCustomerId)
-
返回的是聚合根,而不是贫血的 PO 对象
3.5 领域事件(Domain Event)
当领域中发生重要的事情时,发布事件。
public class Order {
public void pay() {
this.status = OrderStatus.PAID;
// 发布领域事件
DomainEventPublisher.publish(new OrderPaidEvent(this.id, this.totalAmount));
}
}
// 事件处理器
@EventHandler
public class OrderPaidEventHandler {
public void handle(OrderPaidEvent event) {
// 扣减库存、发送通知等
inventoryService.reserve(event.getOrderId());
notificationService.sendPaymentConfirmation(event.getOrderId());
}
}
领域事件的价值:
-
解耦:订单不需要知道支付后要做什么
-
可扩展:新增需求时只需要加事件处理器
-
最终一致性:跨聚合的操作通过事件异步完成
四、分层架构:让领域模型不受污染
DDD 典型的四层架构:
┌─────────────────────────────────────┐
│ User Interface Layer (接口层) │ ← Controller、DTO
├─────────────────────────────────────┤
│ Application Layer (应用层) │ ← ApplicationService、事务
├─────────────────────────────────────┤
│ Domain Layer (领域层) │ ← Entity、ValueObject、DomainService
├─────────────────────────────────────┤
│ Infrastructure Layer (基础设施层) │ ← Repository 实现、MQ、DB
└─────────────────────────────────────┘
依赖规则:只能向下依赖,领域层不依赖任何外层!
举个例子:
// 应用层:编排流程
public class OrderApplicationService {
private OrderRepository orderRepository;
private InventoryService inventoryService;
@Transactional
public void placeOrder(PlaceOrderCommand command) {
// 1. 构建领域对象
Order order = Order.create(command.getCustomerId(), command.getItems());
// 2. 执行领域逻辑
order.place(); // 领域方法
// 3. 检查库存(领域服务)
inventoryService.checkAndReserve(order.getItems());
// 4. 持久化
orderRepository.save(order);
}
}
五、实战:从贫血模型到充血模型
贫血模型(反模式)
// 数据对象
public class Order {
private Long id;
private Integer status;
private BigDecimal totalAmount;
// 只有 getter/setter
}
// Service 里写所有逻辑
public class OrderService {
public void cancelOrder(Long orderId) {
Order order = orderDAO.selectById(orderId);
if (order.getStatus() == 2) {
throw new Exception("已支付不能取消");
}
order.setStatus(3);
orderDAO.update(order);
}
}
问题:
-
业务逻辑散落在各个 Service
-
Order 对象没有行为,只是数据容器
-
无法保证业务规则一致性(到处都能 setStatus)
充血模型(DDD)
// 领域对象:有数据也有行为
public class Order {
private OrderId id;
private OrderStatus status;
private Money totalAmount;
public void cancel() {
if (status == OrderStatus.PAID) {
throw new OrderCannotCancelException("已支付订单不能取消");
}
this.status = OrderStatus.CANCELLED;
// 发布领域事件
DomainEventPublisher.publish(new OrderCancelledEvent(this.id));
}
// 不提供 setStatus,只能通过业务方法修改状态
}
优势:
-
业务规则封装在领域对象内
-
状态变更有迹可循(通过方法名就知道发生了什么)
-
测试简单(不需要数据库,直接测试领域对象)
六、DDD 的常见误区
误区 1:DDD = 微服务
-
DDD 是设计方法,微服务是架构风格
-
可以在单体应用中用 DDD,也可以在微服务中不用 DDD
-
但 DDD 的限界上下文确实为微服务拆分提供了指导
误区 2:DDD 必须用充血模型
-
充血模型是 DDD 的常见实现方式,但不是唯一方式
-
简单的 CRUD 场景用贫血模型也没问题
-
只有复杂业务逻辑才需要 DDD
误区 3:为了 DDD 而 DDD
-
不要过度设计,小项目用 DDD 是杀鸡用牛刀
-
DDD 的价值在于"驾驭复杂度",简单系统没必要
误区 4:DDD 就是设计很多类
-
实体、值对象、聚合根...是工具,不是目的
-
关键是理解业务、建立统一语言、划分清晰边界
七、DDD 在 Java 生态中的实践
7.1 框架选择
-
Spring Boot:最常见,用 @Service、@Repository 分层
-
Axon Framework:专为 CQRS + Event Sourcing 设计
-
JMolecules:提供 DDD 注解(@Entity、@ValueObject)
7.2 持久化
// 领域层:接口
public interface OrderRepository {
Order findById(OrderId id);
void save(Order order);
}
// 基础设施层:JPA 实现
@Repository
public class OrderJpaRepository implements OrderRepository {
@PersistenceContext
private EntityManager em;
public Order findById(OrderId id) {
OrderPO po = em.find(OrderPO.class, id.getValue());
return OrderMapper.toDomain(po); // PO → 领域对象
}
public void save(Order order) {
OrderPO po = OrderMapper.toPO(order); // 领域对象 → PO
em.merge(po);
}
}
7.3 事件驱动
// 使用 Spring Event
@Service
public class OrderDomainService {
@Autowired
private ApplicationEventPublisher eventPublisher;
public void payOrder(Order order) {
order.pay();
eventPublisher.publishEvent(new OrderPaidEvent(order.getId()));
}
}
// 事件监听
@Component
public class InventoryEventListener {
@EventListener
@Async
public void handle(OrderPaidEvent event) {
// 扣减库存
}
}
八、何时使用 DDD?
适合 DDD 的场景:
-
✅ 业务逻辑复杂,规则多变
-
✅ 需要长期维护迭代的系统
-
✅ 团队有一定的技术能力
-
✅ 需要多团队协作(划分限界上下文)
不适合 DDD 的场景:
-
❌ 简单的 CRUD 系统
-
❌ 短期项目、原型验证
-
❌ 数据分析、报表类系统
-
❌ 团队不熟悉 DDD(学习成本高)
九、总结:DDD 的本质
DDD 不是银弹,它的核心价值在于:
-
以业务为中心:技术服务于业务,而不是反过来
-
统一语言:减少沟通成本,代码即文档
-
清晰边界:通过限界上下文降低复杂度
-
封装变化:把易变的业务规则封装在领域模型中
记住 Eric Evans 的话:"DDD is not about technology, it's about discussions." DDD 的精髓在于和领域专家的深入交流,而不是炫技式地堆砌设计模式。
2万+

被折叠的 条评论
为什么被折叠?



