DDD(领域驱动设计)-大聚合物

10

我正在学习Eric Evans的领域驱动设计。聚合概念对我来说很清晰并且我觉得它非常有趣。现在我在考虑一个聚合的例子,例如:

银行账户(1)---->(*)交易。

BankAccount
BigDecimal calculateTurnover();

BankAccount是一个聚合根。为了计算营业额,我应该遍历所有交易并将所有金额相加。Evans认为我应该使用仓储库只加载聚合根。在上述情况下,可能会有几千个交易,我不想一次性将它们全部加载到内存中。

在仓储模式的背景下,聚合根是客户端代码从仓储库中加载的唯一对象。

仓储库封装了对子对象的访问 - 对于调用方来说,它会自动加载它们,要么在加载根对象时同时加载它们,要么在实际需要它们时进行懒加载。

您如何建议在DDD聚合中实现calculateTurnover?

2个回答

6
正如您所指出的那样,将数千个实体加载到聚合中并不是一种可扩展的解决方案。这不仅会导致性能问题,而且可能还会遇到并发问题,正如 Vaughn Vernon 在他的Effective Aggregate Design系列中强调的那样。
您是否希望每个交易都可以在 BankAccount 聚合中使用,或者您只关心营业额?
如果您只需要营业额,那么您应该在实例化您的 BankAccount 聚合时建立这个值。如果您使用 SQL,您的数据存储技术可能会有效地计算它(例如索引 JOINs)。也许您还需要考虑在数据存储中将其作为预先计算的值(当您开始处理每个银行账户的数百万笔交易时会发生什么)?
但是也许您仍然需要在您的领域中使用交易?那么您应该考虑拥有一个单独的 Transaction 存储库。
我强烈建议阅读上面链接的 Vaughn Vernon 关于聚合设计的系列文章。

首先感谢您的迅速回复。我最近已经订购了Vernon的书,很快就会读完并回到这个帖子继续讨论。 - Marcin Cinik
1
如果您了解DDD的基础知识和术语,可以先阅读我回答中链接的那个三部分PDF系列。 - Dave New

1
你选择了一个非常有趣的例子 :)
实际上,我在向不熟悉事件溯源(ES)的任何人解释时,都会使用“Account1->*Transaction”。
作为开发人员,我曾经学过(很久以前)使用实体交互。因此,我们有一个“客户”记录,并且它有一个当前状态。我们以某种方式更改记录的状态(地址、税务信息、折扣等),并存储结果。我们从未完全知道发生了什么,但我们有最新的状态,而且由于那是我们业务的当前状态,这就够了。当然,我们需要处理的第一个问题之一是并发性,但我们有处理方式,即使不是特别好,但“可行”。
由于某种原因,会计学科似乎没有完全接受这一点。我们为什么不只是拥有“账户”的最新状态呢?我们将加载相关记录,更改余额并保存状态。奇怪的是,大多数人可能会对此感到不安,但对于我们其他数据来说,这似乎是可以接受的。

会计领域通过将更改事件注册为一系列 Transaction 条目来解决了这个问题。因此,如果您丢失了帐户记录和最新余额,您总是可以遍历所有交易以获取最新余额。这就是事件溯源。

在ES中,通常会加载聚合根(AR)的整个事件列表以获得其最新状态。当加载所有事件会导致性能问题时,通常会有处理大量事件的机制:快照。通常只存储最新的快照。快照包含聚合的完整最新状态,并仅应用快照版本之后的事件。

ES的一个巨大优势是,我们可以提出新的查询,然后简单地将所有事件应用于查询处理程序并确定结果。也许类似于:“去年搬家两次的客户有多少”。这是相当随意的,但使用“传统”的方法,答案很可能是我们将从今天开始收集该信息,并在明年可用,因为我们没有保存 CustomerMoved 事件。在ES中,我们可以搜索 CustomerMoved 事件并随时获得结果。

这让我想到了你的例子。你可能不想加载所有交易。相反,存储"营业额"并在操作中进行计算。如果"营业额"是一个新的需求,那么一次性处理所有应收账款就可以把它提速。你仍然可以在某个地方拥有一个calculateTurnover()方法,但你不会经常运行它。在那些情况下,你需要加载所有针对某个应收账款的交易。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接