如何有效的建模聚合(二)

你们最尊敬的翻译官:

当然在此声明,由于翻译的这篇文章,已经被作者收录进IDDD的第十章:聚合篇,所以有书的同学还是看书比较好,这部分翻译一是纠正我在中文版中的一些不理解,二是通过翻译加深对聚合与建模的理解,三呢也是对DDD思想的宣传吧,希望更多的开发可以意识到自己的狭隘思想,作为一个引路人,希望这篇聚合,可以让你信服。

2.让聚合在一起工作

在第一部分中,我们集中于设计若干小的聚合以及它们的内部构件。在第二部分中我将讨论聚合如何引擎其他的聚合,以及如何利用最终的一致性来保持独立的聚合实例的一致性。

当我们在设计聚合的时候,我们可能渴望一个组合式的结构,以允许我们对深层对象图进行遍历,但这不是模式的动机。DDD声明一个聚合可以保持一个到其他聚合的根对象的引用。但是我们心里应该时刻记住,不要将引用的聚合放在引用它的一致性边界内.什么意思呢??就是被你所引用的聚合的一致性边界,不属于对其引用的聚合的一致性边界的范畴。通过引用其他聚合的根,并不会使得两个聚合合成一个聚合,它们还是两个聚合,而且分别具有自己的一致性边界。我们来看图5,这也很好的解释了聚合即一致性边界:
这里写图片描述

上图中的关系,我们用java来实现的话,类似于如下这样:

    public class BacklogItem extends ConcurrencySafeEntity{
        ...
        private Product product;
        ...
    }

代码的直译就是BacklogItem持有了到Product的直接对象关联

结合已经讨论过的内容和接下来的内容,这有一些启示:
1.聚合BacklogItem和被引用的聚合Product,这二者不可以在同一个事务内被修改。在一个事务内只能有一个聚合被修改
2.如果你正在一个单独的事务中修改多个实例,这可能是一个强烈的迹象,表明你的一致性界限是错误的。如果是这样的话,这很可能是一个错失的建模机会;你的领域统一语言中的概念并没有被发现,它正在挥着结实的臂膀朝着你吼呢~~
3.如果你正打算应用观点2的意见,并且这样做会影响到所有前面声明的警告的大集合,那么这可能是一种迹象,告诉您需要采用最终一致性来替代我们执行所使用的原子性一致性。

如果你并没有持有任何的引用,那么自然你就也就不能修改其他的聚合。所以要消除单个事务中修改多个聚合的诱惑,那么可以在一开始就避免这种情况的发生(有点狠啊,直接不引用其他聚合)。但是这会导致过度限制,因为领域模型经常需要有一些关联关系。我们能做些什么来促进必要的关联,防止事务的滥用或过度的失败,并允许模型保持一定的性能和拓展性呢??
这里写图片描述

2.1 规则:通过标识符引用其他的聚合

更倾向于仅通过它们全局唯一的标识符来引用对外部聚合的引用,而不是持有一个直接的对象引用。图6就是这样的一个例子,重构的代码如下:

    public class BacklogItem extends concurrencySafeEntity{
        ...
        private ProductId productId;
        ...
    }

带有推断的对象引用的聚合因此自动变小了,因为引用从来没有被热切地加载。这个模型可以执行的更好,因为实例加载的时间更少了,而且内存占用也变小了。使用更小的内存对内存分配开销和垃圾回收都具有积极的影响。

2.2 Model Navigation(模型导航)

通过标识符来引用并不能完全阻止模型的导航。有的会在聚合内部使用仓储(repository)去查找。这种技术被称为discounted domain model(断开连接的领域模型),它实际上是一种延迟加载的形式。但是,有一种不同的推荐方法:在调用聚合行为之前,通过使用仓储或者领域服务去查找依赖对象。一个客户端应用服务可以控制这一点,然后再分派到聚合:

public class ProductBacklogItemService ...{
    ...
    @Transaction
    public void assignTeamMemberToTask(
        String aTenantId,
        String aBacklogItemId,
        String aTaskId,
        String aTeamMemberId
        ){
        BacklogItem backlogItem = 
            backlogItemRepository.backlogItemOfId(
                new TenantId(aTenantId),
                new BacklogItemId(aBacklogItemId));
        Team ofTeam = 
            teamRepository.teamOfId(
                backlogItem.tenantId(),
                backlogItem.teamId());
        backlogItem.assignTeamMemberToTask(
            new TeamMemberId(aTeamMemberId),
            ofTeam,
            new TaskId(aTaskId))
    }
    ...
}

