image

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

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

Введение


Сразу хочу предупредить, что статья скорее ориентирована на разработчиков, которые только знакомятся с паттернами проектирования, читают умные книжки, а потом пытаются все это дело применить, так сказать, в «продакшине». Весьма в тему будет упомянуть разработку с помощью frameworks, которые используют ActiveRecord (Например Yii, Laravel и др.), ведь именно благодаря ActiveRecord я продолжаю наступать на грабли и учиться решать различные проблемы.

Паттерн Repository


Буквально в нескольких словах предлагаю рассмотреть, что-же такое Repository.
Репозиторий представляет собой концепцию хранения коллекции для сущностей определенного типа.

Подробнее об этом паттерне можно прочитать:


В общем, информации достаточно много и понять, что такое Repository, достаточно «легко».

Старт с Repository


Если Вы разрабатывали средние и/или большие (не в плане нагрузки, а скорее с большой кодовой базой и длительной поддержкой) проекты, то скорее всего сталкивались с недостатками и проблемами, которые возникают при использовании ActiveRecord. Основные можно выделить в небольшой список:

  • Нарушение единой ответственности.
  • Из первого пункта следует, что Ваши «модели» могут быть весьма «жирными».
  • У новичков формируется неправильное понятие MVC, где M понимают как модель и это == 1 класс, преимущественно ActiveRecord.
  • Весьма ресурсозатратная организация импорта/экспорта данных, если нужно по какой-то причине работать с большим кол-во записей за раз.
  • Неудобно, а иногда и не реально писать кастомные запросы на SQL в случае необходимости.

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

Так вот, за несколько лет работы с frameworks в основе которых лежит ActiveRecord, я сталкивался скорее-всего со всеми его недостатками. И как-то начитавшись умных книжек и статей, при проектировании архитектуры нового проекта, я решил внедрить паттерн Repository.

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

Пошел поменял статус на Systems Architect
image

А действительно ли Ваш «репозиторий» это Repository ?


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

Однако задача стояла, подменить выборку в первом случае из файла, а в другом из стороннего API. Когда я начал копаться и разбираться во всем этом деле, я заметил, что мои «репозитории» возвращают модели. Да, все верно, мой якобы паттерн Repository возвращает все те-же модели, которые продолжают гулять по всему проекту.

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

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

Без паники, тесты


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

Что мы имеем?


Итак, у нас вышел дополнительный слой абстракции, который требует большего порога входа и больше времени на разработку с КПД стремящимся в 0, так-как модели как гуляли по проекту, так и продолжают гулять.

Пошел поменял статус на Junior Assistant

Более детально разбираемся в проблеме


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

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

Перерывая информацию по теме, я нашел вот такое приложение на Laravel:

https://github.com/Bottelet/Flarepoint-crm/

Давайте посмотрим на пример UserRepository:

https://github.com/Bottelet/Flarepoint-crm/blob/develop/app/Repositories/User/UserRepository.php

Один из методов я хочу разобрать тут (на случай, если все это пропадет):

...
    public function create($requestData)
    {
        $settings = Settings::first();
        $password =  bcrypt($requestData->password);
        $role = $requestData->roles;
        $department = $requestData->departments;
        $companyname = $settings->company;
        if ($requestData->hasFile('image_path')) {
            if (!is_dir(public_path(). '/images/'. $companyname)) {
                mkdir(public_path(). '/images/'. $companyname, 0777, true);
            }
            $settings = Settings::findOrFail(1);
            $file =  $requestData->file('image_path');
            $destinationPath = public_path(). '/images/'. $companyname;
            $filename = str_random(8) . '_' . $file->getClientOriginalName() ;
            $file->move($destinationPath, $filename);
            
            $input =  array_replace($requestData->all(), ['image_path'=>"$filename", 'password'=>"$password"]);
        } else {
            $input =  array_replace($requestData->all(), ['password'=>"$password"]);
        }
        $user = User::create($input);
        $user->roles()->attach($role);
        $user->department()->attach($department);
        $user->save();
        Session::flash('flash_message', 'User successfully added!'); //Snippet in Master.blade.php
        return $user;
    }
...

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

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

  • Далее, Repository — это абстрактное хранилище, поэтому он не может знать про Session, так-как может потребоваться сохранить что-то с помощью консольного вызова.

  • Опять таки, результатом отдается модель, которая бесконтрольно «гуляет» по приложению. Никто не защищает Вас от использования всей магии ActiveRecord.

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

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

