基于DDD的.NET开发框架 - ABP领域服务
ABP是“ASP.NET Boilerplate Project (ASP.NET样板项目)”的简称。
ASP.NET Boilerplate是一个用最佳实践和流行技术开发现代WEB应用程序的新起点,它旨在成为一个通用的WEB应用程序框架和项目模板。
ABP的官方网站:http://www.aspnetboilerplate.com
ABP官方文档:http://www.aspnetboilerplate.com/Pages/Documents
Github上的开源项目:https://github.com/aspnetboilerplate
一、概念介绍
领域服务(或DDD中的服务)用于执行领域操作和业务规则。Eric Evans描述了一个好的服务应该具备下面三个特征:
1、和领域概念相关的操作不是一个实体或者值对象的本质部分。
2、接口定义在领域模型其他元素的条款中。
3、操作是无状态的。
跟获得或返回一个数据传输对象的应用服务方法(DTO)不同,领域服务获得或者返回一个领域对象(比如实体或值类型)。
一个领域服务可以用于应用服务,也可以用于其他的领域服务,但不能直接用于展现层,服务层才直接用于展现层。
二、IDomainService接口和DomainService类
ABP定义了IDomainService接口,所有的领域服务都按照惯例实现了该接口。当实现时,领域服务会以transient自动注册到依赖注入系统。
此外,领域服务(可选地)可以从DomainService类继承。因此,它可以使用一些继承的属性,比如logging,本地化等等。当然,如果没有继承,如果需要的话也可以注入这些属性。
三、示例
假设我们有一个任务管理系统并且有将一个任务派给一个人的业务规则。
1、创建一个接口
首先我们为该服务定义一个接口(不是必须的,但是这样是一个好的实践):
public interface ITaskManager : IDomainService { void AssignTaskToPerson(Task task, Person person); }
可以看到,TaskManager服务使用领域对象工作:一个Task 和一个Person。命名领域服务时存在一些惯例。它可以是TaskManager,TaskService或者TaskDomainService...
2、服务实现
先来看看下面这个实现:
public class TaskManager : DomainService, ITaskManager { public const int MaxActiveTaskCountForAPerson = 3; private readonly ITaskRepository _taskRepository; public TaskManager(ITaskRepository taskRepository) { _taskRepository = taskRepository; } public void AssignTaskToPerson(Task task, Person person) { if (task.AssignedPersonId == person.Id) { return; } if (task.State != TaskState.Active) { throw new ApplicationException("Can not assign a task to a person when task is not active!"); } if (HasPersonMaximumAssignedTask(person)) { throw new UserFriendlyException(L("MaxPersonTaskLimitMessage", person.Name)); } task.AssignedPersonId = person.Id; } private bool HasPersonMaximumAssignedTask(Person person) { var assignedTaskCount = _taskRepository.Count(t => t.State == TaskState.Active && t.AssignedPersonId == person.Id); return assignedTaskCount >= MaxActiveTaskCountForAPerson; } }
上面的代码定义了两个业务规则:
一个任务为了能够派给一个新人,它应该是Active(激活)的状态
一个人可以最多可以有3个激活的任务。
你可能想知道为啥第一次检测时抛出了一个ApplicationException,而第二次检查时抛出了UserFriendlyException,请关注后面博客的异常处理。这根领域服务根本无关。这里这样处理的想法是这样的,UI必须先要检查一个任务的状态,否则不应该允许我们将它派给一个人。这是一个应用程序的错误,并且我们可以向用户隐藏这个错误。对于第二个友好的异常信息,UI检查更加困难,而且我们可以向用户显示一个可读的错误信息。这只是一个例子而已。
3、调用应用服务
现在,来看看如何在一个应用服务中使用TaskManager:
public class TaskAppService : ApplicationService, ITaskAppService { private readonly IRepository<Task, long> _taskRepository; private readonly IRepository<Person> _personRepository; private readonly ITaskManager _taskManager; public TaskAppService(IRepository<Task, long> taskRepository, IRepository<Person> personRepository , ITaskManager taskManager) { _taskRepository = taskRepository; _personRepository = personRepository; _taskManager = taskManager; } public void AssignTaskToPerson(AssignTaskToPersonInput input) { var task = _taskRepository.Get(input.TaskId); var person = _personRepository.Get(input.PersonId); _taskManager.AssignTaskToPerson(task, person); } }
Task应用服务使用给定的DTO(输入)和仓储来检索相关的task和 person,并将它们传给 TaskManager(领域服务)。
四、讨论
基于上面的例子,你可能会存在下面的疑问。
1、为何不只使用应用服务
我们可以简单地说,它不是应用服务要干的活。因为领域逻辑不是一个用例(use-case),而是一个 业务操作。我们可以在不同的用例中使用相同的“将一个任务派给一个人”的逻辑。比如说我们以后会更新这个任务,并且将这个任务派给其他人。因此,我们可以使用相同的领域逻辑,这个逻辑就是“将一个任务派给一个人”,我们不用考虑这个具体的人和具体的任务。此外,我们可能有两个不同的UI(一个移动端应用和一个web应用)来共享相同的领域。
应用服务层中有一个应用服务方法,但却使用到了领域层的三个业务逻辑,因为在领域层中,获取单个task和person都各自为一个业务逻辑,将一个任务派给一个人又是一个业务逻辑。在应用服务层,我们只需要获得一个人和一个任务就行,然后将该任务派给这个人,根本不需要考虑这个人和这个任务的获取细节,也不用考虑任务派发的细节,因为这完全不是应用层考虑的事儿。
如果你的领域很简单,只有一个UI并且将一个任务派发给一个人在单点处就可以完成,那么你可以跳过领域服务,然后在应用服务层实现该逻辑。虽然这不是DDD的最佳实践,但是ABP不会强制你这么设计。
2、如何强制使用领域服务
你可以看到,应用服务只能做下面的事情:
public void AssignTaskToPerson(AssignTaskToPersonInput input) { var task = _taskRepository.Get(input.TaskId); task.AssignedPersonId = input.PersonId; }
开发这个应用服务的开发者可能不知道存在一个TaskManager,而且可以直接将给定的 PersonId设置给任务的 AssignedPersonId。那么,如何阻止他这样做呢?基于这些,在DDD领域中存在很多讨论和使用到的模式。我们不会涉及得很深,但是可以提供一种简单的方式。
我们可以将Task改成下面这样:
public class Task : Entity<long> { public virtual int? AssignedPersonId { get; protected set; } //...其他成员 public void AssignToPerson(Person person, ITaskPolicy taskPolicy) { taskPolicy.CheckIfCanAssignTaskToPerson(this, person); AssignedPersonId = person.Id; } }
可以将AssignedPersonId的setter改成protected。这样,它就不能在Task实体类之外改变了。添加一个需要一个Person和ITaskPolicy的参数。CheckIfCanAssignTaskToPerson方法检查这是否是一个有效的派发,如果无效就抛出一个适当的异常。最后,应用服务方法应该是这个样子的:
public void AssignTaskToPerson(AssignTaskToPersonInput input) { var task = _taskRepository.Get(input.TaskId); var person = _personRepository.Get(input.PersonId); task.AssignToPerson(person, _taskPolicy); }
现在,不存在将一个任务派给一个人的第二种方法了。我们应该总是要使用AssignToPerson方法,而且不能跳过业务规则了。