在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 {
...
}
}
我希望执行大于100的" no Limit
"规则,此规则是属于服务层还是更多的是存储库问题?
乍一看似乎应该在Repository层强制执行,但是第二个想法,它不一定是存储库本身的实际限制。它更多的是由属于实体模型的业务约束驱动的规则。但是Page
也不是域实体,它更像是Repository层的属性。
对我而言,这种验证逻辑似乎陷入了业务规则和存储库问题之间。验证逻辑应该放在哪里?
答案 0 :(得分:5)
“这更像是由业务约束所驱动的规则 实体模型“
这些规则通常不是业务规则,由于技术系统的限制(例如,保证系统的稳定性),它们很简单(很可能是没有业务专家参与的开发人员)。他们通常在应用层中找到自然的家,但如果更实际,可以放在其他地方。
另一方面,如果业务专家对资源/成本因素感兴趣并决定将其推向市场,以便客户可以支付更多费用以便一次查看更多信息,那么这将成为业务规则;企业真正关心的事情。
在这种情况下,规则检查肯定不会进入Repository
,因为业务规则会隐藏在基础架构层中。不仅如此,Repository
非常低级,可以用于自动脚本或其他您不希望应用这些限制的过程。
实际上,我通常会应用一些CQRS原则,并避免完全通过存储库进行查询,但这是另一个故事。
答案 1 :(得分:3)
我的红旗与@plalx确定的相同。具体做法是:
更多的是由业务约束驱动的规则 实体模型
很有可能,两件事之一正在发生。两者的可能性较小的是业务用户试图将域模型定义为技术应用程序。每隔一段时间,你就会有一个对技术有足够了解的商业用户试图插入这些东西,他们应该被倾听 - 作为一个关注,而不是一个要求。用例不应定义性能属性,因为它们是应用程序本身的接受标准。
这导致更可能的情况,因为业务用户在用户界面方面描述分页。同样,这是应该谈论的事情。但是,这不是用例,因为它适用于域。限制数据集大小绝对没有问题。重要的是如何限制这些尺寸。有一个明显的担忧是可以撤回太多数据。例如,如果您的域包含数万种产品,则可能不希望返回所有这些产品。
要解决此问题,您还应该了解为什么您的方案可以首先返回太多数据。纯粹从存储库的角度来看它时,存储库仅用作CRUD工厂。如果您关注的是开发人员可以对存储库执行的操作,那么还有其他方法可以对大型数据集进行分页,而不会影响域中的技术或应用程序问题。如果您可以安全地推断出分页的方面是应用程序实现所拥有的东西,那么在应用程序服务中完全在域外使用分页代码绝对没有错。让应用程序服务重新理解应用程序的分页要求,解释这些要求,然后非常具体地告诉域名它想要什么。
考虑使用带有标识符数组的GetById()方法,而不是使用某种GetAll()方法。您的应用程序服务执行"搜索"并确定应用程序期望看到的内容。好处可能不会立即显现,但是当您搜索数百万条记录时,您会怎么做?如果你想考虑使用像Lucene,Endeca,FAST或类似的东西,你真的需要破解域吗?当您或者如果您想要更改技术细节并发现自己必须实际触摸您的域名时,对我来说,这是一个相当大的问题。当您的域开始为多个应用程序提供服务时,所有这些应用程序是否会共享相同的应用程序要求?
最后一点是我发现最能回家的那一点。几年前,我处于同样的境地。我们的域名在存储库中有分页,因为我们有一个业务用户,他有足够的摇摆和足够的技术知识是危险的。尽管团队反对,但我们被推翻了(这是对自己的讨论)。最终,我们被迫将分页置于域内。第二年,我们开始在其他应用程序的概念内使用该域名。实际的业务规则从未改变,但我们搜索的方式确实如此 - 取决于应用程序。这使我们不得不提出另一套方法来适应,并承诺未来的和解。
当我们最终通过允许应用程序拥有应用程序来避免域中的这些持续更改时,第四个使用域的应用程序(用于外部第三方使用)进行了协调。自己的要求并提供一种方法来促进特定的问题 - 例如"给我这些特定的产品"。以前的方法"给我20个产品,以这种方式排序,具有特定的偏移量"绝不描述域名。每个应用程序确定了一个"分页"最终意味着自己以及它如何加载这些结果。最好的结果,在分页集合中间颠倒顺序等等。这些都被淘汰了,因为它们更接近实际职责,我们授权应用程序,同时仍保护域名。我们使用服务层作为所考虑内容的描述" safe"。由于服务层充当域与应用程序之间的中间人,因此,例如,如果应用程序请求了超过一百个结果,则我们可以拒绝服务级别的请求。通过这种方式,应用程序不仅可以做任何令人满意的事情,而且对于正在应用于正在进行的调用的技术限制,域名仍然高兴无视。
答案 2 :(得分:2)
乍一看,它似乎应该在存储库中强制执行 层,但第二个想法,它不一定是实际的 存储库本身的限制。它更像是一个由...驱动的规则 属于实体模型的业务约束。
实际上存储库仍然是域。他们是域和数据映射层之间的中介。因此,您仍应将它们视为域。
因此,存储库接口实现应该强制执行域规则。
总之,我会问自己:我是否想允许存储库从任何域操作中对抽象数据进行非分页访问?。答案应该是可能不是,因为这样的域可能拥有数千个域对象,并且尝试同时获取太多域对象将是次优检索,不是吗? / p>
* 由于我不知道哪种语言正在使用OP,并且我发现编程语言对此Q& A不重要,我将解释使用C#的可能方法,OP可以将其转换为任何编程语言。
对我来说,强制执行每个查询不超过100个结果规则应该是一个贯穿各领域的问题。与@plalx has said on his answer相反,我真的相信可以用代码表达的东西是要走的路,它不仅是一个优化问题,而是一个强制执行整个解决方案的规则。
根据我上面所说的,我会设计一个Repository
抽象类来提供整个解决方案中的一些常见行为和规则:
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...
}
现在,只要实现涉及返回对象集合的操作,您就可以在任何EnsureValidPagination
的实现中调用IRepository<T>
,这也将继承Repository<T>
。
如果您需要对某个特定域强制执行此类规则,您可以设计另一个抽象类,派生出类似我上面描述的那些,并在那里引入整个规则。
就我而言,我总是实现一个解决方案范围的存储库基类,如果需要,我会在每个域上专门化它,并将其用作特定域存储库实现的基类。
我同意这不是针对特定领域的规则。但应用和 演示文稿也不属于域名。存储库可能有点太过分了 扫描和低级别对我来说 - 如果命令行数据实用程序 想要获取大量项目并仍然使用相同的域 和持久层作为其他应用程序?
想象一下,您已按如下方式定义了存储库界面:
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
(这只是一个示例流程,它可能是它的变体)
域应该在所有可能的场景中使用防弹,或者用于消费或交互的用例。因此,您应该考虑以下场景:域交互。
我们不应该更少哲学,更愿意看到现实世界的场景,整个规则可以通过两种方式实现:
有些人认为我们正在讨论技术要求,但对我来说,域规则因为它还强制执行良好的编码实践。 为什么?因为我真的认为,在一天结束时,你不可能想要获得整个域对象集合,因为分页有很多种类,一个是无限滚动分页 ,它也可以应用于命令行界面并模拟 get all 操作的感觉。因此,强制您的整个解决方案做正确的事情,并避免获取所有操作,并且可能域名本身的实现方式不同于没有分页限制的情况
顺便说一下,你应该考虑以下策略:域强制你不能检索超过100个域对象,但是它上面的任何其他层也可以定义低于100的限制:你可以&#39 ; t一次获得超过50个域对象,否则UI将遇到性能问题。这不会打破域名规则,因为如果你人为地限制你在规则范围内可以获得的内容,域名就不会哭。答案 3 :(得分:0)
可能在应用层,甚至是演示文稿。
如果您希望该规则适用于所有前端(Web,移动应用程序等),请选择“应用程序”。如果限制与特定设备一次能够在屏幕上显示的数量有关,请选择“演示”。
[编辑澄清]
从其他答案和评论来看,我们真的在谈论防御性编程以保护性能。
它不能在域层IMO中,因为它是程序员到程序员的事情,而不是域要求。当您与铁路领域专家交谈时,他们是否会提出或关注一次可以从任何一组列车中取出的最大列车数量?可能不是。它不在普遍存在的语言中。
基础设施层(存储库实施)是一个选项,但正如我所说,我发现在如此低的水平上控制事物是不方便和过度限制的。 Matías建议实现参数化存储库无疑是一个优雅的解决方案,因为每个应用程序都可以指定自己的最大值,所以为什么不 - 如果你真的想在XRepository.GetAll()
上对整个应用空间应用广泛的扫描限制。 / p>