DDD结构示例



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

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

上下文:

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

  • 拥有众多球员的球队
  • 玩家
  • 固定装置,有2个团队和2个半部分
  • Half,它有2种格式和许多组合
  • FormationPlayed,已播放多个位置
  • PositionalPlayed,它有1个Player和一个位置值对象
  • 组合,可以有两种类型,并且有许多移动
  • Move可以有两种类型,有一个Player和一个事件值对象

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

  • 团队可以独立存在,AR也是如此
  • 玩家可以独立存在,AR也是
  • 夹具在删除时,也必须删除其一半,AR也是如此
  • 一半必须是卫浴装置中的实体
  • FormationPlayed必须在删除half时删除,所以也许这应该是half中的一个实体
  • 当一个编队被删除时,PositionPlayed必须被删除,所以相信这应该是FormationPlayed中的一个实体
  • 组合在某种意义上可以独立存在,尽管它与特定的比赛半场有关。也许这可能是由最终一致性决定的AR
  • 删除组合时必须删除Move,所以相信这应该是组合中的实体

问题:

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

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

您在上面的设计中看到任何错误吗?如果是,你会改变什么?

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

举个例子,我的意思是

  • http://www.soccerstats.com/——《史记》是真实的世界
  • https://www.easports.com/fifa--游戏是在电脑里玩的

如果Fixture被删除,你会触发FixtureDeleted事件,导致其相应的FormationPlayed实体也被删除吗?

Udi Dahan写道:不要删除,只是不要。如果一个实体有一个生命周期,并且该生命周期有一个结束,那么您可以标记它,但不会删除该实体。

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

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

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

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

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

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

关于这一点有几个想法——ddd通常是为更复杂的用例保存的(Greg Young:"这是你获得竞争优势的地方吗?")。聚合的大部分力量来自于这样一个事实,即它们确保了状态变化的一致性。当你真正的问题是数据输入和报告时,这往往是小题大做。

检测和补救不一致往往比试图正确预防更容易/更便宜;并且考虑到成本,可能对企业来说是令人满意的。需要记住的事情。

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

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

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

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

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

一个人为的例子是,如果一名球员在一个赛季中进球超过10球,就会为你订购球员球衣。请注意,虽然"目标"已经存在于事件流中,但业务规则并没有。这纯粹是一个领域模型。

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

这里的关键思想——目标是总的目标。购买球衣的决定是由集体做出的,并与世界分享。

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

根据洋葱架构的域模型,你能给我举一些好的例子吗?

尽管听起来很奇怪,但最好的地方是函数式编程类型。Mark Seemann的博客包含了许多重要的想法,这些想法将对这里有所帮助。

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

你认为公认的观点是,对于这种规模的域,应该采用贫血模型吗

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

最新更新