Шарувата архітектура для Yii додатків
Товсті контролери та моделі – неминуча проблема всіх середніх та великих проектів, заснованих на MVC-фреймворках таких як Yii та Laravel. Головна причина виникнення товстих контролерів та моделей це “Active Record” – потужний та важливий компонент таких фреймворків.
Проблема: Active Records порушує принцип єдиної відповідальності (Single Responsibility Principle)
"Active Record" - архітектурний патерн, підхід до проектування доступу до даних у БД. Він був описаний Мартіном Фаулером у 2003 році в книзі “Patterns of Enterprise Application Architecture” і широко застосовується у PHP-фреймворках.
Незважаючи на всю потужність Active Record (AR), цей патерн порушує принцип єдиної відповідальності (SRP), тому що AR-модель:
- Працює із запитами та збереженням даних
- Занадто багато знає про інші моделі, що існують в системі (через стосунки)
- Занадто часто безпосередньо впливає на бізнес-логіку програми (оскільки конкретна імплементація зберігання даних тісно пов'язана з бізнес-логікою програми)
Таке порушення SRP зручне для швидкої розробки, коли потрібно створити прототип програми якнайшвидше, але вкрай шкідливо для додатків, що розрослися. Божественні моделі та товсті контролери складно тестувати та підтримувати. Використання моделей усюди в коді веде до величезної кількості проблем, коли потрібно щось змінити в структурі БД (у будь-якому проекті зміни неминучі).
Вирішення проблеми просте: поділити відповідальність на кілька шарів з використанням ін'єкцій залежності. Такий підхід також полегшує тестування, тому що дозволяє мокати ті шари, які в цьому тесті не тестуються.
Рішення: Шарувата архітектура для PHP MVC Frameworks
Для реалізаціїшаруватої архітектури нам знадобиться 'dependency injection container' (DIC) — об'єкт, який знає як створювати та конфігурувати об'єкти. У Yii та Laravel всю магію беруть на себе фреймворки і немає потреби самому створювати такий клас контейнера.
У цьому прикладі `UserService` впроваджується в `SiteController`, `UserRepository` впроваджується в `UserService` та AR моделі `User` та `Logs` впроваджуються в `UserRepository` class.
Шар контролерів
Сучасні MVC-фреймворки, такі як Laravel та Yii беруть на себе такі обов'язки контролерів як обробка вхідних даних, правила роутингу та HTTP-запитів, тоді як пре-процесинг даних у таких фреймворках винесений в окремі компоненти, такі як middleware у Laravel та behavior у Yii. Тому програмісту залишається дописати до контролера всього кілька рядків коду.
Обов'язок контролера полягає в отриманні запиту від користувача та надсиланні користувачеві результатів. Контролер не повинен містити жодної бізнес логіки, інакше її неможливо буде використовувати повторно, що призведе до дублювання коду. Також це додасть труднощів при зміні типу відповіді контролера, наприклад для API. Якщо в контролері немає нічого зайвого, то змінити формат виводу з відображення view на відповідь API не складе ніяких труднощів.
Занадто тонкий шар контролера здається неправильним і так як контролер - це точка входу програми, багато розробників просто поміщають весь код саме туди, не замислюючись про архітектуру. В результаті до контролера додаються такі зони відповідальності як:
- Бізнес-логіка, що призводить до неможливості повторного використання коду
- Безпосередня зміна станів моделі (будь-які зміни в БД призводять до величезної кількості змін у коді)
- Використання відносин моделей (наприклад складні запити з використанням зв'язків моделей; знову, якщо щось потрібно поміняти в БД, доведеться змінювати сотні місць за кодом)
Приклад контролера, який виконує занадто багато:
-
Чому цей контролер поганий:
Контролер має бути простий. Все, що він має робити, — отримувати запит і віддавати відповідь. Наприклад, такий:
Куди ж подіти все інше? У нижчі шари.
Шар сервісів
Шар сервісів — це бізнес логіки. Тут і тільки тут має бути інформація про бізнес процеси та взаємозв'язки між бізнес моделями. Це абстрактний шар і він буде різним для різних додатків, але загалом загальним залишається незалежність від типу вхідних даних (це відповідальність контролера) та типу зберігання даних (це відповідальність нижчих шарів).
На цьому етапі виникає одна з найнебезпечніших проблем. Часто контролер повертають AR модель. І як результат уявлення (або контролер, якщо йдеться про API) знає про атрибути та відношення моделі. Це призводить до необхідності множинних правок, якщо необхідно змінити поле в БД.
Ось приклад використання AR моделі у поданні:
Виглядає просто, але якщо потрібно перейменувати поле `first_name` змінювати доведеться всі уявлення, що загрожує помилками, тому що щось можна пропустити або забути. Просте рішення - використовуватиDTO – Data Transfer Objects.
Data Transfer Objects
Дані із сервісного шару необхідно обернути в простий незмінний об'єкт. Він не може бути змінений після того, як створений, тому сеттери в такому класі не потрібні. Більше того, DTO клас повинен бути незалежним, він не повинен успадковуватись від Active Record моделі. Часто бізнес-модель може не збігатися з AR-моделлю.
Використання DTO дозволяє унеможливити використання AR-моделі в контролері або представленні. Також DTO-об'єкт дозволяє відокремити фізичне зберігання даних від логічного представлення бізнес-моделі. Якщо щось змінюється на рівні БД, міняти доведеться лише DTO-об'єкт, а не всі контролери та уявлення.
Приклад простого класу DTO:
Декоратори вистав
Для відокремлення логіки уявлень (наприклад, колір кнопки в залежності від статусу), має сенс використовувати додатковий шар декораторів. Декоратор - патерн, який дозволяє прикрасити основний об'єкт, обернувши його зміненими методами. Це часто потрібно для шматочків логіки в уявленнях. Об'єкт DTO може частково виконувати роль декоратора, але тільки для загальних методів, таких як форматування дати. DTO представляє бізнес-модель, тоді як декоратор лише прикрашає дані за допомогою HTML для специфічних сторінок.
Так виглядає уявлення без декоратора:
Цей приклад простий, але у складнішій логіці запросто можна заплутатися. На допомогу приходить декоратор:
Тепер можна використовувати атрибути моделі у поданні без зайвих умов та логіки, що робить уявлення читальнішим.
Декоратори також можна поєднувати один з одним:
Кожен декоратор виконає свою роботу і перетворить лише ту частину, яку він відповідає. Така динамічнавкладеність декораторів дозволяє комбінувати можливості декораторів без створення нових класів.
Шар репозиторіїв
Репозиторій працює із конкретним способом зберігання даних. Найкраще впроваджувати репозиторій через інтерфейс, тоді його буде легко підмінити у разі зміни типу БД. При зміні сховища даних достатньо створити новий клас репозиторію, що реалізує такий інтерфейс і не доведеться змінювати весь код.
Репозиторій отримує дані з БД та керує роботою кількох AR-моделей. AR-модель у цьому контексті виконує роль Entity. Entities - це примітивний об'єкт системи, який зберігає в собі інформацію, але не знає нічого про те, як він з'явився (тільки створений чи отриманий із БД) або як зберегти чи оновити свій стан. Збереження та оновлення AR моделей – це відповідальність репозиторію. Такий підхід допомагає розділити відповідальність – управління AR-моделями лежить на репозиторії, а самі AR-моделі – примітивні сховища даних.
Приклад методу репозиторію:
Шари, які дотримуються правил єдиної відповідальності
У щойно створеному Yii або Laravel додатку є тільки папки сontrollers, models, та views. Ні Yii, ні Laravel не додають додаткові шари приклад програми. Проста та інтуїтивно зрозуміла навіть для новачків MVC структура спрощує роботу з фреймворком, але важливо розуміти, що така архітектура – це лише приклад, а не стандарт чи стиль, що насаджується фреймворком.
Розділивши завдання на шари з єдиною відповідальністю, ми отримуємо гнучку та розширювану архітектуру, яку простіше підтримувати:
- Entities - примітивні моделі даних
- Репозиторій працює з БД
- Шар сервісу з бізнес-логікою
- Контролер, що спілкується зі сторонніми сервісами та кінцевимикористувачами.
Оригінал статті англійською тут.
На жаль зовсім не згадано про виділення валідації, приклади з Laravel - там валідація на Request з можливістю FormRequest зав'язана, в YII класи для валідації можна створювати успадковуючись від \yii\base\Model і варто обов'язково відзначити, що в репозиторії дані необхідно передавати вже валідними, у них жодних перевірок не повинно бути.
Ну і насправді такі статті без прикладів для новачків дорога в пекло.
Так, валідацію змарнувала. Все правильно, валідація повинна бути на Request, контролер не повинен пропускати у нижчі шари невалідні дані.
І загалом підкидаю ідею — створити гіт-сторінку з добіркою реп, де такі підходи грамотно використовуються.
Я хочу переписати base app для yii у такому стилі, як буде вільний час
А «шари» можуть працювати з відповіддю програми (response) чи тільки контролер може? Тоді як шари повинні сказати контролеру, що щось пішло не так? Повернути false, а проблеми в getErros() чи кидати винятки? Далі з'являється якийсь франкенштейн, який містить і логіку, і дані, і верстку. =)
Ні, працювати з відповіддю може тільки контролер. Уявіть, що кожен шар у вас фізично знаходиться на різних машинах (така собі розподілена система). Як би ви організували обробку помилок? По-перше можна повертати заздалегідь визначені коди помилок (всі шари знають про угоду та коди). Також повідомлення про помилку можна передавати в одному з полів відповіді. По-друге, якщо шар не впевнений у своєму підлеглому шарі — можна зробити try catch до виклику сервісу/репозиторію і обробити помилку, підготувавши правильний формат для більш високого шару.
А ви зустрічали на гітхабі якесь розширення наYii2, обсягу рівня модуля, написане у зазначеному у статті стилі, щоб точніше зрозуміти про що мова. Як кажуть краще один раз побачити, ніж сто разів почути, а ваші пояснення можна зовсім по-різному інтерпретувати.
Ні, не зустрічала. Така архітектура — необхідність великих проектів, а такі проекти в паблік не викладаються. Для дрібних проектів така архітектура вимагає багато часу та сил, а так як дрібний модуль не буде особливо розростатися, великого сенсу в цьому немає.
>> Так, валідацію змарнувала. Все правильно, валідація повинна бути на Request, контролер не повинен пропускати у нижчі шари невалідні дані.
У першому Yii (з другим не працював) була можливість валідацію зробити в AR і це зменшувало дублювання коду. Одна й та сама модель валідувалася завжди однаково у різних контролерах. Імхо, такий підхід теж має право на життя.
Звичайно має, як і загалом існуючий «стиль». Він корисний для швидкої розробки прототипу чи розробки дрібних проектів. Для великого проекту він міщає, тому складніше підтримувати і змінювати код, коли його багато і все перемішують. Валідацію AR моделі повністю не виключити, оскільки такі речі як унікальність поля, наприклад, інакше і не перевірити. Але при правильному розділенні завдань - явні помилки введення користувача - не рівень AR моделі.