Як не треба використовувати патерн Repository

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
  • Тести, було б непогано.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.