我目前正在使用.net和nhibernate为旅游行业的客户开发一些相当大的应用程序,并且遇到了实施DDD的一些问题,以及团队内部就最佳方式进行分歧的问题。我希望有人可以提供一些指导。
目前,我们已经在域外实现了一个服务层,每个聚合根服务([EntityName] Service)。 所有其他层使用这些服务通过GetByThis()和GetByTheOther()等方法获取对聚合根的引用。我们从其他层调用域名都是通过这些服务进行的。
服务包含对存储库的注入引用(在其他地方没有引用),并且还负责所有保存/更新行为和管理事务性。 服务方法的复杂性越来越高,有时候行为似乎属于域,就像条件创建逻辑一样(如果property = this set child对象,则为其他东西)。 我们的域实体主要有简单的方法,如GetByThis()和HasAThing()。我觉得我们正在失去我们领域的表现力。
我的主要问题是:
修改
感谢@ david-masters和@ guillaume31精心设计的答案。
你帮助我解决了我感觉到的“臭味代码”。
首先,我应该说我们有一个(非常)遗留的oracle DB来应对,因此Id生成要求(以及其他问题)。
对于任何看过这个问题的人来说,这两个答案都提出了很好的建议,但对我来说,这是最好的建议:
“从务实的角度来看,我会问自己:如果我想参与我的Domain层的一部分并在另一个应用程序中重用它,它是否包含我需要利用域中的所有业务规则和行为那个新的申请?如果没有,那么可能意味着当前在应用程序端的一些部分需要移动到域层。“
考虑到这一点,我重新评估了我们的域和服务层,现在相信我已经解决了我们的设计问题
答案 0 :(得分:12)
应用程序服务层不应包含域逻辑。应用程序服务的目的是“协调”。它不应该做出任何域决定;所有业务决策都应该在域对象或域服务中。应用程序服务接收来自使用者(通常是UI)的调用,并调用域和基础结构服务中的方法。应用程序服务不应该像您所描述的那样具有crud名称。它们应该有描述用例的有意义的动词。以下是银行应用程序的应用程序服务的示例:
public class AccountService : IAccountService
{
//These are injected via dependency injection on the constructor
private readonly IAccountRepository _accountRespository;
private readonly IEmailNotificationService _emailNotificationServce;
public void FreezeAccount(Guid accountId)
{
Account account = _accountRespository.GetById(accountId);
using (IUnitOfWork unitOfWork = UnitOfWorkFactory.Create())
{
account.Freeze();
_accountRespository.Save(account);
_emailNotificationServce.Send(CreateFreezeNotification(account));
}
}
}
我建议您的实体/聚合不保留对存储库的引用,因此根本没有依赖关系。如果聚合需要来自第二个聚合的信息来做出决定,那么应用程序服务应从其存储库中获取第二个聚合并通过方法将其传递给第一个聚合。
我会将相同的原则应用于域服务。如果需要域服务(通常情况下,用例需要在一个事务中涉及多个聚合(尽管您应该尝试通过更好地设计聚合来避免这种情况以减少并发问题)),那么应用程序服务应首先获取所需的聚合,然后将它们传递给域服务。然后,域服务可以在聚合上调用域逻辑。
应在此应用程序服务级别处理事务。如您所见,所有被调用和持久化的逻辑都包含在UnitOfWork中。只有当此块完成且没有错误时,事务才会完成。
关于ID:我总是选择Guid而不是数据库ID。我发现生活更容易,避免了你描述的问题。如果您的数据库需要使用ID(例如INT IDENTITY列),那么您可以将其设为辅助ID属性,并使用Guid ID进行域名管理以节省开销吗?
答案 1 :(得分:5)
服务层是否应包含如此多的逻辑?如果不是在哪里呢 去吗?
我假设你在这里谈论Application层中的服务,而不是Domain层。看起来你的域名对象几乎是贫血的,有些人认为这是一种反模式,但对此有很多争论。
从务实的角度来看,我会问自己:如果我想参与我的Domain层的一部分并在另一个应用程序中重用它,它是否包含我需要利用域中的所有业务规则和行为新申请?如果没有,则可能意味着当前在应用程序端的某些部分需要移动到域层。
请注意,我在这里谈的是纯粹的域业务规则,而不是特定于应用程序的规则。例如,某些操作必须通过具有4个步骤的向导执行,在最后一步结束时要求用户进行确认,并且在最后一步之后将所有修改刷新到持久存储中特定的业务规则,而不是域规则。因此,不应将它们移动到域层。
如果域,应该聚合根持有对存储库的引用吗?
IMO一个聚合根不应该拥有对它自己的存储库的引用,并且知道如何存储它自己,因为它打破了持久性的无知,并在域对象中引入了一个额外的责任,它会混淆它。但是,聚合根可能偶尔会持有对另一个实体的存储库的引用。
如何处理交易?
我想说有两种类型的交易:
“应用程序”层中的“用户”事务,即“工作单元”。这些是跨越用例的高级事务,或者是Web应用程序中网页生命周期的最后一个事务(“在视图中打开会话”)。 ORM框架通常提供管理这些交易的工具。
域层中的事务。它们可以由域对象或服务启动。例如:FundsTransferService.Transfer()可以在内部使用事务。在这里,您可以使用平台的基本事务处理。
实体(或聚合根)是否应保留对域的引用 服务?如果是这样,他们应该如何获得参考文献?
是的,实体有时可以调用域服务。域服务包含不属于任何实体的域规则和行为。您可以对这些依赖项进行硬编码或将它们注入实体中,具体取决于所需的解耦级别。
为了获得实体的新ID,我们必须调用存储的 我们已经包装在存储库中的过程。你会在哪里 参考这个?需要创建的实体上的一些复杂方法 许多子实体需要参考这个吗?
我不建议为按需访问的实体生成ID。正如大卫指出的那样,当你新建实体时,在语言层面生成一个Guid通常是个更好的主意。
如果您仍想使用ID生成路径,则调用ID生成器通常是您实体的Factory而不是实体本身的工作。