在Web应用程序中DDD聚合真的是一个好主意吗?


40

我将深入研究领域驱动设计,而我遇到的一些概念从表面上看很有意义,但是当我更多地考虑它们时,我不得不怀疑这是否真的是一个好主意。

例如,聚合的概念很有意义。您可以创建较小的所有权域,这样就不必处理整个域模型。

但是,当我在Web应用程序上下文中考虑此问题时,我们经常访问数据库以拉回小的数据子集。例如,页面可能仅列出订单数量,并带有单击链接以打开订单并查看其订单ID的链接。

如果我理解正确的聚集,我通常会使用存储库模式返回,将包含成员的OrderAggregate GetAllGetByIDDelete,和Save。好的,听起来不错。但...

如果我调用GetAll列出我所有的订单,那么在我看来,这种模式将需要返回整个汇总信息列表,完整的订单,订单行等...当我只需要该信息的一小部分时(仅标头信息)。

我想念什么吗?还是在这里使用某种程度的优化?我无法想象有人会主张在不需要时返回全部信息。

当然,可以在您的存储库上创建诸如之类的方法GetOrderHeaders,但这似乎违背了首先使用诸如存储库之类的模式的目的。

谁能为我澄清一下?

编辑:

经过大量研究后,我认为这里的脱节之处在于,纯存储库模式与大多数人认为的存储库模式不同。

Fowler将存储库定义为使用集合语义的数据存储,通常存储在内存中。这意味着创建整个对象图。

Evans修改了存储库以包括聚合根,因此截取了存储库以仅支持聚合中的对象。

大多数人似乎将存储库视为美化的数据访问对象,在其中您只是创建获取所需数据的方法。正如Fowler的企业应用程序架构模式中所描述的那样,这似乎并不是目的。

还有一些人认为存储库是一个简单的抽象,主要用于简化测试和模拟,或将持久性与系统的其余部分分离。

我猜答案是,这是一个比我最初想象的要复杂得多的概念。


4
“我想答案是,这是一个比我最初想象的要复杂得多的概念。” 这是非常真实的。
2011年

根据您的情况,您可以为聚合根对象创建一个代理,该代理仅在需要时才有选择地检索和缓存数据
Steven A. Lowe11年

我的建议是在根聚合关联中实现延迟加载。因此,您可以检索根列表,而无需加载太多对象。
朱利诺2015年

4
近6年后,这仍然是一个很好的问题。阅读红皮书中的章节后,我会说:不要使集合体太大。试图从您的域中选择一些顶级概念并将其声明为“治本之道”是很诱人的,但DDD却主张较小的聚合。并减轻上述效率低下的问题。
CPT。Senkfuss

您的集合应该尽可能小,同时对域自然且有效(挑战!)。此外,对于您的存储库,使用高度特定的方法非常好,并且很理想。
Timo

Answers:


30

不要使用域模型和聚合进行查询。

实际上,您要问的是一个足够普遍的问题,因此已经建立了一组原则和模式来避免这种情况。它称为CQRS


2
@Mystere Man:不,这为了提供所需的最少数据。这是分离读取模型的主要目的之一。它还有助于预先解决一些并发问题。当将CQRS应用于DDD时,有很多好处。
2011年

2
@Mystere:如果您阅读了我链接的文章,我会很惊讶您会错过它。请参阅标题为“查询(报告)”的部分:“当应用程序正在请求数据时……应在一次对查询层的调用中完成此操作,作为回报,它将获得一个包含所有所需数据的单个DTO。这样做的原因是,通常查询数据的次数要比执行域行为的次数多得多,因此,通过对此进行优化,可以增强系统的感知性能。”
2011年

4
这就是为什么我们不使用存储库在CQRS系统中读取数据的原因。我们将编写一个简单的类来封装查询(使用方便或需要的任何技术,通常在这里很适合使用ADO.Net或Linq2Sql或SubSonic),仅返回(全部)手头任务所需的数据,以避免在DDD存储库的所有普通层中拖动数据。如果我们要向域发送命令,则仅使用存储库来检索聚合。
2011年

