如何处理多个领域事件 - 企业工艺


领域事件描述了对您的领域有重要意义的事件。通常涉及3个方:事件生产者,事件消费者和事件调度员:

  • 事件生成器  是领域实体(准确地说是聚合根)。每个实体可以在业务事务期间生成一个或多个域事件。业务事务通常由SQL事务编排。
  • 当业务事务成功完成时,事件调度程序将  获取每个实体生成的所有域事件,并将它们分派给事件使用者。
  • 事件使用者  是提前订阅域事件的类。订阅可以与命令和查询处理程序订阅处理命令和查询的方式类似地完成。为此,创建一个IEventHandler <T>  接口并使用反射来匹配事件处理程序的事件:
    public sealed class Messages
    {
        private readonly IServiceProvider _provider;

        public Messages(IServiceProvider provider)
        {
            _provider = provider;
        }

        public void Dispatch(IDomainEvent domainEvent)
        {
            Type type = typeof(IDomainEventHandler<>);
            Type[] typeArgs = { domainEvent.GetType() };
            Type handlerType = type.MakeGenericType(typeArgs);

            dynamic handler = _provider.GetService(handlerType);
            handler.Handle((dynamic)domainEvent);
        }
    }

关于是否可以仅使用领域事件进行应用程序间通信(与外部系统通信)或者应用程序间内部和内部应用程序通信(应用程序内的类之间的通信),存在不同的意见。我不建议将事件用于内部应用程序通信,因为它使程序流程变得复杂并且更难以推理。类之间的交互最好使用常规技术完成(直接方法调用),例如返回操作的结果并将其传递给下一个方法。
当您仅将领域事件用于应用程序间通信时,您的所有领域事件使用者都会调用外部应用程序来通知他们应用程序中的重要更改。这些调用通常采用将消息放在消息总线上的形式,但您也可以使用该应用程序的API直接发送电子邮件或调用外部应用程序。

合并域事件
有时,您无法按原样调度域模型生成的事件,需要先将它们合并。
例如,假设您有以下订单取消的域事件的事件生成器:

public class Order
{
    public void Cancel()
    {
        /* ... */

        AddDomainEvent(new OrderCanceledEvent(Id));
    }
}

public class OrderCanceledEvent : IDomainEvent
{
    public int OrderId { get; }

    public OrderCanceledEvent(int orderId)
    {
        OrderId = orderId;
    }
}

一个消费者将领域事件转为消息总线的消息:

public class OrderCanceledEventHandler : IDomainEventHandler<OrderCanceledEvent>
{
    private readonly MessageBusGateway _gateway;

    public OrderCanceledEventHandler(MessageBusGateway gateway)
    {
        _gateway = gateway;
    }

    public void Handle(OrderCanceledEvent domainEvent)
    {
        _gateway.SendOrderCanceledMessage(domainEvent.OrderId);
    }
}

到现在为止还挺好。现在,假设您要实现另一个用例。管理员需要能够停用客户帐户。这种停用必须自动取消所有未结订单,并向消息总线发送单独的(仅一个)消息。
你会怎么做?直截了当的方法:

public class Customer
{
    public void Deactivate()
    {
        foreach (Order order in Orders)
        {
            order.Cancel();
        }

        AddDomainEvent(new CustomerDeactivatedEvent(Id));
    }
}

public class CustomerDeactivatedEventHandler : IDomainEventHandler<CustomerDeactivatedEvent>
{
    public void Handle(CustomerDeactivatedEvent domainEvent)
    {
        _gateway.SendCustomerDeactivatedMessage(domainEvent.CustomerId);
    }
}

消费者会生成几条消息:每个订单一个,另外一个消息消费者自己使用,您需要以某种方式取消不必要的域事件,只留下客户停用的事件 。换句话说,合并  域事件。

一种方法是在order.Cancel()  方法中引入一个flag参数,并根据调用方法的位置传递true  或false  :

