Задержка создания и отправки доменных событий

Я использую шаблон Domain Events в течение некоторого времени. Он позволяет нам инкапсулировать как можно больше поведения на уровне предметной области и предоставить другим частям нашего приложения удобный способ подписаться на события предметной области.

В настоящее время мы используем статический класс, который наши объекты предметной области могут вызывать для создания событий:

static class DomainEvents
{
    public static IEventDispatcher Dispatcher { get; set; }

    public static void Raise<TEvent>(TEvent e)
    {
        if (e != null)
        {
            Dispatcher.Dispatch(e);
        }
    }
}

Как видите, это не более чем прокладка для IEventDispatcher, которая на самом деле выполняет работу по диспетчеризации или публикации событий.

Наша реализация диспетчера просто использует наш контейнер IoC (StructureMap) для поиска обработчиков событий для указанного типа события.

public void Dispatch<TEvent>(TEvent e)
{
    foreach (var handler in container.GetAllInstances<IHandler<TEvent>>())
    {
        handler.Handle(e);
    }
}

Это нормально работает в большинстве случаев. Тем не менее, есть несколько проблем с этим подходом:

События следует отправлять только в том случае, если сущность успешно сохраняется

Возьмите следующий класс:

public class Order
{
    public string Id { get; private set; }
    public decimal Amount { get; private set; }

    public Order(decimal amount)
    {
        Amount = amount;
        DomainEvents.Raise(new OrderRaisedEvent { OrderId = Id });
    }
}

В конструкторе Order мы поднимаем OrderRaisedEvent. На нашем прикладном уровне мы, скорее всего, создадим экземпляр заказа, добавим его в «сеанс» нашей базы данных, а затем зафиксируем/сохраним изменения:

var order = new Order(amount: 10);
session.Store(order);

session.SaveChanges();

Проблема здесь в том, что событие домена возникает до того, как мы успешно сохранили нашу сущность Order (совершив транзакцию). Если бы сохранение не удалось, мы бы все равно отправили события.

Лучшим подходом будет постановка в очередь событий до тех пор, пока сущность не будет сохранена. Однако я не уверен, как лучше всего это реализовать, сохраняя при этом строго типизированные обработчики событий.

События не должны создаваться, пока объект не будет сохранен

Еще одна проблема, с которой я сталкиваюсь, заключается в том, что наши идентификаторы объектов не устанавливаются/назначаются до тех пор, пока не сохранится объект (RavenDB - session.Store). Это означает, что в приведенном выше примере идентификатор заказа, переданный событию, на самом деле null.

Поскольку я не уверен, как на самом деле генерировать идентификаторы RavenDB заранее, одним из решений может быть отсрочка создания событий до тех пор, пока сущность не будет фактически сохранена, но опять же я не знаю, как лучше всего это реализовать - возможно, поставить в очередь коллекцию Func<TEntity, TEvent>?


person Ben Foster    schedule 27.12.2013    source источник
comment
Некоторые считают публикацию событий домена ответственность репозитория.   -  person Alexander Langer    schedule 27.12.2013
comment
Многие также возразят, что репозитории — это антипаттерн :) В нашем случае мы не используем репозитории, мы работаем напрямую с RavenDB.   -  person Ben Foster    schedule 27.12.2013
comment
Я думаю, что единица работы необходима, если мы хотим публиковать события только тогда, когда агрегаты сохраняются. События прикрепляются к единице работы (связанной с текущим потоком?), когда вызывается DomainEvents.raise(event). Таким образом, репозиторий кажется естественным выбором для запуска фиксации единицы работы.   -  person Yugang Zhou    schedule 27.12.2013
comment
Согласитесь, единица работы — это хороший способ соотнести диспетчеризацию событий, хотя она и не зависит от наличия репозитория (см. мой ответ).   -  person Ben Foster    schedule 27.12.2013


Ответы (2)


Одно из решений (предложенное @synhershko) — перенести диспетчеризацию событий домена за пределы домена. Таким образом, мы можем убедиться, что наша сущность сохраняется, прежде чем мы вызовем какие-либо события.

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