9
“我无法想象有人会主张在不需要时返回全部信息。” 我想说的是您对这个说法完全正确。不需要时,不要检索全部信息。这是应用于DDD的CQRS的核心。您不需要聚合即可查询。通过不同的机制获取数据,然后始终如一地进行操作。
2011年

2
@qes确实,最好的解决方案是不使用DDD进行查询(读取):)但是您仍然在Command部分中使用DDD,即用于存储或更新数据。所以我有一个问题,当您需要更新数据库中的数据时,是否始终使用带有实体的存储库?假设您只需要更改列中的一个小值(某种形式的开关),是否仍将整个实体加载到App层中,更改一个值(属性),然后将整个实体保存回DB中?也有点过分杀伤力了吗?
安德鲁

8

我在如何最好地在域驱动设计中使用存储库模式方面感到挣扎,但仍在挣扎。在第一次使用它之后,我想到了以下做法:

  1. 存储库应该很简单;它仅负责存储和检索域对象。所有其他逻辑应在其他对象中,例如工厂和域服务。

  2. 存储库的行为就像一个集合,就好像它是内存中集合根的集合一样。

  3. 存储库不是通用DAO,每个存储库都有其唯一且狭窄的接口。存储库通常具有特定的查找器方法,使您可以按域搜索集合(例如:给我所有用户X的未结订单)。存储库本身可以在通用DAO的帮助下实现。

  4. 理想情况下,finder方法将仅返回聚合根。如果这样做效率低下,它还可能返回仅包含所需值的只读值对象(如果这些值对象也可以用域表示,则为加号)。最后,存储库还可以用于返回聚合根的子集或子集的集合。

  5. 这些选择取决于所使用的技术,因为您需要找到一种使用所使用的技术最有效地表达域模型的方法。


当然,这绝对是一个复杂的主题。很难将理论付诸实践,尤其是当将两种截然不同的理论组合成一个实践时。
塞巴斯蒂安·帕滕2014年

6

我认为您的GetOrderHeaders方法根本不会破坏存储库的目的。

DDD(除其他事项外)与确保通过聚合根(例如,您没有OrderDetailsRepository)来获得所需的内容有关,但它并没有限制您的提及方式。

如果OrderHeader是Domain概念,则应按此定义它,并具有适当的存储库方法来检索它们。只要确保您要通过正确的聚合根即可。


也许我在这里混淆了概念,但是我对存储库模式的理解是通过使用用于持久性的标准接口来将持久性与域分离。如果您必须为特定功能添加自定义方法,则似乎又需要重新关联。
Erik Funkenbusch

1
持久性机制与域分离,但不持久化。如果您发现自己说诸如“我们需要在此处列出订单标题”之类的字眼,则需要在您的域中对OrderHeader建模,并提供一种从存储库中检索它们的方法。
埃里克·金

另外,不要挂在“持久性的标准接口”上。没有通用的存储库模式可以满足所有可能的应用程序的需求。除了标准的“ GetById”,“ Save”等之外,每个应用程序还将具有许多存储库方法。这些方法是起点,而不是终点。
埃里克·金

4

我对DDD的使用可能不被认为是“纯粹的” DDD,但是我已经对DB数据存储使用了DDD来适应以下现实世界的策略。

  • 聚合根具有关联的存储库
  • 关联的存储库仅由该聚合根使用(它不是公开可用的)
  • 存储库可以包含查询调用(例如,GetAllActiveOrders,GetOrderItemsForOrder)
  • 服务公开了存储库的公共子集和其他非按需操作(例如,从一个银行帐户向另一个银行帐户转账,LoadById,搜索/查找,CreateEntity等)。
  • 我使用Root-> Service-> Repository stack。DDD服务仅假定用于实体无法回答自己的任何事情(例如,LoadById,TransferMoneyFromAccountToAccount),但在现实世界中,即使root应该能够自己“回答/执行”这些命令。请注意,授予实体访问另一个聚合根服务的权限没有错!但是,请记住,您将不包括在服务(GetOrderItemsForOrder)中,而是将其包括在存储库中,以便聚合根可以使用它。请注意,服务不应像存储库那样公开任何打开的查询。
  • 我通常在域模型中(通过接口)抽象定义存储库,并提供单独的具体实现。我在域模型中完全定义了服务,并注入了具体的存储库以供使用。

