Статьи

10 лучших практик Ruby on Rails

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

Дорога к руинам

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

Путь к славе

Название говорит само за себя: лучшие практики. Они являются лучшими и наиболее широко используются по причине. Вот некоторые преимущества:

  1. Ремонтопригодность
  2. читабельность
  3. утонченность
  4. Ускоренное развитие
  5. СУХОЙ код

Давайте начнем.

Следуйте рекомендациям по стилю сообщества Ruby on Rails:

На каждом языке программирования мы видели как уродливый, так и красивый код. Стиль кода варьируется от человека к человеку, что может привести к задержке при привлечении новых разработчиков. Очень важно иметь руководство по стилю, управляемому сообществом, так как оно играет жизненно важную роль в обеспечении согласованного стиля в кодовой базе. Проекты, как правило, состоят из небольших и больших команд, в которых участвуют люди с различным опытом и стилями программирования. Следование руководству по стилю сообщества Ruby — моя лучшая практика # 1, и вот некоторые из стилевых предпочтений, которые я действительно хотел бы подчеркнуть:

Два космических отступа

Это одно из наиболее широко адаптированных и согласованных руководств по стилю в сообществе Ruby. Используйте 2 пробела вместо 4 пробела. Давайте посмотрим на пример:

4 пробела

def some_method some_var = true if some_var do_something else do_something_else end end 

Отступ в 2 пробела

 def some_method some_var = true if some_var do_something else do_something_else end end 

Последний намного чище и читабельнее. Кроме того, это будет более очевидно в большом файле с большим количеством отступов.

Определите методы предиката с помощью ?

В Ruby у нас есть соглашение для методов, которое возвращает true или false . Эти методы также известны как методы предикатов, и условием является конец имени с вопросительным знаком ( ? ). В большинстве языков программирования вы увидите методы или имена переменных, определенные как is_valid или is_paid и т. Д. Ruby не одобряет этот стиль и поощряет их в более человеческом языке, например, object.valid? или fee.paid? (также обратите внимание, что префикс is_ отсутствует), поддерживая идиоматичность и читаемость Ruby.

Итерация: используйте each вместо

Почти все программисты на Ruby используют each вместо for итерации коллекции. Это просто более читабельно.

* за *

 for i in 1..100 ... end 

* каждый *

 (1..100).each do |i| ... end 

Видеть?

Условия: используйте, unless вместо !if :

Если вы обнаружите, что используете оператор if с отрицательным условием, то есть:

 if !true do_this end 

или

 if name != "sarmad" do_that end 

тогда вы должны использовать эксклюзив Руби, unless . Как это:

 unless true do_this end 

или

 unless name == "sarmad" do_that end 

Опять же, речь идет о читабельности. Однако, если вам нужно задействовать else в вашем условном выражении, никогда не используйте.

Плохо

 unless user.save #throw error else #return success end 

Хорошо

 if user.save #return success else #throw error end 

Короткие замыкания**

Короткое замыкание — это термин, используемый для ранних выходов из методов при определенных условиях. Рассмотрим этот пример:

 if user.gender == "male" && user.age > 17 do_something elsif user.gender == "male" && user.age < 17 && user.age > 5 do_something_else elsif user.age < 5 raise StandardError end 

В этом случае необходимо пройти все условия, чтобы выяснить, не достиг ли пользователя возраст менее 5 лет, и выдвинуть исключение. Предпочтительным способом является:

 raise StandardError if user.age < 5 if user.gender == "male" && user.age > 17 do_something elsif user.gender == "male" && user.age < 17 #we saved a redundant check here do_something_else end 

Более эффективно возвращаться рано, когда выполняется определенное условие.

Совет: я настоятельно рекомендую вам подробно ознакомиться с руководствами по стилю здесь (Ruby) и здесь (Rails) .

Написать тесты

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

Давайте перечислим некоторые преимущества тестирования:

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

СУХОЙ (не повторяй себя)

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

Используйте абстрактные классы : представьте, что у вас есть эти два класса:

 class Mercedes def accelerate "60MPH in 5 seconds" end def apply_brakes "stopped in 4 seconds" end def open_boot "opened" end def turn_headlights_on "turned on" end def turn_headlights_off "turned off" end end class Audi def accelerate "60MPH in 6.5 seconds" end def apply_brakes "stopped in 3.5 seconds" end def open_boot "opened" end def turn_headlights_on "turned on" end def turn_headlights_off "turned off" end end 

