Infrastructure VS Domain, или как реализовать правила предметной области

grigori

( ͡° ͜ʖ ͡°)
Команда форума
@Adelf, как это поможет проверить на null?
достаточно распространенная ошибка - при left join не проверить в результате запроса наличие данных из второй таблицы,
null неявно конвертируется в пустую строку, и здравствуй, неконсистентность
 
Последнее редактирование:

Adelf

Administrator
Команда форума
в яваскриптах всегда так делают.
а тут... сделать array_filter какой-нибудь.
 

Sufir

Я не волшебник, я только учусь
Продолжение темы... События предметной области и реакция на них.
В общем, суть в чём, при некоторых действиях над сущностью необходимо выполнять дополнительные действия, которые не относятся к самой предметной области напрямую. Например логирование, генерация какого-нибудь кеша, уведомление стороннего RPC о произошедших изменениях, отправить письма и т.п.
Например изменился у клиента статус или изменен менеджер отвечающий за клиента. Была мысль сделать что-то в таком духе:
PHP:
Client {
    function changeManager(Manager $manager) {
        if ($manager->isSomething()) {
            $this->doSomething();
        }

        if ($manager->isSomethingElse() && $this->isSomething) {
            $this->doSomethingElse();
            $manager->doSomething();
        }

         $this->managerId = $manager->getId();
         $this->events[] = new ManagerChangedEvent($clientId, $managerId);
    }
}
PHP:
CommandHandler {
    function execute(ChangeManager $command) {
        // ...
  
        $eventDispatcher->dispatch($client->flushEvents())
    }
}
Но, если передавать в событие только непосредственно связанные с ним данные их может быть не достаточно. Например, если изменили менеджера и при этом статус клиента такой-то и у клиента есть задолженности и/или что-то ещё, то сделать одно, иначе другое/третье/не выполнять никаких действий. А в ивенте мы знаем только ID клиента и менеджера.
Передавать во все ивенты DTO со всеми вообще данными по сущности? В обработчике отдельно их запрашивать? Или попробовать что-то изобразить через "наблюдатель"?

Или вообще подойти как-то иначе к вопросу? Может ивенты и не нужны? Можно, конечно прямо в обработчике команд делать проверки, но как-то не красиво это. Слишком много наружу вылазит из сущности и перегружается обработчик.
PHP:
CommandHandler {
    function execute(ChangeManager $command) {
        // ...
        // как-то это "грязно"
        if ($client->inStatus() && $client->managerId() != $newManager->getId()) {
            $client->changeManager(Manager $manager);
            $rpc->action();
            $notifier->send();
            // и т.д.
        }
    }
}
В общем как подобные вещи делаются? Куда смотреть?
 
Последнее редактирование:

Sufir

Я не волшебник, я только учусь
Ты можешь просто прочитать состояние в слушателе.
Не понял. Слушателе события, а откуда я его там считаю? Можешь немного подробнее обрисовать идею или ссылку на примерчик?

Ты не должен менять состояние Manager'а внутри Client'а.
Да, это сюда же, к этому вопросу и его расширение. При выполнении действий могут требоваться и действия предметной области над связанными сущностями, например при изменении статуса клиента нужно так же что-то проделать и с его менеджером. Или ещё такой кейс - при увольнении менеджера освободить всех его клиентов...
Туда же вынести, где и вспомогательные действия не относящиеся к домену выполнять?
И как это чисто технически организовать лучше, ведь в отдельных случаях зависимых сущностей, над которыми нужно произвести действие может быть очень много. И в итоге возможно всё сводится к UPDATE ... WHERE rel=1, но теория предполагает загрузить в память несколько десятков/сотен объектов и отдельно произвести действия и сохранить. Но это уже вопрос отдельный...
 
Последнее редактирование:

Вурдалак

Продвинутый новичок
Не понял. Слушателе события, а откуда я его там считаю?
По id из события достать из базы клиента и узнать его статус. Ещё один вариант: подписаться на событие StatusChangedEvent, таким образом ты будешь сообщать о статусе.

Проще общаться, если речь идет о каких-то конкретных понятиях, а не о doSomething и doSomethingElse.

При выполнении действий могут требоваться и действия предметной области над связанными сущностями, например при изменении статуса клиента нужно так же что-то проделать и с его менеджером.
Клиент сгенерирует событие, по событию можно выполнить команду ЧтоТоПроделайСМенеджером($managerId, ...).
 

Sufir

