Pull to refresh

Comments 100

Получился ответ на вопрос «почему не надо использовать ActiveRecord». :)

Не, ну, в принципе, AR можно спрятать «под капот» — завести каждой entity по интерфейсу, где будут только методы бизнес-логики, и работать везде с этими интерфейсами, а AR-методы использовать только внутри entity (плюс всякие save в репозиториях, ну и, возможно, всякие setAttributes() в фабриках ввиду сложности с plain-object-style-конструкторами в AR). Тогда вроде бы всех этих проблем можно избежать. Остается только вопрос «зачем».
Ну тут холиварный вопрос. Я много раз убеждался, что многим технологиям и подходам есть место, главное понимать как их «готовить» и для чего они. Просто AR — это raw-разработка (чтобы было просто и легко).

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

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

Для «бложека» AR будет адекватным выбором, для «сделайте как вконтакте. только лучше...» скорее всего не подойдет.

Видите, я это проверил на своем горьком опыте.

А с чего то, что "бизнес-логика разбросана по всему проекту абы как", это следствие AR? У него другие проблемы. Распухание модели. Не всегда понятно с чем мы работаем в конкретный момент (экземпляр или билдер например). Нарушение принципа единой ответственности. Не ясное положение кода для смежных операций над несколькими моделями. И потому создаются билдеры (хранящие логику создания модели), призенторы, фильтры, трансформеры, "репозитории" (которые обычно для большенства есть хранилице именованных запросов) и т. д. и т. п. И всё это может легко и плавно внедряться с ростом проекта. Главное быть последовательным.

Это следствие не столько AR, сколько распространенной практики написания кода «по мануалам к фреймворкам». А при внедрении нормальных практик AR оказывается нужен, как собаке пятая нога.
На мой взгляд, использование AR предполагает, что модель сама знает, как себя сохранять. В таких условиях использование запросов не через интерфейс AR или внешних источников приведёт к появлению в системе объектов, не являющихся моделями AR. Т.е. появляются описанные вами проблемы и без применения Repository, так что не в нём дело.

Мне кажется, что оптимальный способ использования AR вместе с Repository — использование модели для описания поведения самой модели, а Repository — для описания именнованных запросов (т.е. поведения коллекции). Получается как бы Read-only Repository :) По сути, просто разделение классов для обеспечения SRP.
Тут скорее вся суть в том, что Repository подразумевает абстракцию, тоесть не важно модель, не модель, массив не массив, нужен единый формат возврата, чтобы можно было менять источники данных. Работая с моделью по большому счету вы привязываетесь к контексту АР, тоесть абстракция уже не совсем абстракция.

«Получается как бы Read-only Repository :) По сути, просто разделение классов для обеспечения SRP. » — да, согласен. И при этом это уже получается не совсем Repository, а скорее просто чуть-более удобное разделение.
Тут скорее вся суть в том, что Repository подразумевает абстракцию, тоесть не важно модель, не модель, массив не массив, нужен единый формат возврата

Мне кажется вы путаете Repository и ServiceLocator. Второй больше подходит для полиморфной подмены реализаций через биндинг на интерфейс, а первый это по сути есть сама коллекция с методами доступа к своим элементам, не более того. Я бы даже предложил выносить методы вида save и delete из репозиториев, ибо за это лучше пусть отвечают операционные сервисы или, на худой конец, EntityManager.
он говорит о том, что в репозитории важен единый интерфейс для подмены например из ServiceLocator. Путаницы нет.

Я бы даже предложил выносить методы вида save и delete из репозиториев, ибо за это лучше пусть отвечают операционные сервисы

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

он говорит о том, что в репозитории важен единый интерфейс для подмены например из ServiceLocator

Единый интерфейс для чего?

репозиторий — абстракция работы с хранилищем

Не согласен, репозиторий это абстракция коллекции. Для сохранения и удаления лучше применять EntityManager или сервисы.
Единый интерфейс для чего?

для репозитория. Интерфейс включает в себя набор методов, их сигнатуру, их ответ.

Не согласен, репозиторий это абстракция коллекции

ок, работа с хранилищем, представленным в виде коллекции.

Для сохранения и удаления лучше применять EntityManager или сервисы.

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

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

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

А вот внутри себя оно может использовать и EntityManager, и некий сервис, и http-клиент, и pdo-соединение, и работу с файловой системой. В этом суть репозиториев

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

у автора проблема в том, что он использовал AR-модели вместо сущностей, создавая «протечки» слоев.
А можно ссылочку на это ограничение Repository?
это не ограничение репозитория. Это то, для чего он вводится в доменную модель — единственный источник данных. Собственно любая книга по DDD вам про это говорит как ключевое понятие.

Для вас Repository это Фасад, но на мой взгляд уж очень загруженный.

для меня репозиторий — это доступ к хранилищу.
у автора проблема в том, что он использовал AR-модели вместо сущностей, создавая «протечки» слоев.

Ранее это был экземляр класс с ActiveRecord, однако теперь мой репозиторий мог возвращать массив или коллекцию