У нас есть три дублирующих метода open_boot , turn_headlights_on и turn_headlights_off . Мы не будем обсуждать, почему у нас не должно быть дублирования кода в этом посте, вы можете прочитать об этом здесь . На данный момент это просто против принципа СУХОЙ. Лучшей практикой здесь является использование наследования классов и / или абстрактных классов. Давайте перепишем наш класс, чтобы решить проблему.

 class Car # Uncomment the line below if you want this class to be uninstantiable # ie you can't make an instance of this class. # You can only inherit other classes from it. # self.abstract = true def open_boot "opened" end def turn_headlights_on "turned on" end def turn_headlights_off "turned off" end end class Mercedes < Car def accelerate "60MPH in 5 seconds" end def apply_brakes "stopped in 4 seconds" end end class Audi < Car def accelerate "60MPH in 6.5 seconds" end def apply_brakes "stopped in 3.5 seconds" end end 

Есть идея? Намного лучше!

Используйте Модули
Модули, с другой стороны, являются гибким способом обмена поведением между классами. Причины использования модулей (композиции) вместо наследования выходят за рамки этого поста. Достаточно сказать, что модули — это «есть», а наследование — это «есть» отношение между классами и поведением:

 class Newspaper def headline #code end def sports_news #code end def world_news #code end def price #code end end class Book def title #code end def read_page(page_number) #code end def price #code end def total_pages #code end end 

Допустим, нам нужно добавить метод print в оба класса без дублирования кода. Используйте модуль, например так:

 module Printable def print #code end end 

Измените классы, чтобы они включали этот модуль:

 class Newspaper #This wil add the module's methods as instance methods to this class include Printable def headline #code end def sports_news #code end def world_news #code end def price #code end end class Book #This wil add the module's methods as instance methods to this class include Printable def title #code end def read_page(page_number) #code end def price #code end def total_pages #code end end 

Это очень мощная и полезная техника. Мы также можем добавить методы модуля в качестве методов класса, используя extend Printable вместо include Printable .

Умное использование Enums

Допустим, у вас есть модель с именем Book и столбец / поле, в котором вы хотите сохранить статус черновика, завершенного или опубликованного. Вы обнаружите, что делаете что-то вроде этого:

 if book.status == "draft" do_something elsif book.status == "completed" do_something elsif book.status == "published" do_something end 

или

 if book.status == 0 #draft do_something elsif book.status == 1 #completed do_something elsif book.status == 2 #published do_something end 

Вы должны посмотреть на Enums! Определите столбец status как целое число, в идеале не нуль ( null: false ), а значение по умолчанию для статуса, который ваша модель должна иметь после создания, т.е. Теперь определите перечисления в вашей модели следующим образом:

 enum status: { draft: 0, completed: 1, published: 2 } 

Теперь вы можете переписать код как:

 if book.draft? do_something elsif book.completed? do_something elsif book.published? do_something end 

Выглядит отлично, не так ли? Это не только дает вам эти методы предикатов с именами, но также дает вам методы для переключения между определенными состояниями.

  • book.draft!
  • book.completed!
  • book.published!

Эти методы будут переключать статус, соответствующий методу. Какой элегантный инструмент иметь в своем арсенале.

Жирные Модели, Тощие Контроллеры и Проблемы

Еще одна лучшая практика — не допускать логику, не связанную с ответом, из контроллеров Примерами кода, который вам не нужен в контроллере, является любая бизнес-логика или логика изменения постоянства / модели. Например, у кого-то может быть такой контроллер:

 class BooksController < ApplicationController before_action :set_book, only: [:show, :edit, :update, :destroy, :publish] # code omitted for brevity def publish @book.published = true pub_date = params[:publish_date] if pub_date @book.published_at = pub_date else @book.published_at = Time.zone.now end if @book.save # success response, some redirect with a flash notice else # failure response, some redirect with a flash alert end end # code omitted for brevity private # Use callbacks to share common setup or constraints between actions. def set_book @book = Book.find(params[:id]) end # code omitted for brevity end 