Как использовать Repository правильно?


  • Ну во первых, Вы должны четко понимать зачем Вам нужен данный паттерн проектирования.

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

  • Далее, если вы используете frameworks с ActiveRecord наверное в 99% случаях Repository будут избыточны, так-как позиция самого ActiveRecord — это некая комбинация Repository/Entity/Presenter, а в случае с Yii2, так еще и фильтров и валидации. Соответственно, чтобы действительно правильно и производительно завернуть весь ActiveRecord в Repository, Вам потребуется построить внушительный слой абстракции и целую инфраструктуру.

  • Если все-же необходимо по какой-то причине подружить Yii, Laravel (или что-то подобное) с Repository, скорее-всего лучшим вариантом будет использовать Doctrine. Для Yii2 и Laravel5 расширения точно есть, значит кто-то все-же подобным занимается.

Реализация паттерна Repository или что-то типа того


Я нашел статью, в которой описывается реализация паттерна Repository для Laravel5 (скорее всего для Yii2 будет примерно то-же самое). Однако по моему личному мнению в ней скорее описывается структурированный подход к написанию запросов с помощью ActiveRecord. С одной стороны удобно, уменьшаются дубли кода, худеют модели и архитектура более изящна. С другой стороны Repository не совсем выполняют свою роль «абстрактного хранилища», так-как идет работа с моделями и полная привязка к ActiveRecord со всей его магией.

Опасность может быть в следующем: при смене источника данных (обратите внимание, не обязательно менять базу или framework, достаточно получить данные из друго-го ресурса, например из стороннего API или сделав сложный кастомный запрос с помощью query builder), если Вы работали с моделями, а новая реализация вернет массив или коллекцию, то скорее всего Вы не сможете гарантировать стабильную работу Вашего приложения. Так-как попросту Вы не знаете (если проект большой и пишется не только Вами), какие методы, аксессоры/мутаторы и прочие прелести моделей были использованы и где.

image

Выводы


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

  • Вы должны четко понимать зачем используете Repository, да и вообще любой паттерн проектирования. Не достаточно просто знать или понимать как его реализовать, куда важнее понимать для чего Вы хотите его использовать и действительно ли это необходимо.
  • Не практикуйте Ваши только-что полученные знания на новом коммерческом проекте. Потренируйтесь на кошках или «домашнем» проекте.
  • Не пытайтесь играть с Repository в frameworks с ActiveRecord. Повторюсь: практически всегда это будет избыточно, за исключением тех вариантов, когда Вы действительно знаете, что делаете и отдаете себе полный отчет о последствиях.
  • Расширяйте свой кругозор просматривая другие инструменты. Не будьте one-framework-developer
  • Тесты, было бы неплохо.
Поделиться с друзьями
-->

