在哪里验证域驱动设计中的分页逻辑?

Where to validate pagination logic in domain driven design?

在 DDD 中,分页查询的验证逻辑应该放在哪里? 例如,如果服务层收到一个集合查询,其参数看起来像(在 Go 中),尽管可以用任何语言回答:

// in one file
package repositories

type Page struct {
   Limit int
   Offset int
}

// Should Page, which is part of the repository 
// layer, have validation behaviour?
func (p *Page) Validate() error {
   if p.Limit > 100 {
      // ...
   }
}

type Repository interface {
  func getCollection(p *Page) (collection, error)
}



// in another file
package service

import "repositories"

type Service struct {
  repository repositories.Repository
}

// service layer
func (s *Service) getCollection(p *repositories.Page) (pages, error) {
    // delegate validation to the repository layer?
    // i.e - p.Validate()

    // or call some kind of validation logic in the service layer
    // i.e - validatePagination(p)

    s.repository.getCollection(p)
}

func validatePagination(p *Page) error {
  if p.Limit > 100 {
    ...
  }
}

我想执行一个 "no Limit greater than 100" 规则,这个规则属于服务层还是更像是一个存储库问题?

乍一看似乎应该在 Repository 层强制执行,但转念一想,这不一定是 repository 本身的实际限制。它更多是由属于实体模型的业务约束驱动的规则。然而 Page 也不是真正的域实体,它更像是存储库层的 属性。

对我来说,这种验证逻辑似乎介于业务规则和存储库问题之间。验证逻辑应该放在哪里?

At first glance it seems like it should be enforced at the Repository layer, but on second thought, it's not necessarily an actual limitation of the repository itself. It's more of a rule driven by business constraints that belongs on the entity model.

实际上 存储库仍然是域 。它们是域和数据映射层之间的中介。因此,您仍应将它们视为 domain.

因此,存储库接口实现应该强制执行域规则。

总而言之,我会问自己:我是否要允许 non-paginated 存储库从任何域操作访问抽象数据?。答案应该是 可能不是 ,因为这样的域可能拥有数千个域对象,并且尝试一次获取太多域对象将是次优检索,不是吗?

建议

* 由于我不知道当前使用哪种语言的 OP,而且我发现编程语言在这个问答中并不重要,我将解释一个可能的使用 C# 的方法,OP 可以将其翻译成任何编程语言.

对我来说,每个查询 不超过 100 个结果的规则应该是一个 cross-cutting 问题。与 相反,我真的相信可以用代码表达的东西是可行的方法,它不仅是一个优化问题,而且是对整个解决方案强制执行的规则。

根据我上面所说的,我将设计一个 Repository 抽象 class 来提供整个解决方案中的一些通用行为和规则:

public interface IRepository<T>
{
      IList<T> List(int skip = 0, int take = 0);
      // Other method definitions like Add, Remove, GetById...
}

public abstract class Repository<T> : IRepository<T>
{
      protected virtual void EnsureValidPagination(int skip = 0, int take = 0)
      {
           if(take > 100)
           {
                 throw new ArgumentException("take", "Cannot take more than 100 objects at once");
           }
      }

      public IList<T> List(int skip = 0, int take = 0)
      {
           EnsureValidPagination(skip, take);

           return DoList<T>(skip, take);
      }

      protected abstract IList<T> DoList(int skip = 0, int take = 0);

      // Other methods like Add, Remove, GetById...
}

现在,只要您执行涉及返回对象集合的操作,您就可以在任何 IRepository<T> 的实现中调用 EnsureValidPagination,该实现也将继承 Repository<T>

如果您需要对某些特定域实施此类规则,您可以设计另一个抽象 class 派生一些像我上面描述的,并引入那里有整个规则。

在我的例子中,我总是实现一个 solution-wide repository base class 并且如果需要我将它专门用于每个域,我将它用作基于 class 到特定域存储库实现。

回答一些@guillaume31 comment/concern 他的回答

I agree that it isn't a domain-specific rule. But Application and Presentation aren't domain either. Repository is probably a bit too sweeping and low-level for me -- what if a command line data utility wants to fetch a vast number of items and still use the same domain and persistence layers as the other applications?

