本篇目錄
介紹###
領域服務(或DDD中的服務)用於執行領域操作和業務規則。Eric Evans描述了一個好的服務應該具備下面三個特征:
- 和領域概念相關的操作不是一個實體或者值對象的本質部分。
- 接口定義在領域模型其他元素的條款中。
- 操作是無狀態的。
跟獲得或返回一個數據傳輸對象的應用服務方法(DTO)不同,領域服務獲得或者返回一個領域對象(比如實體或值類型)。
一個領域服務可以用於應用服務,也可以用於其他的領域服務,但不能直接用於展現層,服務層才直接用於展現層。
IDomainService接口和DomainService類###
ABP定義了IDomainService接口,所有的領域服務都按照慣例實現了該接口。當實現時,領域服務會以transient自動注冊到依賴注入系統。
此外,領域服務(可選地)可以從DomainService類繼承。因此,它可以使用一些繼承的屬性,比如logging,本地化等等。當然,如果沒有繼承,如果需要的話也可以注入這些屬性。
樣例###
假設我們有一個任務管理系統並且有將一個任務派給一個人的業務規則。
創建一個接口
首先我們為該服務定義一個接口(不是必須的,但是這樣是一個好的實踐):
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個激活的任務。
你可能想知道為啥第一次檢測時拋出了一個ApplicationException,而第二次檢查時拋出了UserFriendlyException,請關注后面博客的異常處理。這根領域服務根本無關。這里這樣處理的想法是這樣的,UI必須先要檢查一個任務的狀態,否則不應該允許我們將它派給一個人。這是一個應用程序的錯誤,並且我們可以向用戶隱藏這個錯誤。對於第二個友好的異常信息,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);
}
}
Task應用服務使用給定的DTO(輸入)和倉儲來檢索相關的task和 person,並將它們傳給 TaskManager(領域服務)。
一些討論###
基於上面的例子,你可能會存在下面的疑問。
何不只使用應用服務
你可能會問,為什么不使用應用服務實現領域服務中的邏輯呢?
我們可以簡單地說,它不是應用服務要干的活。因為領域邏輯不是一個用例(use-case),而是一個 業務操作。我們可以在不同的用例中使用相同的“將一個任務派給一個人”的邏輯。比如說我們以后會更新這個任務,並且將這個任務派給其他人。因此,我們可以使用相同的領域邏輯,這個邏輯就是“將一個任務派給一個人”,我們不用考慮這個具體的人和具體的任務。此外,我們可能有兩個不同的UI(一個移動端應用和一個web應用)來共享相同的領域。
下面根據個人的理解來畫個圖:
如上圖,應用服務層中有一個應用服務方法,但卻使用到了領域層的三個業務邏輯,因為在領域層中,獲取單個task和person都各自為一個業務邏輯,將一個任務派給一個人又是一個業務邏輯。在應用服務層,我們只需要獲得一個人和一個任務就行,然后將該任務派給這個人,根本不需要考慮這個人和這個任務的獲取細節,也不用考慮任務派發的細節,因為這完全不是應用層考慮的事兒。
如果你的領域很簡單,只有一個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; }
//...其他成員
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方法,而且不能跳過業務規則了。