在这里我们有一个应用服务来解决依赖关系,就可以解放我们的聚合,使其不再依赖于仓储或领域服务。同样,在一个请求中引用多个聚合,是不允许在两个或者两个以上的聚合上修改的。

将模型限制为仅通过标识来使用引用,可能会在为客户端组装和渲染用户界面视图时候更加困难。您可能需要在一个单独的用例中,使用多个仓储去构成视图。如果查询开销比较大,导致性能问题的话,那么可能就值得使用CQRS了。或者你可能需要在推断和直接对象引用之间找到平衡。

如果所有这些建议似乎都导致了一个不那么方便的模型,那么考虑一下它所提供的好处。使聚合更小会导致性能更好的模型,我们还可以添加可伸缩性和分布式。

2.3 Scalability and Distribution(可伸缩性和分布式)

因为聚合没有使用直接到其他聚合的引用,而是通过标识符来引用的,所以他们的持久化状态可以移动到更大的范围。这几乎无限的可伸缩性是通过允许对聚合数据存储进行重新划分来实现的,正如Amazon.com的Pat Helland在他的论文《Life Beyond Disributed Transactions:an Apostate’s Opinion》中所解释的那样。我们在这里所称呼的聚合,它叫做entity,但是它描述的仍然是聚合,只是换了个名字罢了;是具有事务一致性的组成单元。一些NoSql持久性机制支持受亚马逊启发的分布式存储。这些都提供了Helland所称的更低的、有规模的层。在使用分布式存储时,甚至在使用具有相似动机的SQL数据库时,使用标识符来引用都扮演着重要角色。

分布式扩展则超出了存储的范畴。由于在给定的核心领域中总是有多个限界上下文,因此通过标识符来引用可以允许分布式领域模型从远处产生关联。当我们使用事件驱动的方式时候,基于消息的领域事件会将聚合的标识在企业周围发送。在外部限界上下文的消息订阅者使用这些标识符来在他们自己的领域模型中执行操作。通过标识符的引用来形成远程关联或协作。分布式操作是由Helland所谓的two-party activities(双方通讯活动)来管理的;但是在发布-订阅术语中则是multi-party(多方通讯活动)。跨分布式系统的事务不是原子性的。不同的系统最终将多个聚合引入一个一致的状态(即最终一致性)。

2.4 规则:在边界之外使用最终的一致性

在DDD聚合模式定义中发现了一个经常被忽视的语句。当多个聚合必须受到一个简单的客户端请求的影响时,这为我们带来了沉重的压力,我们必须做点什么才可以实现模型的一致性。

DDD p128页

任何跨越多个聚合的规则都不会在每时每刻都被认为是最新的。通过事件处理,批处理,或者其他的更新机制,其他依赖项可以在一定的时间内解决。

因此,如果在一个聚合实例上执行一个命令,还需要在一个或多个其他聚合上执行额外的业务规则,那么使用最终一致性。在一个大规模的、高流量的企业中,接受所有的聚合实例并不是完全一致的,这有助于我们接受最终的一致性在只涉及几个实例的较小的范围内也有意义。

向领域专家咨询,是否可以容忍在修改一个实例和其他的实例之间有一定时间的延迟。领域专家有时会比开发人员更适应延迟一致性的想法。他们意识到在他们的业务中一直存在着现实的延迟,而开发人员则被灌输了一种原子性改变的心态。领域专家常常记得他们的业务操作的计算机自动化之前的日子,当各种各样的延迟一直发生时,一致性从来都不是即时的。因此,领域专家是乐意接受合理的延迟的—–一个足够多的秒,分,小时,或者天—-在一致性出现之前。

在DDD模型中有一个 支持最终一致性的实际的方式。一个聚合命令方法(这里是引用的CQRS的术语,命令方法即会发生副作用的方法,也可以想象为出了查询之外的方法)发布一个领域事件,该领域事件可以及时的发往一个或多个异步的订阅者。

public class BacklogItem extends ConcurrencySafeEntity{
    ...
    public void commitTo(Sprint aSprint){
        ...
        DomainEventPublisher
            .instance()
            .publish(new BacklogItemCommited(
                this.tenantId(),
                this.backlogItemId(),
                this.sprintId()));
    }
}