Я не волшебник, я только учусь
Проще общаться, если речь идет о каких-то конкретных понятиях, а не о doSomething и doSomethingElse.
Ну, вот за пример возьмем изменение менеджера закрепленного за клиентом.
Ещё один вариант: подписаться на событие StatusChangedEvent, таким образом ты будешь сообщать о статусе.
В данном случае статус клиента не изменится (статусами может быть, например "должник", "новый клиент", "просрочен договор", "в проработке" и т.п.), только поменялся его менеджер.
Но при изменении менеджера, если клиент имеет определенный статус, то нужно выполнить некоторые сторонние действия, отправить email и уведомить сторонний RPC что у него сменился менеджер. Для остальных же статусов эти действия выполнять не требуется, но в зависимости от других параметров может понадобится сделать что-то другое.
По id из события достать из базы клиента и узнать его статус.
Это да, но если к примеру для действий (логирования например) мне нужны состояния до действия и после? Я всё-таки думаю два DTO со всеми основными данными сущности в событие отдавать, до и после действия. А уж если для него какие-то специфичные нужны, их уже считывать в самом отдельно.
 

Вурдалак

Продвинутый новичок
В данном случае статус клиента не изменится (статусами может быть, например "должник", "новый клиент", "просрочен договор", "в проработке" и т.п.), только поменялся его менеджер.
В данном — нет. Но в принципе статус когда-то меняется? Вот тогда и происходит событие.

Представь, что ты внешний наблюдатель (коллекторское агенство, например), ты видишь только события:
StatusChangedEvent { clientId: 42, newStatus: debtor }, ManagerChangedEvent { clientId: 42, managerId: 666 }, StatusChangedEvent { clientId: 42, newStatus: ok }.

Ты же можешь на основании этих событий сказать, что сначала клиент стал должником («о, наш клиент»), затем у него поменялся менеджер?

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

Да, тут есть дублирование информации, эта цена за decoupling логики менеджеров-клиентов и коллекторского агенства. Разные bounded contexts, код и данные которых, например, могут физически находится на разных серверах. Ты также сможешь заводить должников из других источников, отдельно тестировать и т.д.

Но возможно для кода коллекторского агенства можно обойтись без DDD, а просто прочитав из базы текущий статус при появлении события ManagerChangedEvent и отправить письмо, это самое простое решение.

Это да, но если к примеру для действий (логирования например) мне нужны состояния до действия и после? Я всё-таки думаю два DTO со всеми основными данными сущности в событие отдавать, до и после действия. А уж если для него какие-то специфичные нужны, их уже считывать в самом отдельно.
Для упрощения жизни ты можешь предыдущий статус добавлять в событие. Но строго говоря, если ты подписываешься на событие StatusChangedEvent, то ты можешь узнать предыдущий статус, если ты его куда-то записываешь.

Записывать все данные в события я бы не стал.
 

Sufir

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

Тем более, что у меня уже есть сущности со своей историей, база существует давно, и на данный момент никакой истории событий нет. Соответственно если у меня сейчас произойдет ManagerChangedEvent { clientId: 42, managerId: 666 } и я попытаюсь найти его статус в истории событий - его там не будет. Что делать? Нагенерить их искусственно по текущему состоянию? Хорошо, нагенерю, но на данный момент с той же базой работает старое приложение с портянками в контроллерах по тысяче строк и более и там ничего генериться не будет, соответственно история не будет адекватна - там изменили статус а у нас в истории последний совсем другой. Просто отрубить старую версию я не могу, это рабочий проект, он будет списан только после полного введения в работу новой версии, с этим тоже приходится считаться... Можно триггер на базу повесить, который на INSERT/UPDATE будет нужные данные писать, тогда будет история, не зависимо откуда данные поступают... В общем это костыли получаются, слишком сложно, не стоит того.

Так-что, все данные в ивент передавать мне тоже совсем не хочется, но видимо это единственная адекватная более-менее простая альтернатива.
 

Sufir

Я не волшебник, я только учусь
Или ещё такой кейс - при увольнении менеджера освободить всех его клиентов...
Туда же вынести, где и вспомогательные действия не относящиеся к домену выполнять?
И как это чисто технически организовать лучше, ведь в отдельных случаях зависимых сущностей, над которыми нужно произвести действие может быть очень много. И в итоге возможно всё сводится к UPDATE ... WHERE rel=1, но теория предполагает загрузить в память несколько десятков/сотен объектов и отдельно произвести действия и сохранить.
UP, т.к. затерялось в обсуждении других вопросов. На вскидку примеры которые приходят в голову:

