本節內容:
領域服務(或服務)用來執行領域操作和業務規則。Eric Evans描述一個好的服務需要三個特點(在他的DDD書里):
- 操作與領域概念(不是一個實體或值對象天生的一部分)相關。
- 接口要按照領域模型的其它元素來定義。
- 操作是無狀態的。
與應用服務獲取/返回DTO(數據傳輸對象)不同,領域服務獲取/返回領域對象(如實體或值類型)。
領域服務可被應用服務或其它領域服務調用,但不直接被展現層(應用服務是針對它的)使用。
IDomainService 接口和DomainService 服務
ABP定義了IDomainService接口,按約定所有的領域服務都要實現它,實現之后,領域服務被自動暫時的注冊到依賴注入系統。
同樣,領域服務(隨意地)可以從DomainService類繼承,因此它可以使用繼承得來的日志、本地化、等屬性。即使不繼承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; } }
此處我們有兩個業務規則:
- 一個Task應當處於活躍狀態,才能分配給一個新的Person。
- 一個Person最多可以分配3個活躍Task
你可能想知道為什么我在第一個檢查里拋出一個ApplicationException,在第二個檢查里拋出UserFriendlyException(見異常處理),這跟領域服務毫不相干,我這么做只是舉個例子,具體怎么做完全取決於你。我認為用戶接口必須檢查一個Task的狀態並且不應該分配給一個Person,我認為這是一個應用錯誤,應當對用戶隱藏。第二個是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); } }
TaskApplicationService使用給定的DTO(輸入)和倉儲來獲取相關的task和person,然后把它們傳遞給TaskManager(領域服務)。
根據以上例子,你可能有些問題想問。
也就是說為什么應用服務不實現領域服務里的業務邏輯?
我們可以簡單的這么說:它不是應用服務的任務,因為它不是一個使用案例,而是一個業務操作。我們可能在另一個使用案例里使用同一個“分配任務給人員”的領域邏輯,例如:我們在另一個界面上,用某種方式更新task,這個更新包含分配任務給另一個人員;我們可能有兩個不同的UI(一個移動應用和一個Web應用)共享相同的領域;或是一個為遠程客戶端提供分配task操作的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的設置器修改成protected,所以在實體類外就不能修改它。添加一個AssignToPerson方法,接受一個Person和一個TaskPolicy。CheckIfCanAssignTaskToPerson方法檢查是否可以分配,並在不能分配時拋出一個對應的異常(此處如何實現不重要)。然后應用服務如下所示:
public void AssignTaskToPerson(AssignTaskToPersonInput input) { var task = _taskRepository.Get(input.TaskId); var person = _personRepository.Get(input.PersonId); task.AssignToPerson(person, _taskPolicy); }
我們把_taskPolicy注入給ITaskPolicy並傳給AssignToPerson方法。至此就沒有第二方式能把一個任務分配給一個人員了,我們只能使用AssignToPerson,再也跳不過業務規則。
英文原文:http://www.aspnetboilerplate.com/Pages/Documents/Domain-Services