Таки нет, дело не в AR, а именно в нарушении семантики возвращаемых репозиторием моделей.
Это то, для чего он вводится в доменную модель — единственный источник данных

Именно источник. Должен ли источник включать логику модификации этой коллекции? Думаю что нет. Замечу, что я не резкий противник save/delete в Repository, я просто не вижу в них смысла, ибо методы будут по сути иметь вид:
Спойлер
public function save($entity){
  $this->getEntityManager()->save($entity);
}

public function delete($entity){
  $this->getEntityManager()->delete($entity);
}


Естественно при использовании нормальной ORM. Без таковой, save/delete в Repository более оправданное, но все же сомнительное решение.
для меня репозиторий — это доступ к хранилищу

Похоже у нас разное представление репозитория, для меня это всего лишь фассад над EntityManager и DQL.
Именно источник. Должен ли источник включать логику модификации этой коллекции?

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

а что в это страшного?
Похоже у нас разное представление репозитория, для меня это всего лишь фассад над EntityManager и DQL.

это обертка над чем угодно, что будет возвращать модели соответственно интерфейсу. Оберткой над EntityManager и DQL это будет только в случае использования доктрины, что не обязательно.
Меня осенило: кажется вы про репозитории в доктриновской парадигме, где они являются обертками в основном над различными файндерами. Это очень частный и не совсем классический вариант тех репозиториев, о которых речь в статье.
придираетесь к словам

Ну уж извините, важно слово — источник )
а что в это страшного?

При рефакторинге такого рода «тупые» методы обычно удаляются за ненадобностью. Страшного ничего нет, но зачем? Я поддерживаю идею «Repository как интерфейс коллекции Aggregate Root» и возможно в этом случае save и delete будут более объемными, но лично я еще не сталкивался с таким.

Меня осенило: кажется вы про репозитории в доктриновской парадигме, где они являются обертками в основном над различными файндерами. Это очень частный и не совсем классический вариант тех репозиториев, о которых речь в статье

Совершенно верно. Да, в классическом смысле репозиторий это смесь доктриновского с самим EntityManager. Да, у автора не совсем доктрина в статье.
Советую посмотреть Laracast по репозиториям и их декорации для гибкого использования.
Когда я начал копаться и разбираться во всем этом деле, я заметил, что мои «репозитории» возвращают модели. Да, все верно, мой якобы паттерн Repository возвращает все те-же модели, которые продолжают гулять по всему проекту.

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


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

" О том, какими должны быть хорошие модели, нужно говорить отдельно."
Скорее всего вы имеете ввиду Entity? Как я уже упоминал понятие «модель», в 99% подразумевается как класс. А это может быть доменная модель или как в DDD domain model и состоять из сотни классов.

Было-бы здорово, если бы Вы показали примеры тех «моделей», которые имеете ввиду.
В 99% процентов случаев репозиторий должен возвращать экземпляр класса либо коллекцию (массив) экземпляров класса.
Вообще, со словом «модель» часто возникают непонимания из-за разных контекстов. Класс реализует модель сущности из модели предметной области. А ещё некоторые библиотеки/фреймворки именуют базовый класс для сущности моделью, типа User extends Model, внося ещё больше путаницы. «В нашей реализации модели вашего бизнеса активно используется модель сущности „Пользователь“ на базе основного класса модели фреймворка X» — тут у каждого слова «модель» разное значение, потому что оно употребляется в разных контекстах.
Кстате, для расширения кругозора я покопал symfony3, там работа с данными мне понравилась куда больше. Однако там гоняются сущности, которые вообще не завязаны на фреймворк. Хоть кода там куда больше, но выходит весьма гибко и красивее с ориентиром на будущее.
потому что в симфони вообще нет этого слоя. Лишь принято юзать доктрину.
В хорошей гибкой архитектуре приложения Enterprise уровня уровень бизнес-логики вообще и её данных в частности не завязан на фреймворк и систему хранения. Для бизнес-логики её данные хранятся в каких-то абстрактных хранилищах, а сами данные (объекты) вообще не знают, что они хранятся где-то кроме оперативной памяти, для них самих они как были созданы через конструктор, так и существуют вечно со всеми своими бизнес-зависимостями, а зависимостей уровня фреймворка или системы хранения не имеют — все такие вещи выносятся из уровня бизнес-логики.
Довольно часто используют repository чтобы вынести из модели какие-то кастомные запросы и каждый раз не писать их через query builder. Например (Yii2) есть у нас запрос: User::find()->where(['active' => 1])->all() Если он используется больше чем в одном месте — начинают его пихать в: хелперы, компоненты, в саму модель. На мой взгляд, хоть это и не будет репозиторием в чистом его виде, лучше это все засунуть в UserRepository->getActive(), тем самым разгрузив модель. Название метода «getActive» поможет понять его смысл, если критерий «активности» изменится — правим только в одном месте ну и т.д.
Опять таки да, это удобно, возможно даже оправданно, но это не есть реализация самого паттерна Repository, так-как Вы все-равно завязаны на моделях.

