[UA] Предметно-орієнтована архітектура Rails
- Час: 20-30 хвилин
- Рівень: Середній/Просунутий
- Код: GitHub
- Ресурси:
Дана стаття описує структуру проекту написаного на Rails і в ній використані ідеї з вищезазначених статтей. Окрім того приклад містить в собі можливість досить просто використовувати автоматичні додатки для перевірки якості коду. Головними вимогами до проекту є:
- Розділення перегляду (репрезентації) та бізнес-логіки (вашого домену)
- Розділення залежностей (gems) і як результат - можливість виконувати юніт тести в ізольованому середовищі
- Рішення повинно бути простим і зрозумілим (Rails чудовий фреймворк і ми не збираємось з ним боротись)
TLDR - Github repo та commit з усіма змінами застосованими до нового проекту на Rails
Розділення перегляду та бізнес-логіки
Першим кроком є чітке розділення перегляду та бізнес-логіки в структурі
проекту (та в вашій голові). Для досягнення даного результату ми
створимо нову папку representations/
і перемістимо в неї все що нам
потрібно для того щоб показати суб’єкти домену. В прикладі ними є:
representations/
assets/
controllers/
decorators/
public/
views/
vendor/
routes.rb
Я надаю перевагу використанню декораторів замість helper тому тут немає папки
helpers/
.
Далі нам потрібно побудувати структуру тек для суб’єктів та логіки домену
- жодне з цих двох понять не повинно бути присутнім в частині проекту,
що відповідає за представлення. Отож давайте створимо нову теку
domain/
і перемістимо в неї моделі та налаштування для бази даних:domain/
contexts/
database.yml
Назва contexts/
тут є посиланням на шаблон Bounded Context в теорії
Предметно-орієнтованого програмування. Ви можете назвати їх по-іншому і мати будь-яку структуру
тек всередині.
Тепер нам потрібно налаштувати Rails для роботи з новою структурою тек. Дані налаштування
знаходяться всередині файлу config/application.rb
і використовують API Rails::Application
Після цієї зміни Rails буде працювати з новою структурою тек так, ніби оригінальна ніколи не змінювалась - autoloading, eager loading, asset compilation - всі ці процеси будуть повністю функціональні.
На мою особисту думку представлення
ApplicationController
таApplicationRecord
якConcern
покращує гнучкість коду, тому в даному прикладі вони єConcerns
і є додатковий файлconfig/initializers/draper.rb
для того щоб ‘Draper’ зміг з ними працювати
Розділення середовищ та побудова незалежних тестів
Раніше ми розділили представлення і предметну область, тепер окремі тести для кожної частини будуть великим плюсом для проекту. Правильного написані тести будуть швидші, ізольовані та незалежні. Спершу підготуємо середовище для них:
- Створимо окремі
Gemfile
таGemfile.lock
для представлення та предметної області - Налаштовуємо головний
Gemfile
так щоб він використовував нові специфічні для кожної областіGemfile
- Налаштуємо незалежні тестові середовища для представлення та предметної області
Додавання додаткових Gemfile
не є чимось складним - ми просто створюємо нові файли і переміщуємо
в них залежності (gem) з головного.
Налаштувати головний Gemfile
для роботи з розподіленими залежностями є також доволі простим - bundler
вже має метод для завантаження додаткових файлів, якщо виникнуть проблеми при завантаженні розподілених
залежностей ви побачите ті самі помилки що і при завантаженні звичайного Gemfile
Налаштування незалежних тестових середовищ є найскладнішою частиною (і найімовірніше саме тут виникнуть
додаткові проблеми при рості проекту). Перший крок - запустити команду rspec --init
в теках representations/
та domain/
. В результаті нові таки representations/spec
та domain/spec
будуть додані.
spec/spec_helper.rb
також буде додано автоматично, проте spec/rails_helper.rb
автоматично створено
не буде і нам доведеться додати і налаштувати його вручну.
Налаштування тестового середовища предметної області
Для початку ми копіюємо файл spec/rails_helper.rb
в domain/spec/rails_helper.rb
і видаляємо з нього все до лінії
RSpec.configure do |config|
. Це робиться для того щоб не завантажувати жодних залежностей - ми їх завантажимо
вручну пізніше. Після цього в нас не буде можливості запустити тести, проте це лише перший крок.
Далі ми завантажуємо всі необхідні залежності:
- завантажуємо
active_record
таrspec-rails
- завантажуємо залежності тестового середовища
- створюємо Application для роботи з
rspec-rails
(нажаль найбільш слабка частина)
- під’єднуємось до бази даних
- завантажуємо предметну область (спільні concerns в першу чергу оскільки немає механізму автозавантаження)
- завантаження файлів initializer/support
налаштування тестового середовища для представлення
Налаштування тестового середовища для представлення є досить схожим - єдина різниця це залежності якими завантажуємо:
- завантажуємо
action_controller
таrspec-rails
- створюємо Application для
rspec-rails
та завантажуємо routes
- завантажуємо залежності
- завантажуємо код представлення (спільні concerns в першу чергу оскільки немає механізму автозавантаження)
Тепер у нас є змога запускати різні тести в залежності від контексту і для кожного контексту:
- тести можуть включати лише юніт-тести
- ми змушені залишатися всередині контексту при написанні тесту
- завантаження/перезавантаження середовища є швидким (завантаження файлів тривало 2.65 секунди коли тести запускалося з головного проекту і лише 0.9 секунд якщо запускалась незалежно)
Оскільки файли налаштування тестового середовища знаходяться всередині тек
representations/
таdomain/
, ці тeки не можуть бути всерединіapp/
- тому що Rails спробує завантажити ці файли в production.
Фінальні частини
Як я вже згадував у попередній статті, я вважаю що тести,
що знаходяться в головній теці spec/
, test/
не повинні бути юніт-тестами
і завжди тестувати декілька компонентів проекту. Протилежне твердження є
істинним для тестів що знаходяться в теках representations/spec/
та domain/spec
завжди повинні бути юніт-тестами.
Одна проблема з даним налаштуванням є те що для того щоб запускати тести
всередині ізольованого середовища ви повинні мати окремі Gemfile.lock
і це може
спричинити різницю у версіях gem які використовується для тестів що запускаються в
ізоляції і тестів що допускаються як частина глобальної тестової системи. Давайте
напишемо тест який би нам повідомляв якщо така ситуація станеться:
Приклад проекту також включає Git hooks які будуть встановлені
на ваш проект якщо ви запустите ./bin/setup
і будуть автоматично виконані перед
та після того як ви зробити commit. pre-commit hook запускає rubocop для перевірки
всіх змін які будуть включені в commit, post-commit hook надає вам можливість запускати
rails_best_practices, reek, brakeman і mutant для вашого коду.
Підсумок
Мені дуже подобається гнучкість даної архітектури - при потребі можна заізолювати будь-яку частину коду і ставитись до неї як до незалежного unit. В той же час вона, здебільшого, використовує Rails API - тож ми не боремося з Rails, скоріше це ще один спосіб для організації коду. Мені кортить випробувати дану архітектуру з більш складними та legacy проектами - її застосування повинно бути доволі простим в обох випадках.
Посилання: