Проектирование на основе доменов: предотвращение анемичных доменов и моделирование ролей реального мира
Я ищу несколько советов о том, насколько я должен быть обеспокоен избеганием модели анемичной области. Мы только начинаем с DDD и боремся с параличом анализа простых проектных решений. Последний момент, на котором мы остаемся, - это то, к чему относится определенная бизнес-логика, например, Order
объект, который имеет такие свойства, как Status
и т. д. Теперь скажите, что я должен выполнить команду, как UndoLastStatus
потому что кто-то ошибся с заказом, это не так просто, как просто изменить Status
поскольку другая информация должна быть зарегистрирована и свойства изменены. Сейчас в реальном мире это чисто административная задача. Таким образом, с моей точки зрения, у меня есть два варианта:
Вариант 1: добавить метод в порядок, чтобы что-то вроде
Order.UndoLastStatus()
Хотя это и имеет смысл, на самом деле это не отражает область. ТакжеOrder
является первичным объектом в системе, и если все, что включает порядок, помещено в класс порядка, вещи могут выйти из-под контроля.Вариант 2: Создать
Shop
объект, и с этим имеют различные услуги, которые представляют разные роли. Так что я мог бы иметьShop.AdminService
,Shop.DispatchService
, а такжеShop.InventoryService
, Так что в этом случае я быShop.AdminService.UndoLastStatus(Order)
,
Теперь во втором варианте у нас есть нечто, что намного больше отражает область, и позволит разработчикам поговорить с бизнес-экспертами о похожих ролях, которые действительно существуют. Но это также направляется к анемичной модели. Какой будет лучший путь в целом?
4 ответа
Вариант 2 наверняка приведет к процессуальному коду.
Может быть проще в разработке, но гораздо сложнее поддерживать.
Сейчас в реальном мире это чисто административная задача
Задачи "администрирования" должны быть частными и вызываться через публичные, полностью "доменные" действия. Предпочтительно - все еще написан в легком для понимания коде, который управляется из домена.
Как я понимаю - проблема в том, что UndoLastStatus
имеет мало смысла для эксперта в области.
Скорее всего, они говорят о создании, отмене и заполнении заказов.
Что-то в этом роде может подойти лучше:
class Order{
void CancelOrder(){
Status=Status.Canceled;
}
void FillOrder(){
if(Status==Status.Canceled)
throw Exception();
Status=Status.Filled;
}
static void Make(){
return new Order();
}
void Order(){
Status=Status.Pending;
}
}
Мне лично не нравится использование "статусов", они автоматически передаются всему, что их использует - я вижу в этом ненужную связь.
Так что я хотел бы что-то вроде этого:
class Order{
void CancelOrder(){
IsCanceled=true;
}
void FillOrder(){
if(IsCanceled) throw Exception();
IsFilled=true;
}
static Order Make(){
return new Order();
}
void Order(){
IsPending=true;
}
}
Для изменения связанных вещей при изменении состояния заказа лучше всего использовать так называемые доменные события.
Мой код будет выглядеть следующим образом:
class Order{
void CancelOrder(){
IsCanceled=true;
Raise(new Canceled(this));
}
//usage of nested classes for events is my homemade convention
class Canceled:Event<Order>{
void Canceled(Order order):base(order){}
}
}
class Customer{
private void BeHappy(){
Console.WriteLine("hooraay!");
}
//nb: nested class can see privates of Customer
class OnOrderCanceled:IEventHandler<Order.Canceled>{
void Handle(Order.Canceled e){
//caveat: this approach needs order->customer association
var order=e.Source;
order.Customer.BeHappy();
}
}
}
Если Орден становится слишком большим, Вы можете проверить, что такое ограниченный контекст (как говорит Эрик Эванс - если бы у него была возможность написать свою книгу снова, он перенес бы ограниченный контекст в самое начало).
Короче говоря - это форма разложения, управляемая доменом.
Идея относительно проста - нормально иметь несколько Орденов с разных точек зрения, или контекстов.
Например, заказ из контекста покупки, заказ из контекста учета.
namespace Shopping{
class Order{
//association with shopping cart
//might be vital for shopping but completely irrelevant for accounting
ShoppingCart Cart;
}
}
namespace Accounting{
class Order{
//something specific only to accounting
}
}
Но обычно достаточно того, что сам домен избегает сложности и легко разложим, если вы слушаете его достаточно внимательно Например, вы можете услышать от экспертов такие термины, как OrderLifeCycle, OrderHistory, OrderDescription, которые можно использовать в качестве якорей для декомпозиции.
NB: Имейте в виду - я не понимаю вашего домена.
Вполне вероятно, что те глаголы, которые я использую, совершенно странны для него.
Похоже, вы не гоните этот домен из тестов. Взгляните на работу Роба Венса, особенно его работы по исследовательскому моделированию, инверсии времени и активно-пассивному.
Я бы руководствовался принципами GRASP. Примените принцип дизайна информационного эксперта, то есть вы должны назначить ответственность за класс, который, естественно, имеет больше информации, необходимой для выполнения изменения.
В этом случае, поскольку изменение статуса заказа затрагивает другие объекты, я бы заставил каждый из этих низкоуровневых доменных объектов поддерживать метод применения изменения по отношению к самому себе. Затем также используйте слой службы домена, как описано в варианте 2, который абстрагирует всю операцию, охватывая несколько объектов домена по мере необходимости.
Также см. Шаблон Фасад.
Я думаю, что наличие такого метода, как UndoLastStatus, в классе Order выглядит немного неправильно, потому что причины его существования в каком-то смысле выходят за рамки порядка. С другой стороны, наличие метода, который отвечает за изменение статуса заказа, Order.ChangeStatus, прекрасно подходит в качестве модели предметной области. Статус заказа - это правильная концепция домена, и изменение этого статуса должно осуществляться через класс Order, поскольку он владеет данными, связанными со статусом заказа, - это обязанность класса Order поддерживать себя согласованным и в надлежащем состоянии.,
Еще один способ думать о том, что объект Order - это то, что сохраняется в базе данных, и это "последняя остановка" для всех изменений, примененных к Order. Проще рассуждать о том, каким может быть действительное состояние заказа с точки зрения заказа, а не с точки зрения внешнего компонента. В этом суть DDD и ООП, что облегчает людям понимание кода. Кроме того, для выполнения изменения состояния может потребоваться доступ к закрытым или защищенным элементам, и в этом случае лучшим вариантом будет наличие метода в классе заказа. Это одна из причин, почему анемичные доменные модели осуждаются - они снимают ответственность за поддержание согласованности состояний с классом-владельцем, тем самым нарушая инкапсуляцию среди других вещей.
Одним из способов реализации более конкретной операции, такой как UndoLastStatus, было бы создание OrderService, который предоставляет домен и как внешние компоненты работают с доменом. Затем вы можете создать простой командный объект, подобный этому:
class UndoLastStatusCommand {
public Guid OrderId { get; set; }
}
У OrderService будет метод для обработки этой команды:
public void Process(UndoLastStatusCommand command) {
using (var unitOfWork = UowManager.Start()) {
var order = this.orderRepository.Get(command.OrderId);
if (order == null)
throw some exception
// operate on domain to undo last status
unitOfWork.Commit();
}
}
Таким образом, теперь модель домена для Order раскрывает все данные и поведение, которые соответствуют Order, но OrderService и сервисный уровень в целом объявляют различные виды операций, которые выполняются над заказом, и предоставляют домен для использования внешние компоненты, такие как уровень представления.
Также подумайте над изучением концепции доменных событий, которая рассматривает анемичные доменные модели и способы их улучшения.