Проектирование на основе доменов: предотвращение анемичных доменов и моделирование ролей реального мира

Я ищу несколько советов о том, насколько я должен быть обеспокоен избеганием модели анемичной области. Мы только начинаем с 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 и сервисный уровень в целом объявляют различные виды операций, которые выполняются над заказом, и предоставляют домен для использования внешние компоненты, такие как уровень представления.

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

Другие вопросы по тегам