然后,这些订阅者将检索出一个不同的但是与之对应的聚合实例,并基于它执行它们的行为。每个订阅者在一个单独的事务中执行,遵守聚合规则,每个事务只修改一个实例。

如果订阅者与另一个客户端发生并发性争用,导致其修改失败,会发生什么情况?如果订阅者没有告知消息传递机制成功投递的话,那么可以重试修改。消息将会被再次投递,一个新的事物将开启,开启一次新的尝试去执行必要的命令,和相应的提交commit。这个重试过程可以继续,直到达到一致性,或者直到达到重试限制为止。如果完全失败,可能有必要进行补偿,或至少对失败做报告以等待人为介入。

在这个特定的例子中,通过发布BacklogItemCommited事件来完成什么?回想一下,BacklogItem已经包含了Sprint的标识,我们对维护一个毫无意义的双向关联是毫无兴趣的。相反,这个事件允许最终创建一个CommitedBacklogItem,这样Sprint就可以做一个工作承诺的记录。因为每一个CommitedBacklogItem都有一个ordering(优先级,排序用的)属性,它允许Sprint给每个BacklogItem一个不同于Product和Release所给的的优先级,而这与BacklogItem实例自己的BusinessPriority评估记录是没有任何关联的。因此,Product和Release都可以持有相似的关联,也就是ProductBacklogItem和ScheduledBacklogItem。

这个例子演示了如何在一个限界上下文中使用最终一致性,但是同样的技术也可以像前面描述的那样应用于分布式的风格。

2.5 问问这是谁的工作

一些领域场景可能会在确定是应该使用事务还是最终一致性时非常具有挑战性。那些以传统方式使用DDD的可能倾向于事务一致性。而使用CQRS的则更倾向于与选择最终一致性,那么到底哪种才是正确的呢??坦率地说,这两种方法都没有提供特定于领域的答案,只是技术上的偏好而已。有没有更好的方法来打破束缚??

与Eric Evans(他是DDD之父)讨论这个问题时,他揭示了一个非常简单和合理的指导原则。在检查用例(或故事)时,反复问问自己,执行用例来确保数据一致性是否应该是用户的工作??如果是的话,那么就尝试去使用事务性一致性,即原子性,但是前提是要遵循聚合的其他规则。如果如果这是其他用户的工作,或者是其他系统的工作的话,那么就允许使用最终一致性吧。这一点的智慧不仅为我们提供了一种便利的方式来选择哪种一致性方式,它还帮助我们对我们的领域有了更深的了解。它揭露了真正的系统不变量:即必须保持事务性的一致性。这种理解比默认的技术方式上的倾向更有价值。

这是一个关于聚合的经验法则的小技巧。由于还有其他的因素需要考虑,它可能并不总是导致事务一致性和最终一致性的最终答案,但通常会为模型提供更深入的了解。在第三部分中,当团队重新访问他们的聚合边界时,这个指导原则将被使用。

2.6 打破规则的理由

经验丰富的DDD实践者有时可能会决定在单个事务中对多个聚合实例进行更改,但这只是有充分的理由。那么这么做的原因到底是什么呢?我在这里讨论了四个原因。你可能会经历这一些和其他的。

2.6.1原因一:用户界面方便

有时,用户界面作为一种便利,允许用户一次性定义许多事物的共同特征,以便批量创建。团队成员可能经常想要创建多个待办事项列表作为一个批处理。用户界面允许他们在一个部分中填写所有公共属性,然后逐个逐个地对每个属性进行区分,消除重复的动作。所有的新待办事项列表都将立即被计划(创建):

public class ProductBacklogItemService ...  {
    ...
    @Transactional
    public void planBatchOfProductBacklogItems(
        String aTenantId,  String productId,
        BacklogItemDescription[] aDescriptions)  {


        Product product =
            productRepository. productOfId (
                new TenantId(aTenantId) ,
                new ProductId(productId) ) ;


        for(BacklogItemDescription desc : aDescriptions)  {
            BacklogItem plannedBacklogItem =
                product.planBacklogItem (
                    desc.summary( ) ,
                    desc.category( ) ,
                    BacklogItemType.valueOf(
                        desc.backlogItemType( ) ) ,
                    StoryPoints.valueOf(
                        desc. storyPoints ( ) ) ) ;


            backlogItemRepository. add(plannedBacklogItem) ;
        }
    }
    ...
}

