DDD结构示例

DDD structure example

我正在尝试使用 DDD 和 onion/hexagonal/clean 架构构建应用程序(使用 Java 和 Spring)。我发现找到有关概念本身的指导比实际如何实施它们更容易。尤其是 DDD 似乎很难找到有启发性的例子,因为每个问题都是独一无二的。我在 SO 上看过很多有用的例子,但我仍然有疑问。我想知道通过我的示例是否会对我和其他人有所帮助。

我希望你能原谅我在这里问了一个以上的问题。这个例子似乎太大了,我在多个问题中重复它是没有意义的。

上下文:

我们有一个应显示有关足球统计信息的应用程序并具有以下概念(为简单起见,我没有包括所有属性):

正如您所想象的,在这里尝试找出哪些东西是聚合根是很棘手的。

问题:

  1. 以上设计有没有发现错误?如果是你会改变什么?
  2. Fixture - Half - FormationPlayed - PositionPlayed 聚合似乎太大,所以我想知道您是否同意可以使用最终一致性将其拆分为 Fixture - Half 和 FormationPlayed - PositionPlayed。我找不到的例子是 Java 中是如何实现的?如果 Fixture 被删除,您是否会触发 FixtureDeleted 事件导致其相应的 FormationPlayed 实体也被删除?
  3. 我想构建一个不了解其持久化方式的领域模型(根据洋葱架构)。我的理解是这里的域实体不应该有代理键,因为这涉及持久性。我还认为实体应该只通过 id 引用其他聚合中的实体。例如,PositionPlayed 如何在域模型中引用 Player?
  4. 最初的目的只是让客户端获取数据并显示出来。最终,我希望客户能够自己执行 CRUD,并且我希望发生这种情况时,所有不变量都由域模型保持在一起。拥有两个域模型,一个简单的用于数据检索,一个丰富的用于稍后执行的操作,它会简化事情吗(你能告诉我或给我举个例子来解释如何做)吗?可以说是两个 BC。我问的原因是,当最初我们只想在数据库中显示统计信息时,想出一个丰富的领域模型似乎相当耗时,但我也不想给自己制造麻烦,如果它更好的话鉴于稍后设想的用例,现在创建一个丰富的领域模型。我想知道,如果我只为数据检索创建一个更简单的模型,DDD 中的哪些概念可以忽略(例如,我是否仍需要分解大型聚合?)

我希望这一切都有意义。如果需要,显然很乐意进一步解释。意识到我在这里问了很多,我可能混淆了一些想法。如果您能对此给出任何答案和智慧,我们将不胜感激!

Do you see any errors in the above design? If so what would you change?

可能会有一个大问题:您的系统是记录簿吗?或者它只是跟踪 "real world" 中发生的事件。从某种意义上说,汇总的目的是确保记录簿内部一致,但如果您不是记录簿....

举例说明我的意思

If Fixture were deleted, would you fire a FixtureDeleted event that causes its corresponding FormationPlayed entities to also be deleted?

Udi Dahan 写道:Don't Delete, Just Don't。如果一个实体有一个生命周期,并且那个生命周期有一个结束,那么你标记它,但你不删除实体。

I want to construct a domain model that has no understanding of the way that it will be persisted (as per onion architecture)

太棒了!请注意,您可以在网上找到的很多示例都没有正确说明这一部分——由于历史原因,许多模型演示与它们对持久性的副作用紧密相关。

My understanding is that domain entities here should not have surrogate keys because this relates to persistence. I also believe that entities should only reference entities in other aggregates by ids. How then, for example, would PositionPlayed reference Player in the domain model?

啊 -- 好的,这个很有趣。不要将持久层中使用的代理键与域模型中的标识符混淆。例如,当我查看我在亚马逊上的购买历史时,我的每个订单(大概是一个汇总)都有一个与之关联的订单号。这意味着域级别知道 OrderNumber 作为值类型。后端的持久性解决方案可能会在存储该数据时引入代理键,但模型不会使用这些键。

请注意,我选择了一个例子,其中聚合显然是权威——顺序只真正存在于模型中。当现实世界是记录簿时,您通常没有可用的唯一标识符(莱昂内尔·梅西的 PlayerId 是什么?)

The reason I ask is that a rich domain model seems rather time consuming to come up with when initially we only want to display stats in the database

关于此的一些想法 -- 通常用于更复杂的用例(Greg Young:"is this where you get a competitive advantage?")。聚合的大部分力量来自于它们确保状态变化的一致性这一事实。当您真正的问题是数据输入和报告时,它往往是多余的。

检测和纠正不一致通常 easier/cheaper 比尝试正确预防;考虑到成本,企业可能会满意。需要注意的事项。

The application is keeping track of events in the real world. At the moment, they are recorded manually in a database. Can you be explicit why you believe the distinction is important?

非常粗略 -- 事件表示已经发生的事情。域否决它们为时已晚;现实世界不在域的控制范围内。 此外,我们必须记住,由于现实世界是记录簿,现实世界中可能发生了领域模型还不知道的事情(事件的报告可能会延迟、丢失、重新排序) , 等等)。

聚合应该是真理的来源。这意味着他们只能管理数字世界中的实体。

您可以创建的一种信息资源是梅西一个赛季的进球报告。因此,每次报告一个目标时,您 运行 一个更新报告汇总的命令。那不是贫血——不完全是——但它不是很有趣。它实际上只是一个视图(在 CQRS 术语中,它是一个读取模型),您可以从事件历史中重新创建。它没有任何智能。

兴趣聚合体是那些根据给定的信息自行做出决定的聚合体。

一个人为的汇总示例是,如果一名球员在一个赛季中进球超过 10 个,则为您订购该球员的球衣。请注意,虽然 "goals" 已存在于您的事件流中,但业务规则并未存在。那纯粹是领域模型的事情。

因此,它的工作方式是每次出现目标事件时,您将加载 JerseyPerchasing 聚合,并告诉它目标。并且该汇总将确保这是一个新目标(不是以前报告过的目标),并确定是否需要订购球衣的目标数量,检查球衣的订单是否已经下达。

这里的关键思想 -- 目标是关于集合的内容。购买球衣的决定由集体做出,并与全世界共享。

后来,您意识到有时一名球员被交易,然后打进第 10 个球。作为一项业务,你必须确定这是否意味着你得到一件球衣(哪一件?)或每件球衣一件球衣,或者如果他在一个赛季为特定球队打进 10 球,你可能只订购球衣。所有这些逻辑都进入聚合。

a domain model as per onion architecture that, can you point me to any good examples?

尽管听起来很奇怪,但最好看的地方是函数式编程类型。 Mark Seemann's blog 包含许多对此处有帮助的重要想法。

要牢记的主要思想是模型位于底部。该应用程序将状态传递给模型,然后取回状态(在 CQS 术语中,您 查询 模型)。该应用程序负责与持久性组件共享从模型中获得的结果。

do you believe the accepted view would be that an anaemic model should be adopted for a domain this size

在你只是 re-organizing 来自 t 的信息的情况下现实世界更容易消费?是的 - 加载文档、更新文档、存储文档对我来说比过度使用一堆聚合建模更有意义。但是不要读太多——我对你的模型的了解并不比你在这里写的更多。如果在评估来自现实世界的信息方面存在真正的业务复杂性,那么答案就会不同。