Должна ли доменная модель поддерживать себя согласованной при использовании событий?
Я работаю над приложением, в котором мы пытаемся использовать модель предметной области. Идея состоит в том, чтобы сохранить бизнес-логику внутри объектов в доменной модели. Теперь многое делается для объектов, подписывающихся на связанные объекты, чтобы реагировать на изменения в них. Это делается через PropertyChanged и CollectionChanged. Эта работа хорошо, за исключением следующего:
Сложные действия: когда многие изменения должны обрабатываться как группа (а не отдельные изменения свойства / коллекции). Должен ли я / как я могу "строить" транзакции?
Постоянство: я использую NHibernate для постоянства, и это также использует установщики открытых свойств классов. Когда NHibernate попадает в свойство, выполняется много бизнес-логики (что кажется ненужным). Должен ли я использовать пользовательские сеттеры для NHibernate?
В целом кажется, что использование всей логики в модели предметной области делает модель предметной области довольно сложной. Есть идеи???
Вот примерная проблема (извините за дерьмовый инструмент, который я использую):
Вы можете увидеть, как Project my контейнер и объекты под ним реагируют друг на друга, подписавшись. Теперь изменения в сети выполняются через NetworkEditor, но этот редактор не знает о NetworkData. Эти данные могут даже иногда быть определены в другой сборке. Поток идет от пользователя->NetworkEditor->Network->NetworkData и всех других заинтересованных объектов. Это не похоже на масштаб.
1 ответ
Я боюсь, что комбинация событий DDD и PropertyChanged/CollactionChanged теперь может быть лучшей идеей. Проблема в том, что если вы основываете свою логику на этих событиях, управлять сложностью будет крайне сложно, так как один PropertyChanged ведет к другому, а другой - и вскоре вы теряете контроль.
Другая причина, по которой события ProportyChanged и DDD не совсем подходят, заключается в том, что в DDD каждая бизнес-операция должна быть максимально явной. Имейте в виду, что DDD должен привнести технические вещи в мир бизнеса, а не наоборот. И на основании PropertyChanged/CollectionChanged, кажется, не очень явно.
В DDD главная цель состоит в том, чтобы сохранить согласованность внутри агрегата, другими словами, вам необходимо смоделировать агрегат таким образом, чтобы любая вызванная вами операция агрегата была действительной и согласованной (если, конечно, операция завершается успешно).
Если вы строите свою модель правильно, вам не нужно беспокоиться о "построении" транзакции - операция над агрегатом должна быть самой транзакцией.
Я не знаю, как выглядит ваша модель, но вы могли бы рассмотреть возможность перемещения обязанностей на один уровень "вверх" в совокупном дереве, вполне возможно, добавив дополнительные логические объекты в процесс, вместо того, чтобы полагаться на события PropertyChanged.
Пример:
Предположим, у вас есть набор платежей со статусами, и при каждом изменении платежа вы хотите пересчитать общий баланс заказов клиентов. Вместо того, чтобы подписывать изменения на сбор платежей и вызывать метод у клиента при изменении сбора, вы можете сделать что-то вроде этого:
public class CustomerOrder
{
public List<Payment> Payments { get; }
public Balance BalanceForOrder { get; }
public void SetPaymentAsReceived(Guid paymentId)
{
Payments.First(p => p.PaymentId == paymentId).Status = PaymentStatus.Received;
RecalculateBalance();
}
}
Возможно, вы заметили, что мы пересчитываем баланс одного заказа, а не баланс всего клиента - и в большинстве случаев это нормально, так как клиент принадлежит к другому агрегату, и его баланс можно просто запросить при необходимости. Это как раз та часть, которая демонстрирует эту "согласованность только внутри совокупности" - мы не заботимся о каком-либо другом агрегате на данный момент, мы имеем дело только с одним заказом. Если это не подходит для требований, то домен смоделирован неправильно.
Я хочу сказать, что в DDD нет единой хорошей модели для каждого сценария - вы должны понимать, как бизнес работает, чтобы быть успешным.
Если вы посмотрите на приведенный выше пример, вы увидите, что нет необходимости "строить" транзакцию - вся транзакция находится в SetPaymentAsReceived
метод. В большинстве случаев одно действие пользователя должно приводить к одному конкретному методу на объекте без агрегата - этот метод явно относится к бизнес-операции (конечно, этот метод может вызывать другие методы).
Что касается событий в DDD, существует концепция событий домена, однако они не связаны напрямую с техническими событиями PropertyChanged/CollectionChanged. События домена указывают на бизнес-операции (транзакции), которые были выполнены агрегатом.
В целом кажется, что использование всей логики в модели предметной области делает модель предметной области довольно сложной.
Конечно, это происходит так, как предполагается для сценариев со сложной бизнес-логикой. Однако, если домен смоделирован правильно, то эту сложность легко контролировать и контролировать, и это одно из преимуществ DDD.
Добавлено после предоставления примера:
Хорошо, а как насчет создания агрегатного корня с именем Project - когда вы создаете агрегатный корень из репозитория, вы можете заполнить его NetworkData, и операция может выглядеть так:
public class Project
{
protected List<Network> networks;
protected List<NetworkData> networkDatas;
public void Mutate(string someKindOfNetworkId, object someParam)
{
var network = networks.First(n => n.Id == someKindOfNetworkId);
var someResult = network.DoSomething(someParam);
networkDatas.Where(d => d.NetworkId == someKindOfNetworkId)
.ToList()
.ForEach(d => d.DoSomething(someResult, someParam));
}
}
NetworkEditor не будет работать в сети напрямую, а через Project с использованием NetworkId.