DDD => 根聚合中的行为:实例化其他根聚合
DDD => behaviour in root aggregate : instanciate other root aggregate
我有 2 个根聚合:
- 发票
- 投诉
我有一条规定:"I can't delete an invoice if a complaint is opened on it"。
关于我对发票汇总的删除行为,我想检查是否存在投诉,例如:
Complaint complaint = ComplaintRepository.findByInvoiceId(invoiceId);
if(complaint.isOpened) {
throw new Exception("Open Complain...");
}
else{
...
}
我和我的同事对此意见不一。
他们对我说,我不能在我的行为中实例化投诉,因为投诉不在我的聚合中。
我的意见是我不能在 Invoice Class 中有 Complaint 属性,但是:
- 我可以用一个值对象引用一个(他们对此没问题)
- 我可以 read/load 一个实例,因为我没有调用它的行为...
你对此有什么看法吗?
从技术上讲,您可以按照您的建议进行操作:从某种角度来看,如果您通过构造函数注入或方法注入将 ComplaintRepository 接口注入到发票中,您就是在使发票依赖于存储库和投诉的合同,这几乎是允许的。
当你说你不能保留对投诉的引用时你是对的,但是你可以在需要时将 DDD 工件(例如 factories/repositories/entities)注入到操作中 运行 .
然而,您必须问自己的要点是:您真的希望两个不同聚合之间存在这种级别的耦合吗?在这一点上,他们是如此紧密地结合在一起,他们几乎无法在没有一个和另一个的情况下运作。
考虑到所有这些,您可能会遇到投诉可能只是发票合计的一部分的情况(尽管您的发票合计可能还有其他责任,您将开始与 "Design Small Aggregates"目标)。如果您考虑一下,这就是不变量 "I can't delete an invoice if a complaint is opened on it" 所提出的。
如果总而言之,将投诉建模为发票合计的一部分不切实际,您还有一些其他选择:
使这些聚合最终保持一致:与其尝试删除 "one shot" 中的发票,不如在一次操作中将其标记为要删除。此操作会在您的消息传递机制中触发某种领域事件。此事件 "InvoiceFlaggedForDeletion" 然后将检查发票上的投诉。如果您没有任何投诉,请将其删除。如果您有投诉,请回滚删除标志。
将删除过程放在域服务中。这样,域服务将协调检查投诉并在适当时删除发票的工作。这种方法的缺点是您的 Invoice 实体对其规则不太明确,但从 DDD 的角度来看,这有时是一种可以接受的方法。
这条语句:
I have 2 root aggregate : - invoice - complaint`
还有这个
And I have a rule who say : "I can't delete an invoice if a complaint is opened on it"`
如果您遵循不让数据库事务大于一个聚合的规则(并且您应该尝试遵循它,这是一个很好的规则),则它们是互斥的。
聚合是事务边界,这意味着聚合内部发生的事情与将来在同一个聚合中发生的事情是高度一致的(无论如何,不变量都会保持不变,聚合总是在一个有效的状态)。
但是,不同聚合实例之间发生的事情最终是一致的,这意味着没有更高级别的协调,没有什么可以阻止(多个聚合的)系统进入无效状态。聚合只对它们拥有的数据负责。
像你这样的代码:
Complaint complaint = ComplaintRepository.findByInvoiceId(invoiceId);
//
// at this time a new complain could be added!!!
//
if(complaint.isOpened) {
throw new Exception("Open Complain...");
}
else{
invoiceRepository.delete(invoiceId);// and this would delete the invoice although there is a complain on this invoice!!!
}
将无法遵守业务规则 I can't delete an invoice if a complaint is opened on it
,除非它包含在大于单个聚合的事务中。
话虽如此,您有两个类似 DDD 的选项:
检查您的设计:将两个聚合合并为一个,例如,使 Compliant 成为 Invoice 中的嵌套实体。
使用更高级别的协调器,将发票的 "deletion" 建模为长 运行 业务流程。为此,您可以使用 Saga/Process 管理器。 "simplest" 这样的 Saga 也会删除 Invoice 删除后添加的 Complains。一个更复杂的 Saga 甚至可以阻止在 Invoice 被删除后添加 Complain(为此它需要以某种方式拦截 Complain 的开始)。
聚合根不应包含对存储库的引用。这种方法有很多问题。相反,从应用程序服务(命令处理程序)中的存储库加载所有对象并传递给域进行操作。如果操作包含多个聚合,则域逻辑错误(缺少概念)或者您可能需要域服务。无论哪种方式,聚合最好不要向存储库询问任何内容。
另一个考虑因素应该是 - 删除 发票在此域中意味着什么?
参见 - http://udidahan.com/2009/09/01/dont-delete-just-dont/
在这种情况下,如果您挑战领域专家,也许 'deleting' 发票方面的要求只是来自他们接受过有关数据库的培训,并隐式地将他们的实际要求转化为供您尝试的解决方案有帮助。
也许他们真正谈论的是取消发票?还是存档?还是反过来?
无论如何,所有这些都可以让您在发票上模拟状态转换,而不必担心 'orphan' 投诉。
这会引发思考——如果发票被取消,投诉应该怎么办?是否应该通知投诉的所有者?投诉是否应该经历它自己的状态转换?这可能由 InvoiceCancelled
事件触发。
在 DDD 中,每当您看到与删除相关的要求和对孤立记录的担忧时,通常都暗示需要进行更深入的知识分析才能理解域的真正意图。
我有 2 个根聚合: - 发票 - 投诉
我有一条规定:"I can't delete an invoice if a complaint is opened on it"。
关于我对发票汇总的删除行为,我想检查是否存在投诉,例如:
Complaint complaint = ComplaintRepository.findByInvoiceId(invoiceId);
if(complaint.isOpened) {
throw new Exception("Open Complain...");
}
else{
...
}
我和我的同事对此意见不一。 他们对我说,我不能在我的行为中实例化投诉,因为投诉不在我的聚合中。 我的意见是我不能在 Invoice Class 中有 Complaint 属性,但是: - 我可以用一个值对象引用一个(他们对此没问题) - 我可以 read/load 一个实例,因为我没有调用它的行为...
你对此有什么看法吗?
从技术上讲,您可以按照您的建议进行操作:从某种角度来看,如果您通过构造函数注入或方法注入将 ComplaintRepository 接口注入到发票中,您就是在使发票依赖于存储库和投诉的合同,这几乎是允许的。
当你说你不能保留对投诉的引用时你是对的,但是你可以在需要时将 DDD 工件(例如 factories/repositories/entities)注入到操作中 运行 .
然而,您必须问自己的要点是:您真的希望两个不同聚合之间存在这种级别的耦合吗?在这一点上,他们是如此紧密地结合在一起,他们几乎无法在没有一个和另一个的情况下运作。
考虑到所有这些,您可能会遇到投诉可能只是发票合计的一部分的情况(尽管您的发票合计可能还有其他责任,您将开始与 "Design Small Aggregates"目标)。如果您考虑一下,这就是不变量 "I can't delete an invoice if a complaint is opened on it" 所提出的。
如果总而言之,将投诉建模为发票合计的一部分不切实际,您还有一些其他选择:
使这些聚合最终保持一致:与其尝试删除 "one shot" 中的发票,不如在一次操作中将其标记为要删除。此操作会在您的消息传递机制中触发某种领域事件。此事件 "InvoiceFlaggedForDeletion" 然后将检查发票上的投诉。如果您没有任何投诉,请将其删除。如果您有投诉,请回滚删除标志。
将删除过程放在域服务中。这样,域服务将协调检查投诉并在适当时删除发票的工作。这种方法的缺点是您的 Invoice 实体对其规则不太明确,但从 DDD 的角度来看,这有时是一种可以接受的方法。
这条语句:
I have 2 root aggregate : - invoice - complaint`
还有这个
And I have a rule who say : "I can't delete an invoice if a complaint is opened on it"`
如果您遵循不让数据库事务大于一个聚合的规则(并且您应该尝试遵循它,这是一个很好的规则),则它们是互斥的。
聚合是事务边界,这意味着聚合内部发生的事情与将来在同一个聚合中发生的事情是高度一致的(无论如何,不变量都会保持不变,聚合总是在一个有效的状态)。
但是,不同聚合实例之间发生的事情最终是一致的,这意味着没有更高级别的协调,没有什么可以阻止(多个聚合的)系统进入无效状态。聚合只对它们拥有的数据负责。
像你这样的代码:
Complaint complaint = ComplaintRepository.findByInvoiceId(invoiceId);
//
// at this time a new complain could be added!!!
//
if(complaint.isOpened) {
throw new Exception("Open Complain...");
}
else{
invoiceRepository.delete(invoiceId);// and this would delete the invoice although there is a complain on this invoice!!!
}
将无法遵守业务规则 I can't delete an invoice if a complaint is opened on it
,除非它包含在大于单个聚合的事务中。
话虽如此,您有两个类似 DDD 的选项:
检查您的设计:将两个聚合合并为一个,例如,使 Compliant 成为 Invoice 中的嵌套实体。
使用更高级别的协调器,将发票的 "deletion" 建模为长 运行 业务流程。为此,您可以使用 Saga/Process 管理器。 "simplest" 这样的 Saga 也会删除 Invoice 删除后添加的 Complains。一个更复杂的 Saga 甚至可以阻止在 Invoice 被删除后添加 Complain(为此它需要以某种方式拦截 Complain 的开始)。
聚合根不应包含对存储库的引用。这种方法有很多问题。相反,从应用程序服务(命令处理程序)中的存储库加载所有对象并传递给域进行操作。如果操作包含多个聚合,则域逻辑错误(缺少概念)或者您可能需要域服务。无论哪种方式,聚合最好不要向存储库询问任何内容。
另一个考虑因素应该是 - 删除 发票在此域中意味着什么?
参见 - http://udidahan.com/2009/09/01/dont-delete-just-dont/
在这种情况下,如果您挑战领域专家,也许 'deleting' 发票方面的要求只是来自他们接受过有关数据库的培训,并隐式地将他们的实际要求转化为供您尝试的解决方案有帮助。
也许他们真正谈论的是取消发票?还是存档?还是反过来?
无论如何,所有这些都可以让您在发票上模拟状态转换,而不必担心 'orphan' 投诉。
这会引发思考——如果发票被取消,投诉应该怎么办?是否应该通知投诉的所有者?投诉是否应该经历它自己的状态转换?这可能由 InvoiceCancelled
事件触发。
在 DDD 中,每当您看到与删除相关的要求和对孤立记录的担忧时,通常都暗示需要进行更深入的知识分析才能理解域的真正意图。