ABP官方文檔翻譯 3.4 領域服務
領域服務
- 介紹
- IDomainService接口和DomainService類
- 示例
- 創建接口
- 服務實現
- 使用應用服務
- 一些探討
- 為什麽只有應用服務?
- 如何強制使用領域服務?
介紹
領域服務(或者在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方法,並且不能跳過業務規則。
返回主目錄
ABP官方文檔翻譯 3.4 領域服務