Где создавать события домена, зависящие от персистентности - служба, хранилище или пользовательский интерфейс? - PullRequest
31 голосов
/ 04 мая 2011

Моему приложению ASP.NET MVC3 / NHibernate требуется отключать и обрабатывать различные события, связанные с объектами моего домена. Например, объект Order может иметь такие события, как OrderStatusChanged или NoteCreatedForOrder. В большинстве случаев эти события приводят к отправке электронной почты, поэтому я не могу просто оставить их в приложении MVC.

Я прочитал События домена Udi Dahan и десятки других мыслей о том, как делать такие вещи, и я решил использовать хост на основе NServiceBus, который обрабатывает сообщения о событиях. Я провел несколько проверок концепции, и это, кажется, работает хорошо.

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

Другая проблема заключается в том, что в некоторых случаях событие связано с объектом, который находится под совокупным корнем. В приведенном выше примере Note сохраняется путем добавления его в коллекцию Order.Notes и сохранения заказа. Это создает проблему в том, что трудно оценить, какие события должны запускаться при сохранении Order. Я бы хотел избежать необходимости извлекать текущую копию объекта и искать различия перед сохранением обновленной копии.

  • Уместно ли для пользовательского интерфейса вызывать эти события? Он знает, какие события произошли, и может инициировать их только после того, как сервисный уровень успешно сохранит объект. Что-то не так с контроллером, запускающим события домена.

  • Должно ли хранилище запускать события после успешного сохранения?

  • Должен ли я вообще отделять события, иметь в хранилище хранилище объект Event, который затем выбирается службой опроса и , а затем превращается в событие для NServiceBus (или обрабатывается непосредственно из служба опросов)?

  • Есть ли лучший способ сделать это? Может быть, объекты моего домена помещают в очередь события, которые запускаются сервисным уровнем только после сохранения объекта?

  • Обновление: У меня есть сервисный уровень, но он кажется громоздким и чрезмерным, чтобы он прошел процесс сравнения, чтобы определить, какие события должны быть запущены при сохранении заданного совокупного корня. Поскольку некоторые из этих событий являются гранулированными (например, «статус заказа изменен»), я думаю, что мне придется извлечь копию объекта БД, сравнить свойства, чтобы создать события, сохранить новый объект, а затем отправить события в NServiceBus, когда операция сохранения успешно завершена.

Обновление

После того, как я опубликовал ответ ( значительно ниже ), я закончил тем, что встроил в мои доменные объекты свойство EventQueue, которое было List<IDomainEvent>. Затем я добавил события в качестве изменений в домене, и это позволило мне сохранить логику в домене, что я считаю уместным, поскольку я запускаю события, основываясь на том, что происходит внутри сущности.

Затем, когда я сохраняю объект на своем уровне обслуживания, я обрабатываю эту очередь и фактически отправляю события на служебную шину. Первоначально я планировал использовать устаревшую базу данных, в которой использовались идентифицирующие PK, поэтому мне пришлось пост-обрабатывать эти события для заполнения идентификатора сущности, но в итоге я решил переключиться на Guid.Comb PK, который позволяет мне пропустить это. шаг.

Ответы [ 6 ]

9 голосов
/ 12 декабря 2011

Мое решение заключается в том, что вы инициируете события как на уровне домена, так и на уровне обслуживания.

Ваш домен:

public class Order
{
    public void ChangeStatus(OrderStatus status)
    {
        // change status
        this.Status = status;
        DomainEvent.Raise(new OrderStatusChanged { OrderId = Id, Status = status });
    }

    public void AddNote(string note)
    {
        // add note
        this.Notes.Add(note)
        DomainEvent.Raise(new NoteCreatedForOrder { OrderId = Id, Note = note });
    }
}

Ваш сервис:

public class OrderService
{
    public void SubmitOrder(int orderId, OrderStatus status, string note)
    {
        OrderStatusChanged orderStatusChanged = null;
        NoteCreatedForOrder noteCreatedForOrder = null;

        DomainEvent.Register<OrderStatusChanged>(x => orderStatusChanged = x);
        DomainEvent.Register<NoteCreatedForOrder>(x => noteCreatedForOrder = x);

        using (var uow = UnitOfWork.Start())
        {
            var order = orderRepository.Load(orderId);
            order.ChangeStatus(status);
            order.AddNote(note);
            uow.Commit(); // commit to persist order
        }

        if (orderStatusChanged != null)
        {
            // something like this
            serviceBus.Publish(orderStatusChanged);
        }

        if (noteCreatedForOrder!= null)
        {
            // something like this
            serviceBus.Publish(noteCreatedForOrder);
        }
    }
}
7 голосов
/ 05 мая 2011

