领域驱动设计模式:聚合、值对象、领域事件、领域服务与分层架构
领域驱动设计,简称DDD。说实话,我第一次接触这个概念是在一个大型电商项目中。当时团队被复杂的业务逻辑搞得焦头烂额,代码里到处都是if-else,改一个需求要翻遍十几个文件。后来我们引入了DDD,才慢慢把局面扭转过来。
今天我就把DDD中最核心的几个概念——聚合、值对象、领域事件、领域服务,以及分层架构——给你讲透。这些东西不是理论空谈,都是我在实战中踩过坑、填过坑后沉淀下来的经验。
1. 值对象:不可变的小零件
先说说值对象。很多人觉得它不重要,其实恰恰相反。值对象是DDD里最容易被忽视、但最有价值的概念之一。
值对象的特点是:没有唯一标识,只描述属性。比如地址、金额、颜色、坐标这些。两个值对象如果属性值相同,我们就认为它们是同一个对象。
核心特征:
- 不可变性——创建后就不能修改
- 通过属性值比较相等性
- 没有全局唯一ID
- 可以包含行为(比如金额的加法)
举个例子。你想想看,一个订单里的收货地址,如果地址变了,是修改原来的对象还是新建一个?按DDD的规矩,应该新建。因为地址变了,本质上就是一个新地址。
// Java 示例:值对象
public final class Address {
private final String province;
private final String city;
private final String detail;
public Address(String province, String city, String detail) {
this.province = province;
this.city = city;
this.detail = detail;
}
// 只提供getter,不提供setter
public String getProvince() { return province; }
public String getCity() { return city; }
public String getDetail() { return detail; }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof Address)) return false;
Address address = (Address) o;
return province.equals(address.province) &&
city.equals(address.city) &&
detail.equals(address.detail);
}
@Override
public int hashCode() {
return Objects.hash(province, city, detail);
}
}
我的经验:我曾经在一个项目中把金额设计成实体,结果每次修改金额都要走一遍ORM的更新流程,性能差不说,还容易产生并发问题。后来改成值对象,用不可变的方式处理,问题迎刃而解。
2. 聚合:业务一致性的边界
聚合是DDD里最难理解、也最容易出错的概念。说白了,聚合就是一组相关对象的集合,我们把它当作一个整体来操作。
每个聚合都有一个聚合根,它是访问聚合的唯一入口。外部不能直接操作聚合内部的子对象,必须通过聚合根来操作。
为什么要这么设计?我遇到过这样一个场景:订单和订单项。如果允许外部直接修改订单项的数量和价格,那订单的总金额就可能不一致。这就是数据不一致的根源。
// Java 示例:聚合根
public class Order {
private OrderId id; // 聚合根ID
private List<OrderItem> items;
private Money totalAmount;
// 外部只能通过聚合根的方法来操作
public void addItem(Product product, int quantity) {
OrderItem item = new OrderItem(product, quantity);
items.add(item);
recalculateTotalAmount();
}
public void removeItem(OrderItemId itemId) {
items.removeIf(item -> item.getId().equals(itemId));
recalculateTotalAmount();
}
private void recalculateTotalAmount() {
this.totalAmount = items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
}
避坑指南:我曾经把一个聚合设计得太大,包含了十几个实体。结果每次操作都要加载大量数据,性能惨不忍睹。后来我学乖了:聚合要小,越小越好。一个聚合通常只包含3-5个对象就够了。
3. 领域事件:解耦的利器
领域事件是DDD里我最喜欢的概念。它让不同聚合之间可以松耦合地通信。
举个例子。订单支付成功后,需要做三件事:更新订单状态、发送通知、更新库存。如果把这些逻辑都写在订单服务里,代码会变得非常臃肿。用领域事件就清爽多了。
// Java 示例:领域事件
public class OrderPaidEvent implements DomainEvent {
private final OrderId orderId;
private final Money paidAmount;
private final LocalDateTime occurredOn;
public OrderPaidEvent(OrderId orderId, Money paidAmount) {
this.orderId = orderId;
this.paidAmount = paidAmount;
this.occurredOn = LocalDateTime.now();
}
// getter...
}
// 事件处理器
@Component
public class OrderPaidEventHandler {
@EventListener
public void handle(OrderPaidEvent event) {
// 1. 更新订单状态
orderRepository.updateStatus(event.getOrderId(), OrderStatus.PAID);
// 2. 发送通知
notificationService.sendPaymentSuccess(event.getOrderId());
// 3. 更新库存
inventoryService.deductStock(event.getOrderId());
}
}
你想想看,如果以后要加一个新功能,比如支付成功后给用户积分,只需要新增一个事件处理器就行,完全不用改原有代码。这就是开闭原则的完美体现。
4. 领域服务:处理跨聚合的逻辑
有些业务逻辑不属于任何一个实体或值对象。比如转账,它涉及两个账户。这时候就需要领域服务出场了。
领域服务是无状态的,它只负责协调多个聚合完成一个业务操作。
// Java 示例:领域服务
public class TransferService {
private final AccountRepository accountRepository;
public TransferService(AccountRepository accountRepository) {
this.accountRepository = accountRepository;
}
public void transfer(AccountId fromId, AccountId toId, Money amount) {
Account from = accountRepository.findById(fromId);
Account to = accountRepository.findById(toId);
// 业务逻辑:扣款和入账
from.withdraw(amount);
to.deposit(amount);
// 保存聚合
accountRepository.save(from);
accountRepository.save(to);
// 发布领域事件
DomainEventPublisher.publish(new TransferCompletedEvent(fromId, toId, amount));
}
}
我的建议:领域服务不要做得太厚。如果发现领域服务里全是if-else,那说明业务逻辑放错地方了。应该把逻辑下沉到实体或值对象里。
5. DDD分层架构
最后说说分层架构。DDD推荐四层架构:
| 层次 | 职责 | 包含内容 |
|---|---|---|
| 用户接口层 | 接收用户请求,返回响应 | Controller、DTO、View |
| 应用层 | 编排业务场景,协调领域对象 | ApplicationService、DTO转换 |
| 领域层 | 核心业务逻辑 | 实体、值对象、聚合、领域服务、领域事件 |
| 基础设施层 | 技术实现 | Repository实现、消息队列、缓存 |
这里有个关键原则:依赖倒置。领域层不依赖任何外部框架,基础设施层依赖领域层定义的接口。
我见过太多项目把业务逻辑写在Controller里,或者直接操作数据库。这样做的后果就是:业务逻辑散落一地,改一个需求要动好几个层。
嗯,这里要注意:应用层和领域层的区别。应用层只负责编排,不包含业务逻辑。真正的业务规则必须放在领域层。我见过有人把业务逻辑写在应用层,结果领域层变成了贫血模型,这就失去了DDD的意义。
总结一下我的经验:
- 值对象用不可变,能省很多并发问题
- 聚合要小,边界要清晰
- 领域事件是解耦神器,多用没坏处
- 领域服务只做协调,不做具体逻辑
- 分层架构的核心是依赖倒置
DDD不是银弹,但它确实能帮我们应对复杂业务。我建议你从小项目开始尝试,先在一个模块里用起来,慢慢体会它的好处。别一上来就全盘推翻重写,那样风险太大。
公众号:蓝海资料掘金营,微信deep3321