public class Order
{
    public void Cancel(bool generateEvent)
    {
        /* ... */

        if (generateEvent)
        {
            AddDomainEvent(new OrderCanceledEvent(Id));
        }
    }
}

这是一种糟糕方式。从域建模的角度来看,这个标志没有意义:它引入了一个技术问题,与取消订单的过程没有任何关系。

另一种进行合并的方法是以一个为另一个超集的形式定义域事件之间的关系,然后在生成新事件时检查该关系:

public abstract class DomainEvent
{
    protected virtual Type SupersetFor => null;

    public bool IsSupersetFor(DomainEvent domainEvent)
    {
        return domainEvent.GetType() == SupersetFor;
    }
}

public class CustomerDeactivatedEvent : DomainEvent
{
    protected override Type SupersetFor => typeof(OrderCanceledEvent);
}

public abstract class AggregateRoot
{
    private readonly List<DomainEvent> _domainEvents = new List<DomainEvent>();
    public IReadOnlyList<DomainEvent> DomainEvents => _domainEvents;

    protected virtual void AddDomainEvent(DomainEvent newEvent)
    {
        if (_domainEvents.Any(existing => existing.IsSupersetFor(newEvent)))
            return;

        _domainEvents.Add(newEvent);
    }
}

这种实现看起来更好。它提供了一种声明性方法来定义应该取消哪个域事件(或者更确切地说,不是首先生成的),并且不需要实体中的任何其他逻辑。现在,当订单尝试生成订单取消事件时,如果该集合已包含客户停用事件,则不会将其添加到集合中。
不过,这种方法完全忽略了这一点。订单取消必须始终  伴随产生域名事件。取消发生在客户帐户停用期间的事实不会使取消事件不那么重要。无论在什么情况下,事件还是一个事件。

事实上,域事件合并的整个概念是有缺陷的。事件生成者和事件使用者之间的联系是有原因的 - 因此您可以根据业务需求以不同方式处理这些事件。当客户停用时,当前的业务要求不是发送订单取消消息。处理它的最佳方法是在调度员方面。

为此,您需要:

  • 收集与该聚合事件集合中的客户聚合相关的所有事件,
  • 修改事件调度程序,以便在调度之前减少聚合的事件集合。

internal class EventDispatcher :
    IPostInsertEventListener,
    IPostDeleteEventListener,
    IPostUpdateEventListener,
    IPostCollectionUpdateEventListener
{
    public void OnPostUpdate(PostUpdateEvent ev)
    {
        DispatchEvents(ev.Entity as AggregateRoot);
    }

    public void OnPostDelete(PostDeleteEvent ev)
    {
        DispatchEvents(ev.Entity as AggregateRoot);
    }

    public void OnPostInsert(PostInsertEvent ev)
    {
        DispatchEvents(ev.Entity as AggregateRoot);
    }

    public void OnPostUpdateCollection(PostCollectionUpdateEvent ev)
    {
        DispatchEvents(ev.AffectedOwnerOrNull as AggregateRoot);
    }

    private void DispatchEvents(AggregateRoot aggregateRoot)
    {
        if (aggregateRoot == null)
            return;

        // New functionality
        IDomainEvent[] reduced = EventReducer.ReduceEvents(aggregateRoot.DomainEvents);

        foreach (IDomainEvent domainEvent in reduced)
        {
            Messages.Dispatch(domainEvent);
        }

        aggregateRoot.ClearEvents();
    }
}

现在您有了一个新的EventReducer  类,它负责减少聚合中的域事件集合。它纯粹是函数性的,因此可以很容易地进行测试。如果需要,您也可以轻松修改它。不再需要生产者方面的错综复杂的逻辑。

总结

  • 实体是域事件的生产者。
  • 事件消费者者将域事件转换为对外部应用程序的调用
  • 事件调度程序则是事件生成器和事件使用者之间的中介。
  • 不要在生产者端合并领域事件。
  • 相反,在分派事件调度程序之前减少事件调度程序中的领域事件。