Найпоширеніші помилки під час розробки вебзастосунку на Ruby on Rails

Ruby on Rails (або просто «Rails») — це популярний опенсорсний фреймворк, заснований на мові програмування Ruby, який прагне спростити та впорядкувати процес веб-розробки.
Rails побудована на принципі «угода важливіша за конфігурацію». Простіше кажучи, це означає, що за замовчуванням Rails припускає, що професійні розробники дотримуватимуться «стандартних» найкращих практик (щодо назв, структури коду тощо), і якщо вони це роблять, усе працюватиме «автоматично й магічно», без потреби щось уточнювати. Така парадигма має свої переваги, проте не обійшлося й без підводних каменів. Зокрема, «магія», яка відбувається за лаштунками фреймворку, іноді може призводити до головного болю, плутанини та проблем на кшталт «що, чорт забирай, відбувається?». А ще можуть бути небажані наслідки для безпеки та продуктивності.
Отже, хоча Rails проста у використанні, з нею також неважко нажити проблем. Цей туторіал розгляне 10 найпоширеніших проблем, розповість, як їх уникнути, і назве причини, що їх спричиняють.
Помилка 1. Забагато логіки в контролері
Rails побудована на основі MVC-архітектури. У спільноті Rails ми говоримо про товсту модель і тонкий контролер, але останнім часом я бачив кілька застосунків на Rails, які порушували цей принцип. Адже так легко помістити всю логіку представлення (якій саме місце в хелпері) або логіку моделі в сам контролер.
Проблема в тому, що об’єкт контролера почне порушувати принцип єдиної відповідальності, роблячи майбутні зміни коду складними й схильними до помилок. Загалом, ось типи логіки, які можуть бути у вашому контролері:
Обробка сесії та cookies. Сюди також може входити автентифікація/авторизація або будь-яка інша операція з cookies, яка вам потрібна.
Вибір моделі. Логіка пошуку потрібного об’єкта моделі за параметрами, переданими із запиту. В ідеалі це має бути виклик єдиного методу find, який встановлює змінну, що згодом використовується для формування відповіді.
Керування параметрами запиту. Збір параметрів запиту та виклик відповідного методу моделі.
Рендеринг/редирект. Рендеринг результату (html, xml, json тощо) або редирект, якщо це необхідно.
Хоча це все ще порушує принцип єдиної відповідальності, це своєрідний мінімум, якого фреймворк Rails вимагає від контролера.
Помилка 2. Забагато логіки в представленні
ERB, «коробковий» шаблонізатор Rails, відкриває чудові можливості для створення сторінок із різним вмістом. Однак, якщо не бути обережним, можна зрештою отримати великий файл, у якому HTML перемішаний із Ruby-кодом, яким важко керувати й який проблематично підтримувати. Також це може призвести до великої кількості повторень, що порушує принцип DRY (don’t repeat yourself).
Це може проявитися багатьма способами. Один із них — надмірна умовна логіка в представленні. Як простий приклад розгляньмо випадок, коли в нас є доступний метод current_user, який повертає поточного авторизованого користувача. Часто в представленнях з’являються, наприклад, такі логічні конструкції:
<h3>
Welcome,
<% if current_user %>
<%= current_user.name %>
<% else %>
Guest
<% end %>
</h3>
Кращий спосіб упоратися з подібним: переконатися, що об’єкт, який повертає current_user, завжди заданий, незалежно від того, чи увійшов хтось у систему, і що він розумно відповідає на методи, використані в представленні (іноді це називають null-об’єктом). Наприклад, ви можете визначити хелпер current_user в app/controllers/application_controller таким чином:
require 'ostruct'
helper_method :current_user
def current_user
@current_user ||= User.find session[:user_id] if session[:user_id]
if @current_user
@current_user
else
OpenStruct.new(name: 'Guest')
end
end
Це дозволило б замінити попередній код представлення лише одним рядком:
<h3>Welcome, <%= current_user.name -%></h3>
Кілька додаткових рекомендацій щодо Rails:
Використовуйте шаблони (layouts) і partials, щоб інкапсулювати те, що повторюється на ваших сторінках.
Використовуйте презентери/декоратори, такі як гем Draper, щоб інкапсулювати логіку представлення в Ruby-об’єкт. Потім ви можете додати в цей об’єкт методи для логічних операцій, які інакше опинилися б у коді представлення.
Помилка 3. Забагато логіки в моделі
З огляду на рекомендації мінімізувати логіку в представленнях і контролерах, останнє місце в MVC, куди нам залишилося покласти всю логіку, — це модель, чи не так?
Ну, не зовсім.
Багато Rails-розробників насправді роблять цю помилку, і зрештою їхні класи моделей ActiveRecord перетворюються на величезні файли, які не лише порушують принцип єдиної відповідальності, а й стають кошмаром для підтримки.
Такі речі, як генерація email-сповіщень, звернення до зовнішніх сервісів, конвертація даних в інші формати тощо, не варто робити в ActiveRecord-моделях, призначення яких — робити трохи більше, ніж пошук і збереження даних у базі.
Але якщо така логіка не повинна жити в представленні, контролері й моделі — то де ж тоді?
Поговорімо про «прості старі Ruby-об’єкти» (POROs). Маючи такий всеосяжний фреймворк, як Rails, недосвідчені розробники часто не хочуть створювати власні класи поза фреймворком. Однак винесення логіки з моделі в POROs — часто саме те, що лікар прописав, щоб уникнути надто складних моделей. За допомогою POROs ви можете інкапсулювати email-сповіщення чи взаємодію з API у власні класи, а не тримати їх усередині ActiveRecord-моделі.
Отже, з урахуванням сказаного, єдина логіка, яка може залишитися у вашій моделі, це:
Конфігурація ActiveRecord (наприклад, зв’язки та валідації).
Прості методи мутації для інкапсуляції оновлення кількох атрибутів і збереження їх у базі.
Обгортки доступу, які приховують внутрішню інформацію моделі (наприклад, метод full_name, що поєднує поля first_name і last_name у базі даних).
Складні запити (складніші, ніж простий find). Загалом, ви не повинні використовувати цей чи будь-який інший метод побудови запитів за межами самого класу моделі.
Помилка 4. Звалище із загальних допоміжних класів
Ця помилка — своєрідний наслідок помилки 3. Як уже говорилося, структура Rails робить акцент на названих компонентах (модель, представлення та контролер) як основі MVC. Існують досить чіткі визначення того, що належить до класів кожного з цих компонентів, але іноді нам потрібні методи, які не підходять жодному з трьох.
Генератори Rails створюють директорію helpers і новий клас хелпера. Стає надто спокусливо почати складати туди функціональність, яка формально не належить ні моделі, ні представленню, ні контролеру.
Хоча Rails, звісно, є MVC-орієнтованим фреймворком, ніщо не заважає вам створювати власні типи класів і додавати відповідні директорії для їхнього коду. Якщо у вас є додаткова функціональність, подумайте, які методи групуються разом, і знайдіть добрі назви для класів, що їх міститимуть. Використання Rails — не привід відсувати найкращі практики об’єктно-орієнтованого проєктування на другий план.
Помилка 5. Використання забагато гемів
Ruby on Rails підтримується багатою екосистемою гемів, які разом забезпечують практично будь-яку можливість, про яку тільки може подумати розробник. Це дуже зручно для швидкого створення складних застосунків, але я також бачив багато застосунків, де кількість гемів була непропорційно великою порівняно з наданою функціональністю.
Це призводить до кількох проблем. Зловживання гемами робить Rails-процеси більшими, ніж потрібно. Це може уповільнити роботу в продакшні. Окрім розчарування користувачів, це може призвести до потреби в більшому обсязі пам’яті сервера, що збільшить операційні витрати. Також такий застосунок довше запускається, що уповільнює розробку й робить автоматичні тести повільними (а повільні тести, як правило, запускають нечасто).
Майте на увазі, що кожен гем, який ви підключаєте до застосунку, своєю чергою залежить від інших гемів, а ті — від інших і так далі. Тож додавання гемів має накопичувальний ефект. Наприклад, додавання гема rails_admin підтягне ще понад 11 гемів, що на 10% більше, ніж базова конфігурація Rails.
На момент написання статті свіжий Rails 4.1.0 містив 43 геми у файлі Gemfile.lock. Це явно більше, ніж входить у Gemfile, і включає всі геми, які стандартний набір гемів Rails додає як залежності.
Ретельно зважуйте всі додаткові витрати щоразу, коли додаєте новий гем. Наприклад, розробники часто додають гем rails_admin, бо він надає симпатичний веб-інтерфейс до структури моделей. Але насправді це не більше ніж модний інструмент перегляду бази даних. Навіть якщо ваш застосунок потребує адміністраторів із розширеними правами, ви, ймовірно, не захочете давати їм прямий доступ до БД, і краще розробите власну систему адміністрування, ніж додаватимете цей гем.
Помилка 6. Ігнорування лог-файлів
Хоча більшість Rails-розробників знають про стандартні лог-файли під час розробки, вони не приділяють багато уваги інформації в них. І хоча багато застосунків у продакшні покладаються на інструменти моніторингу логів, такі як Honeybadger чи New Relic, так само важливо стежити за логами під час розробки й тестування застосунку.
Як уже згадувалося, Rails робить багато «магії», особливо всередині моделей. Асоціації дозволяють дуже просто будувати зв’язки між таблицями та виводити все в представленнях. Увесь SQL генерується автоматично. Але як дізнатися, що згенерований SQL ефективний?
Один із прикладів, з яким ви часто стикатиметеся, називається проблемою запитів N+1. Хоча проблема добре відома, єдиний спосіб побачити її на практиці — переглянути SQL-запити у ваших логах.
Скажімо, у вас є такий запит у типовому блог-застосунку, де ви показуєте всі коментарі для вибраного набору постів:
def comments_for_top_three_posts
posts = Post.limit(3)
posts.flat_map do |post|
post.comments.to_a
end
end
Коли ми дивимося в лог-файл на запит, що викликав цей метод, ми бачимо приблизно таке: один запит отримав три пости, після чого ще три запити отримали коментарі до кожного з них:
Started GET "/posts/some_comments" for 127.0.0.1 at 2014-05-20 20:05:13 -0700
Processing by PostsController#some_comments as HTML
Post Load (0.4ms) SELECT "posts".* FROM "posts" LIMIT 3
Comment Load (5.6ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?
Comment Load (0.4ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?
Comment Load (1.5ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = ?
Rendered posts/some_comments.html.erb within layouts/application (12.5ms)
Completed 200 OK in 581ms (Views: 225.8ms | ActiveRecord: 10.0ms)
Active Record у Rails дозволяє суттєво скоротити кількість запитів, дозволяючи заздалегідь указати всі зв’язки, які буде завантажено. Це робиться викликом методу includes (або preload) на об’єкті Arel (ActiveRecord::Relation). З includes ActiveRecord гарантує, що всі вказані зв’язки буде завантажено мінімальною кількістю запитів, наприклад:
def comments_for_top_three_posts
posts = Post.includes(:comments).limit(3)
posts.flat_map do |post|
post.comments.to_a
end
end
Коли наведений вище код виконується, ми бачимо в логах, що всі коментарі отримано одним запитом замість трьох:
Started GET "/posts/some_comments" for 127.0.0.1 at 2014-05-20 20:05:18 -0700
Processing by PostsController#some_comments as HTML
Post Load (0.5ms) SELECT "posts".* FROM "posts" LIMIT 3
Comment Load (4.4ms) SELECT "comments".* FROM "comments" WHERE "comments"."post_id" IN (1, 2, 3)
Rendered posts/some_comments.html.erb within layouts/application (12.2ms)
Completed 200 OK in 560ms (Views: 219.3ms | ActiveRecord: 5.0ms)
Набагато ефективніше.
Проблема N+1 — лише один приклад із безлічі неефективностей, які можуть ховатися «під капотом» вашого застосунку, якщо ви не приділяєте цьому достатньо уваги. Висновок із цього пункту: під час розробки перевіряйте development- і test-логи, щоб помітити (і виправити!) неефективності в коді, що будує запити.
Перегляд логів — чудовий спосіб знайти неефективний код і виправити його до того, як застосунок піде в продакшн. Інакше ви не зможете бути впевнені в продуктивності системи, доки вона не почне «жити», — адже база даних, з якою ви працювали під час розробки й тестування, ймовірно, виявиться значно меншою за продакшн-базу. Навіть якщо у новому застосунку продакшн-БД спочатку невелика і все працює нормально, згодом вона зросте, і проблеми, описані в цьому прикладі, робитимуть систему дедалі повільнішою.
Якщо ви помітите, що ваші логи забиті купою непотрібної інформації, ви можете їх очистити.
Помилка 7. Відсутність автоматичних тестів
Ruby on Rails за замовчуванням надає широкі можливості для автоматичного тестування. Багато Rails-розробників пишуть дуже складні тести в стилях TDD і BDD, а також використовують ще потужніші тестові фреймворки з гемів (rspec і cucumber).
Попри те, як легко додати автоматичні тести у ваш застосунок, я був неабияк здивований, коли мені траплялося приєднуватися до проєктів, де буквально не було жодного тесту (або в кращому разі дуже мало). Можна сперечатися, наскільки всеосяжними мають бути тести, але цілком очевидно, що принаймні якісь автоматичні тести мають бути в кожному застосунку.
Загальне правило: має бути щонайменше один високорівневий інтеграційний тест для кожної дії ваших контролерів. Колись у майбутньому інші розробники захочуть розширити чи змінити код або оновити версію Ruby on Rails, і тестовий фреймворк дозволить їм переконатися, що базова функціональність застосунку працює. Додаткова перевага — майбутні розробники отримають чітке уявлення про весь функціонал застосунку.
Помилка 8. Блокування на зверненнях до зовнішніх сервісів
Сторонні сервіси, як правило, дуже легко інтегруються в Rails-застосунок за допомогою гемів, що обгортають їхні API. Але що станеться, якщо ваш зовнішній сервіс почне збоїти або працювати дуже повільно?
Щоб уникнути блокування на таких запитах, замість прямого звернення до них під час обробки запиту переводьте їх у фоновий режим, якщо це можливо. Кілька популярних гемів:
Delayed job
Resque
Sidekiq
У випадках, коли перевести обробку у фоновий режим неможливо чи недоцільно, переконайтеся, що ваш застосунок достатньо добре обробляє помилки й збої, якщо зовнішній сервіс «лежить» або має проблеми. Також варто протестувати застосунок без зовнішніх сервісів (наприклад, від’єднавши сервер, на якому він працює, від мережі), щоб переконатися, що це не призводить до непередбачуваних наслідків.
Помилка 9. Надмірна прив’язаність до наявних міграцій бази даних
Механізм міграцій бази даних у Rails дозволяє створювати інструкції, які автоматично додають або видаляють таблиці та рядки. Оскільки файли міграцій іменуються послідовно, ви можете програти їх із самого початку, щоб привести порожню БД до тієї ж схеми, що й у продакшні. Це чудовий спосіб керувати поступовими змінами схеми БД вашого застосунку.
Хоча на початку проєкту це добре працює, з часом процес створення БД може займати чимало часу, а міграції іноді губляться, вставляються не в тому порядку або потрапляють з інших Rails-застосунків, що використовують той самий сервер.
Rails створює представлення вашої поточної схеми у файлі db/schema.rb (за замовчуванням), який зазвичай оновлюється під час запуску міграцій. Файл schema.rb можна навіть згенерувати за відсутності міграцій, виконавши команду rake db:schema:dump. Поширена помилка — закомітити нову міграцію в репозиторій, але не закомітити оновлений файл schema.rb.
Коли міграції виконуються надто довго, розробникам не варто боятися очистити стару директорію міграцій, зробити дамп нової схеми й продовжити з нею. Налаштування нового середовища розробки тоді вимагатиме запуску rake db:schema:load, а не rake db:migrate, на яку покладається більшість розробників.
Деякі з цих питань також розглядаються в посібнику з Rails (Rails Guide).
Помилка 10. Коміт конфіденційної інформації в репозиторій вихідного коду
Фреймворк Rails дозволяє легко створювати безпечні застосунки, захищені від багатьох видів атак. Частково це досягається за допомогою секретного токена, яким захищаються сесії браузера. Хоча зараз цей токен зберігається в config/secrets.yml, а цей файл на продакшн-серверах зчитує токен зі змінної оточення, у попередніх версіях Rails токен містився в config/initializers/secret_token.rb. Цей файл часто помилково комітять у репозиторій разом з рештою застосунку. Якщо це станеться, будь-хто з доступом до репозиторію зможе скомпрометувати всіх користувачів вашого застосунку.
Тож переконайтеся, що файл конфігурації вашого репозиторію (наприклад, .gitignore для користувачів git) виключає файл із токеном. Тоді ваші продакшн-сервери зможуть отримувати токен зі змінної оточення або через механізм на кшталт того, що надає гем dotenv.
Підсумок
Rails — потужний фреймворк, який приховує багато неприємних деталей, необхідних для побудови надійного застосунку. І хоча Rails суттєво пришвидшує розробку, розробникам варто звертати увагу на потенційні помилки в коді та проєктуванні, щоб їхні застосунки легко розширювалися й підтримувалися в міру зростання.
Розробникам також слід знати про проблеми, які можуть зробити їхні застосунки повільнішими, менш надійними чи менш безпечними. Важливо вивчити основи фреймворку й переконатися, що ви повністю розумієте архітектуру, дизайн і код упродовж усього процесу розробки. Це дозволить створити якісний застосунок із високою продуктивністю.
За матеріалами сайтів:
Назад



