Когда логика принадлежит бизнес-объекту / сущности, а когда - службе?

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

Как вы определяете, какая логика принадлежит объекту домена, а какая логика принадлежит службе домена?

Пример: у нас есть класс Order для интернет-магазина. Этот класс является сущностью и совокупным корнем (он содержит OrderItems).

Public Class Order:IOrder
{
    Private List<IOrderItem> OrderItems

    Public Order(List<IOrderItem>)
    {
        OrderItems = List<IOrderItem>
    }

    Public Decimal CalculateTotalItemWeight()
    //This logic seems to belong in the entity.
    {
        Decimal TotalWeight = 0
        foreach(IOrderItem OrderItem in OrderItems)
        {
            TotalWeight += OrderItem.Weight
        }
        return TotalWeight

    }
}

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

1) Определите тариф за доставку, необходимый для доставки этого заказа.

2) Распечатайте транспортную этикетку после определения стоимости пересылки.

Для обоих этих действий потребуются зависимости, которые находятся за пределами объекта Order, например внешний веб-сервис для получения ставок почтовых расходов. Как нам достичь этих двух целей? Я вижу несколько вариантов:

1) Закодируйте логику непосредственно в сущности домена, например CalculateTotalItemWeight. Затем мы звоним:

Order.GetPostageRate
Order.PrintLabel

2) Поместите логику в службу, которая принимает IOrder. Затем мы звоним:

PostageService.GetPostageRate(Order)
PrintService.PrintLabel(Order)

3) Создайте класс для каждого действия, которое работает с Order, и передайте экземпляр этого класса Order через Constructor Injection (это вариант варианта 1, но позволяет повторно использовать классы RateRetriever и LabelPrinter):

 Public Class Order:IOrder
{
    Private List<IOrderItem> OrderItems
    Private RateRetriever _Retriever
    Private LabelPrinter _Printer

    Public Order(List<IOrderItem>, RateRetriever Retriever, LabelPrinter Printer)
    {
        OrderItems = List<IOrderItem>
        _Retriever = Retriever
        _Printer = Printer
    }

    Public Decimal GetPostageRate
    {
        _Retriever.GetPostageRate(this)
    }

     Public void PrintLabel
    {
        _Printer.PrintLabel(this)
    }
}

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


person Casey Wilkins    schedule 29.01.2011    source источник
comment
Вряд ли Орден может быть безразличным к своим элементам OrderDetail ... Вы это имели в виду?   -  person Mitch Wheat    schedule 30.01.2011


Ответы (4)


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

Скорее всего, у меня будет код, который отвечает за размещение заказа (какой-то сервис обработчика заказов, на уровне приложения или в обработчике команд), переданный службе, чтобы получить скорость доставки, а затем передать эту ставку в заказ, поэтому предполагаю вариант 2.

Для печати транспортной этикетки я бы предпочел, чтобы домен инициировал событие в соответствии со строками http://www.udidahan.com/2009/06/14/domain-events-salvation/. Затем печатью этикетки будет заниматься отдельный обработчик. Опять же, логика заключается в том, что способ печати этикеток, вероятно, будет отличаться независимо от того, как вы создаете заказ, поэтому имеет смысл сохранить это отдельно. Использование доменного события кажется самым чистым способом гарантировать, что этикетка будет напечатана в нужное время, не требуя, чтобы Заказ (или, действительно, обработчик заказов) знал о логике печати.

person David    schedule 30.01.2011
comment
Мой инстинкт состоит в том, чтобы следовать вашей линии мышления, но если мы будем следовать этой линии мышления, получим ли мы в конечном итоге анемичную область со всей нашей реальной логикой за пределами наших сущностей? - person Casey Wilkins; 30.01.2011
comment
Я тоже беспокоился об этом, но на самом деле все не так. Определенно какая-то логика осталась на уровне приложений, но я все равно считаю, что это правильно. - person David; 30.01.2011
comment
Просто перечитайте свой исходный вопрос еще раз. Если вы вызываете внешний веб-сервис для выяснения стоимости доставки, то эта логика вне зависимости от домена. Я бы сказал, что использование внешнего веб-сервиса - это решение на уровне приложения. Если бы были правила, которые вы применили к полученной информации - например, вы применяете скидку к ставке для постоянных клиентов или что-то в этом роде - эта логика все еще может применяться в домене, поэтому вы должны передать результат веб-службы обратно в домен. - person David; 30.01.2011
comment
Спасибо за вклад, Дэвид. Ваш ответ побудил меня пересмотреть некоторые принципы DDD, и я думаю, что пытаюсь вложить слишком много поведения в объекты своей области, когда это поведение на самом деле не связано. Я согласен с другими постерами здесь, что 2) - лучшее решение для этого случая. - person Casey Wilkins; 31.01.2011

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

http://danhaywood.com/2010/04/30/accessing-domain-services-from-entities/

person kamal    schedule 30.01.2011

Я бы использовал (2).

Это не добавляет сложности вашему элементу заказа.

Мне это кажется естественным использованием вспомогательной службы.

Обновление: в ответ на комментарий: на странице вики говорится:

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

person Mitch Wheat    schedule 29.01.2011
comment
По вашему мнению, использование №2 подталкивает нас к анемичной модели предметной области? - person Casey Wilkins; 30.01.2011
comment
Я так не думаю, насколько я понимаю, это нормально, когда отдельный объект принимает кучу других объектов и принимает решения на основе их состояния, если рассматриваемые объекты не изменяются или не трансформируются. - person Mitch Wheat; 30.01.2011
comment
Если мы изменим состояние заказа, установив для него свойство PostageRate ... Считается ли это преобразованием заказа? Означает ли что-то столь незначительное, что мы изменим наше первоначальное решение? Кажется, что DDD настолько переполнен этими уловками 22, что его очень сложно реализовать. - person Casey Wilkins; 30.01.2011
comment
Я бы спросил, принадлежит ли PostageRate к пункту заказа. Возможно, он принадлежит опубликованному заказу (истории) или другому объекту? - person Mitch Wheat; 30.01.2011

Моя точка зрения: домен - это то, что содержит логику вашего приложения без ущерба для инфраструктуры. Логика заключается в том, что при подтверждении заказа печатается этикетка и определяется стоимость доставки. Это должно быть в домене.

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

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

person flq    schedule 29.01.2011
comment
Итак, вы бы использовали один из упомянутых мною подходов или что-то другое? - person Casey Wilkins; 30.01.2011