Наиболее частые ошибки при разработке веб приложения на Ruby on Rails

Ruby / Ruby on Rails
Александр Вихор
02-07-2020 17:24:00


Ruby on Rails (или просто “Rails”) — это популярный опенсорсный фреймворк, основанный на языке программирования Ruby, который стремится упросить и упорядочить процесс веб-разработки.

Rails построена на принципе соглашения по конфигурации. Проще говоря, это означает, что по умолчанию Rails предполагает, что профессиональные разработчики будут следовать “стандартным” наилучшим практикам (таким, как названия, структура кода и так далее), и, если они это делают, все будет работать “автоматически и магически”, без надобности что-то уточнять. Такая парадигма имеет свои преимущества, однако не обошлось и без подводных камней. В частности, “магия”, которая происходит за кулисами фреймворка, иногда может приводить к головнякам, путанице и проблемам вроде “что, черт возьми, происходит?”. А еще могут быть нежелательные последствия в области безопасности и производительности.

Соответственно, в то время, как Rails проста в использовании, с ней также нетрудно заработать проблем. Этот туториал рассмотрит 10 наиболее популярных проблем, расскажет, как их избежать и назовет причины, которые их вызывают.

Ошибка 1. Слишком много логики в контроллере

Rails построена на основе MVC-архитектуры. В коммьюнити Rails мы говорим о толстой модели и тонком контроллере, но в последнее время я наблюдал несколько приложений на Rails, которые нарушали этот принцип. Ведь так легко поместить всю логику представления (которой самое место в хелпере) или логикумодели в сам контроллер.

Проблема в том, что объект контроллера начнет нарушать принцип единственной обязанности, делая будущие изменения кода сложными и подверженными ошибкам. В общем, вот типы логики, которые могут быть в вашем контроллере:

Обработка сессии и 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:

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

Используйте декораторы, такие, как Draper gem для инкапсуляции логики представления в Ruby-объект. Затем вы можете добавить методы в этот объект, чтобы представить логические операции, которые в противном случае были бы внутри кода представления.

Ошибка 3. Слишком много логики в модели

Учитывая рекомендации, говорящие минимизировать логику в представлениях и контроллерах, последнее место в MVC, куда нам осталось положить всю логику — это модель, так?

Ну, не совсем.

Многие Rails-разработчики на самом деле делают эту ошибку и в конечном счете их классы моделей в ActiveRecord, ведущие к файлам Mongo, — не только нарушают принцип единственной обязанности, но и являются кошмаром для техподдержки.

Такая функциональность, как генерация email-оповещений, обращение ко внешним сервисам, конвертирование данных в другие форматы и тому подобное не стоит делать с помощью ActiveRecord-моделей, назначение которых — делать чуть больше, чем поиск и хранение данных в базе.

Но если такая логика не должна храниться в представлении, в контроллере и модели — то где же тогда?

Давайте поговорим о “простых старых Ruby-объектах” (POROs). С таким всеобъемлющим фреймворком, как у Rails, неопытные разработчики часто не хотят создавать свои собственные классы вне фреймворка. Однако, часто вынесение логики модели внутрь POROs — это то, что доктор прописал во избежание усложнения моделей. С POROs вы можете инкапсулировать такие вещи, как email-уведомления или взаимодействия с API в их собственные классы, а не хранить их внутри ActiveRecord-модели.

Итак, исходя из сказанного, единственная логика, которая может остаться в вашей модели, это:

Конфигурация ActiveRecord (например, взаимосвязи и валидация).

Простые методы мутации для инкапсуляции обновления части параметров и сохранения их в базе.

Врапперы доступа, которые скрывают внутреннюю информацию модели (метод full_name, который сочетает поля first_name и last_name в базе данных).

Сложные запросы (более сложные, чем find). Вообще говоря, вы не должны использовать этот метод или любой другой метод построения очереди за пределами класса модели как такового.

Ошибка 4. Свалка из общих вспомогательных классов

Эта ошибка — своего рода следствие ошибки 3. Как уже говорилось, структура Rails делает акцент на названных компонентах (таких, как модель, представление и контроллер) в качестве основы MVC. Есть неплохие определения видов сущностей, которые принадлежат классам каждого из этих компонентов, но иногда нам бывают нужны методы, которые не подходят ни одному из трех.

Генераторы в Rail создают директорию хелпера и новые класс хелпера. Становится слишком заманчивым начать создавать функциональность, которая вовсе не понадобится ни модели, ни представлению, ни контроллеру.

Хотя Rails, конечно, и является MVC-центрированным фреймворком, ничто не мешает вам создать свои собственные типы классов и добавить подходящие каталоги для кода этих классов. Если у вас предусмотрена дополнительная функциональность, подумайте, какие методы группируются и найдите хорошие названия для классов, которые содержат эти методы. Используя Rails не повод, чтобы позволить лучшим практикам объектно-ориентированного проектирования отойти на второй план.

Ошибка 5. Использование слишком большого количества гемов

Ruby on Rails поддерживается богатой экосистемой гемов, которые в совокупности обеспечивают практически любую возможность, о которой только разработчик мог подумать. Это очень удобно для быстрого создания сложных приложений, но я также видел много приложений, где число гемов было непропорционально большим по сравнению с предоставляемой функциональностью.

Это приводит к нескольким проблемам 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) ELECT "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). C 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, которая в действительности может быть только примером того множества неэффективностей, которые живут “под капотом” вашего приложения, если вы не уделяете этому много внимания. Вывод из этого пункта состоит в том, что вам нужно проверять девелоперский и тестерский лог во время разработки — чтобы проверить (и устранить) неэффективности в коде, который занимается построением запросов.

