Как стать автором
Обновить

Комментарии 28

Раз уж мы легким движением руки пришли к CQRS, предлагаю немного продолжить рассуждения на эту тему.


Для меня основной смысл в разделении на команды и запросы с точки зрения работы с базой такой:


1) Write model у нас для некоей сущности Foo одна, которая Aggregate Root в терминах DDD. Это "толстая" модель, содержащая методы бизнес-логики. Типичная команда выглядит как-то так (во избежание холивора опустим вопрос способа работы с domain events):


$foo = $fooRepository->findById($fooId);
$foo->doSomethingUseful();
$fooRepository->persist($foo);

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


2) Read models для ровно того же "foo" у нас от 1 до N, причем более одной запросто может быть даже в одном bounded context-е. А вот здесь нам нужен максимум Query Builder (и, возможно, пригодится примитивный односторонний маппер, умеющий только гидрацию — просто чтобы писать меньше букв, хотя это спорный вопрос, возможно, получится столько же букв в другом месте).


Выходит, и Active Record, и Data Mapper для Read models будут только мешать: нам на выходе все, что нужно, это банальная readonly-структура — и они для разных Read models сильно разные.


Возьмем для примера Хабр, чтобы далеко не ходить. Вот у нас список постов в ленте, а вот у нас список постов справа в блоке "что обсуждают". Что, для того, чтобы вывести количество комментариев, нам разве нужен aggregate root, в котором будет все комментарии? Нет, конечно, мы просто сделаем join, group by и count(), либо подзапрос (оставим в стороне вопрос денормализации). Так зачем нам тут вообще Eloquent model или Doctrine Entity, и куда мы там, простите, засунем comments_count и views_count? Вопрос риторический. А раз для read models они не нужны, то и Repository тут ни к чему, не так ли?

Посыл верный. На моем проекте по многим причинам мы перешли к POPO классам для read моделей. В статье же посыл немного иной — «если хочется поиграться с паттернами — то лучше так, а не бесполезный Репозиторий». Не стал статью перегружать ещё и этим. Возможно, зря.

Я как-то не уловил пользу от таких "репозиториев", которые и не репозитории вовсе, а непонятно что. Никакого принципиального отличия от Eloquent scopes я не вижу — просто зачем-то вынесли скоупы в отдельный класс, как будто это решит проблему нарушения SRP самим (анти)паттерном Active Record (не решит). Аргумент про кэширование вроде бы как бы валиден, но тут все сразу развалится на вопросе инвалидации в случаях чуть сложнее тривиальных.


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

Написал большое добавление к статье. Спасибо что подтолкнули.

По поводу дополнения.


Я не видел еще ни одного крупного проекта с использованием Active Record, который бы не превратился через пару лет в неподдерживаемое месиво, и скоупы с кэшами здесь наименьшая из проблем. Впрочем, и в проектах с Доктриной часто происходит то же самое. Корень проблемы тут я вижу не столько в Active Record vs. Data Mapper, сколько в порочности анемичных моделей как таковых.

Ну read-модели по определению анемичны. А write-модели… да. Сложно команду научить и заставить делать write-сущности без геттеров и сеттеров. У меня не получилось.

Я про write-модели, конечно же.


Без CQRS обойтись без геттеров и не получится, но у вас, вроде как, вон, Read-модели есть. Если и с CQRS не получается, то надо разобраться в том, что мешает.

С удовольствием бы прочел ваше видение применения read/write моделей. В частности как вы видите обновление сложных write сущностей
Спасибо за статью, было полезно, но есть вопрос.

Если мы говорим о SOLID, то принцип Single Responsibility мы нарушаем при использовании репозиториев вместе с Eloquent.
Ведь при изменении логики модели, нам придется поменять и модель и сам репозиторий ( queries). В итоге, при таком подходе, получаем код, который сложно поддерживать, так так при правке бд придется править и смотреть все в куче мест.
Как быть с этим?

Сам же подход Repository позволяет модели не заморачиваться о том, как ее и где сохраняют. А тут не совсем этого добивается автор статьи. По факту, получились чуть более навороченные scopes.
А какой профит вы получаете от использования такого «Repository» + ActiveRecord (паттерн + антипаттерн)?

Может ошибаюсь, но ваш solid от части не solid, во всех местах где вы используете ActiveRecord и тем более Eloquent реализацию ActiveRecord.
Отвечу сам себе.

Профита — нет.

Это просто проявление творчества над инструментом Laravel и Eloquent в частности. Усложнение восприятия кода, поскольку разработчику могут быть не очевидны мотивы такого решения.

