【问题标题】:Implementing Udi's Fetching Strategy - How do I search?实施 Udi 的获取策略 - 我如何搜索?
【发布时间】:2026-01-22 12:10:01
【问题描述】:

背景

Udi Dahan 建议将fetching strategy 作为用于数据访问的有用模式。我同意。

这个概念是明确角色。例如,我有一个聚合根 - 客户。我希望客户在我的应用程序的几个部分中 - 可供选择的客户列表、客户详细信息的视图以及用于停用客户的按钮。

似乎 Udi 会为这些角色中的每一个建议一个界面。所以我有ICustomerInList 非常基本的详细信息,ICustomerDetail 包括最近购买的 10 种产品,IDeactivateCustomer 有一种方法可以停用客户。每个接口都暴露了我的客户聚合根,以便在每种情况下完成工作。我的客户聚合根实现了所有这些接口。

现在我想为这些角色中的每一个实施一个获取策略。每个策略都可以将不同数量的数据加载到我的聚合根中,因为它位于一个接口后面,只暴露所需的信息位。

实现这部分的一般方法是询问服务定位器或其他样式的依赖注入。此代码将获取您想要的接口,例如ICustomerInList,并找到一个获取策略来加载它(IStrategyForFetching<ICustomerInList>)。这个策略是由一个类实现的,该类知道只加载具有 ICustomerInList 接口所需的信息位的客户。

到目前为止一切顺利。

问题

您传递给服务定位器或IStrategyForFetching<ICustomerInList> 的内容。我看到的所有示例都仅通过已知 ID 选择一个对象。这种情况很简单,调用代码通过这个id,会得到具体的接口。

如果我想搜索怎么办?或者我想要客户列表的第 2 页?现在我想传递更多获取策略所需的术语。

可能的解决方案

我见过的一些示例使用谓词 - 如果特定聚合根应该是结果集的一部分,则返回 true 或 false 的表达式。这在条件下工作得很好,但是让前 n 个客户回来而不是更多呢?或者获取搜索结果的第 2 页?或者结果如何排序?

我的第一反应是开始向我的IStrategyForFetching<ICustomerInList> 添加泛型参数,现在它变成了IStrategyForFetching<TAggregateRoot, TStrategyForSelecting, TStrategyForOrdering>。这很快变得复杂而丑陋。不同的存储库使情况更加复杂。一些存储库仅在使用特定策略进行选择时提供数据,有些仅提供某些类型的排序。我希望能够灵活地实现可以采用排序功能的通用存储库以及仅返回以特定方式排序的聚合根的专用存储库。

听起来我应该应用开始时使用的相同模式 - 如何明确角色?我是否应该使用有效负载 Y(搜索/排序参数)来实现获取 X(聚合根)的策略?

编辑 (2012-03-05)

如果我不是每次都返回聚合根,这一切仍然有效。如果每个接口由不同的 DTO 实现,我仍然可以使用 IStrategyForFetching。这就是这种模式强大的原因 - 获取和返回的内容不必以任何方式映射到聚合根。

我最终使用了IStrategyForFetching<TEntity, TSpecification>。 TEntity 是我想要得到的东西,TSpecification 是我想要得到它的方式。

【问题讨论】:

    标签: domain-driven-design aggregateroot fetching-strategy marker-interfaces


    【解决方案1】:

    你遇到过CQRS吗? Udi 是一个big proponent of it,它的目的就是解决这个确切的问题。

    最基本形式的概念是将域模型与查询分开。这意味着域模型仅在您想要执行命令/提交事务时发挥作用。您不会使用聚合和实体中的数据在屏幕上显示信息。相反,您创建一个单独的数据访问服务(或一组),其中包含提供每个屏幕所需的准确数据的方法。这些方法可以接受标准对象作为参数,因此可以使用您想要的任何标准进行搜索。

    这是如何工作的快速序列:

    • 屏幕显示上周已下订单的客户列表。
    • UI 调用 CustomerQueryService,传递日期作为条件。
    • CustomerQueryService 执行的查询仅返回此屏幕所需的字段,包括每个客户的聚合 ID。
    • 用户在列表中选择一个客户,然后选择执行“Make Important Customer”操作/命令。
    • UI 将 MakeImportantCommand 发送到包含客户 ID 的命令服务(或 DDD 术语中的应用服务)。
    • 命令服务使用命令中传递的 ID 从存储库中获取客户聚合,调用必要的方法并更新数据库。

    使用 CQRS 架构构建您的应用程序为您提供了许多关于性能和可扩展性的可能性。您可以通过为每个视图、最终一致性和事件溯源创建包含非规范化表的单独查询数据库来进一步采用这个简单的示例。有很多关于 CQRS 的视频/示例/博客,我认为您会很感兴趣。

    我知道您的问题是关于“获取策略”的,但我注意到他在 2007 年写了这篇文章,他很可能认为 CQRS 是它的继承者。

    总结一下我的回答:

    1. 不要尝试从您的域聚合项目中削减 DTO。相反,只需创建单独的查询服务,为您提供量身定制的查询以满足您的需求。
    2. 阅读 CQRS(如果您还没有的话)。

    【讨论】:

    • 感谢您的观点!我将这种技术与我读过很多的 CQRS 一起使用。 IStrategyForFetching<ICustomerInList> 不必返回聚合,它可以只为列表中的客户返回一个投影。我的建议是,与其直接与CustomerQueryService 交谈,我们可以请求角色 ICustomerInList 并且系统的一部分知道如何为我们获取该角色。它类似于IValidate<Customer> 标记接口模式。
    【解决方案2】:

    添加到David Masters 的响应中,我认为所有获取策略接口都在增加不必要的复杂性。让客户 AR 实现在 UI 之后建模的各种接口是对 AR 类的不必要约束,您将花费大量精力来尝试强制执行它。此外,它是一种脆弱的解决方案。如果视图需要与客户相关但不属于客户类的数据怎么办?然后是否强制客户类和相应的 ORM 映射包含该数据?为什么不只使用一组单独的类来进行查询并使用它呢?这允许您在它们所属的位置处理获取策略 - 在存储库中。此外,获取策略接口抽象真正增加了什么价值?它可能是应用程序中正在发生的事情的适当模型,但对实现它没有帮助。

    【讨论】:

    • 同样,聚合根不必实现每个接口 - 可以使用 DTO。这些接口可以被认为是视图模型。 Udi 建议服务层以与领域层不同的速率变化。因此,在每一层之间有一个间接层是很好的。我可以向“系统”询问ICustomerInList 并通过CQRS 我直接从数据库中读取DTO。我的问题是关于“系统”的。 IStrategyForFetchingIFetchCustomersInLists.