**您不必带回整个汇总。但是,如果您想要更多,则必须询问根目录,而不是其他服务或存储库。这是延迟加载,可以通过手动延迟加载(将适当的存储库/服务注入根目录)来手动完成,也可以使用和ORM支持。

在您的示例中,如果我想在单独的调用中加载详细信息,则可能会提供仅带订单标头的存储库调用。请注意,通过使用“ OrderHeader”,我们实际上是在域中引入了一个附加概念。


3

您的域模型以最纯粹的形式包含您的业务逻辑。支持业务运营的所有关系和运营。概念图上缺少的是应用程序服务层的概念,服务层环绕领域模型,并提供了业务领域的简化视图(如果可以的话,是一个投影),该视图允许域模型根据需要进行更改不会直接影响使用服务层的应用程序。

走得更远。聚合的想法是存在一个对象,即聚合根,负责维护聚合的一致性。在您的示例中,订单将负责操纵其订单行。

在您的示例中,服务层将公开诸如GetOrdersForCustomer之类的操作,该操作仅返回查看订单摘要列表所需的内容(您将其称为OrderHeaders)。

最后,存储库模式不只是一个集合,还允许声明性查询。在C#中,您可以使用LINQ作为查询对象,或者大多数其他O / RM也提供查询对象规范。

存储库在域和数据映射层之间进行中介,就像内存中的域对象集合一样。客户端对象以声明方式构造查询规范,然后将其提交给存储库以使其满意。(来自Fowler的存储库页面

看到您可以针对存储库创建查询,因此提供方便的方法来处理常见查询也很有意义。即,如果您只想要订单的标头,则可以创建一个仅返回标头的查询,并从存储库中的便捷方法中公开它。

希望这有助于澄清问题。


0

我知道这是一个古老的问题,但我似乎得出了不同的答案。

在创建存储库时,通常会包装一些缓存的查询。

Fowler将存储库定义为使用集合语义的数据存储,通常存储在内存中。这意味着创建整个对象图。

将这些存储库保存在服务器中。它们不只是将对象传递给数据库!

如果我在具有列出订单页面的Web应用程序中,则可以单击以查看详细信息,我可能希望我的订单列表页面具有有关订单的详细信息(ID,名称,金额,日期)帮助用户决定要看哪一个。

此时,您有两个选择。

  1. 您可以查询数据库,然后精确拉回列出清单所需的内容,然后再次查询以拉出需要在详细信息页面上看到的各个详细信息。

  2. 您可以进行1个查询,以拉回所有信息并将其缓存。在下一页请求中,您从服务器ram而不是数据库中读取。如果他的用户回击或选择了下一页,则您仍在零访问数据库。

实际上,如何实现它以及实现细节。如果我最大的用户有10个订单,则可能要选择选项2。如果我说的是10,000个订单,则需要选项1。在以上两种情况下,在其他许多情况下,我都希望存储库隐藏该实现细节。

向前看,如果我能在订单列表页面上获得一张票来告诉用户上个月他们在订单上花费了多少(汇总数据),我宁愿编写逻辑以在SQL中进行计算并再次往返于数据库还是您宁愿使用服务器内存中已经存在的数据来计算它?

以我的经验,聚合可以带来巨大的好处。

  • 它们是真正有效的重用部分代码。
  • 它们通过将业务逻辑放在核心层中来简化代码,而不必钻取基础结构层来让sql server来完成。
  • 它们还可以减少所需的查询数量,从而极大地加快了您的响应速度,因为您可以轻松地缓存它们。
  • 我正在编写的SQL通常更易于维护,因为我经常只是询问所有内容并计算服务器端。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.