DDD - модификации дочерних объектов в агрегате

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

Заказ (совокупный корень) создается с несколькими строками заказа (дочерними объектами). Согласно бизнес-правилам, каждая линия OrderLine должна сохранять идентичность на протяжении всего срока действия заказа. OrderLines имеют много (20+) свойств и могут довольно часто видоизменяться, прежде чем Order будет считаться «заблокированным». Кроме того, существуют инварианты, которые должны выполняться на корневом уровне; например, каждая строка заказа имеет количество, и общее количество для заказа не может превышать X.

Я не уверен, как смоделировать этот сценарий при рассмотрении изменений в OrderLines. У меня есть 4 варианта, которые я могу представить, но ни один из них не кажется удовлетворительным:

1) Когда придет время изменить OrderLine, сделайте это, используя ссылку, предоставленную корнем. Но я теряю возможность проверить инвариантную логику в корне.

var orderLine = order.GetOrderLine(id);
orderLine.Quantity = 6;

2) Вызвать метод по заказу. Я могу применить всю инвариантную логику, но тогда я застрял с большим количеством методов для изменения многих свойств OrderLine:

order.UpdateOrderLineQuantity(id, 6);
order.UpdateOrderLineDescription(id, description);
order.UpdateOrderLineProduct(id, product);
...

3) Это могло бы быть проще, если бы я рассматривал OrderLine как объект значения, но он должен поддерживать ту же идентификацию в соответствии с бизнес-требованиями.

4) Я могу получить ссылки на OrderLines для модификаций, которые не влияют на инварианты, и просмотреть Order для тех, которые влияют. Но что тогда, если на инварианты влияет большинство свойств OrderLine? Это возражение является гипотетическим, поскольку только несколько свойств могут влиять на инварианты, но это может измениться по мере раскрытия большей бизнес-логики.

Любые предложения приветствуются ... не стесняйтесь, дайте мне знать, если я говорю недобросовестно.


person Cork    schedule 23.05.2012    source источник


Ответы (6)


  1. Не оптимален, так как позволяет нарушить инвариант домена.

  2. Это приведет к дублированию кода и ненужному взрыву метода.

  3. То же, что и 1). Использование объекта значения не поможет сохранить неизменность домена.

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

person Dmitry    schedule 23.05.2012
comment
Спасибо за ответ - думаю, это, наверное, мой лучший выбор из представленных. На самом деле я надеялся, что кто-то предложит лучший шаблон, который я, возможно, упустил;) Я немного подозрительно отношусь к его использованию, потому что он не чистый. Или, может быть, лучший способ выразить это ... непоследовательно, как указал @eulerfx. Но я думаю, что на данный момент подойдет ... - person Cork; 24.05.2012
comment
Я знаю, что уже принял это как ответ ... но я подумал о другом подходе. Можно ли иметь в заказе метод Save (IOrderLine)? Затем я могу передать ссылку Order, избегая множества детализированных методов и все же позволяя Order применять инварианты. - person Cork; 08.06.2012

Один недостаток 4 по сравнению с 2 - это непоследовательность. В некоторых случаях может быть полезно поддерживать определенную последовательность в отношении обновления позиций заказа. Может быть не сразу понятно, почему одни обновления выполняются через заказ, а другие через позицию заказа. Более того, если в строках заказа содержится 20+ свойств, возможно, это признак того, что существует возможность группировки среди этих свойств, что приводит к меньшему количеству свойств в строке заказа. В целом подход 2 или 4 хорош, если вы убедитесь, что операции атомарны, согласованы и соответствуют повсеместно используемому языку.

person eulerfx    schedule 24.05.2012
comment
Спасибо за ответ! Я согласен с непоследовательностью, но считаю, что это мой лучший вариант. На самом деле мы начали с множества свойств строки заказа, находящихся в порядке, но, продолжая анализировать область, мы обнаружили, что строка заказа была более подходящим местом. Не исключено, однако, что мы немного увлеклись ... - person Cork; 24.05.2012

Есть пятый способ сделать это. Вы можете запустить событие домена (например, QuantityUpdatedEvent(order, product, amount)). Позвольте агрегату обработать это внутренне, пройдя список строк заказа, выбрав тот, у которого есть соответствующий продукт, и обновите его количество (или делегируйте операцию OrderLine, что еще лучше)

