Pull to refresh

Comments 20

>При вынесении бизнес логики в сущности, значительно упрощается описание кода, рефакторинг и тестирование.

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

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

Для тестирования бизнес-логики размещенной в сущности необходимо:

  • Экземпляра сущности.

Для тестирования бизнес-логики размещенной в сервисе необходимо:

  • Экземпляр сервиса.

  • Зависимости сервиса, либо их моки.

  • Экземпляра сущности или её мок.

И в чем тут простота?

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

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

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

Видимо у нас разные понятия о сущностях или тестах.

В моем понимании сущность это Data Mapper + Rich Domain Model в которую помещается вся бизнес-логика для реализации которой не труебуется взаимодействие с другими сервисами. Таким образом ни для её создания ни для вызова её методов никакие другие сервисы не нужны, и тестировать её можно простым Unit-тестом.

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

При вынесении бизнес логики в сущности, значительно упрощается описание кода, рефакторинг и тестирование.

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

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

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

Фаулер неправ, так бывает. Возможно он представлял что-то свое.

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

И тут просто вопрос в терминах - что такое бизнес логика? Я считаю, что поведение модели тоже входит в это понятие.

Чем лучше? Пример какой-нибудь приведите пожалуйста?

Вычисления, связанные непосредственно с моделью, это только вычисления с ее полями. Например, можно сделать вычисляемое свойство fullName по firstName+lastName.

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

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

"Отправить email после создания заказа" - мало похоже на бизнес логику, хоть и является требованием. Да это часть процесса, но это скорее инфраструктура. Зачастую ошибка тех, кто против этого подхода, что они думают, что ВСЯ кодовая база отправляется в сущности: отправка емейлов, очереди, регистрация, авторизация. Это не так. В сущности отправляется логика, которая может нарушить целостность системы и истинные инварианты, чтобы этого сделать было нельзя.

Является ли критической проблемой, что емейл будет не отослан? Наверное нет.

А является ли критической проблемой, что баланс юзера установился неправильно, из-за того, что человеком малопосвященным всем ньюансам системы, забыл перед этим вызвать какой то калькулятор или чекер, и просто засетил рандомный баланс? Видимо да. И решением этого будет инкапсуляция логики изменения баланса в сущность юзера $user->addMoney($money, $moneyChecker, $banking). И никто уже не забудет в флоу добавления баланса, проверить там что то и какие то операции вызвать.

Продублирую (давал в другом ответе уже) старый доклад на эту тему https://ocramius.github.io/doctrine-best-practices/#/32

мало похоже на бизнес логику

Нет, в том и дело, это именно бизнес-логика, это обсуждается на уровне бизнеса. "Если заказ было создан успешно, то надо отправить письмо". "Если сумма заказа больше N, то уведомить менеджера". А как вы значения в поля модели записываете, как раз не обсуждается, бизнесу это неинтересно.

они думают, что ВСЯ кодовая база отправляется в сущности

Вот я как раз встречал сторонников логики в сущности, которые так думают.

В сущности отправляется логика, которая может нарушить целостность системы и истинные инварианты

А вот нет таких инвариантов. В одном сценарии поле required, в другом нет. Раньше было not required, теперь стало required. Это зависит исключительно от желания бизнеса.

Является ли критической проблемой, что емейл будет не отослан? Наверное нет.

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

Наверное нет.
Видимо да.

Ага, хорошие критерии определения того, что помещать в сущность.

И решением этого будет инкапсуляция логики изменения баланса в сущность юзера

Почему не в сервис изменения баланса? Как вообще юзер может сам менять свой баланс? Это совершенно неправильная модель.

И никто уже не забудет в флоу добавления баланса, проверить там что то и какие то операции вызвать

Этим свойством обладает любое выделение в метод, он необязательно должен быть в сущности.

Ну как это не забудет, в сущности 1000 методов (потому что есть 1000 сценариев, а менять поля вне сущности мы не хотим), он пропустил что уже есть такой метод, и создал новый, который ставит как ему надо без всяких проверок.

 А как вы значения в поля модели записываете, как раз не обсуждается, бизнесу это неинтересно.

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

А вот нет таких инвариантов

У вас нет, у других есть. Например не может быть баланс меньше кредитного лимита или 0, ну не может и всё. А если может, значит нет такого инварианта действительно

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