События домена должны быть инициированы в ... Домене. Вот почему они являются доменными событиями.

public void ExecuteCommand(MakeCustomerGoldCommand command)
{
    if (ValidateCommand(command) == ValidationResult.OK)
    {
        Customer item = CustomerRepository.GetById(command.CustomerId);
        item.Status = CustomerStatus.Gold;
        CustomerRepository.Update(item);
    }
}

(а затем в классе Customer, следующее):

public CustomerStatus Status
{
    ....
    set
    {
        if (_status != value)
        {
            _status = value;
            switch(_status)
            {
                case CustomerStatus.Gold:
                    DomainEvents.Raise(CustomerIsMadeGold(this));
                    break;
                ...
            }
        }
    }

Метод Raise сохраняет событие в хранилище событий. Он также может выполнять локально зарегистрированные обработчики событий.

3 голосов
/ 04 мая 2011

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

В этом случае ответственность вашего уровня обслуживания будет зависеть от взаимодействия с моделью вашего домена

public Order GetOrderById(int id) {
  //...
  var order = orderRepository.get(id);
  //...
  return order;
}

public CreateOrder(Order order) {
  //...
  orderRepositroy.Add(order);
  if (orderRepository.Submitchanges()) {
    var email = emailManager.CreateNewOrderEmail(order);
    email.Send();
  }
  //...
}

Обычно в конечном итоге создаются объекты 'manager'такие как OrderManager для взаимодействия с заказами и сервисный уровень для работы с POCO.

Уместно ли для интерфейса вызывать эти события?Он знает, какие события произошли, и может инициировать их только после того, как сервисный уровень успешно сохранит объект.Что-то не так с контроллером, запускающим события домена.

Нет.У вас возникнут проблемы, если будут добавлены новые действия, а разработчик не знает или забудет, что электронное письмо должно быть запущено.

Должно ли хранилище запускать события после успешного сохранения?

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

Есть ли лучший способ сделать это?Может быть, объекты моего домена помещают в очередь события, которые запускаются сервисным уровнем только после сохранения объекта?

Да, похоже, это должно обрабатываться вашим сервисным уровнем.

1 голос
/ 06 мая 2011

Решение оказалось основанным на реализации этих методов расширения на объекте сеанса NHibernate.

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

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

Итак, я добавил несколько методов к моей базе данных репозитория:

public bool IsDirtyEntity(T entity)
{
    // Use the extension method...
    return SessionFactory.GetCurrentSession().IsDirtyEntity(entity);
}

public bool IsDirtyEntityProperty(T entity, string propertyName)
{
    // Use the extension method...
    return SessionFactory.GetCurrentSession().IsDirtyProperty(entity, propertyName);
}

Затем в моемметод Save службы, я могу сделать что-то вроде этого (помните, что я использую NServiceBus здесь, но если бы вы использовали статический класс событий домена Udi Dahan, он бы работал аналогично):

var pendingEvents = new List<IMessage>();
if (_repository.IsDirtyEntityProperty(order, "Status"))
    pendingEvents.Add(new OrderStatusChanged()); // In reality I'd set the properties of this event message object

_repository.Save(order);
_unitOfWork.Commit();

// If we get here then the save operation succeeded
foreach (var message in pendingEvents)
    Bus.Send(message);

Потому что в некоторыхВ случаях, когда Id объекта не может быть установлен до тех пор, пока он не будет сохранен (я использую Identity целочисленные столбцы), мне может потребоваться выполнить его после завершения транзакции, чтобы получить Id для заполнения свойств в моих объектах событий.Поскольку это существующие данные, я не могу легко переключиться на тип идентификатора, назначенного клиентом.

0 голосов
/ 11 декабря 2011

Думаю, у вас должна быть доменная служба, как сказал Дэвид Гленн.

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

Ваша доменная служба должна содержать методы, которые четко указывают, что вы хотите делать с вашим доменным объектом, например: RegisterNewOrder, CreateNoteForOrder, ChangeOrderStatus и т. Д.

public class OrderDomainService()
{
    public void ChangeOrderStatus(Order order, OrderStatus status)
    {
        try
        {
            order.ChangeStatus(status);
            using(IUnitOfWork unitOfWork = unitOfWorkFactory.Get())
            {
                IOrderRepository repository = unitOfWork.GetRepository<IOrderRepository>();
                repository.Save(order);
                unitOfWork.Commit();
            }
            DomainEvents.Publish<OrderStatusChnaged>(new OrderStatusChangedEvent(order, status));
        }

    }
}
0 голосов
/ 04 мая 2011

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

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

...