DDD要求一个事务只允许修改一个聚合根。为什么要有如此严格的限制?让我们来考虑一个简单的场景,我们有2个聚合根分别是Role(角色)
和Page(页面权限)
,一个 Role
可以拥有有0..n个Page
。当用户在页面权限维护
功能模块里删除一个 Page 实例时,需要级联删除 Role 与该 Page 实例的关联数据。传统的做法是让 PageService
依赖 RoleService
,在 deletePage(pageId)
方法内调用 roleService.removeAllPage(pageId)
方法来移除所有对已删除 Page 实例的关联。但页面权限维护
功能模块的作者怎么知道到底有哪些模块关联了 Page 实例? 随着系统的演进,当其他模块也需要关联 Page 时,还要及时通知页面权限维护
功能模块的作者增加新的删除代码,这违反了Open-Close原则。
我们可以使用领域事件以响应式编程的方式解决这个问题。要实现领域事件,有许多框架可供选择,Spring框架自带的事件机制是最简单的。
在 PageRepository
的 delete()
方法里发出页面权限已删除
事件:
public class PageRepository ... {
public void delete(String id) {
pageMapper.deleteById(id);
domainEvents.publish(PageDeleted.now(id));
}
}
在 RoleApplicationService
里响应页面权限已删除
事件:
public class RoleApplicationService {
/** 响应页面已删除领域事件. */
@EventListener
void handlePageDeleted(PageDeleted pageDeleted) {
this.roleRepository.deleteAllOwnedPages(pageDeleted.getPageId());
}
}
PageDeleted
是一个携带了关键信息的值对象:
/** 页面相关领域事件. */
public interface PageEvent extends DomainEvent {
String getPageId();
default String getAggregateId() {
return getPageId();
}
@Value
class PageDeleted implements PageEvent {
@NonNull String eventId = MybatisUtil.nextId().toString();
@NonNull Instant when;
@NonNull String pageId;
public static PageDeleted now(String pageId) {
return new PageDeleted(Instant.now(), pageId);
}
}
}
-
Spring
框架自带的消息机制是同步的,想要更好的性能可以使用 greenrobot/EventBus 或 guava/EventBus -
projectreactor 是一个相对简单且完备的响应式框架,Spring的亲儿子。而且有针对 Kafka 和 RabbitMQ 的桥接。
-
提到响应式框架当然少不了老牌的 RxJava
事件溯源 是一个挺大的话题,目前还没有实际尝试过。