Комментарии (100)


  1. symbix
    14.12.2016 13:00

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

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


    1. nepster-web
      14.12.2016 13:34
      +2

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

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


      1. zelenin
        14.12.2016 17:11
        +1

        raw => RAD


        1. nepster-web
          14.12.2016 17:30

          Понял, благодарю.


      1. symbix
        14.12.2016 17:14
        +1

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

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


        1. nepster-web
          14.12.2016 17:30

          Да, все верно. Об этом я и говорю, что нужно знать что, зачем и где применять.

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

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


        1. ellrion
          14.12.2016 17:33
          +1

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


          1. symbix
            14.12.2016 17:58

            Это следствие не столько AR, сколько распространенной практики написания кода «по мануалам к фреймворкам». А при внедрении нормальных практик AR оказывается нужен, как собаке пятая нога.


  1. HedgeSky
    14.12.2016 13:09
    +1

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

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


    1. nepster-web
      14.12.2016 13:39
      +3

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

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


      1. Delphinum
        14.12.2016 23:02

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

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


        1. zelenin
          14.12.2016 23:30

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

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

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


          1. Delphinum
            15.12.2016 00:23

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

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

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

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


            1. zelenin
              15.12.2016 00:39
              +1

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

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

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

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

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

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


              1. veydlin
                26.03.2017 14:34

                Pullman PL-1016
                Какая разница, какой он, все равно модифицировать, за каркас более чем пойдет


                1. zelenin
                  15.12.2016 13:25

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

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

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

                  для меня репозиторий — это доступ к хранилищу.


                  1. Delphinum
                    15.12.2016 13:40

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

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

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

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


                    1. zelenin
                      15.12.2016 13:49

                      Именно источник. Должен ли источник включать логику модификации этой коллекции?

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

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

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


                      1. Delphinum
                        15.12.2016 13:54

                        придираетесь к словам

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

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

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

                        Совершенно верно. Да, в классическом смысле репозиторий это смесь доктриновского с самим EntityManager. Да, у автора не совсем доктрина в статье.


  1. Lubaev
    14.12.2016 13:20
    -1

    Советую посмотреть Laracast по репозиториям и их декорации для гибкого использования.


  1. FractalizeR
    14.12.2016 17:24
    +2

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

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


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


    1. nepster-web
      14.12.2016 17:35
      +1

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

      Было-бы здорово, если бы Вы показали примеры тех «моделей», которые имеете ввиду.


      1. VolCh
        14.12.2016 21:44
        +1

        В 99% процентов случаев репозиторий должен возвращать экземпляр класса либо коллекцию (массив) экземпляров класса.


      1. VolCh
        15.12.2016 09:24
        +1

        Вообще, со словом «модель» часто возникают непонимания из-за разных контекстов. Класс реализует модель сущности из модели предметной области. А ещё некоторые библиотеки/фреймворки именуют базовый класс для сущности моделью, типа User extends Model, внося ещё больше путаницы. «В нашей реализации модели вашего бизнеса активно используется модель сущности „Пользователь“ на базе основного класса модели фреймворка X» — тут у каждого слова «модель» разное значение, потому что оно употребляется в разных контекстах.


    1. nepster-web
      14.12.2016 17:42

      Кстате, для расширения кругозора я покопал symfony3, там работа с данными мне понравилась куда больше. Однако там гоняются сущности, которые вообще не завязаны на фреймворк. Хоть кода там куда больше, но выходит весьма гибко и красивее с ориентиром на будущее.


      1. zelenin
        14.12.2016 18:40
        +2

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


      1. VolCh
        15.12.2016 09:31
        +2

        В хорошей гибкой архитектуре приложения Enterprise уровня уровень бизнес-логики вообще и её данных в частности не завязан на фреймворк и систему хранения. Для бизнес-логики её данные хранятся в каких-то абстрактных хранилищах, а сами данные (объекты) вообще не знают, что они хранятся где-то кроме оперативной памяти, для них самих они как были созданы через конструктор, так и существуют вечно со всеми своими бизнес-зависимостями, а зависимостей уровня фреймворка или системы хранения не имеют — все такие вещи выносятся из уровня бизнес-логики.


  1. amylabs
    14.12.2016 17:35

    Довольно часто используют repository чтобы вынести из модели какие-то кастомные запросы и каждый раз не писать их через query builder. Например (Yii2) есть у нас запрос: User::find()->where(['active' => 1])->all() Если он используется больше чем в одном месте — начинают его пихать в: хелперы, компоненты, в саму модель. На мой взгляд, хоть это и не будет репозиторием в чистом его виде, лучше это все засунуть в UserRepository->getActive(), тем самым разгрузив модель. Название метода «getActive» поможет понять его смысл, если критерий «активности» изменится — правим только в одном месте ну и т.д.


    1. nepster-web
      14.12.2016 17:40

      Опять таки да, это удобно, возможно даже оправданно, но это не есть реализация самого паттерна Repository, так-как Вы все-равно завязаны на моделях.

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

      А если у Вас еще нет тестов, то тут провал. Будете в ручную тестировать весь проект.


      1. amylabs
        14.12.2016 17:42

        Ну в save это вопрос решаемый, через костыли, но решаемый =)


        1. nepster-web
          14.12.2016 17:44
          +1

          Как упоминал SamDark в своем докладе:
          «Хорошая архитектура — это дорого, Плохая — еще дороже».


      1. Delphinum
        14.12.2016 23:07
        +1

        Опять таки да, это удобно, возможно даже оправданно, но это не есть реализация самого паттерна Repository, так-как Вы все-равно завязаны на моделях

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

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


    1. SamDark
      15.12.2016 02:20

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


      1. ittakir
        26.03.2017 07:06
        -1

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


        1. Delphinum
          15.12.2016 12:27
          +1

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

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

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


          1. nepster-web
            15.12.2016 12:34

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

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


            1. Delphinum
              15.12.2016 12:40
              +1

              Тоесть я имею ввиду сейчас понятие «модель» в контексте AR

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

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

              Формат данных хоть и может меняться, но это не меняет формата модели, да и AR никак не влияет на ее (модели) формат. Магические методы AR (если таковые и имеются) могут быть дополнены вполне конкретными getters для описания структуры конкретной модели.


        1. SamDark
          15.12.2016 13:04

          Он не должен.


  1. ggrnd0
    14.12.2016 18:12
    +1

    ТС, по поводу https://github.com/Bottelet/Flarepoint-crm/blob/develop/app/Repositories/User/UserRepository.php#create


    Это ни коим образом не репозиторий, вызов $user->save() как бы намекает, что тут используется AR.
    А на заборе тоже много чего написано…
    Здесь всего лишь некоторый сервис принимающий $requestData.
    Использование bcrypt вполне обосновано, так как указанные данные скорее всего идут прямо из контроллера и сервис просто и не рассчитывался для работы в других условиях.


    Условно, репозиторий должен выглядеть так


    Крайне советую обратить внимание, что в репозитории после вызовов save/update не вызывается session.flush() или transaction.commit() — данные вызовы не являются частью ответственности репозитория!


    Так же, указанный репозиторий не лишен недостатков.
    в первую очередь метод findAll() может привести к OutOffMemoryException, но если честно так просто ее избежать не получится. Основной принцип — ограничивать максимальное число возвращаемых элементов. Часто используется число 1000 — на это влияет большое количество факторов...


    Так же примечательно отсутствие проверки прав пользователя на выполнение действий.
    Репозиторий — это легковесная оболочка абстрагирующая от конкретного способа хранения данных.
    Это все что он должен делать.
    И проверка прав пользователя в репозитии может сильно затормозить приложение.
    Проверяйте права пользователя до обращения к репозиторию и желательно кешируйте результат.


    Репозиторий не должен:


    1) мапить.
    Репозиторий работает только с Entity. Принимает только Entity и возвращает только Entity.
    Так же репа может принимать/возвращать коллекцию состоящую только из Entity, идентификаторы и конечно QueryBuilder (о нем далее).


    2) строить запросы. (Реальность такова, что не всегда можно следовать этому требованию).
    Если вам необходимо на основе некоторых параметров получить данные из БД, то вы либо передаете в метод уже готовый запрос, который необходимо выполнить, либо передаете QueryBuilder, который сможет этот запрос построить.
    Проще всего это реализуется с использованием IQueryable в C#. (если кто-то знает вариант лучше — пишите)


    Конечно, с одной стороны методы getByName() и getByCity() выглядят достаточно неплохо.
    Но использование QueryBuilder избавляет вас от написания тысяч методов фильтрации на каждый чих.


    3) фиксация изменений.
    Никаких session.flush() или context.SaveChanges() в методах save/update/etc!
    Репозиторий не знает и знать не может сколько запросов ему придется выполнить в рамках одной сессии.
    Самый просто вариант, предоставить в репе метод Commit() при вызове которого изменения будут зафиксированы.


    4) транзакции — аналогично фиксации изменений.


    Так как между контроллером и репозиторием обычно (и это правильно) расположены еще несколько слоев бизнес-логики (как минимум 1 промежуточный слой так и вовсе необходим), то возможности вызвать repository.Commit() у сервиса или контроллера может и не быть, так как они не имеют понятия к каким репозиториям обращались нижестоящие слои.
    Поэтому фиксация изменений имеет смысл делать автоматической используя AOP или дургой доступный механизм.


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


    1. ggrnd0
      14.12.2016 18:18
      +1

      Так же добавлю.
      Паттерн подразумевает работу с объектами. В отношении к паттерну, их называют сущности — Entity.


      Сам паттерн не накладывает требования к типам используемым в качестве Entity.


      Судя по коментарию https://habrahabr.ru/post/316836/#comment_9964106 symfony3 как раз использует правильный репозиторий.


      1. nepster-web
        14.12.2016 18:39
        +1

        Да, но это скорее вопросы к Doctrine.

        Основной целью моей статьи было показать, что не всегда правильное понятие и особенно реализацию находят паттернам проектирования. И я это сам на себе и проверил.


      1. oxidmod
        14.12.2016 19:04

        Репозитории доктрины предоставляют лишь стандартный круд + произвольную фильтрацию. Но часто какието сложные запросы выносят в кастомные репозитории поверх доктриновских


        1. nepster-web
          14.12.2016 19:09

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


          1. oxidmod
            14.12.2016 19:17

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


          1. VolCh
            14.12.2016 21:13

            Доктриновские репозитории могут имплементить интерфейсы DDD репозитории


        1. VolCh
          14.12.2016 21:32

          Доктриновские репозитории по сути только read обеспечивают


  1. mnv
    14.12.2016 20:25
    +1

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


  1. greabock
    14.12.2016 21:19
    +1

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

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


  1. springimport
    14.12.2016 21:27
    -2

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


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


  1. VolCh
    14.12.2016 21:39

    В Entity, как правило, сосредотачивают логику, относящуюся только к данной сущности и её агрегатам. Часто получается, что там сосредоточено большая часть логики домена. А вот тупые геттеры-сеттеры часто не нужны, особенно сеттеры.


  1. Fantyk
    14.12.2016 23:54
    +1

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


    1. nepster-web
      15.12.2016 00:05
      +1

      С старался в моделях держать только реляции и скоупы. Вопрос в том, что я понятия не имею как модели использовали другие разработчики. Проект писался около года, и пережил около 5 разработчиков. Что, где, куда, как и кто вызывал не ясно.


    1. symbix
      15.12.2016 17:22

      А что вы называете толстыми моделями? (Давайте забудем про AR, пусть будет POPO).

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


      1. SamDark
        15.12.2016 17:26

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


        1. VolCh
          15.12.2016 17:35

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


        1. symbix
          15.12.2016 19:18
          +1

          Если следовать DDD, то:

          1) Entity, который работает с другими entities, называется aggregate root и ровно для того и нужен, это одно из ключевых понятий DDD и он практически всегда довольно толстый.
          2) Domain service бывает нужен, но совершенно не в тех случаях, как это практикуется в Symfony «по мануалам», а тогда, когда какие-либо действия в домене не принадлежат какому-либо объекту домена.


          1. VolCh
            16.12.2016 11:21
            +1

            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.


      1. Fantyk
        15.12.2016 18:06

        Вы меня раскусили, мне нравятся anemic model:)

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

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

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

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

        Какой должна быть правильная «богая модель» для этого кейса?


        1. symbix
          15.12.2016 19:23

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

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


          1. Fantyk
            16.12.2016 10:33

            Вы правы, у модели конечно же должен быть метод create.

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


        1. VolCh
          16.12.2016 11:41

          В доменный сервис создания пользователя (в его роли может выступать репозиторий) передаём при его инстанцировании (или вызове метода создания) конкретный сервис хранения файлов, реализующий абстрактный интерфейс хранилища файлов. Получаем изоляцию доменной логики от инфраструктурной и возможность малой кровью менять логику хранения и само хранилище. В собственно экземпляре класса пользователя храним идентификатор, полученный от файлового сервиса. Задача преобразования этого идентификатора в URL, контент или файловый дескриптор для предоставления клиенту решается вне слоя доменной логики. Хотя сам метод преобразования может находиться в классе пользователя, получая в качестве аргумента ссылку на сервис, типа

          $user->getAvatarAsUrl($this->get('blob_storage'); 
          


  1. Yago
    15.12.2016 00:02
    +1

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

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

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

    Вот сижу и думаю, а какие еще могут быть подходы? Или это нормально, что модули друг с другом общаются через неявный use классов-репозиториев ($entityManager->getRepository(OtherModuleEntity::class))?


    1. VolCh
      15.12.2016 09:04
      +1

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

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

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


      1. veydlin
        26.03.2017 01:48

        Я недавно видел робота-пылесоса за 2к, ещё тогда подумал купить и сделать его умней, но в квартире слишком много мест где он будет бесполезен и я передумал

        Может вам такой вариант будет предпочтительней? Уже готовый корпус, пылесборник и т.п. бери и модифицируй


        1. VolCh
          15.12.2016 12:20

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

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


          1. Delphinum
            15.12.2016 12:31

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


      1. Yago
        15.12.2016 11:25

        Да, доктрина, но без Symfony.

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

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


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


      1. ggrnd0
        15.12.2016 11:48

        Плохо то, что знают о первичном ключе.

        И как вы предлагаете идентифицировать объекты без первичного ключа?


        1. VolCh
          15.12.2016 12:08
          -1

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


          1. SamDark
            15.12.2016 13:08
            +1

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


            1. VolCh
              15.12.2016 17:13

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


              1. SamDark
                15.12.2016 17:26

                Именно.


          1. ggrnd0
            15.12.2016 13:15

            Так необходимость их различать возникает достаточно редко.


            И как бы то ни было, первичный ключ БД все равно будет присутствовать в модели.
            И у вас все равно будет базовый репозиторий с методом getById(id), где id будет первичным ключом БД…


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


            1. Delphinum
              15.12.2016 13:44
              -1

              репозиторий с методом getById(id), где id будет первичным ключом БД

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


              1. ggrnd0
                15.12.2016 16:42

                Я не говорю, что там не будет getByCode()


                Я говорю, что наряду с ним в обязательном порядке будет getById()


                1. roversochi
                  27.03.2017 10:37

                  Дорожку перерезал, подключил напрямую к ногам ардуины. Набросал скетч. Вроде крутит немного быстрее, но мощность существенно упала. Предполагаю, что надо все-таки подключить его через L293 с нормальным внешним питанием, а не через ULN2003?


            1. VolCh
              15.12.2016 17:16

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


  1. hlogeon
    15.12.2016 02:00
    +3

    Не пытайтесь играть с Repository в frameworks с ActiveRecord

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

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

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

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

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


    1. nepster-web
      15.12.2016 10:56
      -1

      Какая проблема в том, что бы не использовать 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 — никак не задача репозитория. „

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


      1. hlogeon
        15.12.2016 14:31

        Да, я об этом и говорю. То есть в статье упоминаю о том, что не нужно строить свои инфраструктуры и использовать 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
        Это, кстати, применимо не только к доктрине.

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

        Да. Часто это относится к каждому из нас, не так ли?


    1. darked
      15.12.2016 11:57
      +4

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

      Так называемая анемичная модель. Которых да, рекомендуют избегать. Анемичная доменная модель


      1. hlogeon
        15.12.2016 14:33
        +1

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


  1. boom
    15.12.2016 11: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


  1. darked
    15.12.2016 12:15

    Не до конца понимаю выводы поста. Особенно не использовать его с 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;
    	}
    


  1. prowwid
    15.12.2016 16:05
    +1

    Статья задела больную тему.

    Как уже говорилось, 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 и т.п.

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


    1. nepster-web
      15.12.2016 16:10

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

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


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

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

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


      1. SamDark
        15.12.2016 16:34

        Да чего там писать? Гидрация делается не так сложно: https://github.com/samdark/hydrator


        1. nepster-web
          15.12.2016 17:12

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

          Это нужно будет самостоятельно все обходить?


          1. SamDark
            15.12.2016 17:25

            Да.


      1. prowwid
        15.12.2016 17:36

        Как раз о проблемах своей неправильной реализации я и рассказывал.

        Я не спорю, Вы рассказали о своих проблема и донесли до читателей опыт полученый методом проб и ошибок, что есть хорошо так как кому-то это может сохранить много нервов.
        В статье Вы аргументировали многое примерами реализаций, ссылками на другие статьи и собственным опытом, но мне иенно не очень понравился пункт в выводах:
        Не пытайтесь играть с 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 врядли выйдет.


    1. VolCh
      15.12.2016 17:20

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

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


      1. SamDark
        15.12.2016 17:27

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


        1. VolCh
          15.12.2016 17:40

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


          1. prowwid
            15.12.2016 18:49
            +1

            Какой-нибудь Row Data Gateway не лучше будет?

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


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

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

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


          1. SamDark
            15.12.2016 23:01

            Какой-нибудь Row Data Gateway не лучше будет?

            Одинаково.


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

            Да.


        1. prowwid
          15.12.2016 18:37

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


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

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

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


          1. SamDark
            15.12.2016 23:03

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


            1. prowwid
              16.12.2016 11:55

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

              Извините, я Вас неправильно понял. В этом я с Вами абсолютно согласен и собственно сам об этом и говорил=)


      1. prowwid
        15.12.2016 18:30

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

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


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


        1. VolCh
          16.12.2016 11:48

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