Давайте вместо этого переместим эту сложную логику в соответствующую модель:

 class Book < ActiveRecord::Base def publish(publish_date) self.published = true if publish_date self.published_at = publish_date else self.published_at = Time.zone.now end save end end class BooksController < ApplicationController before_action :set_book, only: [:show, :edit, :update, :destroy, :publish] # code omitted for brevity def publish pub_date = params[:publish_date] if @book.publish(pub_date) # success response, some redirect with a flash notice else # failure response, some redirect with a flash alert end end # code omitted for brevity private # Use callbacks to share common setup or constraints between actions. def set_book @book = Book.find(params[:id]) end # code omitted for brevity end 

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

  • Контроллеры должны только делать простые запросы к модели. Сложные запросы должны быть перемещены в модели и разбиты в многократно используемых областях . Контроллеры должны в основном содержать обработку запросов и логику, связанную с ответами.

  • Любой код, который не связан с запросом и ответом и непосредственно связан с моделью, должен быть перенесен в эту модель.

  • Любой класс, который представляет структуру данных, должен помещаться в каталог app / models как модель не-ActiveRecord (класс без таблиц).

  • Используйте PORO (Plain Old Ruby Objects) классы Ruby, когда логика принадлежит конкретному домену (печать, библиотека и т. Д.) И не соответствует контексту модели (ActiveRecord или Non-ActiveRecord). Вы можете поместить эти классы в app / models / some_directory / . Все, что находится внутри каталога app /, автоматически загружается при запуске приложения, поскольку оно включается в путь автозагрузки Rails. PORO также можно размещать в каталогах app / models / беспокойства и приложения / контроллеры / беспокойства .

  • Поместите ваши PORO, Модули или Классы в каталог lib /, если они не зависят от приложения и могут использоваться с другими приложениями.

  • Используйте модули, если вам нужно извлечь общие функциональные возможности из других несвязанных функциональных возможностей. Вы можете поместить их в каталог app / * и в каталог lib /, если они не зависят от приложения.

  • Слой «Сервис» — это еще одно действительно важное место для поддержки vanilla MVC, когда код приложения растет, и становится трудно решить, куда поместить конкретную логику. Представьте, что вам нужен механизм для отправки SMS-уведомлений или уведомлений по электронной почте некоторым подписчикам при публикации книги или push-уведомлений на их устройства. Вы можете создать службу уведомлений в приложении / services / и запустить службу, если вы работаете.

Интернационализация / локализация

Интернационализируйте свое приложение с самого начала. Не оставляй это на потом, иначе оно будет преследовать тебя позже. Хорошие сайты не могут полагаться на один язык, они обычно имеют большую языковую цель. Чем больше тем лучше. Это одна из лучших практик интернационализации наряду с развитием. Вот почему Rails поставляется с жемчужиной I18n , что означает важность интернационализации вашего приложения. Вы можете прочитать больше об этом здесь .

Это дает вам следующее из коробки:

  • Поддержка английского и аналогичных языков из коробки
  • Позволяет легко настраивать и расширять все для других языков

Это позволяет вам установить язык по умолчанию и изменить язык в соответствии с местоположением или предпочтениями пользователя.

Вот краткий пример преобразования не интернационализированного HTML- кода в интернационализированный:

 <h1>Books Listing</h1> <table> <thead> <th>Name</th> <th>Author</th> </thead> <tbody> <td> Some book </td> <td> Some author </td> </tbody> </table 

Файлы в каталоге config / locales используются для интернационализации и автоматически загружаются Rails. Каждое приложение Rails по умолчанию имеет конфигурацию / locales / en.yml . Этот файл отвечает за проведение переводов на английский язык. Если вам нужно добавить переводы для большего количества языков, вы можете просто добавить файлы, соответствующие названию локали с расширением .yml . В этом примере мы будем придерживаться en.yml . Позвольте рефакторингу вышеупомянутого HTML использовать интернационализацию:

 <h1><%= t('.title') %></h1> <table> <thead> <th><%= t('.name') %></th> <th><%= t('.author') %></th> </thead> <tbody> <td> Some book </td> <td> Some author </td> </tbody> </table> 

Теперь поместите содержимое для отображения в файл .yml, чтобы обновленный HTML мог извлечь переводы.

 # config/en.yml en: title: "Books Listing" name: "Name" author: "Author" 

Лучшие базы данных

Файл db / schema.rb идет с комментарием вверху, который говорит:

 It's strongly recommended that you check this file into your version control system. 

и

 If you need to create the application database on another system, you should be using db:schema:load, not running all the migrations from scratch. The latter is a flawed and unsustainable approach (the more migrations you'll amass, the slower it'll run and the greater likelihood for issues). 

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

Осторожно! db:schema:load используется только тогда, когда вам нужно создать базу данных приложения в новой системе. Если вы добавляете новые миграции, вы просто должны позволить работе db:migrate . Если вы запустите db:schema:load для существующей и заполненной БД, ваши данные (могут быть производственными данными) будут стерты. Просто запомните эти три простых правила, и все будет в порядке:

  1. Всегда проверяйте schema.rb в вашей системе контроля версий, когда вы добавляете и применяете любые новые миграции.
  2. Используйте db:schema:load при создании базы данных приложения в новой системе.
  3. Используйте db:migrate во всех других случаях, когда вам нужно применить только что добавленные миграции.

Совет: не используйте миграции для добавления данных в БД. Вместо этого используйте db / seeds.rb для этой цели.

Вложенные ресурсы / маршруты

