有没有一种优雅的方法来检查域对象属性上的唯一约束,而又不将业务逻辑移入服务层?


10

我已经适应领域驱动设计大约8年了,即使经过了这些年,仍然有一件事困扰着我。那就是针对域对象检查数据存储中的唯一记录。

2013年9月,马丁·福勒(Martin Fowler)提到了TellDon'tAsk原则,如果可能的话,应将其应用于所有领域对象,然后应返回一条消息,说明操作如何进行(在面向对象的设计中,这通常是通过异常来完成的,操作失败)。

我的项目通常分为许多部分,其中两个部分是“域”(包含业务规则,没有其他内容,该域完全不考虑持久性)和“服务”。知道用于CRUD数据的存储库层的服务。

由于属于对象的属性的唯一性是域/业务规则,因此对于域模块来说应该很长,因此该规则正是它应该在的位置。

为了能够检查记录的唯一性,您需要查询当前数据集(通常是数据库),以查明是否存在另一个假设的记录Name

考虑到域层对于持久性是无知的,并且不知道如何检索数据,而只知道如何对它们进行操作,因此它无法真正地访问存储库本身。

我一直在适应的设计如下所示:

class ProductRepository
{
    // throws Repository.RecordNotFoundException
    public Product GetBySKU(string sku);
}

class ProductCrudService
{
    private ProductRepository pr;

    public ProductCrudService(ProductRepository repository)
    {
        pr = repository;
    }

    public void SaveProduct(Domain.Product product)
    {
        try {
            pr.GetBySKU(product.SKU);

            throw Service.ProductWithSKUAlreadyExistsException("msg");
        } catch (Repository.RecordNotFoundException e) {
            // suppress/log exception
        }

        pr.MarkFresh(product);
        pr.ProcessChanges();
    }
}

这导致服务定义域规则,而不是域层本身,并且规则分散在代码的多个部分。

我提到了TellDon'tAsk原理,因为正如您可以清楚地看到的那样,该服务提供了一个操作(它可以保存Product或引发异常),但是在方法内部,您可以使用过程方法来操作对象。

显而易见的解决方案是Domain.ProductCollection使用Add(Domain.Product)抛出的方法创建一个类ProductWithSKUAlreadyExistsException,但是它缺乏性能,因为您需要从数据存储中获取所有产品,以便在代码中查找某个产品是否已经具有相同的SKU作为您要添加的产品。

你们如何解决这个特定问题?就其本身而言,这并不是真正的问题,我已经让服务层代表某些域规则已有多年了。服务层通常还为更复杂的域操作提供服务,我只是想知道您在职业生涯中是否偶然发现了更好,更集中的解决方案。


当您只要求一个名字并根据是否找到逻辑进行选择时,为什么要提取整个名字列表呢?只要与接口达成协议,您就是在告诉持久层该做什么而不是该怎么做。
JeffO

1
使用“服务”一词时,我们应努力提高清晰度,例如域层中的域服务和应用程序层中的应用程序服务之间的区别。参见gorodinski.com/blog/2012/04/14/…–
Erik Eidt

出色的文章@ErikEidt,谢谢。我想我的设计没错,那么,如果我可以相信Gorodinski先生,他的意思差不多:一种更好的解决方案是让应用程序服务检索实体所需的信息,有效地设置执行环境,并提供将其发送给实体,并且与我一样反对直接将存储库注入到Domain模型中,这主要是通过破坏SRP来实现的。
安迪

引用“告诉,不要问”的引用: “但是就我个人而言,我不使用“告诉-不要问”。
–radbobo

Answers:


7

考虑到域层对于持久性是无知的,并且不知道如何检索数据,而只知道如何对它们进行操作,因此它无法真正地访问存储库本身。

我不同意这部分。特别是最后一句话。

虽然确实应该对持久性不了解,但它确实知道存在“域实体集合”。并且存在涉及整个集合的领域规则。唯一性就是其中之一。并且由于实际逻辑的实现在很大程度上取决于特定的持久性模式,因此在域中必须有某种抽象来指定对此逻辑的需求。

因此,就像创建一个可以查询名称是否已经存在的接口一样简单,然后在您的数据存储区中实现该接口,然后由需要知道该名称是否唯一的人调用。

我想强调一下,存储库是DOMAIN服务。它们是关于持久性的抽象。它是存储库的实现,应该与域分开。域实体调用域服务绝对没有错。一个实体能够使用存储库来检索另一个实体或检索某些无法轻易保存在内存中的特定信息,这没有任何问题。这就是为什么仓库是Evans书中的关键概念的原因。


谢谢你的反馈。Domain.ProductCollection考虑到存储区负责从“域”层检索对象,存储区是否可以真正代表我的想法?
安迪

@DavidPacker我不太明白你的意思。因为应该没有必要将所有项目都保留在内存中。“ DoesNameExist”方法的实现应该(最可能是)数据存储侧的SQL查询。
欣快感'16