Дублирование точек вызова save и delete, теперь его можно сделать $activeRecord->save() и $repository->save($activeRecord), при чем, repository->save объявлен в коде проекта. У кого то может появится желание его расширить…

Допустим такие репозитории имеют право на жизнь, только без save и delete. Тогда можно подумать, а зачем они в Laravel проекте? Есть же решение которое предоставляет сам Eloquent — это scopes, любому Laravel разработчику будет понятно как работают scopes а если нет, то он сможет почитать об этом в документации.

Извините если я вас как то задел. Может мне слишком накипело за всё время разработки на yii, laravel, symfony. Поддержки проектов после нескольких разработчиков, где первый — использовал свой придуманный Repository, второй — после ухода первого, счел это неуместным, и начал писать что попало и где попало. А ты смотришь на это все спустя какое то время и вспоминаешь их не злым тихим словом. Я и сам не раз пытался применить Repository в yii и laravel, но после знакомства с symfony и doctrine мне перехотелось это делать.

К желающим использовать Repository + ActiveRecord(Eloquent), есть маленькая просьба. Пожалуйста, выбирайте и используйте инструменты по назначению — ведь с проектом над которым вы работаете, будет работать такой же человек как вы, только с другими взглядами на эти инструменты.
Не задели. Написал большое добавление к статье. Может как-то обьяснит мою мысль :)
Профита — нет.

Репы, как минимум, позволяют хранить все выборки в одном месте, а не размазывать по проекту. И уже этого одного факта достаточно, чтобы их заюзать.
Вы б почитали коммент до конца, а то такое ощущение что вы только эту цитату прочли.
p.s. Держать запросы в одном месте можно и без Repository.
Признаться, первые два абзаца и последний.

Сейчас прочитал полностью, но комментарий всё равно считаю уместным. Скоупы захламляют модель. После POPO доктрины это особенно чувствуется.
С удовольствием прочитал бы пост о инвалидации кеша, особенно когда используется паттерн декоратор
Декоратор никак не связан с инвалидацией. Самое простое — генерить нормальные доменные эвенты(PostPublished, PostDeleted) и сбрасывать нужные кеши. Но это не всегда просто. Особенно если закешированы прям целые коллекции сущностей. Поэтому, вероятно лучше для например last posts хранить только id записей и потом делать multi-get к кешу.
Если при генерации кеша в декоратрое создается уникальние ключи per user? Потом где правильно сбрасивать эти ключи? На каком слое, в ивент листенерах? Ну не знаю…
Не зря говорят что инвалидации кеша это один из самых сложных задач для программиста
Про ключи per user ничего сказать не могу. Не делал такое. У меня ключ формировался довольно четко и эвент листенер и декоратор спокойно их использовали. Но нужно их генерацию вынести в отдельные классы. А то бывали у нас… несовпадения ключей тех, которые юзали декораторы и тех которые «инвалидировали»
Лично для себя я выбрал, возможно громоздкий, но наиболее правильный, на мой взгляд, способ: кеширующий декоратор должен работать не с самим кешом, а с его адаптером, заточенным под конкретную сущность/задачу. Задача инвалидации перекладывается либо на сам адаптер(удобно при активном кеше), либо вобще на отдельный класс, которыей знает про адаптер и декорируемый обьект (удобно при пассивном кеше).
НЛО прилетело и опубликовало эту надпись здесь
Почему предложенное решение называют аналогом скоупов? Из скоупов всё таки принято возвращать билдер, что позволяет их сцеплять и активно переиспользовать.

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

Как предлагается решать такую задачу без eloquent scopes?
Это не призыв отказаться от скоупов. Предложенные Queries и скоупы можно использовать вместе как раз для кейсов описанных вами. Эти Queries позволяют приложению абстрагиваться от метода доставания Eloquent сущностей. Как следствие, мы может организовать кеширование без изменения всего приложения, а только лишь добавлением новых реализаций интерфейсов и конфигурирования контейнера. Это сродни Open-Close принципу, но немного в другом масштабе.
Если не лезем в логику приложения, то она и не сломается каким-нибудь неудачным копипастом.
Статические скоупы — можем без проблем, что насчет динамических? Добавлять в методы Queries параметры?

Еще интересно, что насчет жадной загрузки, не используете?
Как предлагается решать такую задачу без eloquent scopes?

Criteria pattern

Можете продемонстрировать, как будет выглядеть комбинация criteria и предложенных в статье queries?

Если не отказываться от Eloquent, это не имеет смысла. Удобнее будет использовать scopes внутри queries.


Если отказываться и использовать, скажем, Doctrine, то, например, https://github.com/Happyr/Doctrine-Specification. В принципе, такое можно и для Eloquent сделать, но опять же не вижу смысла.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории