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

stalxed

Новичок
Так нет же. Какой смысл кидать логику в DAL? Завтра перекинем все из MySQL в Redis и придется опять реализовывать там уникальность email?
Если представлять, что репозиторий это коллекция, уникальная коллекция, где в качестве уникального фактора выступает email - то всё встаёт на свои места.
Есть факт - уникальная коллекция, бизнес элемент, находится в domain, определяется как ClientRepositoryInterface, в нём метод add, который может выбрасывать исключение(ну тут phpdoc).
И есть тупые, не интересные реализации, ну например MySQLClientReposotory и RedisClientRepository. Они находятся в инфраструктурном слое. И логики у них нет, они не содержат бизнес правила, они содержат код реализующий интерфейс и всё. И исключение они тоже кидают, которое находится в domain layer.

Типа эта парадигма многослойной архитектуры приправленной DI.
Вот иллюстрация с книги Вернона:
upload_2015-12-23_19-21-34.png
Figure 4.3. The possible Layers when the Dependency Inversion Principle is used. We move the Infrastructure Layer above all others, enabling it to implement interfaces for all Layers below.

P.S.: Я обычно ещё реализую в domain layer что-то вроде InMemoryClientReposotory, шикарно помогает для юнит тестов.
 

Adelf

Administrator
Команда форума
PHP:
 /**
 * @throws LoginAlreadyExists
 * @throws EmailAlreadyExists
 */
Уникальный фактор там все-таки наверно login. А email - это уже немного другая опера.
 

stalxed

Новичок
PHP:
 /**
* @throws LoginAlreadyExists
* @throws EmailAlreadyExists
*/
Уникальный фактор там все-таки наверно login. А email - это уже немного другая опера.
Какая разница? Мы задаём в интерфейсе условия попадания в коллекцию(=репозиторий). А вот реализация этих условий - уже работа инфраструктурного уровня. В InMemoryClientRepository мы можем проверять эти условия и расположить в domain layer.
 

Вурдалак

Продвинутый новичок
Так нет же. Какой смысл кидать логику в DAL?
Я не считаю это как таковой «логикой», это реализация. «Логика» выражена в виде контракта репозитория, который в domain. Если репозиторий не выкидывает исключение в нужном случае, то это баг реализации.

А что предлагаешь ты и как ты будешь бороться с race condition в промежутке между проверкой и ->save()? Стоит ли оно того?

Завтра перекинем все из MySQL в Redis и придется опять реализовывать там уникальность email?
Именно так. Если в MySQL тебе нужно проверить наличие ошибки с кодом 1062 (duplicate entry), то для Redis будет что-то другое. А ты как хотел?
 
Последнее редактирование:

Adelf

Administrator
Команда форума
Ну понятно. В данном случае создаем уникальные индексы и в коде только ловим нужные ошибки? Если так, то я поддерживаю :) Суровая реальность накладывает свой отпечаток...
Просто условие уникальности email для меня... ну это почти тоже самое что Имя фамилия не должны быть пустыми. И последнее мы явно в репозитории проверять не станем.
 

Вурдалак

Продвинутый новичок
Ну понятно. В данном случае создаем уникальные индексы и в коде только ловим нужные ошибки? Если так, то я поддерживаю :) Суровая реальность накладывает свой отпечаток...
Просто условие уникальности email для меня... ну это почти тоже самое что Имя фамилия не должны быть пустыми. И последнее мы явно в репозитории проверять не станем.
«Непустое имя» — это инвариант объекта FirstName/LastName, в то время как «уникальный email» скорее является инвариантом именно хранилища: хранилище должно гарантировать, что у нас не будет 2 агрегата с одинаковым email. По крайней мере, это намного удобнее.
 

Absinthe

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

Adelf

Administrator
Команда форума
ну допустим у нас могут быть и такие условия - может быть только один пользователь с ролью superadmin. это тоже реализовывать на уровне хранилища?
Мне кажется это все-таки реальность давит. удобство unique индексов и т.д.
 

Вурдалак

Продвинутый новичок
Хранилище или уровень приложения?
В данном случае — хранилище.

Я, например, внешние ключи использую, и мне некоторые говорят, что это тоже вынесение части логики в хранилище.
Да, а невозможность выбрать дату рождения несовершеннолетнему — это тоже логика и она вынесена в представление, ога. Если хранилищу нужны foreign/unique keys, то почему бы и нет.

ну допустим у нас могут быть и такие условия - может быть только один пользователь с ролью superadmin. это тоже реализовывать на уровне хранилища?
Универсального способа я не вижу.

