在领域驱动设计中,似乎有很多一致认为实体不应该直接访问存储库。

这是出自Eric Evans的《领域驱动设计》一书,还是来自其他地方?

对其背后的原因有什么好的解释吗?

编辑:澄清一下:我不是在谈论将数据访问从业务逻辑分离到一个单独层的经典OO实践——我是在谈论DDD中的具体安排,即实体根本不应该与数据访问层对话(即它们不应该持有对Repository对象的引用)。

更新:我把奖金给了BacceSR,因为他的答案似乎最接近,但我仍然对此一无所知。如果这是一个如此重要的原则,网上肯定会有一些关于它的好文章,不是吗?

更新:2013年3月,对这个问题的点赞表明人们对此很感兴趣,即使有很多答案,我仍然认为如果人们对此有想法,还有更多的空间。


当前回答

这是一个非常好的问题。我期待着就此进行一些讨论。但我想DDD的几本书里都提到了吉米·尼尔森斯和埃里克·埃文斯。我想通过示例也可以看到如何使用存储库模式。

BUT lets discuss. I think a very valid thought is why should an entity know about how to persist another entity? Important with DDD is that each entity has a responsibility to manage its own "knowledge-sphere" and shouldn't know anything about how to read or write other entities. Sure you can probably just add a repository interface to Entity A for reading Entities B. But the risk is that you expose knowledge for how to persist B. Will entity A also do validation on B before persisting B into db?

正如您所看到的,实体A可以更多地参与到实体B的生命周期中,这可以为模型增加更多的复杂性。

我猜(没有任何例子)单元测试将会更加复杂。

但是我确信总会有这样的场景:您想通过实体来使用存储库。你必须考虑每一种情况才能做出有效的判断。优点和缺点。但是在我看来,存储库实体解决方案有很多缺点。它一定是一个非常特殊的场景,优点抵消了缺点....

其他回答

对我来说,这似乎是与OOD相关的一般良好实践,而不是DDD特有的。

我能想到的原因是:

关注点分离(实体应该与它们的持久化方式分离。因为根据使用场景的不同,可能存在多种策略,其中相同的实体将被持久化) 从逻辑上讲,实体可以在存储库操作的级别之下的级别中看到。较低级别的组件不应该具有较高级别组件的知识。因此,条目不应该有关于存储库的知识。

在理想的情况下,DDD建议实体不应该引用数据层。但是我们并不是生活在理想的世界里。域可能需要引用与它们没有依赖关系的其他业务逻辑域对象。实体以只读的目的引用存储库层来获取值是合乎逻辑的。

真是个好问题。我也在探索的路上,互联网上的大多数答案带来的问题似乎和它们带来的解决方案一样多。

所以(冒着写一些我一年后不同意的东西的风险),这里是我迄今为止的发现。

首先,我们喜欢丰富的领域模型,它为我们提供了高可发现性(我们可以用聚合做什么)和可读性(表达方法调用)。

// Entity
public class Invoice
{
    ...
    public void SetStatus(StatusCode statusCode, DateTime dateTime) { ... }
    public void CreateCreditNote(decimal amount) { ... }
    ...
}

我们希望在不向实体的构造函数中注入任何服务的情况下实现这一点,因为:

Introduction of a new behavior (that uses a new service) could lead to a constructor change, meaning the change affects every line that instantiates the entity! These services are not part of the model, but constructor-injection would suggest that they were. Often a service (even its interface) is an implementation detail rather than part of the domain. The domain model would have an outward-facing dependency. It can be confusing why the entity cannot exist without these dependencies. (A credit note service, you say? I am not even going to do anything with credit notes...) It would make it hard instantiate, thus hard to test. The problem spreads easily, because other entities containing this one would get the same dependencies - which on them may look like very unnatural dependencies.

那么,我们该怎么做呢?到目前为止,我的结论是方法依赖和双重分派提供了一个不错的解决方案。

public class Invoice
{
    ...

