Comments 100
Не, ну, в принципе, AR можно спрятать «под капот» — завести каждой entity по интерфейсу, где будут только методы бизнес-логики, и работать везде с этими интерфейсами, а AR-методы использовать только внутри entity (плюс всякие save в репозиториях, ну и, возможно, всякие setAttributes() в фабриках ввиду сложности с plain-object-style-конструкторами в AR). Тогда вроде бы всех этих проблем можно избежать. Остается только вопрос «зачем».
Да возможно, но вы построите поверх солидный такой слой абстракции и вопрос «зачем» весьма актуален.
Хотя в целях эксперимента и обучения было бы интересно, но по факту в коммерческих проектах скорее-всего лучше так не делать.
В простых проектах я и сам использую AR, но в таких проектах использовать репозитории вообще нет абсолютно никакого смысла.
Для «бложека» AR будет адекватным выбором, для «сделайте как вконтакте. только лучше...» скорее всего не подойдет.
Видите, я это проверил на своем горьком опыте.
А с чего то, что "бизнес-логика разбросана по всему проекту абы как", это следствие AR? У него другие проблемы. Распухание модели. Не всегда понятно с чем мы работаем в конкретный момент (экземпляр или билдер например). Нарушение принципа единой ответственности. Не ясное положение кода для смежных операций над несколькими моделями. И потому создаются билдеры (хранящие логику создания модели), призенторы, фильтры, трансформеры, "репозитории" (которые обычно для большенства есть хранилице именованных запросов) и т. д. и т. п. И всё это может легко и плавно внедряться с ростом проекта. Главное быть последовательным.
Мне кажется, что оптимальный способ использования AR вместе с Repository — использование модели для описания поведения самой модели, а Repository — для описания именнованных запросов (т.е. поведения коллекции). Получается как бы Read-only Repository :) По сути, просто разделение классов для обеспечения SRP.
«Получается как бы Read-only Repository :) По сути, просто разделение классов для обеспечения SRP. » — да, согласен. И при этом это уже получается не совсем Repository, а скорее просто чуть-более удобное разделение.
Тут скорее вся суть в том, что Repository подразумевает абстракцию, тоесть не важно модель, не модель, массив не массив, нужен единый формат возврата
Мне кажется вы путаете Repository и ServiceLocator. Второй больше подходит для полиморфной подмены реализаций через биндинг на интерфейс, а первый это по сути есть сама коллекция с методами доступа к своим элементам, не более того. Я бы даже предложил выносить методы вида save и delete из репозиториев, ибо за это лучше пусть отвечают операционные сервисы или, на худой конец, EntityManager.
Я бы даже предложил выносить методы вида 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. Да, у автора не совсем доктрина в статье.
Когда я начал копаться и разбираться во всем этом деле, я заметил, что мои «репозитории» возвращают модели. Да, все верно, мой якобы паттерн Repository возвращает все те-же модели, которые продолжают гулять по всему проекту.
Я полагаю, нужно уточнить, что ничего плохого в том, что репозиторий возвращает модели нет. О том, какими должны быть хорошие модели, нужно говорить отдельно. Плохо то, что у вас используется ActiveRecord в качестве модели, который тесно связан с базой. Модели в нормальной реализации с хранилищем жестко не связаны и десериализировать их можно из любого набора данных.
Кстати, если в вашем случае вам необходимо было быстрое решение, можно было бы попробовать создавать модели Eloquent на основе данных через их обычный конструктор, передавая в массиве нужные атрибуты.
Скорее всего вы имеете ввиду Entity? Как я уже упоминал понятие «модель», в 99% подразумевается как класс. А это может быть доменная модель или как в DDD domain model и состоять из сотни классов.
Было-бы здорово, если бы Вы показали примеры тех «моделей», которые имеете ввиду.
Так-как если я поменяю источник данных для получения юзеров, например из соц. сети, и даже если умышленно сделаю активную запись из данных, то $user->save() уже будет работать не очевидно. А в проектах с большой кодовой базой и несколькими разработчиками, Вы просто не будете знать, кто и что, и куда «позасовывал».
А если у Вас еще нет тестов, то тут провал. Будете в ручную тестировать весь проект.
«Хорошая архитектура — это дорого, Плохая — еще дороже».
Опять таки да, это удобно, возможно даже оправданно, но это не есть реализация самого паттерна Repository, так-как Вы все-равно завязаны на моделях
Repository это и есть представление коллекции конкретной модели. И совершенно не важно, представлена ли у вас модель в виде Entity или в виде «сырых» данных (массив ObjectValue's), важно лишь то, что данный репозиторий представляет коллекцию данной модели.
Естественно замена одной модели с конкретной семантикой на другую (с иной семантикой) приведет к серьезным проблемам, и паттерн Repository не предназначен для решения этих проблем.
Поэтому в Yii 2 AR разделён на собственно Record и Query к нему. Свой Query легко подсунуть и туда, как раз, переезжают всякие методы scope-ы типа active()
. Но можно и в репозиторий, да.
Или источник данных поменяется на стороннее апи и понятие «активная запись» будет не актуально?
А что, если кастомный запрос заджоинит дополнительные поля, которых ранее не было или наоборот, уберет ненужные поля?
Ваша модель должна иметь четкую структуру. Если в одном запросе возвращается n полей, а в другом m полей, то два эти запроса возвращают две разные модели.
Или источник данных поменяется на стороннее апи и понятие «активная запись» будет не актуально?
По хорошему ваша модель должна описывать бизнес-модель и бизнес-ограничения, а вся дополнительная логика (коей является «активная запись») должна либо выноситься в сервисы инфраструктуры, либо миксоваться в модель при необходимости.
И тут скорее я подвожу к проблеме того, что формат данных может меняться. С модели AR на массив или коллекцию. А вы по сути имеете ввиду сущность. Так как модель AR построена на «магии» (Магические методы в php).
Тоесть я имею ввиду сейчас понятие «модель» в контексте AR
А что особенного у моделей в контексте AR? Это те же строго описанные структуры, но дополненные логикой самосохранения и самоудаления, не более того. Вычлените из них эту логику, и вы вернетесь к Simple Object.
И тут скорее я подвожу к проблеме того, что формат данных может меняться
Формат данных хоть и может меняться, но это не меняет формата модели, да и AR никак не влияет на ее (модели) формат. Магические методы AR (если таковые и имеются) могут быть дополнены вполне конкретными getters для описания структуры конкретной модели.
Он не должен.
Основной целью моей статьи было показать, что не всегда правильное понятие и особенно реализацию находят паттернам проектирования. И я это сам на себе и проверил.
Для репозитория можно воспользоваться этой реализацией. Но, как вы и пишете, с сущностями при этом стоит работать только через репозиторий. Вместо мутаторов на мой взгляд стоит использовать явные геттеры-сеттеры и при необходимости делать преобразование данных в них.
… Один из методов я хочу разобрать тут...
далее перечисляется куча семантических и архитектурных ошибок в методе… хотя достаточно взглянуть на название метода, чтобы понять, что его в репозитории вообще не должно быть. Это как если бы гардеробщица в театре пошивом шуб занималась.
Есть ли кто, кто работал со второй маджентой?
Адекватна ли система моделей с репозиториями и регистрами?
Скорее всего проблемы возникли из-за толстой модели (очень частое явления с AR), но это проблема не репозитория.
Мне прям сразу не нравится этот термин, так как он намекает на то, что anemic model — это хорошо.
Entity, который работает с другими entity или реализует то, что надо делать в сервисе.
1) Entity, который работает с другими entities, называется aggregate root и ровно для того и нужен, это одно из ключевых понятий DDD и он практически всегда довольно толстый.
2) Domain service бывает нужен, но совершенно не в тех случаях, как это практикуется в Symfony «по мануалам», а тогда, когда какие-либо действия в домене не принадлежат какому-либо объекту домена.
Пример толстой модели приводит автор поста: https://github.com/Bottelet/Flarepoint-crm/blob/develop/app/Repositories/User/UserRepository.php#L37
У модели же может быть метод save()?
В этом методе мы берем глобальный репозиторий настроек, делаем поиск по нему, тут же сохраняем изображение, напрямую через файловую систему (что делать когда нужно будет горизонтально масштабироваться? — правильно, все переписывать), тут же в сессию что -то пишем.
Предпочитаю делать все это в сервисном слое, а сессиям даже в сервисном слое не место.
С анемичной моделью весь модуль/бандл можно считать моделью из предметной области.
Можно уцепиться за изображения. У пользователя есть аватар, но хранилище фоток у нас удаленное. Все зависимости мы указываем явно (Долой глобальные фасады). Мне что, создавать пользователя сразу с коннектом к хранилищу фоток?
Какой должна быть правильная «богая модель» для этого кейса?
Хранилище аватарок — это инфраструктурный слой, к domain model отношения не имеет. Конкретная реализация задается через конфигурацию DI. Заливка на хранилище (которое реализовано в инфраструктурном слое) делается либо по событию, порождаемому на уровне model, либо, в простых случаях, можно обойтись double-dispatch.
$user->getAvatarAsUrl($this->get('blob_storage');
Считаю это неправильным подходом.
Мое решение про закрытие репозитория через классы-интерфесы модуля, которые возвращают read-only Entity для сторонних модулей и не дают им повлиять на состояние сущностей других модулей было наглухо отвергнуто под предлогом «слишком сложно, да и не вижу ничего плохого в подходе с работой Entity из других модулей. Ты же не дурак, и не будешь их изменять в том месте».
Вот сижу и думаю, а какие еще могут быть подходы? Или это нормально, что модули друг с другом общаются через неявный use классов-репозиториев ($entityManager->getRepository(OtherModuleEntity::class))?
А вообще, основной проблемой у вас я вижу протекание логики приложения, инфраструктурной логики на уровень бизнес-логики. Плохо не то, что модули доменного уровня общаются друг с другом через EntityManager (Doctrine как я понимаю?), а то, что они вообще знают о его существовании и его используют. Плохо то, что знают о первичном ключе.
Или мы вообще на разных языках говорим и понимаем термины «модуль», «сущность» и «репозиторий» совсем по разному. Например, для вас модуль — это бандл Symfony, а сущность и репозиторий — термины Doctrine.
Не совсем понимаю, даже по описанию, что такое ValueObject? И как использовать DTO совместно с реляциями.
Не могли бы Вы объяснить на примере про ValueObject, минуя стандартное объяснение из книг про объект Money?
А что касается DTO, как быть в случае если есть вот такие данные:
Товар (данные товара)
— реляция на переводы
— реляция на картинки
— реляция на переводы (seo: alt, title)
— реляция на свойства
— реляция на значения
— реляция на дополнительные особенности
— реляция на ассортимент
Тоесть тут получается для каждой реляционной сущности должен быть свой DTO и обработчик, который рекурсивно (или как-то по другому) все это дело завернет?
DTO же вообще простейший объект может только с паблик свойствами, а то и вообще ассоциативный массив используется в качестве DTO в PHP. В сущности, являющейся корнем агрегата в геттерах возвращает не сущности агрегатов, а DTO на их основе, чтобы даже если их поменяет кто, то на модель это не повлияло.
Просто в голове не укладывается подобное допущение, и думаю, что при росте проекта и команды необходимо ограничивать интерфейс как можно жестче, чтобы мы могли заранее избежать многих казусов, даже не допуская трату времени на ошибки из разряда «мне класс позволяет это сделать, так почему бы и нет?».
А вообще, основной проблемой у вас я вижу протекание логики приложения, инфраструктурной логики на уровень бизнес-логики. Плохо не то, что модули доменного уровня общаются друг с другом через EntityManager (Doctrine как я понимаю?), а то, что они вообще знают о его существовании и его используют. Плохо то, что знают о первичном ключе.
Про первичный ключ я грубо написал — работа ведется непосредственно с самой сущностью, но в большинстве случаев в конечном итоге из этой сущности берется только первичный ключ для записи в базу. Есть, конечно, исключения (например, денормализованные данные для поиска в эластике), и в этих исключениях по текущей схеме проектирования мы работаем с репозиториями очень многих сущностей, что меня и напрягает. Слишком много зависимостей между модулями. Мне не очень нравится, что само понятие «модульность» начинает терять свой смысл.
Они могут и совпадать, если это не вредит доменной модели.
репозиторий с методом getById(id), где id будет первичным ключом БД
Не обязательно, вполне применим метод getByCode или getByFullName, где $code или $name + $surname будут первичным ключом в БД. Идентификация уровня модели не всегда == первичному ключу БД, но да, очень часто (а лучше — всегда) эти понятия описывают одно и то же.
Не пытайтесь играть с 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
Это, кстати, применимо не только к доктрине.
Невнимательность, отсутствие практики и опыта делают свое дело.
Да. Часто это относится к каждому из нас, не так ли?
Что вообще за сущность с геттерами и сеттерами без логики? Как раз такой сущность быть не должна.
Так называемая анемичная модель. Которых да, рекомендуют избегать. Анемичная доменная модель
Вот, например, его реализация для ZF2: https://github.com/t4web/Infrastructure/blob/master/src/Repository.php
А вот, например, то как мы им пользуемся: https://github.com/t4web/Mail/blob/master/src/Listener/LogSending.php
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?
Да чего там писать? Гидрация делается не так сложно: https://github.com/samdark/hydrator
Как раз о проблемах своей неправильной реализации я и рассказывал.
Я не спорю, Вы рассказали о своих проблема и донесли до читателей опыт полученый методом проб и ошибок, что есть хорошо так как кому-то это может сохранить много нервов.
В статье Вы аргументировали многое примерами реализаций, ссылками на другие статьи и собственным опытом, но мне иенно не очень понравился пункт в выводах:
Не пытайтесь играть с 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 не лучше будет?
В данном случае, как я уже говорил:
Я разрабатываю библиотеку, которая будет использовать уже сущесутвующий слой доступа к данным для реализации сущностей и репозиториев.
Делать свой собственный слой для получения данных под конкретный фраемворк, у которого этот слой уже реализован — это довольно сложная задача и врядли стоит тратить на это силы.
Моя основная идея — максимально использовать уже имеющуяся функциональность дабы не изобретать велосипеды.
К примеру в 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 используется только как источник данных и остальные классы не завязаны на него намертво
Это описание довольно упрощенное, но без ссылки на гитхаб с кодом, чтобы все можно было посмотреть и увидеть на примерах врядли у меня получится вкратце обьяснить достаточно хорошо, а пока что, извините, в открытом доступе библиотеку предоставить не могу.
Как не нужно использовать паттерн Repository