DDD结构示例

时间:2017-01-24 09:33:59

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

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

我希望你能原谅我在这里提出不止一个问题。这个例子看起来太大了,因为我不能在多个问题中重复它。

上下文

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

  • 团队,有很多玩家。
  • 播放器。
  • 夹具,有2个团队和2个半部。
  • 一半,有2个FormationsPlayed和许多组合。
  • FormationPlayed,有许多PositionsPlayed。
  • PositionPlayed,有1个玩家和一个位置值对象。
  • 组合,可以是两种类型,并且有很多动作。
  • 移动,可以是2种类型,有1个播放器和一个事件值对象。

正如你可以想象的那样,试图弄清楚哪些东西是聚合根源是很棘手的。

  • 团队可以独立存在,因此是AR。
  • 玩家可以独立存在,因此是AR。
  • Fixture,删除后,还必须删除它的Halves,因此是AR。
  • 一半必须是Fixture中的实体。
  • 当删除一半时,必须删除FormationPlayed,所以这可能是半实体。
  • 删除阵型时必须删除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中的哪些概念(例如,我还需要分解大型聚合吗?)
  5. 我希望这一切都有道理。如果需要,显然很乐意进一步解释。意识到我在这里问了很多,我可能会混淆一些想法。你能给予的任何答案和智慧都将不胜感激!

1 个答案:

答案 0 :(得分:3)

  

您是否在上述设计中看到任何错误?如果是这样你会改变什么?

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

我的意思的一个例子

  

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

Udi Dahan写道:Don't Delete, Just Don't。如果某个实体具有生命周期,并且该生命周期已结束,那么您将对其进行标记,但您不会删除该实体。

  

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

大!需要注意的是,你在网上找到的很多例子都没有让这一部分正确 - 由于历史原因,模型的许多演示与他们对持久性的副作用紧密相关。

  

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

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

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

  

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

关于这一点的一些想法 - 通常会保存用于更复杂的用例(Greg Young:"这是你获得竞争优势的地方吗?")。聚合的大部分功能来自于它们确保状态变化的一致性这一事实。当您真正的问题是数据输入和报告时,它往往是过度的。

检测和纠正不一致通常比试图预防正确更容易/更便宜;考虑到成本,可能对业务感到满意。要记住的事情。

  

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

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

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

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

利息总量是根据给出的信息为自己做出决策的。

如果一名球员在一个赛季中得分超过10球,那么总体上的一个人为设想的例子就是为你排球。请注意,虽然"目标"事件流中已存在某些内容,业务规则并不存在。这纯粹是一个领域模型的东西。

因此,这样做的方式是,每次出现目标事件时,您都会加载JerseyPerchasing聚合,然后告诉它目标。这个聚合将确保这是一个新的目标(不是之前报道的那个),并确定是否要求订购衬衫的目标数量,检查是否已经放置了衬衫的订单。 / p>

这里的关键想法 - 目标是汇总被告知的事情。购买运动衫的决定是由聚合物决定的,并与世界分享。

后来,你意识到有时一个玩家被交易,然后得到第10个目标。而且你必须确定作为一个企业是否意味着你为每件球衣得到一件衬衫(哪个?)或一件衬衫,或者如果他在一个赛季为一支特定球队打进10球,你可能只订购球衣。所有这些逻辑都集中在一起。

  

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

最好看的地方,就像听起来一样奇怪,是功能性编程类型之一。 Mark Seemann's blog包含许多有用的重要提示。

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

  

您是否认为接受的观点是,对于这样大小的域,应该采用贫血模型

如果您只是为了更容易消费而重新组织来自现实世界的信息?是的 - 加载文档,更新文档,存储文档对我来说比对一堆聚合建模过度使用更有意义。但是请不要过多地阅读 - 我对你的模型了解得比你在这里写的更多。如果您评估来自现实世界的信息的实际业务复杂性,那么答案就会有所不同。