Служби ін'єкцій 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;
    }
}

Це прийнятий спосіб впровадження вільних синтаксисів в Entities, не порушуючи основних принципів DDD та OOP? Мені це здається прекрасним, оскільки ми відкриваємо лише сервісний рівень, а не інфраструктурний рівень (який вкладається в службу)

Відповіді:


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-клієнта всередині вашої організації? Це звучить як жахлива ідея.

Неправильно - вводити служби в агрегати через його конструктор. Але через діловий метод це нормально і абсолютно нормально.


1
Чому за вказаний вами випадок не відповідальність Служби доменів?
e_i_pi

1
це Служба доменів, але вона вводиться у бізнес-метод. Прикладний шар є лише оркестром,
diegosasw

Я не маю досвіду DDD, але чи не слід викликати службу домену з сервісу прикладних програм і після перевірки доменної служби продовжувати дзвонити методами Entity через цю службу додатків? Я зіткнувся з тією ж проблемою в своєму проекті, оскільки служба домену виконує виклик бази даних через сховище ... Я не знаю, чи це нормально.
Муфлікс

Служба домену повинна впорядковуватись, якщо ви зателефонуєте їй з програми пізніше, це означає, що ви якось обробляєте відповідь, а потім щось робите з нею. Можливо, це звучить як бізнес-логіка. Якщо це так, він належить до шару домену, а додаток пізніше просто вирішує залежність та вводить її в сукупність. Служба домену могла ввести сховище, база даних, що вражає виконання, повинна належати до інфраструктурного рівня (лише реалізація, а не інтерфейс / контракт). Якщо він описує вашу всюдисущу мову, вона належить домену.
diegosasw

5

Чи в межах найкращих практик DDD та OOP вводити послуги на дзвінки методу особи?

Ні, вам нічого не слід вводити всередину вашого доменного шару (сюди входять об'єкти, цінні об'єкти, фабрики та послуги домену). Цей шар повинен бути агресивним для будь-яких фреймворків, сторонніх бібліотек або технологій і не повинен здійснювати жодних дзвінків вводу-виводу.

$order->getLineItems($orderService)

Це неправильно, оскільки Агрегат не повинен потребувати нічого іншого, крім самого себе, щоб повертати товари замовлення. Весь сукупний вже повинен бути завантажений до його виклику методу. Якщо ви відчуваєте, що це має бути ледачий навантаження, то є дві можливості:

  1. Ваші межі агрегатів неправильні, вони занадто великі.

  2. У цьому випадку ви використовуєте агрегат лише для читання. Найкраще рішення - розділити модель запису від моделі читання (тобто використовувати CQRS ). У цій чистішій архітектурі вам заборонено запитувати агрегат, але модель для читання.


Якщо мені потрібен виклик бази даних для перевірки, мені доведеться викликати його в службі додатків і передати результат службі домену або безпосередньо в сукупний корінь, а не вставити сховище в службу домену?
Муфлікс

1
@Muflix так, саме так
Константин Гальбену

3

Ключова ідея тактичних моделей DDD: програма отримує доступ до всіх даних у програмі, діючи на сукупний корінь. Це означає, що єдині об'єкти , доступні за межами доменної моделі, є сукупними коренями.

Корінь узагальненого порядку ніколи не посилатиметься на свою колекцію lineitem, що дозволить вам змінювати колекцію, а також не дає колекції посилань на будь-яку позицію, яка дозволила б вам змінити її. Якщо ви хочете змінити сукупність замовлень, застосовується принцип голлівуду: "Скажи, не питай".

Повернення значень зсередини сукупності є прекрасним, оскільки значення за своєю суттю незмінні; ви не можете змінити мої дані, змінивши свою копію.

Використання доменної служби в якості аргументу для надання сукупності в наданні правильних значень - цілком розумна річ.

Зазвичай ви не використовуєте службу домену, щоб забезпечити доступ до даних, що знаходяться всередині агрегату, оскільки агрегат вже повинен мати доступ до нього.

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

Тож правопис є дивним, якщо ми намагаємось отримати доступ до колекції значень позицій цього порядку. Більш природним було б правопис

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

Звичайно, це передбачає, що позиції вже завантажені.

Звичайна схема полягає в тому, що навантаження агрегату буде включати все стан, необхідне для конкретного випадку використання. Іншими словами, у вас може бути кілька різних способів завантаження одного і того ж агрегату; ваші методи сховища підходять за призначенням .

Цей підхід не є чимось, що ви знайдете в оригінальному Евансі, де він припускав, що агрегат матиме єдину модель даних, пов'язану з ним. Він природніше випадає з CQRS.


Дякую за це Зараз я прочитав близько половини "червоної книги" і вперше відчув, як правильно застосувати Голівудський принцип в інфраструктурному шарі. Перечитавши всі ці відповіді, всі вони вказують на хороші моменти, але я думаю, що у вас є дуже важливі моменти щодо обсягу lineItems()та попереднього завантаження при першому пошуку корінного агрегату.
e_i_pi

3

Взагалі об'єкти цінності, що належать до сукупності, не мають власного сховища. Це сукупна відповідальність кореня за їх заселення. У вашому випадку ваша відповідальність за OrderRepository покладається як на заповнення об'єкта Order, так і на об'єкти значень OrderLine.

Що стосується реалізації інфраструктури OrderRepository, у випадку ORM, це відносини один до багатьох, і ви можете обирати бажаючим або ледачим завантажувати OrderLine.

Я не впевнений, що саме означають ваші послуги. Це досить близько до "Служби прикладних програм". У такому випадку, як правило, не рекомендується вводити служби в об'єднаний корінь / об'єкт / значення об'єкта. Служба прикладних програм повинна бути клієнтом сукупного кореневого / об'єктного / ціннісного об’єкта та сервісу домену. Інша річ, що стосується ваших послуг, це те, що викриття об'єктів цінності в Application Service також не є хорошою ідеєю. До них слід отримати сукупний корінь.


2

Відповідь: безумовно, НЕ, уникайте передачі послуг у сутнісних методах.

Рішення просте: просто нехай сховище Order повертає Замовлення з усіма його LineItems. У вашому випадку агрегат - це Order + LineItems, тому якщо репозиторій не повертає повний агрегат, він не виконує свою роботу.

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

І ще одне: якщо ви можете, намагайтеся уникати цього:

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

Зробіть це замість цього

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

В об'єктно-орієнтованому дизайні ми намагаємося уникати риболовлі в даних про об'єкт. Ми вважаємо за краще просити об’єкт робити те, що ми хочемо.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.