使用DDD模式实现简单API(8/5)

发布和处理领域事件

介绍

领域事件是领域驱动模式的基础,在领域执行时产生,并持有一部分数据。创建领域事件是为了通知领域内的其他部分,告诉它有事情发生了,一般情况下,对方会对此做出响应。

领域事件一般是以过去式命名的不可变的数据容器:

public class OrderPlaced
{
    public Order Order { get; }

    public OrderPlaced(Order order)
    {
        this.Order = order;
    }
}

三种发布领域事件的方法

使用静态的DomainEvents类

简而言之,创建一个名为DomainEvents的静态类,包含Raise方法,当集成根内的事件发生时,立即调用方法。
这里需要注意的是,这会导致事件的处理器也会被立即调用,而此时集成根的内部方法可能还未结束。

public class OrderPlaced
{
    public Order Order { get; }

    public OrderPlaced(Order order)
    {
        this.Order = order;
    }
}   

由集成根内部返回相应的事件,然后在命令处理器发布

这种方法中,集成根会将领域事件返回给它的调用者,然后由他去发布事件:

public List<IDomainEvent> PlaceOrder(Order order)
{
    // business logic....

    List<IDomainEvent> events = new List<IDomainEvent>();
    events.Add(new OrderPlaced(order));

    return events;
}
public class ShopApplicationService
{
    private readonly IOrderRepository orderRepository;
    private readonly IShopRepository shopRepository;
    private readonly IEventsPublisher eventsPublisher;
        
    public void PlaceOrder(int shopId, int orderId)
    {
        Shop shop = this.shopRepository.GetById(shopId);
        Order order = this.orderRepository.GetById(orderId);

        List<IDomainEvent> events = shop.PlaceOrder(order);

        eventsPublisher.Publish(events);
    }
}

添加事件到Events实体集合

这种方式下,每个实体都拥有一个Events集合。当集成根的方法执行时,领域事件的实例被添加到集合中。当方法执行完成后,
调用者会检查所有的实体,获取领域事件然后发布他们。

public abstract class EntityBase
{
    ICollection<IDomainEvent> Events { get; }
}

public class Shop : EntityBase
{
    public List<IDomainEvent> PlaceOrder(Order order)
    {
        // business logic....

        Events.Add(new OrderPlaced(order));
    }
}
public class ShopApplicationService
{
    private readonly IOrderRepository orderRepository;
    private readonly IShopRepository shopRepository;
    private readonly IEventsPublisher eventsPublisher;
        
    public void PlaceOrder(int shopId, int orderId)
    {
        Shop shop = this.shopRepository.GetById(shopId);
        Order order = this.orderRepository.GetById(orderId);

        shop.PlaceOrder(order);

        eventsPublisher.Publish();
    }
}
public class EventsPublisher : IEventsPublisher
{
    public void Publish()
    {
        List<IDomainEvent> events = this.GetEvents(); // for example from EF DbContext

        foreach (IDomainEvent @event in events)
        {
            this.Publish(@event);
        }
    }
}

处理领域事件

处理领域事件的方法直接取决于发布的方式,如果使用DomainEvents静态类,那么就需要立即处理事件。
在另外两种情况下,则可以控制事件何时发布以及何时处理,在事务之内或事务外。

在我看来,应该将领域事件的处理作为事务的一部分,将集成根方法的执行和领域事件的处理视为原子操作。
那么当有大量的事件和事件处理器时,不需要一一去考虑初始化数据库连接、事务的问题。
然后,有时需要与第三方的服务进行交互(例如邮件服务或webservice),在这种场景下是无法保持事务的,所以我们需要其他的机制来处理这种情况,也就是Domain Events Notifications。

领域事件通知

在领域驱动设计中是没有这个术语的,是我对它进行了命名,我觉得它很合适-它通知了领域事件被发布这件事。
整个机制是非常简单的,当我想要通知应用有领域事件被发布时,我会创一个对应的通知类,以及任意个对应通知类的
通知处理器。通知的发布是在事务提交之后进行的,整个流程如下:

  1. 创建数据库事务
  2. 获得集成根
  3. 调用集成根方法
  4. 添加领域事件到事件集合
  5. 发布领域事件并处理
  6. 保存修改到数据库并提交事务
  7. 发布领域事件通知并处理

我如何知道哪个领域事件被发布了呢?
首先需要根据领域事件的类型定义通知:

public interface IDomainEventNotification<out TEventType> where TEventType:IDomainEvent
{
    TEventType DomainEvent { get; }
}

public class DomainNotificationBase<T> : IDomainEventNotification<T> where T:IDomainEvent
{
    public T DomainEvent { get; }

    public DomainNotificationBase(T domainEvent)
    {
        this.DomainEvent = domainEvent;
    }
}

public class OrderPlacedNotification : DomainNotificationBase<OrderPlaced>
{
    public OrderPlacedNotification(OrderPlaced orderPlaced) : base(domainEvent)
    {
    }
}

将所有的通知在IOC容器注册:

protected override void Load(ContainerBuilder builder)
{
    builder.RegisterAssemblyTypes(typeof(IDomainEvent).GetTypeInfo().Assembly)
    .AsClosedTypesOf(typeof(IDomainEventNotification<>));
}

在EventsPublisher中,从IOC容器获取通知的实例,在事务提交之后,所有的通知将被发布:

var domainEventNotifications = new List<IDomainEventNotification<IDomainEvent>>();
foreach (var domainEvent in domainEvents)
{
    Type domainEvenNotificationType = typeof(IDomainEventNotification<>);
    var domainNotificationWithGenericType = domainEvenNotificationType.MakeGenericType(domainEvent.GetType());
    var domainNotification = _scope.ResolveOptional(domainNotificationWithGenericType, new List<Parameter>
    {
        new NamedParameter("domainEvent", domainEvent)
    });

    if (domainNotification != null)
    {
        domainEventNotifications.Add(domainNotification as IDomainEventNotification<IDomainEvent>);
    }             
}

var tasks = domainEventNotifications
    .Select(async (notification) =>
    {
        await _mediator.Publish(notification, cancellationToken);
    });

await Task.WhenAll(tasks);

下图显示了整个流程是如何执行的:

你可能会觉得有许多内容需要记忆,确实如此!但是实际上整个流程还是十分简单直接的,并且我们可以使用IOC拦截器来简化这个流程,
在后面的文章会说到。


总结

  • 领域事件是集成根中发生的过去的某件事,这是领域驱动模式中非常重要的一个概念。
  • 有多种发布和处理领域事件的方法-静态类、返回事件对象、通过集合暴露
  • 领域事件应该在事务内处理
  • 对于非事务内的操作,通过领域事件通知完成
posted @ 2020-03-03 09:53  东方未  阅读(145)  评论(0)    收藏  举报