Постоянство и доменные события с постоянными невежественными объектами

Я изучал предметно-ориентированный дизайн в сочетании с предметными событиями. Мне действительно нравится разделение проблем, которые эти события обеспечивают. Я столкнулся с проблемой с порядком сохранения объекта домена и создания событий домена. Я хотел бы вызывать события в доменных объектах, но я хочу, чтобы они оставались невежественными.

Я создал основной ShoppingCartService, с этим Checkout метод:

public void Checkout(IEnumerable<ShoppingCartItem> cart, Customer customer)
{
    var order = new Order(cart, customer);

    _orderRepositorty.Add(order);
    _unitOfWork.Commit();
}

В этом примере конструктор Order поднимет OrderCreated событие, которое может быть обработано определенными обработчиками. Тем не менее, я не хочу, чтобы эти события возникали, когда сущность еще не сохранена или когда сохранение каким-либо образом завершается сбоем.

Чтобы решить эту проблему, я нашел несколько решений:

1. Поднять события в сервисе:

Вместо того, чтобы вызывать событие в доменном объекте, я мог бы вызывать события в сервисе. В этом случае Checkout метод поднимет OrderCreated событие. Одним из недостатков этого подхода является то, что, глядя на Order доменный объект, неясно, какие события и какими методами вызываются. Кроме того, разработчик должен помнить, чтобы поднять событие, когда заказ создается в другом месте. Это не правильно.


2. Очередь доменных событий

Другой вариант - поставить в очередь события домена и вызвать их при успешном сохранении. Это может быть достигнуто путем using утверждение например:

using (DomainEvents.QueueEvents<OrderCreated>())
{
    var order = new Order(cart, customer);

    _orderRepositorty.Add(order);
    _unitOfWork.Commit();
}

QueueEvents<T> Метод установил бы логическое значение true и DomainEvents.Raise<T> Метод будет помещать в очередь событие, а не выполнять его напрямую. В распоряжении обратного вызова QueueEvent<T> события в очереди выполняются, что гарантирует, что сохранение уже произошло. Это кажется довольно сложным, и требуется, чтобы служба знала, какое событие вызывается в объекте домена. В приведенном мною примере он также поддерживает только один тип события, которое нужно вызвать, однако это можно обойти.


3. Сохраняться в доменном событии

Я мог бы сохранить объект, используя событие домена. Это выглядит нормально, за исключением того факта, что обработчик событий, сохраняющий объект, должен выполняться первым, однако я где-то читал, что события домена не должны зависеть от определенного порядка выполнения. Возможно, это не так важно, и события домена могут каким-то образом знать, в каком порядке должны выполняться обработчики. Например: предположим, у меня есть интерфейс, определяющий обработчик события домена, реализация будет выглядеть так:

public class NotifyCustomer : IDomainEventHandler<OrderCreated>
{
   public void Handle(OrderCreated args)
   {
       // ...
   }
}

Когда я хочу обработать сохранение в использовании обработчика событий, я бы создал другой обработчик, производный от того же интерфейса:

public class PersistOrder : IDomainEventHandler<OrderCreated>
    {
       public void Handle(OrderCreated args)
       {
           // ...
       }
    }
}

Сейчас NotifyCustomer поведение зависит от порядка сохранения в базе данных, поэтому PersistOrder Обработчик событий должен выполняться первым. Допустимо ли, чтобы эти обработчики вводили свойство, например, которое указывает порядок их выполнения? Немного о реализации DomainEvents.Raise<OrderCreated>() метод:

foreach (var handler in Container.ResolveAll<IDomainEventHandler<OrderCreated>>().OrderBy(h => h.Order))
{
    handler.Handle(args);
}


Теперь мой вопрос: есть ли у меня другие варианты? Я что-то пропустил? А что вы думаете о решениях, которые я предложил?

1 ответ

Решение

Либо ваши (транзакционные) обработчики событий включаются в (потенциально распределенную) транзакцию, либо вы публикуете / обрабатываете события после совершения транзакции. Ваше решение "QueueEvents" правильно понимает основную идею, но есть более элегантные решения, такие как публикация через репозиторий или хранилище событий. Для примера взгляните на SimpleCQRS

Вы также можете найти эти вопросы и ответы полезными:

CQRS: хранение событий и их публикация - как мне сделать это безопасным способом?

Обработка ошибок агрегатора событий с откатом


Обновление по пункту 3:

... однако я где-то читал, что доменные события не должны зависеть от определенного порядка выполнения.

Независимо от вашего способа сохранения порядок событий абсолютно имеет значение (в совокупности).

Теперь поведение NotifyCustomer зависит от порядка, сохраняемого в базе данных, поэтому обработчик события PersistOrder должен выполняться первым. Допустимо ли, чтобы эти обработчики вводили свойство, например, которое указывает порядок их выполнения?

Сохранение и обработка событий - это отдельная задача, поэтому не следует использовать обработчик событий. Сначала сохраняй, потом обрабатывай.

Отказ от ответственности: я не знаю, о чем говорю. ⚠️

В качестве альтернативы освещению событий в ShoppingCartService Вы могли бы поднять события в OrderRepository,

В качестве альтернативы организации очередей доменных событий в ShoppingCartService Вы могли бы поставить их в очередь в Order агрегировать путем наследования от базового класса, который обеспечивает AddDomainEvent метод.

public class Order : Aggregate
{
    public Order(IEnumerable<ShoppingCartItem> cart, Customer customer)
    {
        AddDomainEvent(new OrderCreated(cart, customer))
    }
}

public abstract class Aggregate
{
    private List<IDomainEvent> _domainEvents = new List<IDomainEvent>();

    public IReadOnlyCollection<IDomainEvent> DomainEvents => _domainEvents.AsReadOnly();

    public void AddDomainEvent(IDomainEvent domainEvent)
    {
        _domainEvents.Add(domainEvent);
    }
}
Другие вопросы по тегам