ABP官方文档翻译 3.4 领域服务
领域服务
领域服务(或者在DDD中单纯的服务)用来执行领域操作和业务规则。Eric Evans在他的DDD书中描述了一个好的服务有三个特征:
1. 与领域概念关联的操作,但不是实体或值对象的自然组成部分。
2. 接口的定义依照领域模型的其他元素。
3. 操作是无状态的。
不像应用服务那样获取或返回DTO,领域服务获取或返回领域对象(如实体或值对象)。
领域服务可以被应用服务或其他领域服务使用,但不能被展现层(应用层可以)直接使用。
IDomainService接口和DomainService类
ABP定义了IDomainService接口,并约定所有的领域服务都实现这个接口。当被实现时,领域服务自动注册到依赖注入系统,调用类型为临时的。
领域服务可以继承DomainService类。从而,可以使用一些继承属性来记录日志,本地化等等。当然,即使没有继承,如果需要的话也可以注入他们。
假定我们有一个任务管理系统,当分配任务到一个人的时候需要执行业务规则。
首先,我们定义这个服务的接口(不是必须,但最好这样作为最佳实践):
public interface ITaskManager : IDomainService { void AssignTaskToPerson(Task task, Person person); }
如上所见,TaskManager服务使用领域对象工作:Task和Person。命名领域服务有些约定,可以为TaskManager、TaskService或TaskDomainService.....
让我们来看看如何实现:
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个Active状态的任务。
你可能会想为什么第一次检查时抛出ApplicationException而第二次检查时抛出UserFriendlyException。这个和领域服务没有任何关系。在这里仅仅是个例子,抛出哪种类型异常完全由我们自己决定。我认为,用户接口必须检查任务状态,并且不允许我们分配任务到人,这是一个应用错误,应该对用户不可见。第二个对UI来讲是很难检查到的,我们可以给用户一个易读的错误信息。
现在,我们看看如何从应用服务中使用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); } }
任务应用服务使用指定的DTO(input)和仓储获取相关的任务和人,并把他们传递给Task Manager(领域服务)。
基于上面的示例,你可能有些问题。
你可能会说为什么应用服务不实现在领域服务的逻辑?
我们可以简单来说,这不是应用服务的任务。因为这不是一个用例而是业务操作。我们或许会使用同样的“分配一个任务到人”的领域逻辑在不同的用例下。比方说,我们可能有另一个场景,某种情况下更新了这个任务并且这个更新包含分配这个任务到另一个人。所以,我们可以在这里使用相同的领域逻辑。还有,我们可能有两个不同的UI(一个移动应用和一个web应用)共享相同的领域或者我们可能有有一个为远程客户端使用的Web Api,而远程客户端包含分配任务的操作。
如果你的领域比较简单,只有一个UI并且分配任务到人只在一个点完成,那么可以考虑跳过领域服务,直接在应用服务实现逻辑。这对DDD来讲不是最佳实践,但是ABP并不强制。
可以看到,应用服务可以很容易的使用领域服务:
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; } //...other members and codes of Task entity public void AssignToPerson(Person person, ITaskPolicy taskPolicy) { taskPolicy.CheckIfCanAssignTaskToPerson(this, person); AssignedPersonId = person.Id; } }
我们更改了AssignedPersonId的setter为protected。所以,在Task实体类之外将不能更改它。添加一个AssignToPerson方法,接收一个person和task plicy。CheckIfCanAssignTaskToPerson方法检查是否为有效的分配,如果无效则抛出一个合适的异常(它的实现在这里不重要)。然后应用服务方法将变为如下所示:
public void AssignTaskToPerson(AssignTaskToPersonInput input) { var task = _taskRepository.Get(input.TaskId); var person = _personRepository.Get(input.PersonId); task.AssignToPerson(person, _taskPolicy); }
我们注入了ITaskPolicy作为_taskPolicy并把他传递给AssignToPersion方法。现在,没有第二种方式分配一个任务到人了。我们应该总是使用AssignToPerson方法,并且不能跳过业务规则。