DDD инъекционные сервисы на вызовы методов сущностей

11

Краткий формат вопроса

Находится ли в лучших практиках DDD и OOP внедрение служб при вызовах методов сущностей?

Пример длинного формата

Допустим, у нас есть классический случай Order-LineItems в DDD, где у нас есть объект домена, называемый заказом, который также действует как совокупный корень, и этот объект состоит не только из его объектов-значений, но и из коллекции элементов строки Сущности.

Предположим, нам нужен свободный синтаксис в нашем приложении, чтобы мы могли сделать что-то вроде этого (отметив синтаксис в строке 2, где мы вызываем getLineItemsметод):

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

Мы не хотим внедрять какие-либо LineItemRepository в OrderEntity, поскольку это является нарушением нескольких принципов, о которых я могу думать. Но беглость синтаксиса - это то, что нам действительно нужно, потому что его легко читать и поддерживать, а также тестировать.

Рассмотрим следующий код, отметив метод getLineItemsв OrderEntity:

interface IOrderService {
    public function getOrderByID($orderID) : OrderEntity;
    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection;
}

class OrderService implements IOrderService {
    private $orderRepository;
    private $lineItemRepository;

    public function __construct(IOrderRepository $orderRepository, ILineItemRepository $lineItemRepository) {
        $this->orderRepository = $orderRepository;
        $this->lineItemRepository = $lineItemRepository;
    }

    public function getOrderByID($orderID) : OrderEntity {
        return $this->orderRepository->getByID($orderID);
    }

    public function getLineItems(OrderEntity $orderEntity) : LineItemCollection {
        return $this->lineItemRepository->getLineItemsByOrderID($orderEntity->ID());
    }
}

class OrderEntity {
    private $ID;
    private $lineItems;

    public function getLineItems(IOrderServiceInternal $orderService) {
        if(!is_null($this->lineItems)) {
            $this->lineItems = $orderService->getLineItems($this);
        }
        return $this->lineItems;
    }
}

Является ли это приемлемым способом реализации свободного синтаксиса в сущностях без нарушения основных принципов DDD и ООП? Мне кажется, это нормально, так как мы показываем только уровень сервиса, а не уровень инфраструктуры (который вложен в сервис)

e_i_pi
источник

Ответы:

9

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

class Invoice
{
    private $currency;
    private $customerId;

    public function __construct()
    {
    }

    public function sum(InvoiceCalculator $calculator)
    {
        $sum =
            new SumRecord(
                $calculator->calculate($this)
            )
        ;

        if ($sum->isZero()) {
            $this->events->add(new ZeroSumCalculated());
        }

        return $sum;
    }
}

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

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

Вадим Самохин
источник
6

Я потрясен, прочитав некоторые ответы здесь.

Вполне допустимо передавать доменные службы в методы сущностей в DDD, чтобы делегировать некоторые бизнес-вычисления. В качестве примера представьте, что вашему агрегированному корню (сущности) необходим доступ к внешнему ресурсу через http, чтобы выполнить некоторую бизнес-логику и вызвать событие. Если вы не внедрите сервис через бизнес-метод организации, как еще вы это сделаете? Вы бы могли создать http-клиента внутри вашей сущности? Это звучит как ужасная идея.

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

diegosasw
источник
1
Почему рассматриваемый вами случай не является обязанностью доменной службы?
e_i_pi
1
это доменная служба, но она вводится бизнес-методом. Прикладной слой - просто оркестратор,
diegosasw
У меня нет опыта работы с DDD, но не следует ли вызывать доменную службу из службы приложений, и после проверки доменной службы продолжать вызывать методы сущностей через эту службу приложений? Я сталкиваюсь с той же проблемой в моем проекте, потому что доменная служба выполняет вызов базы данных через репозиторий ... Я не знаю, нормально ли это.
Muflix
Доменная служба должна организовать, если вы вызываете ее из приложения позже, это означает, что вы каким-то образом обрабатываете ответ, а затем что-то делаете с ним. Может быть, это звучит как бизнес-логика. Если это так, он принадлежит на уровне домена, и приложение позже просто разрешает зависимость и внедряет ее в совокупность. Служба домена могла бы внедрить репозиторий, чья база данных попадания реализации должна принадлежать на уровне инфраструктуры (только реализация, а не интерфейс / контракт). Если он описывает ваш вездесущий язык, он принадлежит домену.
diegosasw
5