Так-как если я поменяю источник данных для получения юзеров, например из соц. сети, и даже если умышленно сделаю активную запись из данных, то $user->save() уже будет работать не очевидно. А в проектах с большой кодовой базой и несколькими разработчиками, Вы просто не будете знать, кто и что, и куда «позасовывал».

А если у Вас еще нет тестов, то тут провал. Будете в ручную тестировать весь проект.
Ну в save это вопрос решаемый, через костыли, но решаемый =)
Как упоминал SamDark в своем докладе:
«Хорошая архитектура — это дорого, Плохая — еще дороже».
Опять таки да, это удобно, возможно даже оправданно, но это не есть реализация самого паттерна Repository, так-как Вы все-равно завязаны на моделях

Repository это и есть представление коллекции конкретной модели. И совершенно не важно, представлена ли у вас модель в виде Entity или в виде «сырых» данных (массив ObjectValue's), важно лишь то, что данный репозиторий представляет коллекцию данной модели.

Естественно замена одной модели с конкретной семантикой на другую (с иной семантикой) приведет к серьезным проблемам, и паттерн Repository не предназначен для решения этих проблем.

Поэтому в Yii 2 AR разделён на собственно Record и Query к нему. Свой Query легко подсунуть и туда, как раз, переезжают всякие методы scope-ы типа active(). Но можно и в репозиторий, да.

А что, если кастомный запрос заджоинит дополнительные поля, которых ранее не было или наоборот, уберет ненужные поля?
Или источник данных поменяется на стороннее апи и понятие «активная запись» будет не актуально?
А что, если кастомный запрос заджоинит дополнительные поля, которых ранее не было или наоборот, уберет ненужные поля?

Ваша модель должна иметь четкую структуру. Если в одном запросе возвращается n полей, а в другом m полей, то два эти запроса возвращают две разные модели.
Или источник данных поменяется на стороннее апи и понятие «активная запись» будет не актуально?

По хорошему ваша модель должна описывать бизнес-модель и бизнес-ограничения, а вся дополнительная логика (коей является «активная запись») должна либо выноситься в сервисы инфраструктуры, либо миксоваться в модель при необходимости.
Как должно быть, это одно. А как на самом деле — это совсем другое. Тоесть я имею ввиду сейчас понятие «модель» в контексте AR. Честно говоря, я еще не видел кода на Yii2 или Laravel5, которые бы использовали четкие разграничения или беспокоились об инкапсуляции данных.

И тут скорее я подвожу к проблеме того, что формат данных может меняться. С модели AR на массив или коллекцию. А вы по сути имеете ввиду сущность. Так как модель AR построена на «магии» (Магические методы в php).
Тоесть я имею ввиду сейчас понятие «модель» в контексте AR

А что особенного у моделей в контексте AR? Это те же строго описанные структуры, но дополненные логикой самосохранения и самоудаления, не более того. Вычлените из них эту логику, и вы вернетесь к Simple Object.

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

Формат данных хоть и может меняться, но это не меняет формата модели, да и AR никак не влияет на ее (модели) формат. Магические методы AR (если таковые и имеются) могут быть дополнены вполне конкретными getters для описания структуры конкретной модели.
UFO just landed and posted this here
UFO just landed and posted this here
Да, но это скорее вопросы к Doctrine.

Основной целью моей статьи было показать, что не всегда правильное понятие и особенно реализацию находят паттернам проектирования. И я это сам на себе и проверил.
UFO just landed and posted this here
Ну я так понял, лучше вообще еще один слой абстракции строить по верх доктриновских репозиториев, чтобы оставлять возможность и их подменить.
UFO just landed and posted this here
Доктриновские репозитории могут имплементить интерфейсы DDD репозитории
Доктриновские репозитории по сути только read обеспечивают

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

… Один из методов я хочу разобрать тут...

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

Есть ли кто, кто работал со второй маджентой?


Адекватна ли система моделей с репозиториями и регистрами?

В Entity, как правило, сосредотачивают логику, относящуюся только к данной сущности и её агрегатам. Часто получается, что там сосредоточено большая часть логики домена. А вот тупые геттеры-сеттеры часто не нужны, особенно сеттеры.
Почему ваш репозиторий не мог возвращать модели (полученные данные биндить на нужный класс)?
Скорее всего проблемы возникли из-за толстой модели (очень частое явления с AR), но это проблема не репозитория.
С старался в моделях держать только реляции и скоупы. Вопрос в том, что я понятия не имею как модели использовали другие разработчики. Проект писался около года, и пережил около 5 разработчиков. Что, где, куда, как и кто вызывал не ясно.
А что вы называете толстыми моделями? (Давайте забудем про AR, пусть будет POPO).

Мне прям сразу не нравится этот термин, так как он намекает на то, что anemic model — это хорошо.

Entity, который работает с другими entity или реализует то, что надо делать в сервисе.

Работать с другими Entity нормально, в частности это прямая ответственность Aggregate Root. В целом надо смотреть не просто на факт работы чего с чем-то, а насколько эта работа уместна именно в этом месте.
Если следовать DDD, то:

1) Entity, который работает с другими entities, называется aggregate root и ровно для того и нужен, это одно из ключевых понятий DDD и он практически всегда довольно толстый.
2) Domain service бывает нужен, но совершенно не в тех случаях, как это практикуется в Symfony «по мануалам», а тогда, когда какие-либо действия в домене не принадлежат какому-либо объекту домена.
Symfony+Doctrine «по мануалам» далеко не самый лучший пример DDD, хотя и выглядит таковым по терминологии. На их базе можно реализовать почти «идеальный» DDD, но структура проекта будет далекой от «мануально-коробочной». А в последней человека лишь поверхностно знакомого и с DDD, и с Symfony+Doctrine будут сбивать толку термины, имеющие хоть и близкое, но разное значение в этих контекстах, начиная от Entity, Repository, Event и до Application вообще (даже тут, в посте и комментах к нему есть эта путаница). В результате человек будет писать в «manual way» под Symfony+Doctrine, будучи искренне уверенным, что пишет по DDD, но бизнес-логика будет завязана на инфраструктуру, малой кровью нельзя будет ни сменить Symfony на, например, Zend, ни даже Doctrine ORM на Doctrine ODM, а то и MySQL на PostgreSQL или MS SQL.
Вы меня раскусили, мне нравятся anemic model:)

