事件源,一个事件,两个聚合的状态已更改


10

我正在尝试学习DDD和相关主题的方法。我想到了一个简单的有界上下文来实现“银行”的想法:有帐户,可以在它们之间存入,提取和转移资金。保留更改历史也很重要。

我确定了Account实体,并且事件来源可以很好地跟踪其中的更改。其他实体或值对象与该问题无关,因此我不会提及它们。

考虑存款和取款时-这相对简单,因为只修改了一个汇总。

转移时有所不同-必须通过一个MoneyTransferred事件来修改两个聚合。DDD不赞成在一个事务中修改多个聚合。另一方面,事件源的规则是将事件应用于实体并基于它们修改状态。如果事件可以简单地存储在数据库中,那就没有问题。但是,为了防止同时修改事件源实体,我们必须对每个聚合的事件流实施某种版本控制(以保持其事务界限)。版本控制带来了另一个问题-我无法使用简单的结构来存储事件并读回它们以将其应用于聚合。

我的问题是-如何将这三个原则结合在一起:“一个交易合计一笔交易”,“事件->总交易变更”和“防止并发修改”?

Answers:


7

转移时有所不同-必须通过一个MoneyTransferred事件来修改两个聚合。

转移资金是与更新分类账不同的行为。

MoneyTransferred
AccountCredited
AccountDebited

最终使我放松的练习意识到这AccountOverdrawn是一个事件,它描述了帐户的状态,而不考虑此交换中的其他参与者,因此必须针对生成该帐户的帐户运行命令。

您无法像AccountOverdrawn从读取模型中那样合理地得出状态,因为您可能无法知道是否已经看到所有事件-只有聚合本身在任何给定时刻都具有历史的完整视图。

答案当然就在那里用无处不在的语言-贷记或借记帐户以反映银行对客户的义务。

好的,但这意味着我也应该将AccountCredited和AccountDebited事件用于存款和取款,因此我不仅注册更改原因,还注册其他操作引起的更改。如果我想撤销该操作,则无法执行,因为并非所有事件都已注册。

我不能完全肯定,因为您确实有一个自然的相关标识符(对于这种情况),即交易ID本身。

第二件事-这意味着我需要使用传奇。

拼写略有不同:您需要像人一样调度正确的命令

至少有两种方法可以做到这一点。一种是让订户监听MoneyTransferred,然后将这两个命令分派到分类帐。

另一种选择是将事务处理作为一个单独的汇总进行跟踪-将其视为自事务发生以来需要完成的所有事情的清单。因此,MoneyTransferred事件处理程序将调度ProcessTransaction,该进程计划要完成的工作并检查已完成的工作。


好的,但这意味着我也应该将AccountCredited和AccountDebited事件用于存款和取款,因此我不仅注册更改原因,还注册其他操作引起的更改。如果我想撤销该操作,则无法执行,因为并非所有事件都已注册。我该怎么办(事件的因果关系)?第二件事-这意味着我需要使用传奇。那么该如何建模呢?一会儿我就考虑了转账方法。调用时,它将发布事件MoneyTransferred。我不知道应该从什么开始,例如传奇。
cocsackie

是不是-> AccountCreditedAccoundDebited然后是MoneyTransferred?第一个解决方案在一个事务中更新两个聚合(没有任何形式的一致性保证)?也没有可以发布MoneyTransferred- >没有关联的汇总。第二种解决方案似乎更好-ProcessTransaction可以发布MoneyTransferred,并避免在一个事务中进行多个汇总修改,我可以在提交事务后从Account发布事件。对不起,很抱歉。对于初学者来说很难理解-不能仅使用一种模式而不能使用其他模式。
柯萨奇

1

了解基于交易的帐户的一个重要细节:balance属性account实际上是非规范化的一个实例。在那里是为了方便。实际上,一个帐户的余额是其交易的总和,您实际上并不需要该帐户本身就有余额。

牢记这一点,转帐的行为不应是更新,account而是插入transaction

话虽这么说,还有一条重要的规则:添加a的动作transaction应该是原子的,并且要更新(的标准化字段)account

现在,如果我了解聚合的DDD概念,则以下内容似乎相关:

聚合是在给定上下文的业务交易中可以更改的事物的逻辑边界。聚合可以由单个类或由多个类表示。如果一个集合构成一个以上的类,那么其中一个就是所谓的根类或实体。从外部对聚合的所有访问都必须通过根类进行。

因此,就DDD设计而言,我建议:

  1. 有一个汇总代表转移

  2. 聚合由以下对象组成:传输(根对象);根对象链接到两个交易列表(每个帐户一个);并且每个交易列表都链接到一个帐户。

  3. 对传输的所有访问都应由根对象(transfer)进行冥想。

如果您试图实现异步传输支持,那么您的主要代码应该只担心创建处于“待处理”状态的传输。您可能拥有另一个线程或工作,该线程或工作实际上将钱转移(插入交易历史记录,因此更新了余额)并将转帐设置为“已过帐”。

如果您希望实现实时的阻止转移交易,则业务逻辑应创建一个transfer,该对象将实时协调其他活动。

在防止并发问题方面,第一要务是将借方交易插入到源帐户的交易清单中(当然要更新余额)。这将必须在数据库级别(通过存储过程)以原子方式执行。发生借项后,无论并发性问题如何,其余的转账都应能够成功,因为不应该有任何业务规则阻止贷记到目标帐户。

(在现实世界中,银行帐户的概念就是备忘录,它支持懒惰的两阶段提交的概念。备忘录的创建既轻巧又容易,并且可以回滚而不会出现问题。备忘录发布到困难的发布是金钱真正动起来的时候-不能回滚-代表两阶段提交的第二阶段,仅在检查所有验证规则之后才会发生)。


0

我目前也处于学习阶段。从实现的角度来看,这就是我将执行此操作的感觉。

调度引发以下事件的TransferMoneyCommand [MoneyTransferEvent,AccountDebitedEvent]

请注意,在引发这些事件之前,需要执行表面命令验证和域逻辑验证,即帐户是否有足够的余额?

保留事件(使用版本控制)以确保不存在一致性问题。请注意,可能还有另一个并发命令(例如提取所有资金)成功执行并保存了之前的事件,因此聚合的当前状态可能已过时,因此事件在旧状态下引发并且不正确。如果事件保存失败,则需要从头开始重试该命令。

一旦事件成功保存在数据库中,您就可以发布引发的两个事件。

AccountDebitedEvent将从付款人的帐户中删除这笔钱(更新汇总状态和任何相关的视图/投影模型)

MoneyTransferEvent启动Saga / Process Manager。

传奇/流程经理的工作是尝试将收款人的帐户记入贷方,如果失败,则需要将余额贷记至付款人。

Saga /流程经理将发布一个CreditAccountCommand,该命令将应用于收款人的帐户,如果成功,则将引发AccountCreditedEvent。

从事件源的角度来看,如果您想撤消此操作,则此事务中的所有事件都将具有与原始TransferMoneyCommand相同的关联/原因ID,您可以使用该ID来引发撤消/撤消操作的事件。

随时提出有关上述问题或潜在改进的建议。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.