Находится ли в лучших практиках DDD и OOP внедрение служб при вызовах методов сущностей?

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

$order->getLineItems($orderService)

Это неправильно, так как Агрегату не нужно больше ничего, кроме себя, чтобы возвращать элементы заказа. Весь совокупный уже должен быть загружен до его вызова метода. Если вы чувствуете, что это должно быть отложено, есть две возможности:

  1. Границы ваших агрегатов неверны, они слишком велики.

  2. В этом случае вы используете Агрегат только для чтения. Лучшее решение - отделить модель записи от модели чтения (т.е. использовать CQRS ). В этой более чистой архитектуре вам не разрешено запрашивать Агрегат, но модель чтения.

Константин Гальбену
источник
Если мне нужен вызов базы данных для проверки, я должен вызвать его в службе приложений и передать результат службе домена или непосредственно в объединенный корень, а не внедрить репозиторий в службу домена?
Muflix
1
@Muflix да, все верно
Константин Гальбену
3

Основная идея тактических шаблонов DDD: приложение получает доступ ко всем данным в приложении, действуя на основе общего корня. Это означает, что единственными объектами, которые доступны за пределами доменной модели, являются совокупные корни.

Корень агрегата Order никогда не будет давать ссылку на его коллекцию lineitem, которая позволит вам изменить коллекцию, а также не даст коллекцию ссылок на любую позицию, которая позволит вам изменить ее. Если вы хотите изменить агрегат заказа, применяется голливудский принцип: «Скажи, не спрашивай».

Возврат значений из агрегата - это нормально, потому что значения по своей природе неизменны; Вы не можете изменить мои данные, изменив свою копию.

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

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

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems($orderService) as $lineItem) {
  ...
}

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

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

Конечно, это предполагает, что позиции уже загружены.

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

Этот подход не тот, который вы найдете в оригинальном Эване, где он предполагал, что с агрегатом будет связана одна модель данных. Это более естественно выпадает из CQRS.

VoiceOfUnreason
источник
Спасибо за это. Я сейчас прочитал около половины «красной книги», и у меня был первый вкус правильного применения Голливудского принципа на уровне инфраструктуры. Перечитав все эти ответы, они все дают хорошие замечания, но я думаю, что у вас есть несколько очень важных моментов, касающихся объема lineItems()и предварительной загрузки при первом извлечении совокупного корня.
e_i_pi
3

Вообще говоря, объекты-ценности, принадлежащие агрегату, сами по себе не имеют репозитория. Совокупная ответственность root - заполнять их. В вашем случае ответственность вашего OrderRepository состоит в заполнении сущности Order и объектов значений OrderLine.

Что касается реализации инфраструктуры OrderRepository, то в случае ORM это отношение один-ко-многим, и вы можете выбрать либо горячую, либо ленивую загрузку OrderLine.

Я не уверен, что именно означают ваши услуги. Это довольно близко к «Службе приложений». Если это так, то, как правило, не рекомендуется внедрять сервисы в Aggregate root / Entity / Value Object. Служба приложений должна быть клиентом Агрегированного корневого / Entity / Value Object и Domain Service. Еще одна вещь, связанная с вашими сервисами, это то, что показ значимых объектов в Application Service также не очень хорошая идея. Они должны быть доступны по совокупному корню.

ivenxu
источник
2

Ответ: определенно НЕТ, избегайте прохождения служб в методах сущностей.

Решение простое: просто позвольте репозиторию Order вернуть Order со всеми его LineItems. В вашем случае агрегат - это Order + LineItems, поэтому, если репозиторий не возвращает полный агрегат, он не выполняет свою работу.

Более широкий принцип: хранить функциональные биты (например, логика домена) отдельно от нефункциональных битов (например, постоянство).

Еще одна вещь: если вы можете, постарайтесь избежать этого:

$order = $orderService->getOrderByID($orderID);
foreach($order->getLineItems() as $lineItem) {
  ...
}

Сделай это вместо

$order = $orderService->getOrderByID($orderID);
$order->doSomethingSignificant();

В объектно-ориентированном дизайне мы стараемся избегать рыбной ловли в объектных данных. Мы предпочитаем просить объект делать то, что мы хотим.

xpmatteo
источник