Пример толстой модели приводит автор поста: https://github.com/Bottelet/Flarepoint-crm/blob/develop/app/Repositories/User/UserRepository.php#L37

У модели же может быть метод save()?
В этом методе мы берем глобальный репозиторий настроек, делаем поиск по нему, тут же сохраняем изображение, напрямую через файловую систему (что делать когда нужно будет горизонтально масштабироваться? — правильно, все переписывать), тут же в сессию что -то пишем.

Предпочитаю делать все это в сервисном слое, а сессиям даже в сервисном слое не место.
С анемичной моделью весь модуль/бандл можно считать моделью из предметной области.

Можно уцепиться за изображения. У пользователя есть аватар, но хранилище фоток у нас удаленное. Все зависимости мы указываем явно (Долой глобальные фасады). Мне что, создавать пользователя сразу с коннектом к хранилищу фоток?

Какой должна быть правильная «богая модель» для этого кейса?
По DDD, методы entities соответствуют действиям бизнес-логики (а не инфраструктуры), потому метод save у модели может быть только тогда, когда некоторое «сохранение» является бизнес-действием (и это не имеет никакого отношения к персистенции).

Хранилище аватарок — это инфраструктурный слой, к domain model отношения не имеет. Конкретная реализация задается через конфигурацию DI. Заливка на хранилище (которое реализовано в инфраструктурном слое) делается либо по событию, порождаемому на уровне model, либо, в простых случаях, можно обойтись double-dispatch.
Вы правы, у модели конечно же должен быть метод create.

Но, кроме доктрины в сценарии «вызываем flush уже в контроллере», никто не вызывает сохранение в инфраструктурном слое отдельно от бизнес логики (все из-за использования инкрементальных ид в бд).

В доменный сервис создания пользователя (в его роли может выступать репозиторий) передаём при его инстанцировании (или вызове метода создания) конкретный сервис хранения файлов, реализующий абстрактный интерфейс хранилища файлов. Получаем изоляцию доменной логики от инфраструктурной и возможность малой кровью менять логику хранения и само хранилище. В собственно экземпляре класса пользователя храним идентификатор, полученный от файлового сервиса. Задача преобразования этого идентификатора в URL, контент или файловый дескриптор для предоставления клиенту решается вне слоя доменной логики. Хотя сам метод преобразования может находиться в классе пользователя, получая в качестве аргумента ссылку на сервис, типа
$user->getAvatarAsUrl($this->get('blob_storage'); 
Столкнулся с тем, что репозитории нарушают инкапсуляцию модулей довольно во многих проектах. Берут Entity из другого модуля и пользуются первичным ключом этой Entity, чтобы скормить менеджеру при создании других зависимых Entity. При этом интерфейс Entity торчит наружу, и мы вольны делать с ней что угодно.

Считаю это неправильным подходом.

Мое решение про закрытие репозитория через классы-интерфесы модуля, которые возвращают read-only Entity для сторонних модулей и не дают им повлиять на состояние сущностей других модулей было наглухо отвергнуто под предлогом «слишком сложно, да и не вижу ничего плохого в подходе с работой Entity из других модулей. Ты же не дурак, и не будешь их изменять в том месте».

Вот сижу и думаю, а какие еще могут быть подходы? Или это нормально, что модули друг с другом общаются через неявный use классов-репозиториев ($entityManager->getRepository(OtherModuleEntity::class))?
Нормально в общем и в целом, если говорить про модули, сущности и репозитории в контексте DDD. Read-only Entity имеют право на жизнь иногда, но в общем случае это уже не Entity, а ValueObject или вообще DTO. Ограничений на изменение Entity нет в общем случае, кроме одного исключения: агрегаты должны изменяться только через его корень, с другой стороны отдельный репозиторий для агрегатов чаще всего не нужен. Обычный состав модуля: класс сущности корня агрегата, интерфейс репозитория для этой сущности, классы сущностей агрегатов. Типичное для PHP нарушение инкапсуляции модуля — публичный интерфейс корня агрегата отдаёт ссылки на агрегаты напрямую, а их публичные интерфейсы позволяют изменять их состояние, минуя корень. Тут лучший инструмент, не усложняющий приложение, да, «ты же не дурак». Защита от дурака — либо возвращать какие-то read only ValueObject/DTO/массивы/примитивы, либо работать из корня c агрегатами с помощью рефлексии, дергая их непубличные методы/свойства, убрав из публичных все изменения состояния. Модификаторов типа friend, увы, в PHP нет.

А вообще, основной проблемой у вас я вижу протекание логики приложения, инфраструктурной логики на уровень бизнес-логики. Плохо не то, что модули доменного уровня общаются друг с другом через EntityManager (Doctrine как я понимаю?), а то, что они вообще знают о его существовании и его используют. Плохо то, что знают о первичном ключе.

Или мы вообще на разных языках говорим и понимаем термины «модуль», «сущность» и «репозиторий» совсем по разному. Например, для вас модуль — это бандл Symfony, а сущность и репозиторий — термины Doctrine.
Вопрос, по поводу ValueObject и DTO. Я не имел еще дело на практике с данными парадигмами, а скорее вскользь просматривал литературу.

Не совсем понимаю, даже по описанию, что такое ValueObject? И как использовать DTO совместно с реляциями.

Не могли бы Вы объяснить на примере про ValueObject, минуя стандартное объяснение из книг про объект Money?

А что касается DTO, как быть в случае если есть вот такие данные:
Товар (данные товара)
— реляция на переводы
— реляция на картинки
— реляция на переводы (seo: alt, title)
— реляция на свойства
— реляция на значения
— реляция на дополнительные особенности
— реляция на ассортимент

Тоесть тут получается для каждой реляционной сущности должен быть свой DTO и обработчик, который рекурсивно (или как-то по другому) все это дело завернет?

Грубо, VO — объекты, которые сравниваются между собой по значению всех их атрибутов. Пример — объект DateTime. Обычно вам всё равно один и тот же объект это для PHP или разные, если все части даты и времени равны. Ещё пример — статусы различных объектов — могут иметь несколько полей, могут иметь логику перехода из одного статуса в другой, но в рамках задачи вам не важно один и тот же объект или нет — сравнение идёт по значению.

DTO же вообще простейший объект может только с паблик свойствами, а то и вообще ассоциативный массив используется в качестве DTO в PHP. В сущности, являющейся корнем агрегата в геттерах возвращает не сущности агрегатов, а DTO на их основе, чтобы даже если их поменяет кто, то на модель это не повлияло.
Я бы еще добавил, что для VO не имеет значение их идентификация и перманентность. Другими словами можно смело заменить один VO на другой с тем же состоянием, и это ничего не должно изменить.
Да, доктрина, но без Symfony.

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

А вообще, основной проблемой у вас я вижу протекание логики приложения, инфраструктурной логики на уровень бизнес-логики. Плохо не то, что модули доменного уровня общаются друг с другом через EntityManager (Doctrine как я понимаю?), а то, что они вообще знают о его существовании и его используют. Плохо то, что знают о первичном ключе.


Про первичный ключ я грубо написал — работа ведется непосредственно с самой сущностью, но в большинстве случаев в конечном итоге из этой сущности берется только первичный ключ для записи в базу. Есть, конечно, исключения (например, денормализованные данные для поиска в эластике), и в этих исключениях по текущей схеме проектирования мы работаем с репозиториями очень многих сущностей, что меня и напрягает. Слишком много зависимостей между модулями. Мне не очень нравится, что само понятие «модульность» начинает терять свой смысл.
UFO just landed and posted this here
Как минимум не путать понятие идентификатора сущности в предметной области и первичного ключа в базе данных.

Они могут и совпадать, если это не вредит доменной модели.

Они могут совпадать физически, но логически это разные значения.
UFO just landed and posted this here
репозиторий с методом getById(id), где id будет первичным ключом БД

Не обязательно, вполне применим метод getByCode или getByFullName, где $code или $name + $surname будут первичным ключом в БД. Идентификация уровня модели не всегда == первичному ключу БД, но да, очень часто (а лучше — всегда) эти понятия описывают одно и то же.
UFO just landed and posted this here
Я говорю, что наряду с ним в обязательном порядке будет getById()

Не обязательно.
Первичного ключа БД вообще может не быть. Может быть уникальное поле, где хранится идентификатор объекта, но первичным ключом оно являться не будет. А может иметься первичный ключ в таблице, но на модель не маппиться.
Не пытайтесь играть с Repository в frameworks с ActiveRecord

Вы меня, конечно, извините, пожалуйста, но что за фреймворки с Active Record такие? Какая проблема в том, что бы не использовать Eloquent в Laravel, а юзать тот же Doctrine? Настравивается 5 минут. И точно так же работает в обратную стороно — можно использовать Eloquent без Laravel.

Тоесть Repository должен как принимать так и возвращать единый формат, для хранения данных. Как правило это Entity — класс с геттерами и сеттерами без логики

Что вообще за сущность с геттерами и сеттерами без логики? Как раз такой сущность быть не должна.

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

На самом деле это был яркий пример того, когда умных книг не прочитали, а просто краем уха где-то услышали. В любой умной книге будет черным по белому написано, что реализация таких методов, как create — никак не задача репозитория.
Какая проблема в том, что бы не использовать Eloquent в Laravel, а юзать тот же Doctrine?

Да, я об этом и говорю. То есть в статье упоминаю о том, что не нужно строить свои инфраструктуры и использовать Doctrine или что-то подобное. И опять таки, Вы должны четко понимать, нужна ли вас Doctrine или дополнительный слой абстракции.

Что вообще за сущность с геттерами и сеттерами без логики? Как раз такой сущность быть не должна.

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-objects.html#entity-object-graph-traversal

«На самом деле это был яркий пример того, когда умных книг не прочитали, а просто краем уха где-то услышали. В любой умной книге будет черным по белому написано, что реализация таких методов, как create — никак не задача репозитория. „

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


А я вам как раз и говорю, что это не так. Понимать что и зачем ты делаешь, конечно, нужно. Это очевидный факт и касается выбора абсолютно любого инструмента\технологии, начиная от языка программирования. Но рекомендовать не строить свои инфраструктуры(ШТА? С каких пор использование другой ORM, которая не идет в комплекте с фреймворком начало называться построением своей инфраструктуры?) и отказываться от использования Doctrine(пока что никаких аргементов в пользу отказа от использования я у вас не заметил) это, как минимум не очень правильно по отношению к читателям.

http://docs.doctrine-project.org/projects/doctrine-orm/en/latest/reference/working-with-objects.html#entity-object-graph-traversal

Вы наверное упустили тот момент, что в скинутом примере рассматривается вопрос обхода графа зависимостей сущности и там НИКАК не рассмотрен вопрос того, какие сущности должны быть в принципе. Я бы порекомендовал хотя бы ознакомиться сначала с лучшими практиками, прежде, чем раздавать советы. Начать можно отсюда:
https://ocramius.github.io/doctrine-best-practices/#/34
Это, кстати, применимо не только к доктрине.

Невнимательность, отсутствие практики и опыта делают свое дело.

Да. Часто это относится к каждому из нас, не так ли?
Что вообще за сущность с геттерами и сеттерами без логики? Как раз такой сущность быть не должна.

Так называемая анемичная модель. Которых да, рекомендуют избегать. Анемичная доменная модель
Я хотел это услышать от автора статьи, ибо у меня входе прочтения самой статьи сложилось стойкое ощущение, что ему можно оказать существенную помощь, задав такой вот вопрос.
Мы у себя в компании используем вот такой репозиторий: https://github.com/t4web/DomainInterface/blob/master/src/Infrastructure/RepositoryInterface.php

Вот, например, его реализация для ZF2: https://github.com/t4web/Infrastructure/blob/master/src/Repository.php

А вот, например, то как мы им пользуемся: https://github.com/t4web/Mail/blob/master/src/Listener/LogSending.php
Не до конца понимаю выводы поста. Особенно не использовать его с AR, когда как раз он и предлагает решение проблемы автора.
Repository — Martin Fowler
A system with a complex domain model often benefits from a layer, such as the one provided by Data Mapper (165), that isolates domain objects from details of the database access code. In such systems it can be worthwhile to build another layer of abstraction over the mapping layer where query construction code is concentrated.


Как я понимаю на выходе из репозитория и должны быть POxO объекты, что и решило бы проблему независимости от источника данных.
В последнем проекте использую Idiorm, и стараюсь не возвращать объекты сервиса доступа данных, лиюо Domain Model, либо ViewModel, пример метода:
public function GetVisitors()
	{
		$visitors  = \ORM::for_table( $this->table )->find_many();
		$arrResult = array();
		foreach ( $visitors as $visitor ) {
			$arrResult[] = new Visitor( $visitor->id,
				$visitor->incoming_by_code,
				$visitor->from,
				$visitor->visit_date,
				$visitor->region,
				$visitor->district );
		}

		return $arrResult;
	}
Статья задела больную тему.

Как уже говорилось, ActiveRecord, доставляет много проблем в проектах со средней и более обьемной предметной областью.
Фаулер еще в книге «Шаблоны корпоративных приложений» об этом говорил и советовал применять вместо ActiveRecod, к примеру, DataMapper.

Но я не соглашусть с выражением:
Не пытайтесь играть с Repository в frameworks с ActiveRecord.

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

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

Что же касается проблемы с изменением источника данных:
И вот произошел момент, когда мне действительно нужно было подменить реализацию. Я приехал в офис с улыбкой и с мыслями: «Как я все легко подменю, просто создам другой класс и поменяю строку при биндинге».

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

И касательно формата данных:
Да, благодаря интерфейсу я действительно смог легко подменить реализацию, однако формат возвращаемых данных изменился.

не имеет значения какой шаблон проектирования используется, будь то Repository или ActiveRecod — формат возвращаемых данных не должен меняться даже при изменении реализации. Ну а если формат возвращаемых данных нужно изменять, потому что таково новое требование к системе в целом, то мы имеем обычную задачу, а не проблему.

Давайте разберемся на примере Yii2. Допустим мы получаем ActiveRecod в виде обьектов и массивов. Собственно в коде у нас это может выглядеть так:
    /**
    * наша бизнес логика. В функцию мы передаем обьект, который ответственный за поиск пользователей. 
    */
    function doStuffWithUser($userFinder) {
        $user = $userFinder->one(123); // получаем обьект пользователя
        // do some stuff
        $user = $userFinder->asArray()->one(123); // получам массив аттрибутов
    }


    // потом, допустим в контроллере, мы сделаем следующее
    $userFinder = User::find();
    doStuffWithUser($userFinder);

И, естественно, код, который использует переменную "$user" знает какого она типа — обьект или массив.

Допустим, в один прекрасный день мы решим, что пользователей нам нужно хранить в файлах.
Для реализации такого подхода имеется официальное расширение https://github.com/yii2tech/filedb
Собственно нам нужно будет только изменить базовый класс для модели «User».
И, в данном случае, замена источника данных ни коим образом не повлияет на логику метода «doStuffWithUser» так как ActiveRecod, который работает с файловой базой данных так же будет возвращать обьекты или массивы той же структуры что и ActiveRecod работающий с базой данных MySQL/PostgreSQL и т.п.

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

$user = $userFinder->asArray()->one(123)


Если это не всевдо-код, то если пользователь подтянет парочку реляций за собой, особенно вложенных, то мы на массивах далеко не уедем. Поэтому нужны сущности. А там еще нужно обработчики для этих сущностей и пошло поехало, а сути уже есть Doctrine.

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

Тоесть Вы по сути пишите свою мини доктрину для Yii2?
Не увидел у вас вложенностей.
Когда у пользователя есть реляция скажем на профиль и на 10 последних почтов, и еще на настройки к примеру.

Это нужно будет самостоятельно все обходить?
Как раз о проблемах своей неправильной реализации я и рассказывал.

Я не спорю, Вы рассказали о своих проблема и донесли до читателей опыт полученый методом проб и ошибок, что есть хорошо так как кому-то это может сохранить много нервов.
В статье Вы аргументировали многое примерами реализаций, ссылками на другие статьи и собственным опытом, но мне иенно не очень понравился пункт в выводах:
Не пытайтесь играть с Repository в frameworks с ActiveRecord. Повторюсь: практически всегда это будет избыточно, за исключением тех вариантов, когда Вы действительно знаете, что делаете и отдаете себе полный отчет о последствиях.

Просто некоторые люди могут воспринять такой совет слишком радикально и далее работать по принципу «Я на Хабре прочитал, что играть с Repository в frameworks с ActiveRecord не стоит и поэтому буду дулать все по старинке». Я это из личного опыта говорю, т.к. в начале своей карьеры по неопытности допускал такие ошибки слушая подобные радикальные советы от более опытных коллег.

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

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

Это вполне себе рабочий код — типичный вариант получения данных.

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

Вы говорили:
Да, благодаря интерфейсу я действительно смог легко подменить реализацию, однако формат возвращаемых данных изменился. Ранее это был экземляр класс с ActiveRecord, однако теперь мой репозиторий мог возвращать массив или коллекцию.


Так вот, я хотел сказать, что от изменение хранилища или его реализации не должен меняться формат возвращаемых данных. И в примере я хотел показать, что от смены типа хранилища логика не должна обязательно страдать.
Наверно стоит чуть изменить пример, чтобы изменения были видны более явно
Изначальный вариант кода у нас будет:
/**
    * наша бизнес логика. В функцию мы передаем обьект, который ответственный за поиск пользователей. 
    */
    function doStuffWithUser($userFinder) {
        $user = $userFinder->one(123); // получаем обьект пользователя
        // do some stuff
        $user = $userFinder->asArray()->one(123); // получам массив аттрибутов
    }


    // потом, допустим в контроллере, мы сделаем следующее
    function actionIndex () {
        $userFinder = User::find();
        doStuffWithUser($userFinder);
    }



После изменения хранилища с базы данных на файловую систему нам должно было быть достаточно сделать изменение только в контроллере(для конкретно этого примера):
    function actionIndex () {
        $userFinder = UserFromFile::find();
        doStuffWithUser($userFinder);
    }

User и UserFromFile у нас в данном случае выступают двумя реализациями репозитория(только не вдавайтесь в то, что мы меняем в коде User на UserFromFile, я привел эти имена для большей понятности того, что поменялось, а на практике $userFinder должен был быть получен из контейнера по интерфейсу, к примеру UserFinderInterface).
Это просто пример, чтобы показать что формат данных не должен меняться вне зависимости от реализации репозитория.
Тоесть Ваш новый репозиторий должен был возвращать ту же ActiveRecord и все было бы хорошо.
А описанная проблема это больше неправильная реализация поставленной задачи так как если мы меняем результат, который возвращает метод то должны моменять код во всех местах, гда этот метод используется и тут уже не имеет значения какой шаблон проектирования используется.

Надеюсь теперь будет понятнее=)

