1.引言
最近剛學習了下DDD中領域事件的理論知識,總的來說領域事件主要有兩個作用,一是解耦,二是使用領域事件進行事務的拆分,通過引入事件存儲,來實現數據的最終一致性。若想了解DDD中領域事件的概念,可參考DDD理論學習系列(9)-- 領域事件。
Abp中使用事件總線來實現領域事件,而關于事件總線的實現,大家可參考我這篇博文——事件總線知多少,本文將不再贅述。
2.用例分析
當用戶被成功分配任務后,發送郵件和消息通知給用戶。
這個用例比較簡單,沒有太多的復雜邏輯,按照我們傳統的思路,直接在任務編輯方法中添加郵件和消息發送的方法即可,代碼如下:
public void UpdateTask(UpdateTaskInput input)
{
//We can use Logger, it's defined in ApplicationService base class.
Logger.Info("Updating a task for input: " + input);
//獲取是否有權限
bool canAssignTaskToOther = PermissionChecker.IsGranted(PermissionNames.Pages_Tasks_AssignPerson);
//如果任務已經分配且未分配給自己,且不具有分配任務權限,則拋出異常
if (input.AssignedPersonId.HasValue && input.AssignedPersonId.Value != AbpSession.GetUserId() &&
!canAssignTaskToOther)
{
throw new AbpAuthorizationException("沒有分配任務給他人的權限!");
}
var updateTask = Mapper.Map<Task>(input);
var user = _userRepository.Get(input.AssignedPersonId.Value);
//先執行分配任務
_taskManager.AssignTaskToPerson(updateTask, user);
//再更新其他字段
_taskRepository.Update(updateTask);
//發送通知
var message = "You hava been assigned one task into your todo list.";
_smtpEmailSender.Send("ysjshengjie@qq.com", updateTask.AssignedPerson.EmailAddress, "New Todo item", message);
_notificationPublisher.Publish("NewTask", new MessageNotificationData(message), null,
NotificationSeverity.Info, new[] { updateTask.AssignedPerson.ToUserIdentifier() });
}
運行,直接掛掉。原因是很清楚,是由于郵箱配置有誤導致。但是我們思考一下。我們進行任務分配時最關注的是任務被成功分配,而至于通知是否成功發送相對來說是次要的。但是現在卻由于通知發送失敗導致任務無法被成功分配,這是不合理的。
那我們要如何做呢?當然是拆分業務邏輯。而這時領域事件就可以粉墨登場了。
3.使用領域事件
就這個用例而言,“用戶被成功分配任務”就是一個領域事件。下面我們就來實際應用一下。
3.1. 定義事件源
一個領域事件是通過事件源來識別的,我們直接定義一個TaskAssignedEventData
繼承自EventData
即可:
public class TaskAssignedEventData : EventData
{
public User User { get; set; }
public Task Task { get; set; }
public TaskAssignedEventData(Task task, User user)
{
this.Task = task;
this.User = user;
}
}
3.2. 實現事件處理
定義TaskAssignedToUser
事件處理,實現IEventHandler<TaskAssignedEventData>
泛型接口即可:
public class TaskAssignedToUser : IEventHandler<TaskAssignedEventData>, ITransientDependency
{
private readonly ISmtpEmailSender _smtpEmailSender;
private readonly INotificationPublisher _notificationPublisher;
public TaskAssignedToUser(ISmtpEmailSender smtpEmailSender, INotificationPublisher notificationPublisher)
{
_smtpEmailSender = smtpEmailSender;
_notificationPublisher = notificationPublisher;
}
public void HandleEvent(TaskAssignedEventData eventData)
{
var message = "You hava been assigned one task into your todo list.";
//TODO:需要重新配置QQ郵箱密碼
_smtpEmailSender.Send("ysjshengjie@qq.com", eventData.Task.AssignedPerson.EmailAddress, "New Todo item", message);
_notificationPublisher.Publish("NewTask", new MessageNotificationData(message), null,
NotificationSeverity.Info, new[] { eventData.User.ToUserIdentifier() });
}
}
3.3. 事件觸發
我們可以直接在上一節定義的TaskManager
領域服務中觸發領域事件。因為這樣更符合當前領域事件通用語言的表述。
//TaskManager.cs
public void AssignTaskToPerson(Task task, User user)
{
//已經分配,就不再分配
if (task.AssignedPersonId.HasValue && task.AssignedPersonId.Value == user.Id)
{
return;
}
if (task.State != TaskState.Open)
{
throw new ApplicationException("處于非活動狀態的任務不能分配!");
}
task.AssignedPersonId = user.Id;
//使用領域事件觸發發送通知操作
_eventBus.Trigger(new TaskAssignedEventData(task, user));
}
再運行,我們發現雖然沒有接收到消息通知(發送失敗),但任務卻可以成功分配。
4. 一些問題
- 領域事件在哪注冊(訂閱)?
應用程序啟動時Abp根據約定俗成的命名規則將事件源和事件處理注冊到了依賴容器中和事件總線維護的容器中。我們也可以自行在應用服務或領域服務中手動注冊。 - 領域事件在哪觸發(發布)?
事件的觸發同樣也沒有限定,根據需要,可以在應用服務、領域服務、聚合、實體中發布。 - 領域事件的命名?
領域事件的名字要反映出過去發生的事情的概念。
4.最后
由于demo比較簡單,找不到合適的用例,以上使用的用例比較簡單。在復雜的用例中,當需要更新多個聚合時,領域事件的作用就體現出來了,借助領域事件我們可以很好的進行事務拆分,達到最終一致性的目的。
而至于領域事件衍生出來的事件存儲和事件溯源,下次再和大家分享。