规范模式和 DDD

Posted

技术标签:

【中文标题】规范模式和 DDD【英文标题】:Specification pattern and DDD 【发布时间】:2013-08-13 16:31:38 【问题描述】:

在了解更多有关 DDD 模式的个人游戏项目的上下文中,我缺少一个用于过滤器的 Specification 对象。

环顾四周,似乎一切(如 LinQ)都面向 SQL 数据库。然而,对于许多 NoSQL 数据库,大多数查询,即使只是“从表中选择 *”也需要预定义的视图。然而,如果存储库正在映射 Web 服务,那么查询的类型也会更加严格。

考虑到非 SQL 数据库的限制,规范模式是否存在变体?我觉得这需要使用继承和静态声明来支持不同类型的持久性后端。

我应该如何在我的存储库中结合“排序”和“过滤”?例如,考虑 Order 项目列表的存储库。

(Query)findAllSortedByDate;
(Query)findAllSortedByName;
(Query)findAllSortedByQuantity;

因此,当以表格显示时,这些是不同类型的排序。由于我可能会处理大量结果,因此我从未考虑在视图或视图模型中进行排序或过滤。最初我想到了一个 Proyection 类,它可以根据用户操作从存储库中选择正确的查询。但是,如果我想将不同的排序策略与不同的过滤器结合起来,这将无法正常工作。

显然我需要某种类型的“规范”对象,但我不确定是否:

    我应该将它们用于我的存储库,让它们更智能吗?或者我应该将它们用于我的视图模型? 如何正确限制我的规范对象以获得良好的多语言持久性?

最初我考虑将 Repository 用作 collection-like interface 来执行任何查询,但现在我注意到 view model 也可能表现为“有状态”集合-like 接口,而前者是“无状态”的类集合接口。

    总的来说,我应该尝试在我的存储库中保留任何类型的排序/过滤吗?如果所有查询结果都可以加载到内存中,这样做似乎会增加不必要的复杂性。

更新: 为了增加这个问题的趣味性,还要考虑虽然 NoSQL 视图可以过滤和排序,但全文搜索可能需要外部索引引擎,例如 Lucene 或 SQLite-FTS,只为必须排序的查询提供实体的唯一标识并再次过滤。

【问题讨论】:

【参考方案1】:

在 DDD 中,规范是适用于域对象的谓词。它涵盖了“过滤”需求,但不包括“排序”需求,因为排序不使用域对象的布尔函数,而是使用属性选择和排序方向。

当我需要过滤和排序时,我通常会写一个存储库方法:

findAll(Specification<Order> specification, 
        SortingOptions<Order> sortingOptions)

此时,我们根本不需要考虑底层的持久性机制。您的域层存储库接口不应由您的数据存储决定,而应由域需求决定。

如果您的数据源难以创建过滤和/或排序的查询,您可以完美地选择事后在内存中对查询结果中的对象进行过滤/排序。但是,与其在视图模型中这样做,IMO 最好创建一个具体的存储库实现,它将对象加载到内存中并执行排序/过滤操作就在那里。

所有未来的附加表示层都将从中受益,如果数据源(无论是 NoSQL 数据库、Web 服务...)修复了它的缺陷,那么调整存储库实现会更加方便随着时间的推移。

【讨论】:

嗨,规范模式应该返回什么数据类型? IEnumerable、IQueryable、表达式?【参考方案2】:

过滤时

通过“类集合接口”,Fowler 并不是指暴露类似数组或列表的 API:例如,它与 ICollection&lt;T&gt; 无关!一个 Repository 应该encapsulate 持久层的所有技术细节,但它的 API 应该被定义,以便它在业务领域中具有表现力。

您应该将规范视为与域相关的逻辑谓词(实际上它们是域模型中的一等公民),可以组合它们以检查实体的不同质量从集合中选择实体(可以是存储库或简单列表的集合)。例如,在我为一家意大利银行设计的金融领域模型中,我有DurationOfMBondSpecificationStandardAndPoorsLongTermRatingSpecification 等等。

实际上,在 DDD 中,规范来自软件在运行期间必须强制执行的业务需求(通常是合同范围)。它们可以用作过滤器的抽象,但这更像是一种幸运的副作用。

排序中

大多数时候排序(以及切片和分组...)只是一个演示问题。当这是一个业务问题时,应该从领域专家的知识中提炼出适当的比较器(和分组器等)作为领域概念。 然而,即使它只是一个表示问题,在存储库中处理它的效率要高得多。

在 .NET 上,解决这些问题的一种可能(并且非常昂贵)的解决方案是编写一个自定义 LINQ 提供程序(或多个),它可以翻译所有可以用无处不在的查询表示的查询语言到所需的持久层。但是,此解决方案有一个主要缺点,如果您不能从一开始就翻译所有查询,您将永远无法估计使用域更改应用程序的工作量:时间将当您必须对 QueryProvider 进行深度重构以处理新的复杂查询时(这种重构将花费您远远超出您的承受能力)。

为了解决这个问题,在(正在进行中且非常有野心的)Epic 框架中(免责声明:我是核心开发者),我们选择加入规范模式和查询对象模式,提供了一个通用 API,使客户端能够使用规范进行过滤、使用比较器进行排序以及使用整数进行切片,而无需 LINQ 的(不可预测的)成本。

在 Fowler 方案(如下)中,我们将规范 (aCriteria) 和辅助排序要求都传递给存储库:

作为替代方案,您可以只使用custom repositories:如果您没有数千种不同类型的查询,这是迄今为止更便宜的方法。

奖励解决方案

一个快速但仍然正确的解决方案是“只”使用您的持久性语言作为查询。 DDD 用于复杂的操作边界,查询(大多数时候)不起作用:因此您可以简单地使用 SQL 或 NoSQL 数据库提供的语言来检索您需要的投影和/或您需要的实体的identifiers操纵。您将看到您查询的数据集与确保域不变量所需的数据集有何不同!

这就是为什么,例如,有时,序列化文件可能是解决当前问题的域持久性的最佳方法。 毕竟,什么是文件系统,如果不是最广泛的 NoSQL 数据库! :-D

【讨论】:

嗨,规范模式应该返回什么数据类型? IEnumerable、IQueryable、表达式?

以上是关于规范模式和 DDD的主要内容,如果未能解决你的问题,请参考以下文章

[译文] 应用领域驱动设计, 第三章 - 规范模式

MVC模式和DDD模式对比,谁才是银弹?

DDD学习笔录——简介DDD的战术模式问题空间和解空间

DDD/Presenter 模式 VS 用例优化查询

设计模式之美——DDD充血模式

AutoMapper 模式是不是违反 DDD 原则?