Тоесть Вы по сути пишите свою мини доктрину для Yii2?

Нет — ни в коем случае. В Yii2 уже есть ORM и вокруг ActiveRecord много чего центрировано. Я разрабатываю библиотеку, которая будет использовать уже сущесутвующий слой доступа к данным для реализации сущностей и репозиториев.

А там еще нужно обработчики для этих сущностей и пошло поехало, а сути уже есть Doctrine.

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

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

Так что в:
Вы действительно знаете, что делаете и отдаете себе полный отчет о последствиях.

я с вами абсолютно согласен.

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

То есть у вас будут отдельно сущности домена, которые будут маппиться на объекты ActiveRecord, в которых не будет никакой логики кроме save/delete/…?

Даже без логики AR довольно удобен.

Какой-нибудь Row Data Gateway не лучше будет? Или смысл просто взять готовую абстракцию от SQL и ничего лучше не нашлось?
Какой-нибудь Row Data Gateway не лучше будет?

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


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

Моя основная идея — максимально использовать уже имеющуяся функциональность дабы не изобретать велосипеды.
К примеру в YII2 для ActiveRecord имеются такие поведения как ActiveRecord Role и ActiveRecord Variation, которые можно использовать для более гибкого построения модели предметной области.

К примеру: мы можем на уровне базы иметь таблицы «user» и «user_profile» для хранения основной (ник, почта и т.п.) информации о пользователе и его профиль (полное имя, день рождения и т.п.).
Сущность User в предметной области включает данные из обеих таблиц и для сущности не имеет значения как эти данные хранятся и что они разделены на несколько таблиц для обеспечения более оптимальной работы системы, построения отчетов и т.п.
С помощью «ActiveRecord Role» мы можкм отразить обе таблицы на одну сущность User средствами ActiveRecord и получить свою сущность предметной области абстрагированную от источника данных.
Какой-нибудь Row Data Gateway не лучше будет?

