3.4 ABP领域层 - 领域服务

3.4.1 简介

领域服务(或者服务,在DDD模式中)是被用来执行领域操作或者业务规则的。Eric Evans 在他的DDD书中这样说过:一个好的Service应该有以下三个特征:

  1. 与领域概念相关的操作不是Entity或Value Object 的一个自然部分;
  2. 接口是根据领域模型的其它元素定义的;
  3. 操作是无状态的。

领域服务和Application Services 是不同的,Application Services 返回的是DTO,而领域服务返回的是领域对象(实体或者值类型)。

领域服务可以被应用服务和其它的领域服务调用,但是不可以被表现层直接调用(表现层可以直接调用应用服务)。

3.4.2 IDomainService 和 DomainService

ABP 定义了一个 IDomainService 接口,所有的领域服务都必须实现该接口(记住这是一个约定),一旦实现了这个接口,那么领域服务就会通过Dependency Injection 自动的注册到系统中作为一个暂时对象(Transient)。

领域服务也可以继承自 DomainService 类(这是可选的)。
因此,它可以用一些继承而来的属性来做日志记录,本地化等等;即使你不继承该类,如果你需要这些属性也是可以被注入的。

3.4.3 实例

假设我们有一个任务管理系统,并且我们有这样的业务规则, 把任务分配到个人。

1. 创建一个接口

首先,我们为这个服务定义一个接口(不是必须的,但是一个好的实践):

  1. public interface ITaskManager : IDomainService
  2. {
  3. void AssignTaskToPerson(Task task, Person person);
  4. }

正如你所看到的,TaskMananger 用到了领域对象 :Task 和 Person 。这里有一些领域服务的命名约定;例如:TaskMananger, TaskService 或者 TaskDomainService 等等。

2. 实现服务

实现如下:

  1. public class TaskManager : DomainService, ITaskManager
  2. {
  3. public const int MaxActiveTaskCountForAPerson = 3;
  4. private readonly ITaskRepository _taskRepository;
  5. public TaskManager(ITaskRepository taskRepository)
  6. {
  7. _taskRepository = taskRepository;
  8. }
  9. public void AssignTaskToPerson(Task task, Person person)
  10. {
  11. if (task.AssignedPersonId == person.Id)
  12. {
  13. return;
  14. }
  15. if (task.State != TaskState.Active)
  16. {
  17. throw new ApplicationException("Can not assign a task to a person when task is not active!");
  18. }
  19. if (HasPersonMaximumAssignedTask(person))
  20. {
  21. throw new UserFriendlyException(L("MaxPersonTaskLimitMessage", person.Name));
  22. }
  23. task.AssignedPersonId = person.Id;
  24. }
  25. private bool HasPersonMaximumAssignedTask(Person person)
  26. {
  27. var assignedTaskCount = _taskRepository.Count(t => t.State == TaskState.Active && t.AssignedPersonId == person.Id);
  28. return assignedTaskCount >= MaxActiveTaskCountForAPerson;
  29. }
  30. }

我们有如下两个业务规则:

  1. 分配给Person的任务状态应该是Active状态

  2. Person最多只能接受3个任务

你可能感到奇怪, 为啥我在做第一次检测的时候为什么我抛出了一个ApplicationException异常,第二次检测时抛出UserFriendlyException 异常。这个和领域服务没有半毛钱关系;我这样做仅仅是为了提供一个示例。这完全取决于你。我认为用户界面必须获取到任务状态和分配数量错误时的错误消息。并且我认为这是一个应用级的错误,我们可以不向用户展示这个难以理解的错误,我们应该向用户展示一个可读性好的错误消息。这仅仅是一个示例。

3.4.4 应用层调用领域服务

下面示例为我们展示了应用层是如何调用TaskMananger:

  1. public class TaskAppService : ApplicationService, ITaskAppService
  2. {
  3. private readonly IRepository<Task, long> _taskRepository;
  4. private readonly IRepository<Person> _personRepository;
  5. private readonly ITaskManager _taskManager;
  6. public TaskAppService(IRepository<Task, long> taskRepository, IRepository<Person> personRepository , ITaskManager taskManager)
  7. {
  8. _taskRepository = taskRepository;
  9. _personRepository = personRepository;
  10. _taskManager = taskManager;
  11. }
  12. public void AssignTaskToPerson(AssignTaskToPersonInput input)
  13. {
  14. var task = _taskRepository.Get(input.TaskId);
  15. var person = _personRepository.Get(input.PersonId);
  16. _taskManager.AssignTaskToPerson(task, person);
  17. }
  18. }

任务服务层用给定的DTO和仓储资源去检索相关的Task和Person,并且将检索到的结果传递给TaskMananger(领域服务)。

3.4.5 探讨

基于上面的示例,你可能有一些疑问。

1. 为什么不只在应用层实现这些逻辑?

你可能会说为什么不在服务层来实现领域服务里面的业务逻辑。

我们可以简单的说因为这根本不是应用层的任务。因为它不是一个use-case(用例),而是一个业务操作。我们可以用同样(分配任务给用户)的逻辑在不同的用例中。 我们可能会有另外的应用场景,以某种方式更新任务并且这个更新可能包含了分配任务给另外的人。所以,我们可以在这里用相同的领域逻辑。(说白了就是业务规则重用)还有就是,我们可以有2中不同的UI(手持设备应用和Web应用)可以共享相同的领域。

如果你的业务领域相对简单,那么你可以不考虑使用领域服务来实现这些逻辑。在DDD模式中这不是一个最佳实践,但ABP不会强迫你使用这种设计模式。

2. 为什么一定要使用领域服务?

看如下示例:

  1. public void AssignTaskToPerson(AssignTaskToPersonInput input)
  2. {
  3. var task = _taskRepository.Get(input.TaskId);
  4. task.AssignedPersonId = input.PersonId;
  5. }

写这个应用的开发人员可能不知道这里是一个TaskMananger,并直接给任务的AssignedPersonId 分配了 PersonId。 那么,怎么阻止这个的发生呢?在DDD社区有很多关于应该采用那种设计模式的探讨。我们不会做深入的探讨。但是我们会用一个简单的方式来实现。

我们可以改变Task实体,如下所示:

  1. public class Task : Entity<long>
  2. {
  3. public virtual int? AssignedPersonId { get; protected set; }
  4. //...other members and codes of Task entity
  5. public void AssignToPerson(Person person, ITaskPolicy taskPolicy)
  6. {
  7. taskPolicy.CheckIfCanAssignTaskToPerson(this, person);
  8. AssignedPersonId = person.Id;
  9. }
  10. }

我们给属性AssignedPersonId 的set设置为protected。所以,这个属性不可以被外部类修改。添加一个AssignToPerson方法,该方法接受参数类型Person和ITaskPolicy。ITaskPolicy 接口有一个CheckIfCanAssignTaskToPerson 方法来验证任务是否能分配给Person,如果验证不通过将会抛出一个适当的异常。那么应用层的方法将会如下所示:

  1. public void AssignTaskToPerson(AssignTaskToPersonInput input)
  2. {
  3. var task = _taskRepository.Get(input.TaskId);
  4. var person = _personRepository.Get(input.PersonId);
  5. task.AssignToPerson(person, _taskPolicy);
  6. }

现在,没有第二种方式将任务分配给个人。我们应该总是使用AssignToPerson 并且不可以跳过该业务规则。