意思是,当我不想知道所有产品时,不需要将数据存储在内存中的集合中,Domain.ProductCollection而是询问存储库,与询问是否Domain.ProductCollection包含包含产品的产品相同。这次再次通过SKU,而是询问存储库(这实际上是示例),而不是遍历预加载的产品来查询基础数据库。我不是要把所有产品都存储在内存中,除非我必须这样做,否则完全是胡说八道。
安迪

这就引出了另一个问题,那么存储库应该知道一个属性是否应该唯一吗?我一直将存储库实现为非常笨拙的组件,将您传递给存储库的内容保存起来,并尝试根据传递的条件检索数据并将决策投入服务。
安迪

@DavidPacker好吧,“领域知识”在界面中。界面显示“域需要此功能”,然后数据存储提供该功能。如果您具有IProductRepository与的接口DoesNameExist,则该方法应该做什么很明显。但是,应确保“产品”不能与现有产品具有相同的名称,应该在某个域中。
欣快感'16

4

您需要阅读有关集合验证的 Greg Young 。

简短的答案:在您过分深入研究之前,您需要确保从业务角度了解需求的价值。实际上,检测并减轻重复而不是阻止重复的代价是多少?

“唯一性”要求的问题是,很多时候人们想要它们有一个更深层的根本原因- 伊夫·雷恩豪特Yves Reynhout)

更长的答案:我已经看到了很多可能性,但是它们都有一定的权衡。

您可以在将命令发送到域之前检查重复项。这可以在客户端或服务中完成(您的示例显示了该技术)。如果您对逻辑泄漏出域层不满意,可以使用来获得相同的结果DomainService

class Product {
    void register(SKU sku, DuplicationService skuLookup) {
        if (skuLookup.isKnownSku(sku) {
            throw ProductWithSKUAlreadyExistsException(...)
        }
        ...
    }
}

当然,以这种方式完成DeduplicationService的实现将需要了解一些有关如何查找现有功能的知识。因此,尽管它将一些工作推回了领域,但您仍然面临着相同的基本问题(需要对集合验证的回答,竞争条件的问题)。

您可以在持久层本身中进行验证。关系数据库确实擅长于集合验证。在产品的sku列上放置一个唯一性约束,就可以了。该应用程序只是将产品保存到存储库中,如果出现问题,您将遇到约束违规冒泡的情况。因此,应用程序代码看起来不错,并且消除了竞争条件,但是您泄漏了“域”规则。

您可以在您的域中创建一个单独的集合,以表示一组已知的skus。我在这里可以想到两种变化。

一个是类似ProductCatalog的东西。产品存在于其他地方,但是产品和skus之间的关系由保证sku唯一性的目录维护。这并不意味着产品没有任何怀疑。skus由ProductCatalog分配(如果您需要skus是唯一的,则可以通过仅具有一个ProductCatalog集合来实现)。与您的领域专家一起检查普遍存在的语言-如果存在这种情况,这很可能是正确的方法。

另一种选择是更类似于sku预订服务。基本机制是相同的:聚合知道所有skus,因此可以防止引入重复项。但是机制略有不同:在将SKU分配给产品之前,您需要在SKU上获得租赁;在创建产品时,您会将其租约传递给SKU。比赛条件仍然存在(不同的聚合,因此交易不同),但是它具有不同的风格。真正的缺点是,您在没有真正使用域语言的情况下将租赁服务投影到域模型中。

您可以将所有产品实体合并为一个集合-即上述产品目录。当您这样做时,您绝对会获得skus的唯一性,但是代价是额外的争用,修改任何产品确实意味着修改整个目录。

我不喜欢将所有产品SKU从数据库中拉出以在内存中进行操作。

也许您不需要。如果使用Bloom过滤器测试sku ,则可以发现许多独特的skus,而无需完全加载集合。

如果您的用例允许您任意决定拒绝哪个skus,则可以消除所有误报(如果让客户在提交命令之前测试他们提议的sksk,那没什么大不了的)。这样可以避免将集加载到内存中。

(如果您希望得到更多的接受,可以考虑在bloom过滤器中发生匹配的情况下延迟加载skus;有时仍然会冒着将所有skus加载到内存中的风险,但是如果允许的话,这不应该是常见的情况客户端代码以在发送前检查命令是否有错误)。


我不喜欢将所有产品SKU从数据库中拉出以在内存中进行操作。这是我在最初的问题中提出并质疑其性能的明显解决方案。同样,IMO依赖于数据库中的约束来负责唯一性也是很糟糕的。如果您要切换到新的数据库引擎,并且在转换过程中以某种方式失去了唯一性约束,则说明代码已经损坏,因为以前存在的数据库信息不再存在。无论如何,谢谢您的链接,有趣的阅读。
安迪

也许是布隆过滤器(请参阅编辑)。
VoiceOfUnreason
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.