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

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

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

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

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

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


当前回答

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

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

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

// 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>())

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

其他回答

To cite Carolina Lilientahl, "Patterns should prevent cycles" https://www.youtube.com/watch?v=eJjadzMRQAk, where she refers to cyclic dependencies between classes. In case of repositories inside aggregates, there is a temptation to create cyclic dependencies out of conveniance of object navigation as the only reason. The pattern mentioned above by prograhammer, that was recommended by Vernon Vaughn, where other aggregates are referenced by ids instead of root instances, (is there a name for this pattern?) suggests an alternative that might guide into other solutions.

类之间循环依赖的例子(忏悔):

(Time0): Sample和Well这两个类彼此引用(循环依赖)。Well指的是Sample,而Sample指的是Well,这是为了方便(有时是对样品进行循环,有时是对一个板中的所有孔进行循环)。我无法想象样本不会指向它所在的井。

(Time1):一年之后,许多用例实现了....现在有一些情况下,样本不应该指向它所在的井。在一个工作步骤内有临时板。这里的孔指的是样品,而样品又指的是另一个盘子上的孔。正因为如此,当有人试图实现新功能时,有时会出现奇怪的行为。渗透需要时间。

我也从上面提到的这篇关于惰性加载的负面方面的文章中得到了帮助。

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

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

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

这是一个非常好的问题。我期待着就此进行一些讨论。但我想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的生命周期中,这可以为模型增加更多的复杂性。

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

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

这里有点混乱。存储库访问聚合根。聚合根是实体。这样做的原因是关注点的分离和良好的分层。这在小型项目中没有意义,但如果您在一个大型团队中,您会说:“您通过产品存储库访问产品。Product是实体集合的聚合根,包括ProductCatalog对象。如果你想要更新ProductCatalog,你必须通过ProductRepository。”

通过这种方式,您在业务逻辑和内容更新的位置上有非常非常清晰的分离。你不会有一个孩子独自编写了整个程序,对产品目录做了所有这些复杂的事情,当涉及到将其集成到上游项目时,你坐在那里看着它,意识到这一切都必须抛弃。这也意味着当人们加入团队,添加新功能时,他们知道该去哪里以及如何组织程序。

But wait! Repository also refers to the persistence layer, as in the Repository Pattern. In a better world an Eric Evans' Repository and the Repository Pattern would have separate names, because they tend to overlap quite a bit. To get the repository pattern you have contrast with other ways in which data is accessed, with a service bus or an event model system. Usually when you get to this level, the Eric Evans' Repository definition goes by the way side and you start talking about a bounded context. Each bounded context is essentially its own application. You might have a sophisticated approval system for getting things into the product catalog. In your original design the product was the center piece but in this bounded context the product catalog is. You still might access product information and update product via a service bus, but you must realize that a product catalog outside the bounded context might mean something completely different.

Back to your original question. If you're accessing a repository from within an entity it means the entity is really not a business entity but probably something that should exist in a service layer. This is because entities are business object and should concern themselves with being as much like a DSL (domain specific language) as possible. Only have business information in this layer. If you're troubleshooting a performance issue, you'll know to look elsewhere since only business information should be here. If suddenly, you have application issues here, you're making it very hard to extend and maintain an application, which is really the heart of DDD: making maintainable software.

对评论1的回应:好问题。所以并不是所有的验证都发生在领域层。夏普有一个属性“DomainSignature”,它可以做你想要的。它是持久性感知的,但是作为一个属性可以使域层保持干净。它确保在您的示例中,没有名称相同的重复实体。

But let's talk about more complicated validation rules. Let's say you're Amazon.com. Have you ever ordered something with an expired credit card? I have, where I haven't updated the card and bought something. It accepts the order and the UI informs me that everything is peachy. About 15 minutes later, I'll get an e-mail saying there's a problem with my order, my credit card is invalid. What's happening here is that, ideally, there's some regex validation in the domain layer. Is this a correct credit card number? If yes, persist the order. However, there's additional validation at the application tasks layer, where an external service is queried to see if payment can be made on the credit card. If not, don't actually ship anything, suspend the order and wait for the customer. This should all take place in a service layer.

不要害怕在服务层创建可以访问存储库的验证对象。让它远离域层。