30 | 領域事件:提升業務內聚,實現模塊解耦
我們在領域的抽象層定義了領域事件和領域事件處理的接口
IDomainEvent
namespace GeekTime.Domain
{
public interface IDomainEvent : INotification
{
}
}
這是一個空接口,它只是標記出來某一個對象是否是領域事件,INotification 也是一個空接口,它是 MediatR 框架的一個接口,是用來實現事件傳遞用的
namespace MediatR
{
public interface INotification
{
}
}
接着是 IDomainEventHandler
namespace GeekTime.Domain
{
public interface IDomainEventHandler<TDomainEvent> : INotificationHandler<TDomainEvent>
where TDomainEvent : IDomainEvent
{
//這里我們使用了INotificationHandler的Handle方法來作為處理方法的定義
//Task Handle(TDomainEvent domainEvent, CancellationToken cancellationToken);
}
}
同樣這個接口也是繼承了 IDomainEventHandler 接口,它有一個泛型參數是 TDomainEvent,這個 TDomainEvent 約束必須為 IDomainEvent,也就是說處理程序只處理 IDomainEvent 作為入參
實際上該方法已經在 INotificationHandler 中定義好了,所以這里不需要重新定義,只是告訴大家它的定義是什么樣子的
在 Entity 中對領域事件代碼的處理
private List<IDomainEvent> _domainEvents;
public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents?.AsReadOnly();
public void AddDomainEvent(IDomainEvent eventItem)
{
_domainEvents = _domainEvents ?? new List<IDomainEvent>();
_domainEvents.Add(eventItem);
}
public void RemoveDomainEvent(IDomainEvent eventItem)
{
_domainEvents?.Remove(eventItem);
}
public void ClearDomainEvents()
{
_domainEvents?.Clear();
}
將領域事件做一個實體的屬性存儲進來,它應該是一個列表,因為在一個實體操作過程中間可能會發生多件事情,領域事件應該是可以被實體模型之外的代碼讀到,所以暴露一個 ReadOnly 的 Collection
這里還提供幾個方法:添加領域事件,移除領域事件,清除領域事件
這些方法都是在領域模型內部進行調用的
可以看一下之前定義的 Order
public Order(string userId, string userName, int itemCount, Address address)
{
this.UserId = userId;
this.UserName = userName;
this.Address = address;
this.ItemCount = itemCount;
this.AddDomainEvent(new OrderCreatedDomainEvent(this));
}
public void ChangeAddress(Address address)
{
this.Address = address;
//this.AddDomainEvent(new OrderAddressChangedDomainEvent(this));
}
當我們構造一個全新的 Order 的時候,實際上這里可以定義一個事件叫做 OrderCreatedDomainEvent,這個領域事件它的構造函數的入參就是一個 Order,當我們調用 Order 的構造函數時,實際上我們的行為就是在創建一個全新的 Order,所以在這里添加一個事件 AddDomainEvent
同理的比如說 ChangeAddress 被調用了,我們在這里實際上可以定義一個 OrderAddressChangedDomainEvent 類似這樣子的領域事件出來
大家可以看到領域事件的構造和添加都應該是在領域模型的方法內完成的,而不應該是被外界的代碼去調用創建,因為這些事件都是領域模型內部發生的事件
接着看看 OrderCreatedDomainEvent 的定義
namespace GeekTime.Domain.Events
{
public class OrderCreatedDomainEvent : IDomainEvent
{
public Order Order { get; private set; }
public OrderCreatedDomainEvent(Order order)
{
this.Order = order;
}
}
}
那我們如何處理我們的領域事件,接收領域事件的處理應該定義在應用層
namespace GeekTime.API.Application.DomainEventHandlers
{
public class OrderCreatedDomainEventHandler : IDomainEventHandler<OrderCreatedDomainEvent>
{
ICapPublisher _capPublisher;
public OrderCreatedDomainEventHandler(ICapPublisher capPublisher)
{
_capPublisher = capPublisher;
}
public async Task Handle(OrderCreatedDomainEvent notification, CancellationToken cancellationToken)
{
await _capPublisher.PublishAsync("OrderCreated", new OrderCreatedIntegrationEvent(notification.Order.Id));
}
}
}
它繼承了 IDomainEventHandler,這個接口是上面講到的領域事件處理器的接口,它的泛型入參就是要處理的事件的類型 OrderCreatedDomainEvent
為了簡單演示起見,這里的邏輯是當我們創建一個新的訂單時,我們向 EventBus 發布一條事件,叫做 OrderCreated 這個事件
我們在 OrderController 的 CreateOrder 定義了一個 CreateOrderCommand
[HttpPost]
public async Task<long> CreateOrder([FromBody]CreateOrderCommand cmd)
{
return await _mediator.Send(cmd, HttpContext.RequestAborted);
}
CreateOrderCommand
namespace GeekTime.API.Application.Commands
{
public class CreateOrderCommand : IRequest<long>
{
//ublic CreateOrderCommand() { }
public CreateOrderCommand(int itemCount)
{
ItemCount = itemCount;
}
public long ItemCount { get; private set; }
}
}
CreateOrderCommandHandler
public async Task<long> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var address = new Address("wen san lu", "hangzhou", "310000");
var order = new Order("xiaohong1999", "xiaohong", 25, address);
_orderRepository.Add(order);
await _orderRepository.UnitOfWork.SaveEntitiesAsync(cancellationToken);
return order.Id;
}
我們在 CreateOrderCommandHandler 里面創建了一個 Order,然后保存進倉儲,調用了 UnitOfWork 的 SaveEntitiesAsync
啟動程序,直接執行,調用我們的方法,可以看到我們先進入到了創建訂單的處理系統(CreateOrderCommandHandler),接着進入到了領域事件發布的 Publish 的代碼(MediatorExtension),當倉儲存儲完畢之后,進入到了 OrderCreatedDomainEventHandler,也就是說我們在創建完我們的領域模型並將其保存之后,我們的領域事件的處理程序才觸發
在之前講解實現 UnitOfWork 的時候(EFContext),我們的 SaveEntitiesAsync 里面只有一行代碼是 SaveChangesAsync,這里添加了一行代碼,是發送領域事件的代碼 DispatchDomainEventsAsync
public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default)
{
var result = await base.SaveChangesAsync(cancellationToken);
//await _mediator.DispatchDomainEventsAsync(this);
return true;
}
這就是 MediatorExtension 中看到的 DispatchDomainEventsAsync
namespace GeekTime.Infrastructure.Core.Extensions
{
static class MediatorExtension
{
public static async Task DispatchDomainEventsAsync(this IMediator mediator, DbContext ctx)
{
var domainEntities = ctx.ChangeTracker
.Entries<Entity>()
.Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any());
var domainEvents = domainEntities
.SelectMany(x => x.Entity.DomainEvents)
.ToList();
domainEntities.ToList()
.ForEach(entity => entity.Entity.ClearDomainEvents());
foreach (var domainEvent in domainEvents)
await mediator.Publish(domainEvent);
}
}
}
大家可以看到我們發送領域事件實際上是這么一個過程:我們從當前要保存的 EntityContext 里面去跟蹤我們的實體,然后從跟蹤到的實體的對象中獲取到我們當前的 Event,如果 Event 是存在的,就把它取出來,然后將實體內的 Event 進行清除,再然后將這些 Event 逐條地通過中間件發送出去,並且找到對應的 Handler 處理
定義領域事件實際上也非常簡單,只需要在領域模型創建一個 Events 的目錄,然后將領域事件都定義在這里,領域事件需要繼承 IDomainEvent,領域事件的處理器都定義在 DomainEventHandler,在應用層這個目錄下面,我們可以為每一個事件都定義我們的處理程序
總結一下
領域模型內創建事件:我們不要在領域模型的外面去構造事件,然后傳遞給領域模型,因為整個領域事件是由領域的業務邏輯觸發的,而不是說外面的對模型的操作觸發的
另外就是針對領域事件應該定義專有的領域事件處理類,就像我們剛才演示的,在一個特定的目錄,對每一個事件進行定義處理類
還有一個就是在同一個事務里面去處理我們的領域事件,實際上我們也可以選擇在不同的事務里面處理,如果需要在不同的事務里面去處理領域事件的時候,我們就需要考慮一致性的問題,考慮中間出錯,消息丟失的問題
GitHub源碼鏈接:
https://github.com/witskeeper/geektime
本作品采用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。
歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含鏈接: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改后的作品務必以相同的許可發布。
如有任何疑問,請與我聯系 (MingsonZheng@outlook.com) 。