Если там примитивная проверка с наличием одного и только одного суперадмина, то, вероятно, я бы и это мог повесить на ->save(), добавив соответствующее исключение с описанием.
Если действительно более сложная логика, то можно проверять в domain service. Этот сервис можно передавать в фабричный метод сущности, например. Для уверенности, можно команду на выдачу супер-прав сделать с эксклюзивным локом. Это не самое «правильное» решение, но довольно прагматичное.
«Правильное» решение скорее всего бы потребовало ввести дополнительные AR, такие как «заявка на получение суперадминских прав» и «счетчик количества суперадминов», которые бы взаимодействовали между собой с помощью saga. Философия DDD требует, чтобы AR были границами транзакционной целостности. Если перефразировать, AR должен быть валиден всегда и в ->save() он сохранится как одно целое. Проверка «уникальности» AR способом выше не совсем верна, т.к. в момент между проверкой и вызовом ->save() AR может стать уже неуникальным, а значит получается, что либо по определению это не AR, либо эта проверка уникальности все-таки не относится к этому AR, а к какому-то другому. Но это выглядит в данном случае overkill'ом.
 

Redjik

Джедай-мастер
У меня мысль есть, а если уж совсем извратится? Пока что не продумал все детали, но принцип такой.

1) Имеется фабрика value object, которая для каждого конкретного value object инжектит массив правил.
2) В правила может быть заинжектен репозиторий или даже сервис.

Таким образом Value Object остаются тупыми, правила все расписываются в конфиге.

В случае c email
1) В комманду инжектится фабрика VO
2) Фабрика получает реквест данные или DataObject
3) Фабрика генерит VO'ты
4) Доходит до email и инжектит UserRepo в правило проверки (не VO), EmailVO посетителем проверяет набор правил
5) проверенно VO идет в модель User

плюс вижжу - можно делать массовые создания и массовые проверки
минус - race condition все еще остается
 

fixxxer

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

минус - race condition все еще остается
То есть это не решает единственную проблему обычного подхода "в лоб", но создает новые. :)
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Интересный вопрос - про передачу в метод большого количества параметров.
Выделять разные сущности не всегда целесообразно, когда это просто несколько десятков свойств объекта.
Что делать когда половина полей - необязательные?
Я обычно иду по пути передачи массива, и при получении валидирую его через array_diff() на наличие в нем обязательных ключей, но валидацию на empty и приведение "" к null перед записью в базу надо прописывать ручками, простынкой.

Какие еще есть удобные способы передачи кучи параметров в метод?
 
Последнее редактирование:

grigori

( ͡° ͜ʖ ͡°)
Команда форума
сделать что-ли генерацию правил для filter_var_array из аннотаций к методу?
для некритичных по скорости методов, конечно
 

Вурдалак

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

grigori

( ͡° ͜ʖ ͡°)
Команда форума
например, надо выставить куку a=b с флагом http_only
http://php.net/manual/ru/function.setcookie.php
писать foo('a','b',null,null,null,null,true) мне кажется неудобно, я предпочитаю js style
foo([
'name'=>'a',
'value'=>'b',
'http_only'=>'true',
]);
 
Последнее редактирование:

HORO

Новичок
а что непонятного-то? Ниче страшного нет в том чтобы написать несколько null,null если это не часто происходит. И это в разы быстрее и удобнее(тк подсветка есть) чем заморачиваться с массивами.
Если это часто происходит, есть смысл сделать обертку над оберткой.
setCookieHttpOnly()
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
Хорошо, еще раз. У меня есть сущности с десятками свойств, их надо инициализировать. Когда это входные параметры - валидация, тут понятно. Бывает, наоборот, исходящий запрос к внешнему API с большой пачкой параметров, значения которых взяты из базы.
Надо передать кучу параметров.
Задача проверки параметров в методе возникает постоянно. Потому что бывают ошибки, в базе может появиться неконсистентность, кто-то может вызвать мой метод из своего кода. Лучше вывести нормальную ошибку, чем дебажить sql, где в значение внезапно пришел NULL,
Приходится писать простыню if-ов.

Да, валидацию параметров в функциях пишут не часто, а я пишу. Как это упростить?
 

grigori

( ͡° ͜ʖ ͡°)
Команда форума
угу, неплохо, то есть вопрос разделяется на несколько разных случаев:

А. если бизнес-логика или API жестко определяет набор параметров - оборачиваем в цепные вызовы именованных сеттеров
+ удобно писать
- в конфиге такие параметры не задать
Б. выносим список параметров в конфиге - используем сеттер/фабрику/service container
В. надо принять внешние данные - испоьзуем валидатор, который создает VO

Остается вопрос по варианту Г:
Взяли сущность или VO из базы, и надо преобразовать его в DTO для передачи во внешний сервис. Я пишу сервис генерации RPC-запроса и преобразование из сущности в DTO через именованный сеттер. Кто и как его будет вызывать в будущем - я не знаю, валидность данных в базе не означает отсутствие ошибок у тех, кто вызывает мои классы, + нужны упомянутые выше преобразования null<->''<->0<->false. Получается простынка из
if (empty($data->key)){$valid_data['key']=''}else{$valid_data['key']=$data['key']}.
Ни у кого не возникало желания как-то упростить написание таких проверок-преобразований?

Пример из моего прошлого: у покупателя оформлен заказ, и он нажимает кнопку подтверждения. В момент подтверждения данные заказа выбираются из базы и отправляются в сервис обработки по RPC.
 
Последнее редактирование:
Сверху