本文转载自微信公众号《codeeasy》,作者颜华。转载本文请联系codeeasy公众号。要弄清楚使用富领域模型有什么问题,我们需要从应用服务层如何使用富领域模型说起。应用服务就是编舞领域模型有了行为之后,就变成了动作灵活的舞者。然而,很多时候,观众并不想只看某个舞者表演动作。因此,应用服务需要对一个或几个领域模型的行为进行编排,以完成满足某一场景(UseCase)需求的舞蹈。我们看一下应用服务SprintApplicationService中的一个方法:/***SubmitaBacklogItemtoaSprint*@paramaCommand表示客户端发起的命令*/publicvoidcommitBacklogItemToSprint(CommitBacklogItemToSprintCommandaCommand){TenantIdtenantId=newTenantId(aCommand.getTenantId());//Step1:加载一个sprint到内存Sprintsprint=this.sprintRepository().sprintOfId(tenantId,newSprintId(aCommand.getSprintId()));//Step2:加载一个BacklogItem到内存(tenantId,newBacklogItemId(aCommand.getBacklogItemId()));//Step3:将BacklogItem提交给sprint,内存级操作sprint.commit(backlogItem);//Step4:持久化sprintthis.sprintRepository().save(sprint);这里Sprint和BacklogItem是两个聚合根,分别对应一组实体。第一步和第二步从数据库中加载两个聚合到内存中,内存中有两个对象图:当我们执行第三步时(即执行完sprint.commit(backlogItem)),内存中的对象图变为:此时,在sprint的backlogItems集合中,多了一个cb3,它的ordering为3,它的backlogItemId为12。将id为12的backlogItem添加到spint中时,需要做一些验证,并生成一个新的cb3并正确设置其排序值。这些是sprint聚合内部发生的逻辑,应用服务不知道这些领域逻辑,甚至不知道这些逻辑的存在。更复杂的场景可能会导致聚合内多个对象的内存状态发生变化。注意此时只有内存中对象的状态发生了变化。第四步,应用服务委托sprintRepository持久化sprint后,内存对象的变化会反映对应数据库的表(一个或多个)内容的变化(即update或insert数据))-多少应用程序服务不知道表中的任何更改。也正是因为这样的职责划分,才会出现我们在第一篇文章中看到的结果——领域层的代码非常丰富,而应用层的代码非常少,这里看到的只有编排逻辑。这样做带来的好处前两篇已经说了很多,不再赘述。但是这样做有什么问题呢?我们为什么要使用抓鼠器?说不敢用富领域模型,肯定有顾虑。A:PerformanceConcurrencyConflict如果我们只按照面向对象的设计方法来实现富领域模型,可能会导致对象关联过多,内存中的对象图会是这样的:image。服务层可能会加载很多对象到内存中,这会消耗大量内存。另外,修改可能会引起很多对象状态的改变,修改会引起更多的并发冲突。DDD正是为了解决这个问题。它建议将对象分成不同的“组”,也就是我们前面提到的聚合。聚合之间不能进行对象引用,只能使用ID,这样加载一个聚合时,其他聚合不会加载到内存中。image.png黑色链接线表示ID引用。总之,应该使用小聚合来避免性能和修改并发冲突。但是...聚合有多小但聚合有多小?在极端情况下,每个表一个聚合就足够小了,但这又回到了贫血模型。聚合仍应代表业务一致性边界。例如,更改OrderItem和Order的属性应确保不违反某些业务规则。在此前提下,聚合应设计得尽可能小。从IDDD_Sample的代码看不到设计聚合的分析过程,只能看到结果。如果想了解分析过程,推荐阅读《实现领域驱动设计》一书中本例的分析过程。我们将在后续文章中分析另一个开源示例(Library)。那个例子中给出了分析过程的记录,届时将详细说明聚合识别过程。在我实践的过程中,我发现大多数人设计的聚合体都太大了。我最近尝试使用领域故事和事件风暴来识别聚合,发现生成的聚合比以前的聚合更小、更合理。这得益于基于场景的分析,而不是从技术角度建模。《领域驱动设计模式、原理与实践》是另一本关于DDD的好书,它曾经说过“如果你发现一个聚合可能会导致性能和并发问题,你必须回头看看这个聚合是不是设计得太大了”。以前,我总觉得这是一种因果颠倒的无奈之举。现在觉得有一个合理的逻辑:如果是基于场景来分析,聚合的粒度会比较小。如果出现性能和并发问题,说明聚合太大了。可能不能按照场景分析,所以需要根据场景重新审视。再来看聚合的设计,所以这个技术问题本质上是一个模型分析/设计问题,但是分析/设计问题比技术问题更难解决,更难有固定的套路。以后还打算再写一个系列,讲DDD设计过程中的反模式,很多都是讲不合理的聚合设计。接下来,我们就来说说CQRS吧,下面我们重点分析IDDD_Sample这个例子的代码。聚合设计过大的原因之一是开发者考虑了太多查询的需求。合理使用CQRS模式可以避免这个问题。另外,使用CQRS本身也可以解决很多性能问题。下一篇我们来看看IDDD_Sample中CQRS模式是如何使用的。