person Jeroen    schedule 31.07.2012
comment
Когда у объекта есть дочерние элементы, которые можно изменять разными способами, я согласен с тем, что события предметной области обычно являются правильным путем. Джимми Богард также написал несколько замечательных статей, здесь и здесь. - person Justin J Stark; 26.02.2015
comment
@Jeroen, либо я не совсем понимаю ваш ответ, либо вы неправильно поняли концепцию событий домена. Событие - это то, что произошло в прошлом (измененное состояние). Насколько я понимаю, ваш ответ - отправить это событие до каких-либо изменений, а затем позволить обработчику воздействовать на Order или OrderLine… вы можете уточнить свое намерение? - person Wolfgang; 18.12.2020

Событие предметной области - самое надежное решение.

Однако, если это излишне, вы также можете сделать вариант №2 с использованием шаблона Parameter Object - иметь одну функцию ModfiyOrderItem в корне объекта. Отправьте новый, обновленный элемент заказа, а затем внутри Order проверяет этот новый объект и вносит обновления.

Таким образом, ваш типичный рабочий процесс превратится во что-то вроде

var orderItemToModify = order.GetOrderItem(id);
orderItemToModify.Quantity = newQuant;

var result = order.ModifyOrderItem(orderItemToModfiy);
if(result == SUCCESS)
{
  //good
 }
else
{
   var reason = result.Message; etc
}

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

person Mathieson    schedule 14.02.2013
comment
Этот сценарий описан в DDD Quickly. В котором говорится It is possible for the root to pass transient references of internal objects to external ones, with the condition that the external objects do not hold the reference after the operation is finished. One simple way to do that is to pass copies of the Value Objects to external objects. Что я интерпретирую как передачу DTO внешнему объекту для модификации и принятие этого DTO для применения модификаций. В качестве альтернативы у вас могут быть методы в корневом каталоге, которые изменяют дочерний элемент. Такие как order.ChangeItemQuantity(id, quantity) - person Douglas Gaskell; 19.07.2019

Вот еще один вариант, если ваш проект небольшой и вы хотите избежать сложности событий предметной области. Создайте службу, которая обрабатывает правила для Order, и передайте ее методу OrderLine:

public void UpdateQuantity(int quantity, IOrderValidator orderValidator)
{
    if(orderValidator.CanUpdateQuantity(this, quantity))
        Quantity = quantity;
}

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

Это может быть хорошим решением, если ваш проект небольшой и вам не нужна сложность событий предметной области.

Обратной стороной этого метода является то, что вы передаете службу проверки для Order в OrderLine, где она на самом деле не принадлежит. Напротив, создание события домена перемещает логику Order из OrderLine. Затем OrderLine может просто сказать миру: «Эй, я меняю количество». и логика проверки заказа может происходить в обработчике.

person Justin J Stark    schedule 26.02.2015

как насчет использования DTO?

public class OrderLineDto
{
    public int Quantity { get; set; }
    public string Description { get; set; }
    public int ProductId { get; set; }
}

public class Order
{
    public int? Id { get; private set; }
    public IList<OrderLine> OrderLines { get; private set; }

    public void UpdateOrderLine(int id, OrderLineDto values)
    {
        var orderLine = OrderLines
            .Where(x => x.Id == id)
            .FirstOrDefault();

        if (orderLine == null)
        {
            throw new InvalidOperationException("OrderLine not found.");
        }

        // Some domain validation here
        // throw new InvalidOperationException("OrderLine updation is not valid.");

        orderLine.Quantity = values.Quantity;
        orderLine.Description = values.Description;
        orderLine.ProductId = values.ProductId;
    }  
}
  • Единственная проблема здесь в том, что свойство OrderLines имеет общедоступный метод получения, и пользователь этого класса может добавлять элемент в коллекцию. Единственный способ, которым я могу придумать, как это предотвратить, - это скрыть получатель и добавить новый получатель, который вернет коллекцию DTO, если в этом есть необходимость.
  • параметр id метода UpdateOrderLine может быть частью DTO, вероятно, с этим он будет работать лучше
  • вероятно, вы можете принять OrderLine в качестве параметра напрямую вместо OrderLineDto (если вы хотите использовать некоторую OrderLine проверку перед переходом к Order).
person Muflix    schedule 08.02.2020