【问题标题】:DDD structure exampleDDD结构示例
【发布时间】:2017-01-24 09:33:58
【问题描述】:

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

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

上下文:

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

  • 团队,有很多玩家。
  • 播放器。
  • 夹具,有 2 队和 2 半。
  • 一半,有 2 个 FormationsPlayed 和许多组合。
  • FormationPlayed,有很多 PositionsPlayed。
  • PositionPlayed,有 1 个 Player 和一个位置值对象。
  • 组合,可以是2种,有很多招式。
  • Move 可以有 2 种类型,有 1 个玩家和一个事件值对象。

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

  • 团队可以独立存在,AR 也是如此。
  • 玩家可以独立存在,AR 也是如此。
  • Fixture 在删除时也必须删除它的一半,AR 也是如此。
  • 一半必须是 Fixture 中的实体。
  • FormationPlayed 必须在删除 half 时删除,所以也许这应该是 Half 中的一个实体。
  • 在删除 Formation 时必须删除 PositionPlayed,因此请相信这应该是 FormationPlayed 中的一个实体。
  • 某种意义上的组合可以独立存在,但与特定的游戏半场相关。也许这可能是一个由最终一致性绑定的 AR。
  • 删除组合时必须删除移动,因此请相信这应该是组合中的实体。

问题:

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

我希望这一切都有意义。如果需要,显然很乐意进一步解释。意识到我在这里问了很多,我可能混淆了一些想法。非常感谢您对此提供的任何答案和智慧!

【问题讨论】:

  • “可以独立存在”往往被更复杂的域不变量、事务分析和现实生活中的性能问题所取代。域实体不是静态对象,许多域逻辑与它们如何从一种状态转换到另一种状态的更精细细节有关。实体间同步也是一个影响设计的大问题。我建议阅读/观看 Vaughn Vernon 关于聚合设计的任何内容。
  • 谢谢@guillaume31。显然,您很好地理解了这个主题。是的,我看过 Vaughn Vernon 关于这个主题的一些演讲——这些演讲非常精彩——但是因为每个问题都是独一无二的,我仍然对如何实际执行他所说的话感到有些困惑。除了我可以在网上找到的书籍、演讲、存储库和答案之外,我对这个主题的指导很少。我的意思是通过搜索找到的一般性谈话或示例只能解决特定问题。
  • @guillaume31 您是否认为富域模型在这种情况下是正确的,您是否知道任何其他可能对这种特殊情况有帮助的特定链接?
  • 直觉:需求提出了一个比富域模型/DDD 战术模式(聚合、实体等)更简单的解决方案。Martin Fowler 有一个Domain Logic Patterns 列表,您或许可以选择一个更好的从。您仍然可以使用 DDD 的 strategic 模式,如 here 所述。
  • @guillaume31 非常有帮助,谢谢

标签: java domain-driven-design onion-architecture eventual-consistency


【解决方案1】:

您在上述设计中发现任​​何错误吗?如果是这样,你会改变什么?

可能有一个大问题:您的系统是记录簿吗?还是只是跟踪“现实世界”中发生的事件。从某种意义上说,聚合的目的是确保记录簿内部一致,但如果您不是记录簿......

举例说明我的意思

如果 Fixture 被删除,您是否会触发 FixtureDeleted 事件,导致其对应的 FormationPlayed 实体也被删除?

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

我想构建一个不了解其持久化方式的领域模型(根据洋葱架构)

太棒了!请注意,您将在网上找到的许多示例都没有正确处理这部分 - 由于历史原因,许多模型演示与它们对持久性的副作用紧密耦合。

我的理解是,这里的域实体不应该有代理键,因为这与持久性有关。我还认为实体应该只通过 id 引用其他聚合中的实体。那么,例如,PositionPlayed 如何在域模型中引用 Player?

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

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

我问的原因是,当我们最初只想在数据库中显示统计信息时,提出一个富域模型似乎相当耗时

对此有一些想法—— 通常用于更复杂的用例(Greg Young:“这是您获得竞争优势的地方吗?”)。聚合的大部分力量来自于它们确保状态变化的一致性这一事实。当您的真正问题是数据输入和报告时,它往往是矫枉过正的。

检测和纠正不一致性通常比尝试正确预防更容易/更便宜;考虑到成本,可能会让企业满意。需要记住的一点。

应用程序正在跟踪现实世界中的事件。目前,它们被手动记录在数据库中。您能否明确说明为什么您认为这种区别很重要?

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

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

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

兴趣集合是那些根据所提供的信息为自己做出决定的集合。

一个人为的汇总示例是,如果一名球员在一个赛季中的进球数超过 10 个,则为您订购该球员的球衣。请注意,虽然“目标”已经存在于您的事件流中,但业务规则却没有。这纯粹是一个领域模型的事情。

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

这里的关键思想 - 目标是聚合被告知的东西。购买球衣的决定是由集体做出的,并与全世界分享。

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

根据洋葱架构的域模型,你能指出任何好的例子吗?

虽然听起来很奇怪,但最好看的地方是函数式编程类型。 Mark Seemann's blog 包含了很多重要的想法,这些想法在这里会有所帮助。

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

您认为公认的观点是应该为这种规模的域采用贫血模型

如果您只是重新组织来自现实世界的信息以便于消费?是的 - 加载文档、更新文档、存储文档对我来说比使用一堆聚合建模更有意义。但是不要过多地阅读它——我对你的模型的了解并不比你在这里写的更多。如果您在评估来自现实世界的信息方面存在真正的业务复杂性,那么答案会有所不同。

【讨论】:

  • 非常感谢您抽出宝贵的时间来解决这个问题。感谢您在这方面的智慧。如果你能回到你提到的那些点上,那真的会更有帮助。该应用程序正在跟踪现实世界中的事件。目前,它们被手动记录在数据库中。你能说清楚为什么你认为这种区别很重要吗?
  • 关于删除的好点 - 我以前读过那篇文章。我想我的问题更多是关于命令将如何级联并在一个聚合中处理,该聚合被分成更小的部分,而与命令的类型无关。我脑海中的主要斗争是将聚合分解成可管理的块,因此我正在寻找一些验证,以证明我正在考虑这样做的方式是正确的。如果是更新,我描述的领域事件的使用是否正确?
  • 根据洋葱架构重新创建一个域模型,你能指出我有什么好的例子吗?我经常发现遵循洋葱架构但不遵循 ddd 的示例,遵循 ddd 但不遵循洋葱架构的示例,或者遵循两者但对我要解决的问题没有指导意义。 Re ids,这真的很有趣。你会在领域模型中任何需要引用实体的地方创建一个 id 吗?
  • 关于这种情况是否不支持 DDD,您是否认为公认的观点是应该为这种规模的域采用贫血模型,对象仅真正存在以将关系数据提取为对象形式?这根本不是对您的答案的批评,只是想知道您认为最好的方法以及最好的方法。再次感谢。
  • 感谢您为此付出的所有时间。您刚刚添加的答案很有帮助
猜你喜欢
  • 2010-10-17
  • 2019-03-06
  • 1970-01-01
  • 2016-05-17
  • 2012-02-24
  • 2016-11-11
  • 2015-08-20
  • 1970-01-01
  • 2012-08-04
相关资源
最近更新 更多