Мое решение проблемы События должны отправляться только в том случае, если объект успешно сохранен, заключался в создании отложенного диспетчера событий, который ставит события в очередь. Затем мы внедряем диспетчер в нашу единицу работы, гарантируя, что мы сначала сохраняем/сохраняем нашу сущность, а затем генерируем события предметной области:

public class DeferredEventDispatcher : IEventDispatcher
{
    private readonly IEventDispatcher inner;
    private readonly ConcurrentQueue<Action> events = new ConcurrentQueue<Action>();

    public DeferredEventDispatcher(IEventDispatcher inner)
    {
        this.inner = inner;
    }

    public void Dispatch<TEvent>(TEvent e)
    {
        events.Enqueue(() => inner.Dispatch(e));
    }

    public void Resolve()
    {
        Action dispatch;
        while (events.TryDequeue(out dispatch))
        {
            dispatch();
        }
    }
}

public class UnitOfWork
{
    public void Commit()
    {
        session.SaveChanges();
        dispatcher.Resolve(); // raise events
    }
}

По сути, это достигает того же, что и предложено @synhershko, но сохраняет «поднятие» событий в моем домене.

Что касается события не должны создаваться до тех пор, пока объект не будет сохранен, основная проблема заключалась в том, что идентификаторы объектов устанавливались RavenDB извне. Решение, которое держит мой домен в неведении и легко проверяется, состоит в том, чтобы просто передать идентификатор в качестве параметра конструктора. Это то, что я бы сделал, если бы использовал базу данных SQL (обычно передавая Guid).

К счастью, RavenDB предоставляет вам возможность генерировать идентификаторы с использованием стратегии hilo (чтобы мы могли сохранить идентификаторы RESTful). Это из проекта RavenDB Contrib:

public static string GenerateIdFor<T>(this IAdvancedDocumentSessionOperations session)
{
    // An entity instance is required to generate a key, but we only have a type.
    // We might not have a public constructor, so we must use reflection.
    var entity = Activator.CreateInstance(typeof(T), true);

    // Generate an ID using the commands and conventions from the current session
    var conventions = session.DocumentStore.Conventions;
    var databaseName = session.GetDatabaseName();
    var databaseCommands = session.GetDatabaseCommands();
    return conventions.GenerateDocumentKey(databaseName, databaseCommands, entity);
}

Затем я могу использовать это для создания идентификатора и передачи его в мои конструкторы сущностей:

var orderId = session.GenerateIdFor<Order>();
var order = new Order(orderId, 1.99M);
person Ben Foster    schedule 27.12.2013

Следуя нашему обсуждению в Твиттере, вместо того, чтобы делать это из конструктора Order, я просто сделаю это:

var order = new Order(amount: 10);
session.Store(order);
DomainEvents.Raise(new OrderRaisedEvent { OrderId = order.Id });

session.SaveChanges();

Еще лучше — вы можете создать IDocumentStoreListener и сделать это оттуда в зависимости от сохраняемого типа. Я не вижу причин делать это из конструктора - вы хотите вызвать событие, когда заказ был сохранен (или отправлен для сохранения), а не при создании его представления в памяти.

Поскольку это будет использовать генератор HiLo внутри, вам гарантировано, что это не будет обращаться к БД для каждого вызова Store(), и что идентификаторы будут уникальными. Поэтому, если что-то пойдет не так, будет получено сообщение с идентификатором заказа, и когда после нескольких попыток не будет найден заказ с этим идентификатором, вы можете предположить, что что-то пошло не так с его сохранением. Или вы можете попытаться поймать исключения и вызвать другое событие с деталями исключения, имеющими идентификатор этого заказа.

Попытка совместить отправку сообщений и хранение документов в RavenDB в рамках одной и той же транзакции — это действительно излишество. Вместо этого вы должны планировать и строить на случай неудач.

person synhershko    schedule 27.12.2013