Какие то ошибки вам будут стоить ничего, а какие то уроном по бизнес репутации и финансовыми потерями. Все же есть разница, когда пользователь заказал товаров на не свой миллион и получил, и когда он не получил емейл

Ага, хорошие критерии определения того, что помещать в сущность.

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

Почему не в сервис изменения баланса? Как вообще юзер может сам менять свой баланс? Это совершенно неправильная модель.


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

Ну как это не забудет, в сущности 1000 методов (потому что есть 1000 сценариев, а менять поля вне сущности мы не хотим),

Если у вас 1000 сценариев на изменение 1 сущности, скорее всего что то идет не так. Но в целом такая проблема в подходе есть, когда сущность слишком разбухает, но это как раз из-за неверной проектировки больших сущностей. Опять же, был и такой опыт, я все равно вижу больше профита в такой сущности, потому что заходя даже через год на поддержке проекта в какой то код, я не могу сломать логику, потому что я что то забыл, и не вызывал какой то сервис или ивент, перед тем как что то засетить напрямую. Контракты взаимодействия с сущностью мне просто этого не дают.

Опять же DDD и агрегаты. Они в целом не могут существовать на анемиках. Если только не двойной маппинг, где есть отдельно сущности ОРМ и отдельно бизнес сущности.
Получается Фаулер не прав, DDD и Эванс не прав, Окрамиус не прав, Инкапсуляция не права, tell don't ask не право. Не верю )


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

мы обсуждаем инкпасулированный процесс, который влияет на 1 или несколько полей и состояние данных после этого процесса.

Нет, мы обсуждаем, где размещать бизнес-логику. Отправка email это часть бизнес-логики.

Например не может быть баланс меньше кредитного лимита или 0, ну не может и всё

Ага, это хороший пример. А потом приходит бизнес и говорит "А сделайте нам такую штуку, которая называется овердрафт, но не меньше вот этого значения в конфиге в БД", и начинаются глобальные рефакторинги.

а какие то уроном по бизнес репутации и финансовыми потерями

Да, если я не получил свой email с подтверждением заказа, это урон по бизнес репутации компании, и в следующий раз я пойду в другую компанию.

К сожалению да, все зависит от той или иной ситуации и конкретного бизнес процесса.

Нет, правило "Делайте сервис для бизнес-логики" работает нормально для всех бизнес-процессов.

Как вы можете видеть в примере, я как раз передал пару сервисов, которые помогут юзера

Ну и что, что передали? А он захотел и не вызвал. Пользователь не может менять сам свой баланс.

Да, если вы работаете этим сервисом с БД напрямую

Я не предлагал работать с БД напрямую.

Если у вас 1000 сценариев на изменение 1 сущности, скорее всего что то идет не так.

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

Но в целом такая проблема в подходе есть, когда сущность слишком разбухает, но это как раз из-за неверной проектировки больших сущностей.

Нет, это из-за подхода "Все изменяющие методы должны быть в сущности". Если их много, значит и сущность большая.

что кто то просто без этого сервиса засетит баланс

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

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

И с сервисом всё точно так же. Вы вызываете метод addMoney, он делает всю работу. Вы просто всегда работаете с сервисом, а не с сущностью. С сущностью вы работаете только если делаете новый сервис.

Когда вы делаете новый метод в сущности для новой функциональности, вы точно так же можете сломать логику, если забудете сделать вызов moneyChecker в новом коде.

Active Record про маппинг на таблицу и работу с таблицей через модель и это совсем про другое, нежели бизнес логика. При занесении логики в сущность как раз улучшается инкапсулция и тестируемость. Никто не сможет занести в сущность невалдиные для бизнес процессов данные. Вот старый доклад на эту тему https://ocramius.github.io/doctrine-best-practices/#/32

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

$data = $actionHandler->action($dto)
$user->setData($data); // Взяли да забыли вызвать хэндлер и просто составили невалидный $data

vs

$user->action($actionDto)
Ну или если есть зависимости или сложная для одной сущности логика, то можно какой то сервис и передать.
$user->action($actionDto, $actionServiceHelper)

На нейминг не смотреть)

Ни разу не видел удачных реализаций ричмоделей. И первые примеры вызывают сомнения:

  1. Раскидывать свойства энтити по трейтам, кмк, усложняет проект, а не упрощает, нет единого представления сущности. + Есть красивые пакеты для created_at и updated_at.

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

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

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

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

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

Sign up to leave a comment.

Articles