Как не нужно использовать паттерн Repository

image

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

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

Введение


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

Паттерн Repository


Буквально в нескольких словах предлагаю рассмотреть, что-же такое 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
  • Тесты, было бы неплохо.

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

  • 14 декабря 2016 в 13:00

    0

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

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

    • 14 декабря 2016 в 13:34

      +1

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

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

  • 14 декабря 2016 в 13:09

    0

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

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

    • 14 декабря 2016 в 13:39

      +2

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

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

  • 14 декабря 2016 в 13:20

    0

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

© Habrahabr.ru