Обзор логов — хороший способ локализовать неэффективный код и исправить его, прежде чем приложение уйдет в продакшн. В противном случае, вы не сможете быть уверенным в производительности вашей системы до тех пор, пока она не начнет “жить” — вероятно, та база данных, с которой вы работали в процессе разработки и тестирования, окажется куда меньше той, что будет на продакшне. Если вы делаете новое приложение, даже если ваша БД на продакшне изначально небольшая и все поначалу работает нормально, потом она вырастет и проблемы, описанные в этом примере будут делать работу системы все медленнее и медленнее.

Если вы обнаружите, что ваши логи забиты кучей ненужной вам информации, вы можете очистить их.

Ошибка 7. Отсутствие автоматических тестов

Ruby on Rails по умолчанию дает широкие возможности для автоматического тестирования. Многие Rails-разработчики пишут очень сложные тесты с использованием TDD и BDD-стилей, а также используют еще более мощные тест-фреймворки, поставляемые в гемах (rspec и cucumber).

Несмотря на то, как легко добавить автоматические тесты в ваше приложение, я был весьма удивлен, когда мне случалось присоединяться к проектам, где буквально не было ни одного написанного теста (или в лучшем случае, очень мало). В то время, как можно спорить насчет того, какими всеохватывающими тесты должны быть, совершенно ясно, что по крайней мере некоторые автоматические тесты должны присутствовать в каждом приложении.

Общее правило: должен быть как минимум один высокоуровневый интеграционный текст для каждого действия ваших контроллеров. В какой-то момент будущего другие разработчики захотят расширить или модифицировать код, или проапгредить версию Ruby on Rails, а тест-фреймворк позволит им убедиться, что базовая функциональность приложения работает. Дополнительным преимуществом будет то, что это дает будущим разработчикам четкое понимание полного набора функционала приложения.

Ошибка 8. Блокирование обращений ко внешним сервисам

Сторонние сервисы в Rails-сервисах как правило очень легко интегрируется в приложение с помощью гемов, которые содержат их API. Но что произойдет, если ваш внешний сервис начнет сбоить или работать очень медленно?

Чтобы избежать блокировки таких запросов вместо прямого обращения к ним внутри вашего приложения, вам нужно перевести их в фоновый режим, если это возможно. Некоторые популярные гемы:

Delayed job

Resque

Sidekiq

В случаях, когда невозможно или неосуществимо перевести обработку в фоновый режим, вы должны убедиться, что ваше приложение достаточно хорошо обрабатывает ошибки и сбои — если внешний сервис “лежит” или испытывает проблемы. Вам также нужно протестировать ваше приложение без внешних сервисов (возможно, путем удаления сервера, на котором работает ваше приложение, из сети), чтобы убедиться, что это не приводит к непредвиденным последствиям.

Ошибка 9. Getting married to existing database migrations

Механизм миграции базы данных в Rails позволяет создавать команды, которые автоматически добавляют или удаляют таблицы и строки. Так как файлы, который содержат миграции, названы по порядку, вы можете проиграть их с самого начала, чтобы привести пустую БД к тому же виду, что БД на продакшне. Таким образом, это отличный способ управлять гранулированными изменениями в схеме БД вашего приложения, чтобы избежать проблем с Rails.

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

Rails создает представление вашей текущей схемы в файле db/schema.rb (по умолчанию), который обычно обновляется, когда запускается миграция. Файл schema.rb даже может быть сгенерирован, когда нет текущих миграций, через выполнение команды rake db:schema:dump. Частая ошибка — проверить новую миграцию в вашем исходном репозитории, но не обновить файл schema.rb.

Когда миграции выполняются слишком долго, разработчики не должны бояться очистить старую директорию миграции, сделать дамп новой схемы и продолжить с ней. Настройка новой среды разработки потребовала бы выполнения rake db:schema:load, а не rake db:migrate, на которую полагается большинство разработчиков.

Некоторые из этих вопросов также рассматриваются в руководстве по Rails.

Ошибка 10. Проверка конфиденциальной информации в репозитории исходного кода

Фреймворк Rails позволяет легко создавать безопасные приложения, защищающие от многих видов атак. Кое-что достигается с помощью секретного токена, который позволяет получить доступ к сессии браузера. Даже если этот токен хранится в config/secrets.yml, и этот файл считывает токен из окружения, отличного от продакшн-серверов — в последние версии Rails токен включен в config/initializers/secret_token.rb. Этот файл часто по ошибке включают в репозиторий вместе с остальной частью вашего приложения. Если это произойдет, тот, кто получит доступ в репозиторий, получит доступ к пользователям вашего приложения.

Таким образом, вы должны убедиться, что ваш файл конфигурации репозитория (например, .gitignore для пользователей git) исключает файл с токеном. Тогда ваши продуктовые сервера смогут забрать свой токен из окружения, отличного от них, или из механизма, как тот, что поставляет dotenv gem.

Итого

Rails — мощный фреймворк, который скрывает много нелицеприятных деталей, необходимых для построения надежного приложения. И хотя Rails-приложения намного быстрее, разработчики должны обратить внимание на потенциальные ошибки в коде и проектировании, чтобы убедиться, что их приложение легко расширяемое и его легко поддерживать.

Разработчики также должны быть в курсе проблем, которые могут сделать их приложения медленными, менее надежными или менее безопасными. Важно изучить основы фреймворка и убедиться, что вы полностью понимаете архитектуру, дизайн, код — на протяжении всего процесса разработки. Это позволит сделать качественное приложение с высокой производительностью.

По материалам с сайтов: 

mkechinov.ru

toptal.com


Назад