真是个好问题。我也在探索的路上,互联网上的大多数答案带来的问题似乎和它们带来的解决方案一样多。
所以(冒着写一些我一年后不同意的东西的风险),这里是我迄今为止的发现。
首先,我们喜欢丰富的领域模型,它为我们提供了高可发现性(我们可以用聚合做什么)和可读性(表达方法调用)。
// 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>())
我很高兴听到你的意见,喜好和改进!