设计模式与领域驱动设计:聚合中的组合、仓储中的工厂、领域事件中的观察者
说实话,很多同学学完设计模式后,总觉得跟实际项目搭不上边。尤其是碰到领域驱动设计(DDD)这种听起来高大上的东西,更是觉得遥不可及。其实不然。
我个人习惯把设计模式看作是DDD的「基础设施」。你想想看,DDD里那些核心概念——聚合、仓储、领域事件——如果没有设计模式在背后撑着,写出来的代码很快就会变成一锅粥。今天我就结合自己的实战经验,聊聊这三对黄金搭档。
核心观点:聚合离不开组合模式,仓储离不开工厂模式,领域事件离不开观察者模式。这不是巧合,而是DDD对代码组织方式的自然要求。
一、聚合中的组合模式
先说说聚合。我在项目中遇到过不少这样的场景:一个订单聚合根,下面挂着订单项、收货地址、支付信息。这些子对象不能独立存在,它们的生命周期必须跟聚合根保持一致。
这不就是组合模式吗?组合模式的核心思想就是「部分-整体」的层次结构,让客户端可以一致地对待单个对象和组合对象。
// Java 示例:订单聚合中的组合模式
public class Order {
private String orderId;
private List<OrderItem> items; // 组合关系
private ShippingAddress address; // 组合关系
// 聚合根控制子对象的生命周期
public void addItem(Product product, int quantity) {
OrderItem item = new OrderItem(product, quantity);
items.add(item);
}
// 对外暴露的是聚合根的行为,而不是子对象
public Money calculateTotal() {
return items.stream()
.map(OrderItem::getSubtotal)
.reduce(Money.ZERO, Money::add);
}
}
这里有个关键点:外部不能直接操作OrderItem。我曾经见过一个项目,直接把OrderItem的列表暴露出去,结果业务逻辑散得到处都是。嗯,这就是典型的「贫血模型」。
避坑指南:我曾经犯过一个错误——为了图方便,在聚合根上加了getItems()方法返回内部列表。结果其他开发人员直接在外面修改列表,导致聚合根的一致性完全失控。后来我强制要求所有对子对象的操作都必须通过聚合根的方法来完成。
二、仓储中的工厂模式
仓储(Repository)的作用是封装数据访问逻辑,让领域层感觉不到持久化的存在。但问题来了:从数据库里拿出来的数据,怎么还原成领域对象?
这就是工厂模式的用武之地。说白了,工厂模式就是帮你把「创建对象」这件事封装起来。在仓储的实现中,我习惯用工厂方法来重建聚合根。
// C++ 示例:仓储中的工厂方法
class OrderRepository {
public:
Order findById(const std::string& orderId) {
// 从数据库读取原始数据
auto record = database_.query("SELECT * FROM orders WHERE id = ?", orderId);
// 使用工厂方法重建聚合根
return reconstructOrder(record);
}
private:
// 工厂方法:从持久化数据重建领域对象
Order reconstructOrder(const DatabaseRecord& record) {
Order order(record.getId());
order.setStatus(OrderStatus::fromCode(record.getStatus()));
// 重建子对象
for (auto& itemRecord : record.getItems()) {
Product product = productRepository_.findById(itemRecord.getProductId());
order.addItem(product, itemRecord.getQuantity());
}
return order;
}
};
你可能会问:为什么不直接在仓储里new一个Order出来?因为这样就把创建逻辑跟持久化逻辑耦合在一起了。我建议把重建逻辑单独抽出来,这样测试的时候也方便mock。
个人经验:我一般会在仓储内部维护一个「重建工厂」,专门负责从各种数据源(数据库、缓存、消息队列)重建领域对象。这样当数据源变化时,只需要改工厂方法,仓储本身不用动。
三、领域事件中的观察者模式
领域事件是DDD里我最喜欢的概念之一。它让领域对象之间可以松耦合地通信。当一个聚合根的状态发生变化时,它发布一个事件,其他感兴趣的对象可以订阅并做出响应。
这不就是观察者模式吗?观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。当主题对象发生变化时,它会通知所有观察者。
// Java 示例:领域事件中的观察者模式
// 事件发布者
public class Order {
private List<DomainEventListener> listeners = new ArrayList<>();
public void subscribe(DomainEventListener listener) {
listeners.add(listener);
}
public void confirm() {
this.status = OrderStatus.CONFIRMED;
// 发布领域事件
DomainEvent event = new OrderConfirmedEvent(this.orderId);
listeners.forEach(l -> l.onEvent(event));
}
}
// 事件订阅者
public class InventoryService implements DomainEventListener {
@Override
public void onEvent(DomainEvent event) {
if (event instanceof OrderConfirmedEvent) {
OrderConfirmedEvent confirmedEvent = (OrderConfirmedEvent) event;
// 扣减库存
inventoryRepository.deduct(confirmedEvent.getOrderId());
}
}
}
这里要注意的是,领域事件的订阅者不应该直接修改发布者的状态。我曾经见过有人直接在事件处理里修改订单状态,结果造成了循环依赖——订单确认触发库存扣减,库存扣减失败又反过来修改订单状态,最后死循环了。
最佳实践:领域事件应该是「事后通知」性质的。聚合根先完成自己的状态变更,然后再发布事件。事件订阅者只能做「副作用」操作,比如发邮件、更新缓存、触发其他限界上下文的操作。
四、三者如何协同工作
光说理论不够,我画了一张图,展示这三对模式在DDD架构中是如何配合的:
从这张图可以看出,三个模式各司其职:组合模式管结构,工厂模式管创建,观察者模式管通信。它们之间没有重叠,但配合起来天衣无缝。
五、实战中的注意事项
最后,我总结几条实战中容易踩的坑:
- 别把组合做成聚合:组合模式强调「部分-整体」,但DDD的聚合更强调「一致性边界」。不是所有组合关系都适合做成聚合,只有那些需要保持事务一致性的才放进来。
- 工厂别太复杂:我见过有人把工厂方法写得比业务逻辑还长。记住,工厂只负责创建,不要在里面塞业务判断。
- 领域事件别滥用:不是每个方法调用都要发事件。只有那些对业务有意义的「发生了的事情」才值得发布。比如「订单已确认」有意义,「订单对象已创建」就没意义。
- 注意事务边界:领域事件的处理最好在同一个事务里,或者用最终一致性。我踩过坑——事件处理成功了,但聚合根没保存成功,数据就乱了。
我的习惯:在项目初期,我会先画出聚合边界,确定哪些对象是组合关系。然后设计仓储接口,明确工厂方法需要重建哪些对象。最后再规划领域事件,看哪些状态变更需要通知其他模块。这个顺序反过来也行,但千万别跳步。
好了,今天就聊到这里。设计模式和DDD的结合,说白了就是让代码更贴近业务,同时保持技术上的优雅。你如果正在做DDD项目,不妨从这三对模式入手,效果立竿见影。