    // Simple method injection
    public void SetStatus(IInvoiceLogger logger, StatusCode statusCode, DateTime dateTime)
    { ... }

    // Double dispatch
    public void CreateCreditNote(ICreditNoteService creditNoteService, decimal amount)
    {
        creditNoteService.CreateCreditNote(this, amount);
    }

    ...
}

CreateCreditNote()现在需要一个负责创建信用票据的服务。它使用双重分派,将工作完全卸载到负责的服务,同时保持来自Invoice实体的可发现性。

SetStatus()现在对记录器有一个简单的依赖,显然记录器将执行部分工作。

对于后者,为了使客户端代码更简单,我们可以通过IInvoiceService进行日志记录。毕竟,发票日志似乎是发票的固有特性。这样一个单独的IInvoiceService有助于避免对各种操作的各种迷你服务的需求。缺点是,它变得模糊,该服务究竟将做什么。它甚至可能开始看起来像双重分派,而大部分工作实际上仍然在SetStatus()本身中完成。

我们仍然可以将参数命名为“logger”,希望能够揭示我们的意图。不过看起来有点弱。

相反,我将选择请求一个iinvoiceogger(正如我们在代码示例中已经做的那样),并让IInvoiceService实现该接口。客户端代码可以简单地将它的单个IInvoiceService用于所有要求任何此类非常特殊的、发票固有的“迷你服务”的Invoice方法,而方法签名仍然非常清楚地表明它们要求的是什么。

我注意到我没有明确地谈到存储库。记录器是或使用存储库,但让我还提供一个更明确的示例。如果只在一两个方法中需要存储库,我们可以使用相同的方法。

public class Invoice
{
    public IEnumerable<CreditNote> GetCreditNotes(ICreditNoteRepository repository)
    { ... }
}

事实上,这为麻烦的惰性加载提供了另一种选择。

更新:出于历史原因,我把下面的文字留下来了,但我建议100%地避开惰性加载。

对于真正的、基于属性的惰性加载,我目前确实使用构造函数注入,但以一种不考虑持久性的方式。

public class Invoice
{
    // Lazy could use an interface (for contravariance if nothing else), but I digress
    public Lazy<IEnumerable<CreditNote>> CreditNotes { get; }

    // Give me something that will provide my credit notes
    public Invoice(Func<Invoice, IEnumerable<CreditNote>> lazyCreditNotes)
    {
        this.CreditNotes = new Lazy<IEnumerable<CreditNotes>>() => lazyCreditNotes(this));
    }
}

一方面,从数据库加载Invoice的存储库可以自由访问加载相应信用票据的函数,并将该函数注入到Invoice中。

另一方面,创建实际新Invoice的代码只会传递一个返回空列表的函数:

new Invoice(inv => new List<CreditNote>() as IEnumerable<CreditNote>)

(一个自定义的ILazy<out T>可以使我们摆脱难看的转换到IEnumerable,但这会使讨论复杂化。)

// Or just an empty IEnumerable
new Invoice(inv => IEnumerable.Empty<CreditNote>())

我很高兴听到你的意见,喜好和改进!

虽然来晚了,但我还是会说点我的意见。

从性能角度来看,在域模型中抽象REST API操作的存储库和域服务可能是一个重大灾难。我认为无论是域服务(尽管在红皮书中有其他说法!),还是聚合都不应该尝试与它们合作,这两个概念应该只留在应用服务领域,无论你使用Layers还是Hexagon(端口和适配器),它都有与外部世界通信的唯一责任。

通过这种方式,所有昂贵的I/O通信都由一个应用程序服务分配并完全控制。它将:

防止任何类型的性能瓶颈。 在域模型中防止任何类型的全局访问(存储库是全局的)。

建立正确的对象图,在应用服务中使用正确的抓取策略,将纯内存中的对象传递给富域模型。惰性加载会潜入你的代码,并在最痛的地方打击你。

弗农·沃恩给出了一个解决方案:

使用存储库或域服务提前查找依赖对象 调用聚合行为。客户端应用程序服务可以 控制这个问题。