Пример 1. При увольнении менеджера необходимо отвязать от него всех клиентов (точнее его от клиентов, $client->changeGeneralManager() или $client->removeGeneralManager() которых может быть от нескольких штук, до многих десятков). Соответственно пару-тройку сотен объектов закгружать в память, делать это в цикле и потом для каждого делать апдейт ($repository->save()) не выглядит здорово, когда чисто технически всё сводится к UPDATE ... WHERE rel_field=1.

Пример 2. Добавление клиентов в задания на прозвон колл-центру (холодные звонки или ещё что-то в таком роде). В UI, допустим, менеджер выбирает полсотни клиентов и сабмитит. На уровне инфраструктуры всё сводится к multiple insert. Но в домене должно быть что-то вроде:
PHP:
$clients = $clientRepository->findByIds(); // выгружать полсотни или больше сущностей для простого multiple insert?
$ct = CallTask::create($clients); // или $ct->addClient(Client $client);
$repository->save($ct);

Ввести какой-то "сервис", вроде DismissStaff::execute($managetId)?
В каких-то случаях их может быть даже не десятки, как в примерах, а сотни. В общем суть вопроса, как выразить в слое предметной области и как реализовать технически массовые действия над сущностями?
 
Последнее редактирование:

Вурдалак

Продвинутый новичок
Пример 1. При увольнении менеджера необходимо отвязать от него всех клиентов (точнее его от клиентов, $client->changeGeneralManager() или $client->removeGeneralManager() которых может быть от нескольких штук, до многих десятков). Соответственно пару-тройку сотен объектов закгружать в память, делать это в цикле и потом для каждого делать апдейт ($repository->save()) не выглядит здорово, когда чисто технически всё сводится к UPDATE ... WHERE rel_field=1.
DDD way требует именно нескольких апдейтов, даже если их будет несколько сотен. Просто обычно это делается асинхронно.

Но ты можешь, конечно, сделать один UPDATE, выбросить событие со списком затронутых client id, это просто не совсем DDD way, компромисс.

Пример 2. Добавление клиентов в задания на прозвон колл-центру (холодные звонки или ещё что-то в таком роде). В UI, допустим, менеджер выбирает полсотни клиентов и сабмитит. На уровне инфраструктуры всё сводится к multiple insert. Но в домене должно быть что-то вроде:
PHP:
$clients = $clientRepository->findByIds(); // выгружать полсотни или больше сущностей для простого multiple insert?
$ct = CallTask::create($clients); // или $ct->addClient(Client $client);
$repository->save($ct);
Из твоего примера не очень ясно зачем загружать клиентов, просто id-шники не подходят?

Но в целом, если тебе нужно создать полсотни сущностей, то будет полсотни команд.
 

fixxxer

К.О.
Партнер клуба
Вот, кстати, меня тоже мучает вопрос, что делать с репозиториями в случаях bulk update по условию. Представим себе, что affected тут не несколько сотен, а 100 тысяч.
В голову приходит что-то вроде $repository->updateByCriteria, но это путь к анемикам.

Тут любое решение будет performance hack-ом, конечно, но хочется, чтобы он был как можно менее obstructive.
 
Последнее редактирование:

Sufir

Я не волшебник, я только учусь
Из твоего примера не очень ясно зачем загружать клиентов, просто id-шники не подходят?
Подходят, пока бизнес-правила не требуют, что-то такое:
Код:
function addClient(Client $client) {
    if ($client->isActive() && $client->sonethingElse()) {
    }

    throw exception ClientCantBeAdded;
}
Т.е. опишем так: есть "Клиент", есть "Задание". В задание могут добавляться неограниченное кол-во клиентов (как при создании сущности, так и дальше во время жизненного цикла). По факту в UI менеджеру нужно сразу пачкой их десятками добавлять.
По поводу асинхронности думал, первый пример, как вариант, можно закидывать команды в очередь и там выполнять. А во-втором сущность создается одна (Задание), с множеством ссылок на другие (Клиенты). UPD: А, ну, хотя так же, создать сущность одной ссылкой или пустую, если допустимо, а остальных поставить в очередь можно...
100 тысяч - это случай скорее исключительный, конечно, а вот когда десятки и сотни, как в приведенных мной примерах, бывает не редко.
 
Последнее редактирование:
Сверху