假设您定义了一个存储库接口,如下所示:

public interface IProductRepository
{
      IList<Product> List(int skip = 0, int take = 0);
}

接口不会定义对我一次可以拿走多少产品的限制,但请参阅以下 IProductRepository 的实现:

public class ProductRepository : IRepository
{
     public ProductRepository(int defaultMaxListingResults = -1)
     {
          DefaultMaxListingResults  = defaultMaxListingResults;
     }

     private int DefaultMaxListingResults { get; }

     private void EnsureListingArguments(int skip = 0, int take = 0)
     {
          if(take > DefaultMaxListingResults)
          {
               throw new InvalidOperationException($"This repository can't take more results than {DefaultMaxListingResults} at once");
          }
     }

     public IList<Product> List(int skip = 0, int take = 0)
     {
            EnsureListingArguments(skip, take);
     }
}

谁说我们需要 harcode 一次可以获取的最大结果数?如果不同的应用层使用相同的域,我看到你将无法根据这些应用层的特定要求注入不同的构造函数参数。

我看到相同的服务层注入完全相同的存储库实现,但配置不同,具体取决于整个域的消费者。

根本不是技术要求

我想把我的两分钱花在其他回答者达成的一些共识上,我认为这是部分正确的。

共识是一种限制,就像 OP 要求的那样是技术要求而不是业务要求

顺便说一句,似乎没有人把重点放在域可以相互通信这一事实上。也就是说,你没有设计你的域和其他层来支持更传统的执行流程:data <-> data mapping <-> repository <-> service layer <-> application service <-> presentation(这只是一个示例流程,它可能是它的变体).

域在所有可能的场景或用例中都应该是防弹的,它会被消费或交互。因此,您应该考虑以下场景:域交互。

我们不应该少一些哲理,更多地准备好看到现实世界的场景,整个规则可以通过两种方式发生:

  1. 整个项目不允许同时使用超过 100 个域对象
  2. 不允许一个或多个域同时使用超过 100 个域对象

有些人认为我们谈论的是技术要求,但对我来说是域规则因为它还强制执行良好的编码实践为什么? 因为我真的认为,在一天结束时,您不可能想要获得整个域对象集合,因为分页有很多种一个是无限滚动分页,它也可以应用于command-line界面,模拟get all[=]的感觉95=] 操作。所以,强制你的整个解决方案做正确的事情,并避免get all 操作,并且域本身的实现可能与没有时不同分页限制。

顺便说一句,你应该考虑以下策略:域强制你不能检索超过 100 个域对象,但它上面的任何其他层也可以定义低于 100 的限制:你不能一次获取超过 50 个域对象,否则 UI 会遇到性能问题。这不会违反域规则,因为如果您人为地将您可以获得的内容限制在其规则范围内,域不会哭泣。

可能在Application层,甚至Presentation。

如果您希望该规则适用于所有前端(网络、移动应用程序等),请选择应用程序;如果限制与特定设备能够在屏幕上显示的数量有关,请选择演示时间.

[编辑澄清]

从其他答案和评论来看,我们实际上是在谈论保护性能的防御性编程

它不能在域层 IMO 中,因为它是 programmer-to-programmer 的东西,而不是域要求。当您与您的铁路领域专家交谈时,他们是否提出或关心一次可以从任何一组火车中取出的最大火车数量?可能不会。它不是通用语言。

基础结构层(存储库实现)是一个选项,但正如我所说,我发现在如此低的级别上控制事物不方便且限制过多。 Matías 提出的参数化存储库的实现被公认为是一个优雅的解决方案,因为每个应用程序都可以指定自己的最大值,所以为什么不呢 - 如果你真的想对整个应用程序 XRepository.GetAll() 应用广泛的限制 [=26] =].

"It's more of a rule driven by business constraints that belongs on the entity model"

这些规则通常不是业务规则,它们只是由于技术系统限制(例如保证系统的稳定性)而制定的(很可能是由没有业务专家参与的开发人员制定的)。它们通常在应用层找到它们的天然归宿,但如果这样做更实用,也可以放置在其他地方。