Если у вас есть ресурс, который принадлежит другому ресурсу , то рекомендуется определить маршруты дочернего ресурса, вложенные в маршруты родительского ресурса . Например, если у вас есть ресурс Post и ресурс Comment , и у вас установлены следующие ассоциации моделей:

  • Пост модель имеет много комментариев
  • Модель комментариев принадлежит посту

И ваш файл config / rout.rb выглядит так:

 resources :posts resources :comments 

Это определит ваши маршруты следующим образом:

  • http: // localhost: 3000 / posts
  • http: // localhost: 3000 / posts / 1
  • http: // localhost: 3000 / posts / 1 / edit
  • http: // localhost: 3000 / комментарии
  • http: // localhost: 3000 / comments / 1
  • http: // localhost: 3000 / comments / 1 / edit

Что нормально, но не очень хорошая практика. Мы должны определить маршруты комментариев, вложенные в маршруты Post . Вот как:

 resources :posts do resources :comments end 

Теперь это определит ваши маршруты следующим образом:

  • http: // localhost: 3000 / posts
  • http: // localhost: 3000 / posts / 1
  • http: // localhost: 3000 / posts / 1 / edit
  • http: // localhost: 3000 / posts / 1 / comments
  • http: // localhost: 3000 / posts / 1 / comments / 1
  • http: // localhost: 3000 / posts / 1 / comments / 1 / edit

URL-адреса доступны для чтения и указывают на то, что комментарии принадлежат посту с идентификатором 1 . Однако есть небольшая ошибка: вы должны внести некоторые изменения в использование формы Ruby и помощников по URL. Например, в форме комментария у вас есть это:

 <%= form_for(@comment) do |f| %> <!-- form elements removed for brevity --> <% end %> 

Это нужно будет изменить. В настоящее время требуется новый экземпляр Comment , нам нужно изменить его на что-то вроде этого:

 <%= form_for([@comment.post, @comment]) do |f| %> <!-- form elements removed for brevity --> <% end %> 

Обратите внимание на аргумент, передаваемый в помощнике form_for . Теперь это массив, который содержит родительский ресурс первым, а экземпляр Comment — вторым.

Еще одна вещь, которую нам нужно изменить, это все URL-помощники для комментариев :

  <%= link_to 'Show', comment %> 

Будет изменено так:

 <%= link_to 'Show', [comment.post, comment] %> 

И ваша ссылка шоу будет работать. Давайте посмотрим на ссылку Изменить :

 <%= link_to 'Edit', edit_comment_path(comment) %> 

Это будет изменено так:

 <%= link_to 'Edit', edit_post_comment_path(comment.post, comment) %> 

Обращать внимание! И имя помощника ( edit_post_comment_path ), и аргументы (2 аргумента вместо 1) изменены, чтобы заставить его работать с вложенными ресурсами / маршрутизацией.

Используйте Time.zone.now вместо Time.now

Рекомендуется всегда определять часовой пояс вашего приложения по умолчанию в config / application.rb следующим образом:

 config.time_zone = 'Eastern Time (US & Canada)'`. 

Date.today и Time.now всегда дают локальную дату и время машины в часовом поясе машины. Имеет смысл использовать Time.zone.now и Time.zone.today чтобы обойти конфликты между Time.zone.now Time.zone.today на машинах разработки и производственных серверах.

Не помещайте слишком много логики в представления

Представления — это уровень представления, который не должен содержать логику. Вам следует избегать таких проверок:

 <% if book.published? && book.published_at > 1.weeks.ago %> <span>Recently added</span> <% end %> 

или

 <% if current_user.roles.collect(&:name).include?("admin") || (user == book.owner && book.draft?) %> <%= link_to 'Delete', book, method: :delete, data: { confirm: 'Are you sure?' } %> <% end %> 

Что вы можете сделать, это перенести эту условную проверку на вспомогательный модуль, который находится в приложении / помощниках и автоматически доступен во всех представлениях. Вот пример:

 # app/view/helpers/application_helper.rb module ApplicationHelper def recently_added?(book) book.published? && book.published_at > 1.weeks.ago end # current_user is defined in application controller, which can be # accessed from helper modules & methods def can_delete?(book) current_user.roles.collect(&:name).include?("admin") || (user == book.owner && book.draft?) end end 

Изменение вышеупомянутой разметки вида как:

 <% if recently_added?(book) %> <span>Recently added</span> <% end %> 

и

 <% if can_delete?(book) %> <%= link_to 'Delete', book, method: :delete, data: { confirm: 'Are you sure?' } %> <% end %> 

Есть много других мест, где можно это can_delete? Можно использовать метод, но это всего лишь пример отделения логики от представлений.

Вывод

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

Я уверен, что есть другие практики, которые вы считаете важными. Дайте мне знать ваши любимые в комментариях.