Одинаково.


Или смысл просто взять готовую абстракцию от SQL и ничего лучше не нашлось?

Да.

Даже без логики AR довольно удобен.


Вы абсолютно правы, но там где в одном случае все удобно, в другом это удобство может быть надгробием.
Я это говорю из собственного опыта — уже не один проект встречал, где все из «AR довольно удобен.» перерастало в «все пропало Михалыч».
В основном это происходило из-за увеличения сложности предметной области, а из-за подходов заложенных при первоначальном использовании AR уже довольно сложно было что-то менять.

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

P.S. Надеюсь я правильно понял о чем Вы, а то не до конца понятно к какому именно комментарию Ваш ответ.

Так я не говорю, что надо в AR напихивать доменную логику. Я про то, что при реализации репозитория нет разницы, как именно внутри него всё работает.

Я про то, что при реализации репозитория нет разницы, как именно внутри него всё работает.

Извините, я Вас неправильно понял. В этом я с Вами абсолютно согласен и собственно сам об этом и говорил=)
То есть у вас будут отдельно сущности домена, которые будут маппиться на объекты ActiveRecord, в которых не будет никакой логики кроме save/delete/…?

Не совсем, но от части да. Библиотека сейчас не в открытом доступе, поэтому в одном комментарии не получится нормально все описать.
Общие концепции таковы:
  • Разбить модель предметной области на ускоспециализированные классы: сущность, репозиторий, маппер данных(по сути расширение ActiveRecord — не хочу вдаваться в реализацию) и ряд вспомогательных классов средствами которых все будет взаимодействовать между собой
  • Сущность не должна ничего знать о БД — ее область ответственности это бизнесс логика
  • Репозиторий знает как получать данные и сохранять их но абстрагирован от источника данных
  • ActiveRecord используется только как источник данных и остальные классы не завязаны на него намертво


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

Понятно, спасибо. Сам делаю подобное, но на базе Doctrine. Задача минимум — менять какой-нибудь DoctrineOrmUserRepository на DoctrineOdmUserRepository без изменения их клиентов, использующих AbstractUserRepository или UserRepositoryInterface.
Sign up to leave a comment.

Articles