这是否会导致管理不变量的问题?在这种情况下,答案是不,因为不管这些是一次创建还是批量创建都无关紧要。被实例化的对象是完整的聚合,它们自己维护它们自己的不变量。因此,如果一次性创建一批聚合的集合,在语义上与每次重复创建一个集合是没有区别的,那么它就代表了一种可以打破经验法则而不受到惩罚的原因。

Udi Dahan建议避免创建类似于上述的特殊批处理应用程序。相反,将使用[消息总线]将多个应用服务调用组合在一起。这是通过定义一个逻辑消息类型来代表单个调用来完成的,客户端将多个逻辑消息一起发送到同一个物理消息中。在服务器端,[消息总线]在单个事务中处理物理消息,将每个逻辑消息单独传递给一个类,该类是负责处理“计划产品待办事项的消息”的(与应用服务方法的实现相当),要么一起成功,要么一起失败。

2.6.2 原因二:缺乏技术机制

最终的一致性要求使用某种out-of-band(带外)的处理能力,例如消息传递、定时器或后台线程。如果您正在从事的项目没有提供任何这样的机制?尽管我们大多数人会认为这很奇怪,我曾面临过这样的限制。没有消息传递机制,没有后台计时器,也没有其他的本地线程能力。他能做什么呢?

如果我们一不小心,这种情况可能会导致我们重新设计大型集群聚合。虽然这可能会让我们觉得我们遵守了单一事务规则,但正如前面所讨论的那样,它也会降低性能并限制可拓展性。为了避免这种情况,也许我们可以完全地改变系统的聚合,迫使模型解决我们的挑战。我们早已考虑过这样的可能性:项目规范可能会被小心翼翼地保护起来,留给我们很少的空间去协商以前没有想到过的领域概念。这不是真正的DDD方式,但有时确实会发生。这些条件可能使我们无法以合理的方式改变对我们有利的建模环境。在这种情况下,项目动态可能迫使我们在一个事务中修改两个或多个聚合实例。无论这看起来多么明显,这样的决定不应该下的太仓促。

让我们来考虑这样的一个额外的因素,它可以进一步支持从规则中转移:user-aggregate affinity(用户-聚合并置).业务流程是否只有一个用户在任何给定的时间内只关注一组聚合实例?确保用户-聚合并置使得在单个事务中修改多个聚合实例的决策更加可靠,因为它趋向于防止违背不变量和事务冲突的发生。即使有用户-聚合的并置,在极少数情况下,用户仍然可能会面临并发冲突。然而,通过使用乐观的并发性,每个聚合体仍然可以得到保护。无论如何,在任何系统中,并发冲突都可能发生,而且当用户-聚合并置不是我们的盟友时,甚至更频繁。此外,在这极少数的情况下发生了并发冲突的话,从并发冲突中恢复也是很简单。因此,当我们的设计被迫在这种情况下进行时,有时在一个事务中修改多个聚合实例是很有效的。

2.6.3 原因三:全局事务

另一个影响因素是遗留技术和企业政策的影响。其代表之一可能是需要严格遵守对全局、两阶段提交事务的使用。这是一种可能无法推动的情况,至少在短期内是如此。

即使您必须使用全局事务,您也不必在您的本地上下文环境中一次性修改多个聚合实例。如果您可以避免这样做,那么至少可以防止您的核心领域中的事务争用,并且根据您的实际情况来遵守聚合规则。全局事务的不足之处是,如果您能够避免两阶段提交以及与之一致的即时一致性,那么您的系统可能永远无法伸缩

2.6.4 原因四:查询性能

有时候持有对于其他聚合的直接对象引用是最好的选择。这可以用于缓解存储库查询性能问题。这些必须考虑到潜在的规模大小和整体的性能权衡的影响。在第三部分中给出了一个违反标识符引用法则的一个例子。

2.7 遵守的规则

您可能会在企业环境中经历用户界面设计决策、技术模仿或僵化的策略,或者其他因素,这些都需要您做出一些妥协。当然,我们不会去找借口打破聚合的经验法则。从长远来看,遵守这些规则将有利于我们的项目。我们将在必要时保持一致性,并支持最优的性能和高度可伸缩的系统。

2.8 展望第三章

我们现在决定设计小的聚集,以形成真正的业务不变量的边界,更喜欢在聚合之间进通过标识符进行引用,并使用最终的一致性来管理跨聚合的依赖关系。遵守这些规则会如何影响我们的Scrum模型的设计?这是第三部分的重点。我们将看到项目团队如何重新考虑他们的设计,并应用他们新发现的技术。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值