另一方面,如果业务专家对 resource/cost 因素感兴趣并决定将其推向市场,以便客户可以支付更多费用来一次查看更多内容,那么这将成为业务规则;企业真正关心的事情。

在那种情况下,规则检查肯定不会在 Repository 中进行,因为业务规则会被隐藏在基础设施层中。不仅如此,Repository 非常 low-level 并且可以用于自动化脚本或其他您不希望应用这些限制的过程。

实际上,我通常会应用一些 CQRS 原则并避免完全通过存储库进行查询,但那是另一回事了。

对我来说,危险信号与@plalx 识别的相同。具体来说:

It's more of a rule driven by business constraints that belongs on the entity model

很可能发生以下两种情况之一。两者中不太可能的是业务用户正在尝试定义领域模型的技术应用程序。每隔一段时间,您就会有一个对技术有足够了解的业务用户试图插入这些东西,他们应该被倾听——作为一个问题,而不是一个要求。用例不应定义性能属性,因为这些是应用程序本身的验收标准。

这导致了更可能的情况,因为业务用户正在根据用户界面描述分页。同样,这是应该讨论的事情。但是,这不是用例,因为它适用于域。限制数据集大小绝对没有错。重要的是如何限制这些尺寸。有一个明显的担忧,即过多的数据可能会被撤回。例如,如果您的域包含数以万计的产品,您可能不需要所有这些产品 returned.

要解决这个问题,您还应该首先了解为什么您的场景会 return 太多数据。当纯粹从存储库的角度来看时,存储库仅用作 CRUD 工厂。如果您关心的是开发人员可以使用存储库做什么,那么还有其他方法可以对大型数据集进行分页,而不会将技术或应用程序问题渗入域中。如果您可以安全地推断出分页方面是应用程序实现所拥有的东西,那么在应用程序服务中将分页代码完全置于域之外绝对没有错。让应用程序服务执行理解应用程序的分页要求、解释这些要求,然后非常具体地告诉域它想要什么的繁重工作。

考虑使用采用标识符数组的 GetById() 方法,而不是使用某种 GetAll() 方法。您的应用程序服务执行 "searching" 的专门任务并确定应用程序期望看到的内容。好处可能不会立即显现出来,但是当您搜索数百万条记录时,您会怎么做?如果你想考虑使用像 Lucene、Endeca、FAST 或类似的东西,你真的需要为此破解域吗?什么时候,或者如果,你到了想要更改技术细节的地步,而你发现自己不得不真正触及你的领域,对我来说,这是一个相当大的问题。当您的域开始为多个应用程序提供服务时,所有这些应用程序是否会共享相同的应用程序要求?

最后一点是我觉得最中肯的一点。几年前,我处于同样的境地。我们的域在存储库内部有分页,因为我们有一个业务用户,他有足够的影响力和足够的技术知识来构成危险。尽管团队反对,我们还是被否决了(这本身就是一种讨论)。最终,我们被迫将分页放在域内。第二年,我们开始在其他应用程序的业务内部使用域的概念。实际的业务规则从未改变,但我们搜索的方式却改变了——取决于应用程序。这让我们不得不提出另一套方法来适应,并承诺在未来和解。

当我们最终传达的信息是,通过允许应用程序拥有自己的要求并提供一种方法来解决特定问题 - 例如 "give me these specific products"。 "give me twenty products, sorted in this fashion, with a specific offset" 之前的方法根本没有描述域。每个应用程序都确定 "pagination" 对自身的最终意义以及它希望如何加载这些结果。最高结果,在分页集中间颠倒顺序等。这些都被消除了,因为它们更接近它们的实际职责,我们在保护域的同时授权应用程序。我们使用服务层来描述所考虑的内容 "safe"。由于服务层充当域和应用程序之间的 go-between,我们可以在 service-level 拒绝请求,例如,如果应用程序请求超过一百个结果。这样,应用程序就不能随心所欲了,并且域可以愉快地忘记应用于正在进行的调用的技术限制。