Skip to content

Latest commit

 

History

History
1651 lines (1335 loc) · 64.7 KB

README-ruRU.md

File metadata and controls

1651 lines (1335 loc) · 64.7 KB

Вступление

Ролевые модели важны.
-- Офицер Алекс Мёрфи / Робот-полицейский

Целью этого руководства является распространение набора проверенных практик и стилистических рекомендаций при разработки приложений с помощью Ruby on Rails (для версий 3 и 4). Это руководство дополняет уже существующий сборник: Руби: руководство по стилю оформления.

Некоторые из приведенных здесь рекомендаций будут применимы только к Rails 4.0+.

Вы можете создать копию этого руководства в форматах PDF или HTML при помощи Pandoc.

Переводы данного руководства доступны на следующих языках:

Rails: руководство по стилю оформления

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

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

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

Содержание

Конфигурация

  • Код для инициализации приложения помещайте в директорию config/initializers/. Код в этой директории выполняется при запуске приложения. [ссылка]

  • Для каждого гема записывайте код инициализации в одноименный отдельный файл. Например, carrierwave.rb, active_admin.rb и т.д. [ссылка]

  • Уточните настройки для рабочего (development), тестового (test) и промышленного (production) окружений в соответствующих файлах в директории config/environments/. [ссылка]

    • Укажите добавленные вами ресурсы для комплиляции (при наличии):
    # config/environments/production.rb
    # Скомпилируйте дополнительные ресурсы (application.js, application.css,
    # и прочие не JS/CSS уже добавлены).
    config.assets.precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js )
  • Сохраняйте настройки, которые относятся ко всем окружениям, в файле config/application.rb. [ссылка]

  • Создайте дополнительное окружение staging, которое будет очень схоже с вашим окружением production. [ссылка]

  • Храните любые дополнительные файлы конфиругации в формате YAML в директории config/. [ссылка]

    Начиная с Rails 4.2 файлы конфиругации в YAML можно легко загружать при помощи нового метода config_for:

    Rails::Application.config_for(:yaml_file)

Маршрутизация

  • Если вам требуется добавить дополнительные действия к ресурсу REST (и вы уверены, что это вам абсолютно необходимо), то используйте пути member и collection. [ссылка]

    # плохо
    get 'subscriptions/:id/unsubscribe'
    resources :subscriptions
    
    # хорошо
    resources :subscriptions do
      get 'unsubscribe', on: :member
    end
    
    # плохо
    get 'photos/search'
    resources :photos
    
    # хорошо
    resources :photos do
      get 'search', on: :collection
    end
  • Когда вам нужно определить несколько контекстов маршрутизации при помощи member или collection, используйте альтернативную блочную запись. [ссылка]

    resources :subscriptions do
      member do
        get 'unsubscribe'
        # дополнительные маршруты
      end
    end
    
    resources :photos do
      collection do
        get 'search'
        # дополнительные маршруты
      end
    end
  • Используйте вложенные определения маршрутов, чтобы лучше показать отношения между разными моделями ActiveRecord. [ссылка]

    class Post < ActiveRecord::Base
      has_many :comments
    end
    
    class Comments < ActiveRecord::Base
      belongs_to :post
    end
    
    # routes.rb
    resources :posts do
      resources :comments
    end
  • Если существует необходимость делать несколько уровней вложенности, то следует применять опцию shallow: true. Это оградит пользователя от длинных URL posts/1/comments/5/versions/7/edit и вас от применения длинных наименований вроде edit_post_comment_version.

    resources :posts, shallow: true do
      resources :comments do
        resources :versions
      end
    end
  • Используйте определенные в отдельном пространстве имен маршруты, чтобы объединить связанные действия. [ссылка]

    namespace :admin do
      # Направляет /admin/products/* to Admin::ProductsController
      # (app/controllers/admin/products_controller.rb)
      resources :products
    end
  • Избегайте устаревшей обобщенной формы записи маршрутов. Такие маршруты откроют все действия во всех контроллерах для запросов GET. [ссылка]

    # очень плохо
    match ':controller(/:action(/:id(.:format)))'
  • Избегайте использования #match для определения маршрутов. Эта возможность удалена из Rails 4. [ссылка]

Контроллеры

  • Поддерживайте код контроллеров обозримым, контроллеры должны лишь получать данные для шаблонов и не должны реализовывать бизнес-логику. Вся бизнес-логика вашего приложения должна по определению реализовываться в моделях. [ссылка]

  • Каждое действие в котроллере должно (в идеале) вызывать не более одного другого метода (кроме #find или #new). [ссылка]

  • Старайтесь не передавать более двух переменных из контроллера в шаблон. [ссылка]

  • Controller actions specified in the option of Action Filter should be in lexical scope. The ActionFilter specified for an inherited action makes it difficult to understand the scope of its impact on that action. [ссылка]

    # плохо
    class UsersController < ApplicationController
      before_action :require_login, only: :export
    end
    
    # хорошо
    class UsersController < ApplicationController
      before_action :require_login, only: :export
    
      def export
      end
    end

Rendering

  • Prefer using a template over inline rendering. [link]

    # very bad
    class ProductsController < ApplicationController
      def index
        render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>", type: :erb
      end
    end
    
    # good
    ## app/views/products/index.html.erb
    <%= render partial: 'product', collection: products %>
    
    ## app/views/products/_product.html.erb
    <p><%= product.name %></p>
    <p><%= product.price %></p>
    
    ## app/controllers/foo_controller.rb
    class ProductsController < ApplicationController
      def index
        render :index
      end
    end
  • Prefer render plain: over render text:. [link]

    # bad - sets MIME type to `text/html`
    ...
    render text: 'Ruby!'
    ...
    
    # bad - requires explicit MIME type declaration
    ...
    render text: 'Ruby!', content_type: 'text/plain'
    ...
    
    # good - short and precise
    ...
    render plain: 'Ruby!'
    ...
  • Prefer corresponding symbols to numeric HTTP status codes. They are meaningful and do not look like "magic" numbers for less known HTTP status codes. [link]

    # плохо
    # некоторый код
    render status: 403
    # некоторый код
    
    # хорошо
    # некоторый код
    render status: :forbidden
    # некоторый код

Модели

  • Без зазрений совести используйте модели, не базирующиеся на ActiveRecord. [ссылка]

  • Называйте модели говорящими (но короткими) именами без сокращений. [ссылка]

  • Если вам нужна модель, поддерживающая некоторые аспекты ActiveRecord (например, валидации), без привязки к работе с БД используйте библиотеку ActiveAttr. [ссылка]

    class Message
      include ActiveAttr::Model
    
      attribute :name
      attribute :email
      attribute :content
      attribute :priority
    
      attr_accessible :name, :email, :content
    
      validates :name, presence: true
      validates :email, format: { with: /\A[-a-z0-9_+\.]+\@([-a-z0-9]+\.)+[a-z0-9]{2,4}\z/i }
      validates :content, length: { maximum: 500 }
    end

    Более подробный пример (на английском языке) вы найдете здесь: RailsCast #326: ActiveAttr.

  • Не расширяйте свои модели методами, которые реализуют форматирование данных (например, кодогенерацию HTML), кроме случаев, когда это напрямую связано с бизнес-логикой описываемой предметной области. Такие методы с большой вероятностью будут вызываться только из шаблонов представлений, поэтому их лучше разместить во вспомогательных модулях (helpers). Реализуйте в моделях только бизнес-логику и функционал работы с данными. [link]

ActiveRecord

  • Не меняйте стандартных значений ActiveRecord (например, наименования таблиц, первичных ключей и т.д.) без особой нужды. Это оправдано лишь в тех случаях, когда вы работаете с базой данных, схему которой (по разным причинам) нет возможности изменить. [ссылка]

    # плохо (не делайте так, если вы можете изменить схему)
    class Transaction < ActiveRecord::Base
      self.table_name = 'order'
      ...
    end
  • Группируйте макро-методы (has_many, validates и т.д.) в начале определения класса. [ссылка]

    class User < ActiveRecord::Base
      # записывайте стандартную область видимости в начале (если имеется)
      default_scope { where(active: true) }
    
      # после этого записывайте константы
      COLORS = %w(red green blue)
    
      # далее следуют макросы доступа к атрибутам
      attr_accessor :formatted_date_of_birth
    
      attr_accessible :login, :first_name, :last_name, :email, :password
    
      # Энумераторы для `Rails >= 4` после макросов доступа,
      # предпочтительно использовать новых синтаксис для хешей.
      enum gender: { female: 0, male: 1 }
    
      # за которыми следуют макросы ассоциаций
      belongs_to :country
    
      has_many :authentications, dependent: :destroy
    
      # и макросы валидаций
      validates :email, presence: true
      validates :username, presence: true
      validates :username, uniqueness: { case_sensitive: false }
      validates :username, format: { with: /\A[A-Za-z][A-Za-z0-9._-]{2,19}\z/ }
      validates :password, format: { with: /\A\S{8,128}\z/, allow_nil: true }
    
      # после этого идут функции обратного вызова (callbacks)
      before_save :cook
      before_save :update_username_lower
    
      # оставшиеся макросы (например, Devise) дожны записываться в конце
    
      ...
    end
  • Используйте преимущественно has_many :through вместо has_and_belongs_to_many. Применение ассоциации has_many :through дает вам большую свободу в определении дополнительных атрибутов и задании валидаций на модели объединения. [ссылка]

    # не особо (применяется has_and_belongs_to_many)
    class User < ActiveRecord::Base
      has_and_belongs_to_many :groups
    end
    
    class Group < ActiveRecord::Base
      has_and_belongs_to_many :users
    end
    
    # предпочтительное решение (применяется has_many :through)
    class User < ActiveRecord::Base
      has_many :memberships
      has_many :groups, through: :memberships
    end
    
    class Membership < ActiveRecord::Base
      belongs_to :user
      belongs_to :group
    end
    
    class Group < ActiveRecord::Base
      has_many :memberships
      has_many :users, through: :memberships
    end
  • Используйте self[:attribute] вместо read_attribute(:attribute). [ссылка]

    # плохо
    def amount
      read_attribute(:amount) * 100
    end
    
    # хорошо
    def amount
      self[:amount] * 100
    end
  • Преимущественно используйте self[:attribute] = value вместо write_attribute(:attribute, value). [ссылка]

    # плохо
    def amount
      write_attribute(:amount, 100)
    end
    
    # хорошо
    def amount
      self[:amount] = 100
    end
  • Всегда применяйте новый синтаксис валидаций, так называемые "sexy" validations. [ссылка]

    # плохо
    validates_presence_of :email
    validates_length_of :email, maximum: 100
    
    # хорошо
    validates :email, presence: true, length: { maximum: 100 }
  • To make validations easy to read, don't list multiple attributes per validation [link]

    # bad
    validates :email, :password, presence: true
    validates :email, length: { maximum: 100 }
    
    # good
    validates :email, presence: true, length: { maximum: 100 }
    validates :password, presence: true
  • Если определенная разработчиком валидация используется несколько раз или содержит сложные (регулярные) выражения, то стоит ее вынести в отдельный файл валидаторов. [ссылка]

    # плохо
    class Person
      validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i }
    end
    
    # хорошо
    class EmailValidator < ActiveModel::EachValidator
      def validate_each(record, attribute, value)
        record.errors[attribute] << (options[:message] || 'is not a valid email') unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
      end
    end
    
    class Person
      validates :email, email: true
    end
  • Храните файлы определенных вами валидаторов в app/validators. [ссылка]

  • Подумайте о том, чтобы выделить ряд определенных вами валидаторов в отдельный гем, если вы работаете над несколькими схожими приложениями и валидаторы имеют достаточно обобщенные функции. [ссылка]

  • Спокойно применяйте поименованные области поиска (named scopes). [ссылка]

    class User < ActiveRecord::Base
      scope :active, -> { where(active: true) }
      scope :inactive, -> { where(active: false) }
    
      scope :with_orders, -> { joins(:orders).select('distinct(users.id)') }
    end
  • Если поименованная область поиска определяется с помощью lambda с дополнительными параметрами, то такая запись очень быстро может стать слишком сложной. В таком случае лучше определить метод класса, который будет служить той же цели и возвращать объект класса ActiveRecord::Relation. Наверное, этим же образом можно определять и более простые области поиска. [ссылка]

    class User < ActiveRecord::Base
      def self.with_orders
        joins(:orders).select('distinct(users.id)')
      end
    end
  • Order callback declarations in the order, in which they will be executed. For referenece, see Available Callbacks [ссылка]

    # плохо
    class Person
      after_commit/after_rollback :after_commit_callback
      after_save :after_save_callback
      around_save :around_save_callback
      after_update :after_update_callback
      before_update :before_update_callback
      after_validation :after_validation_callback
      before_validation :before_validation_callback
      before_save :before_save_callback
      before_create :before_create_callback
      after_create :after_create_callback
      around_create :around_create_callback
      around_update :around_update_callback
    end
    
    # хорошо
    class Person
      before_validation :before_validation_callback
      after_validation :after_validation_callback
      before_save :before_save_callback
      around_save :around_save_callback
      before_create :before_create_callback
      around_create :around_create_callback
      after_create :after_create_callback
      before_update :before_update_callback
      around_update :around_update_callback
      after_update :after_update_callback
      after_save :after_save_callback
      after_commit/after_rollback :after_commit_callback
    end
  • Поймите принцип работы следующих методов. Они не вызывают валидацию моделей и могут быстро привести к появлению ошибочных записей в базе данных. [ссылка]

    # плохо
    Article.first.decrement!(:view_count)
    DiscussionBoard.decrement_counter(:post_count, 5)
    Article.first.increment!(:view_count)
    DiscussionBoard.increment_counter(:post_count, 5)
    person.toggle :active
    product.touch
    Billing.update_all("category = 'authorized', author = 'David'")
    user.update_attribute(:website, 'example.com')
    user.update_columns(last_request_at: Time.current)
    Post.update_counters 5, comment_count: -1, action_count: 1
    
    # хорошо
    user.update_attributes(website: 'example.com')
  • Используйте дружественную пользователю запись URL. Указывайте в URL какой-то говорящий сам за себя атрибут модели, а не id. Есть несколько путей для достижения такого результата: [ссылка]

    • Переопределите метод to_param в модели. Это метод используется в Rails для генерирования URL к объекту. Стандартная реализация возвращает id объекта (записи) в виде строки. Это поведение можно переопределить и включить некоторый понятный человеку атрибут.

      class Person
        def to_param
          "#{id} #{name}".parameterize
        end
      end

      Чтобы преобразовать эту форму в более адекватную для URL, нужно вызвать метод parameterize на строковом объекте. Идентификатор id объекта должен быть в начале строки, чтобы его мог найти метод find библиотеки ActiveRecord.

    • Используйте гем friendly_id. Эта библиотека создает легко читаемые URL с использованием некоторых говорящих атрибутов моделей вместо id.

      class Person
        extend FriendlyId
        friendly_id :name, use: :slugged
      end

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

  • Используйте find_each для обхода коллекций объектов ActiveRecord. Перебор объектов записей базы данных в цикле (например, с использованием метода all) крайне неэффективен, так как в данном случае в памяти будут созданы все интересующие нас объекты за раз. Для такого рода задач больше подходит пакетная обработка, при которой методы вызывают записи порциями, что значительно сокращает расход памяти. [ссылка]

    # плохо
    Person.all.each do |person|
      person.do_awesome_stuff
    end
    
    Person.where("age > 21").each do |person|
      person.party_all_night!
    end
    
    # хорошо
    Person.all.find_each do |person|
      person.do_awesome_stuff
    end
    
    Person.where("age > 21").find_each do |person|
      person.party_all_night!
    end
  • Rails создает методы обратного вызова для зависимых ассоциаций, поэтому всегда вызывайте метод before_destroy для валидации с параметром prepend: true. [ссылка]

    # плохо (роли будут удалены в любом случае, даже если super_admin? задан)
    has_many :roles, dependent: :destroy
    
    before_destroy :ensure_deletable
    
    def ensure_deletable
      raise "Cannot delete super admin." if super_admin?
    end
    
    # хорошо
    has_many :roles, dependent: :destroy
    
    before_destroy :ensure_deletable, prepend: true
    
    def ensure_deletable
      raise "Cannot delete super admin." if super_admin?
    end
  • Задавайте опцию dependent в ассоциация типа has_many и has_one. [link]

    # плохо
    class Post < ActiveRecord::Base
      has_many :comments
    end
    
    # хорошо
    class Post < ActiveRecord::Base
      has_many :comments, dependent: :destroy
    end
  • When persisting AR objects always use the exception raising bang! method or handle the method return value. This applies to create, save, update, destroy, first_or_create and find_or_create_by. [link]

    # bad
    user.create(name: 'Bruce')
    
    # bad
    user.save
    
    # good
    user.create!(name: 'Bruce')
    # or
    bruce = user.create(name: 'Bruce')
    if bruce.persisted?
      ...
    else
      ...
    end
    
    # good
    user.save!
    # or
    if user.save
      ...
    else
      ...
    end

Запросы ActiveRecord

  • Избегайте интерполяции строк в запросах, это сделает ваш код менее уязвимым к атакам типа SQL injection. [ссылка]

    # плохо (param будет вставлен без экранирования)
    Client.where("orders_count = #{params[:orders]}")
    
    # хорошо (param будет экранирован должным образом)
    Client.where('orders_count = ?', params[:orders])
  • Предпочитайте поименованные подстановки вместо позиционных подстановок, если у вас в запросе их более двух. [ссылка]

    # сойдет
    Client.where(
      'created_at >= ? AND created_at <= ?',
      params[:start_date], params[:end_date]
    )
    
    # хорошо
    Client.where(
      'created_at >= :start_date AND created_at <= :end_date',
      start_date: params[:start_date], end_date: params[:end_date]
    )
  • Отдавайте предпочтение использованию find вместо where(...).take!, find_by!,если вам нужно получить всего одну запись по ее идентификатору. Favor the use offindoverwhere.take!, find_by!, and find_by_id!when you need to retrieve a single record by primary key id and raiseActiveRecord::RecordNotFound` when the record is not found. [ссылка]

    # плохо
    User.where(id: id).take!
    
    # bad
    User.find_by_id!(id)
    
    # bad
    User.find_by!(id: id)
    
    # хорошо
    User.find(id)
  • Отдавайте предпочтение использованию find_by вместо where и find_by_attribute, если вам нужно получить всего одну запись по значению какого-то ее атрибута. [ссылка]

    # плохо
    User.where(first_name: 'Bruce', last_name: 'Wayne').first
    
    # плохо
    User.find_by_first_name_and_last_name('Bruce', 'Wayne')
    
    # хорошо
    User.find_by(first_name: 'Bruce', last_name: 'Wayne')
  • Favor the use of find_by over where.take and find_by_attribute when you need to retrieve a single record by one or more attributes and return nil when the record is not found. [link]

    # bad
    User.where(id: id).take
    User.where(first_name: 'Bruce', last_name: 'Wayne').take
    
    # bad
    User.find_by_id(id)
    # bad, deprecated in ActiveRecord 4.0, removed in 4.1+
    User.find_by_first_name_and_last_name('Bruce', 'Wayne')
    
    # good
    User.find_by(id: id)
    User.find_by(first_name: 'Bruce', last_name: 'Wayne')
  • Используйте where.not вместо простого SQL. [ссылка]

    # плохо
    User.where("id != ?", id)
    
    # хорошо
    User.where.not(id: id)
  • Don't use the id column for ordering. The sequence of ids is not guaranteed to be in any particular order, despite often (incidentally) being chronological. Use a timestamp column to order chronologically. As a bonus the intent is clearer. [link]

    # bad
    scope :chronological, -> { order(id: :asc) }
    
    # good
    scope :chronological, -> { order(created_at: :asc) }
    • Favor the use of ids over pluck(:id). [link]

      # bad
      User.pluck(:id)
      
      # good
      User.ids
  • При явном формулировании запроса в таких методах, как find_by_sql, используйте HEREDOC в сочетании с методом squish. Это позволит вам оформить код SQL читаемым образом с переносами строк и отступами и сохранит подержку подсветки синтаксиса на большинстве платформ (GitHub, Atom, RubyMine). [link]

    User.find_by_sql(<<-SQL.squish)
      SELECT
        users.id, accounts.plan
      FROM
        users
      INNER JOIN
        accounts
      ON
        accounts.user_id = users.id
      # прочие детали...
    SQL

    String#squish удаляет отступы и переносы, таким образом запросы будут отображаться в виде обычных строк, а не в виде вот таких последовательностей:

    SELECT\n    users.id, accounts.plan\n  FROM\n    users\n  INNER JOIN\n    acounts\n  ON\n    accounts.user_id = users.id
    
  • When querying ActiveRecord collections, prefer size (selects between count/length behavior based on whether collection is already loaded) or length (always loads the whole collection and counts the array elements) over count (always does a database query for the count). [link]

    # bad
    User.count
    
    # good
    User.all.size
    
    # good - if you really need to load all users into memory
    User.all.length

Миграции

  • Храните файл schema.rb (или structure.sql) в вашей системе управления версиями. [ссылка]

  • Используйте вызов rake db:schema:load вместо rake db:migrate для инициализации пустой базы данных. [ссылка]

  • Устанавливайте стандартные значения в миграциях, а не в бизнес-логике вашего приложения. [ссылка]

    # плохо (стандартное значение устанавливается в приложении)
     class Product < ActiveRecord::Base
      def amount
        self[:amount] || 0
      end
    end
    
    # хорошо (стандартное значение устанавливается на уровне БД)
    class AddDefaultAmountToProducts < ActiveRecord::Migration
      def change
        change_column_default :products, :amount, 0
      end

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

  • Устанавливайте ограничения на внешние ключи. Начиная с Rails 4.2 библиотека ActiveRecord поддерживает внешние ключи напрямую. [ссылка]

  • При написании миграций для добавления таблиц или столбцов создавайте метод change вместо методов up и down. [ссылка]

    # старый способ
    class AddNameToPeople < ActiveRecord::Migration
      def up
        add_column :people, :name, :string
      end
    
      def down
        remove_column :people, :name
      end
    end
    
    # новый предпочтительный способ
    class AddNameToPeople < ActiveRecord::Migration
      def change
        add_column :people, :name, :string
      end
    end
  • Если вам нужно использовать код модели в миграциях, постарайтесь задать модели так, чтобы не получить в будущем неработающие миграции. [ссылка]

    # db/migrate/<migration_file_name>.rb
    # frozen_string_literal: true
    
    # плохо
    class ModifyDefaultStatusForProducts < ActiveRecord::Migration
      def change
        old_status = 'pending_manual_approval'
        new_status = 'pending_approval'
    
        reversible do |dir|
          dir.up do
            Product.where(status: old_status).update_all(status: new_status)
            change_column :products, :status, :string, default: new_status
          end
    
          dir.down do
            Product.where(status: new_status).update_all(status: old_status)
            change_column :products, :status, :string, default: old_status
          end
        end
      end
    end
    
    # хорошо
    # Задайте `table_name` в специальном классе, чтобы обеспечить единообразие.
    # Вы будете использовать одну и ту же таблицу в любое время после ее создания.
    # В будущем после изменения класса `Product`
    # и изменений в `table_name` миграция не перестанет работать и не произойдет
    # серьезное повреждение данных.
    class MigrationProduct < ActiveRecord::Base
      self.table_name = :products
    end
    
    class ModifyDefaultStatusForProducts < ActiveRecord::Migration
      def change
        old_status = 'pending_manual_approval'
        new_status = 'pending_approval'
    
        reversible do |dir|
          dir.up do
            MigrationProduct.where(status: old_status).update_all(status: new_status)
            change_column :products, :status, :string, default: new_status
          end
    
          dir.down do
            MigrationProduct.where(status: new_status).update_all(status: old_status)
            change_column :products, :status, :string, default: old_status
          end
        end
      end
    end
  • Явно выбирайте наименования для внешних ключей (foreign key), не полагайтесь на автоматически сгенерированные имена ключей: Foreign Keys. [link]

    # плохо
    class AddFkArticlesToAuthors < ActiveRecord::Migration
      def change
        add_foreign_key :articles, :authors
      end
    end
    
    # хорошо
    class AddFkArticlesToAuthors < ActiveRecord::Migration
      def change
        add_foreign_key :articles, :authors, name: :articles_author_id_fk
      end
    end
  • Не используйте необратимые методы миграций в методе change. Обратимые методы можно найти в списке ниже: ActiveRecord::Migration::CommandRecorder [link]

    # плохо
    class DropUsers < ActiveRecord::Migration
      def change
        drop_table :users
      end
    end
    
    # хорошо
    class DropUsers < ActiveRecord::Migration
      def up
        drop_table :users
      end
    
      def down
        create_table :users do |t|
          t.string :name
        end
      end
    end
    
    # хорошо
    # В этом случае при откате будет использоваться блок в `create_table`
    # https://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters.html#method-i-drop_table
    class DropUsers < ActiveRecord::Migration
      def change
        drop_table :users do |t|
          t.string :name
        end
      end
    end

Представления

  • Ни при каких условиях не следует работать с моделями напрямую из представлений. [ссылка]

  • Избегайте сложной логики в представлениях, выделяйте этот код во вспомогательные методы представлений (view helpers) или выносите их в модель. [ссылка]

  • Избегайте повторений кода, используйте отдельные шаблоны и подшаблоны представлений. [ссылка]

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

  • Строки и другие локальные настройки и детали следует выносить из представлений и контроллеров, а также моделей в файлы для конкретных локалей в директории config/locales. [ссылка]

  • Когда вам нужно перевести идентификаторы для моделей ActiveRecord, применяйте контекст activerecord: [ссылка]

    en:
      activerecord:
        models:
          user: Member
        attributes:
          user:
            name: 'Full name'
    

    В этом случае User.model_name.human вернет 'Member' и User.human_attribute_name('name') вернет 'Full name'. Переводы этих атрибутов будут использоваться в качестве идентификаторов в представлениях.

  • Разделяйте файлы с переводами представлений от переводов атрибутов ActiveRecord. Размещайте файлы локалей для моделей в директории locales/models, а используемые в представлениях тексты в директории locales/views. [ссылка]

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

      # config/application.rb
      config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}')]
  • Размещайте общие параметры локализации, например, форматы записи дат и валют, в файлах в корне директории locales. [ссылка]

  • Используйте краткую форму методов I18n: I18n.t вместо I18n.translate и I18n.l вместо I18n.localize. [ссылка]

  • Используйте "ленивый" подход к поиску текстов в представлениях. Допустим, у вас есть следующая структура: [ссылка]

    en:
      users:
        show:
          title: 'User details page'
    

    Значение для users.show.title можно будет найти в шаблоне app/views/users/show.html.haml, например, так:

    = t '.title'
  • Задавайте ключи в контроллерах и моделях при помощи разделения точками, а не с помощью опции :scope. Разделенные точками вызовы проще читать, их иерархия более понятна. [ссылка]

    # плохо
    I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]
    
    # хорошо
    I18n.t 'activerecord.errors.messages.record_invalid'
  • Более подробную информацию по интернационализации (I18n) в Rails можно найти по адресу API интернационализации Rails либо Rails Guides (английский оригинал). [ссылка]

Ресурсы

Применяйте конвейер ресурсов (assets pipeline) для упорядочения структуры вашего приложения.

  • Зарезервируйте директорию app/assets для ваших собственных таблиц стилей, скриптов и/или изображений. [ссылка]

  • Используйте директорию lib/assets для ваших собственных библиотек, которые не реализуют первичный функционал вашего приложения. [ссылка]

  • Код сторонних библиотек, например, jQuery или bootstrap, следует размещать в директории vendor/assets. [ссылка]

  • По возможности используйте упакованные версии необходимых ресурсов, например: [ссылка]

Почтовые модули

  • Называйте почтовые модули по образцу SomethingMailer. Без суффикса Mailer не сразу будет понятно, что это почтовый модуль и какие представления связаны с этим модулем. [ссылка]

  • Создавайте шаблоны представлений в текстовом формате и в формате HTML. [ссылка]

  • Включите вызов ошибок при проблемах с доставкой почты в вашем окружении для разработки. По умолчанию эти вызовы отключены. [ссылка]

    # config/environments/development.rb
    
    config.action_mailer.raise_delivery_errors = true
  • Используйте локальный сервер SMTP, например, Mailcatcher в вашем окружении разработки. [ссылка]

    # config/environments/development.rb
    
    config.action_mailer.smtp_settings = {
      address: 'localhost',
      port: 1025,
      # more settings
    }
  • Указывайте стандартные настройки имени вашего узла. [ссылка]

    # config/environments/development.rb
    config.action_mailer.default_url_options = { host: "#{local_ip}:3000" }
    
    # config/environments/production.rb
    config.action_mailer.default_url_options = { host: 'your_site.com' }
    
    # в вашем классе мейлера
    default_url_options[:host] = 'your_site.com'
  • Если вам нужно указать ссылку на ваш сайт в тексте письма, всегда используйте методы с суффиксом _url, а не _path. Методы с суффиксом _url включают имя вашего узла в текст ссылки, а с суффиксом _path не включают. [ссылка]

    # плохо
    You can always find more info about this course
    <%= link_to 'here', course_path(@course) %>
    
    # хорошо
    You can always find more info about this course
    <%= link_to 'here', course_url(@course) %>
  • Записывайте адреса отправителя и получателя должным образом. Используйте следующий формат: [ссылка]

    # в классе мейлера
    default from: 'Your Name <info@your_site.com>'
  • Убедитесь в том, что метод доставки писем в вашем тестовом окружении обозначен как test: [ссылка]

    # config/environments/test.rb
    
    config.action_mailer.delivery_method = :test
  • Методом доставки почты для разработки и развертывания должен быть smtp: [ссылка]

    # config/environments/development.rb, config/environments/production.rb
    
    config.action_mailer.delivery_method = :smtp
  • При рассылке электронной почты в формате HTML все описания стилей должны быть включены в текст, потому что некоторые почтовые программы неверно обрабатывают внешние таблицы стилей. Однако, это приводит к сложностям в поддержке таких таблиц и повторениям в коде. Для преобразования и внедрения стилей в текст письма существую два схожим по функциональности гема: premailer-rails и roadie. [ссылка]

  • Избегайте рассылки почты параллельно к генерации страницы в ответ на запрос пользователя. Это вызовет задержки в загрузке страницы, и запрос может быть отклонен из-за превышения таймаута, если вы рассылаете много писем. Вы можете преодолеть данное ограничение, вызывая процессы рассылки в фоне, например, при помощи гема sidekiq. [ссылка]

Active Support Core Extensions

  • Prefer Ruby 2.3's safe navigation operator &. over ActiveSupport#try!. [link]

    # bad
    obj.try! :fly
    
    # good
    obj&.fly
  • Prefer Ruby's Standard Library methods over ActiveSupport aliases. [link]

    # bad
    'the day'.starts_with? 'th'
    'the day'.ends_with? 'ay'
    
    # good
    'the day'.start_with? 'th'
    'the day'.end_with? 'ay'
  • Prefer Ruby's Standard Library over uncommon ActiveSupport extensions. [link]

    # bad
    (1..50).to_a.forty_two
    1.in? [1, 2]
    'day'.in? 'the day'
    
    # good
    (1..50).to_a[41]
    [1, 2].include? 1
    'the day'.include? 'day'
  • Prefer Ruby's comparison operators over ActiveSupport Array#inquiry, Numeric#inquiry and String#inquiry. [link]

    # bad - String#inquiry
    ruby = 'two'.inquiry
    ruby.two?
    
    # good
    ruby = 'two'
    ruby == 'two'
    
    # bad - Array#inquiry
    pets = %w(cat dog).inquiry
    pets.gopher?
    
    # good
    pets = %w(cat dog)
    pets.include? 'cat'

Время

  • Настройте в файле application.rb вашу временную зону. [ссылка]

    config.time_zone = 'Eastern European Time'
    # опционально (обратите внимание, возможны только значения :utc или :local,
    # по умолчанию :utc)
    config.active_record.default_timezone = :local
  • Не используйте Time.parse. [ссылка]

    # плохо
    # Подразумевается, что передаваемая строка со временем отражает временную зону вашей ОС.
    Time.parse('2015-03-02 19:05:37')
    
    # хорошо
    Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00
  • Не используйте String#to_time [ссылка]

    # плохо (временная зона по умолчанию равна системной)
    '2015-03-02 19:05:37'.to_time
    
    # хорошо
    Time.zone.parse('2015-03-02 19:05:37') # => Mon, 02 Mar 2015 19:05:37 EET +02:00
  • Не используйте Time.now. [ссылка]

    # плохо
    # Возвращает системное время и не учитывает настройки временной зоны.
    Time.now
    
    # хорошо
    Time.zone.now # => Fri, 12 Mar 2014 22:04:47 EET +02:00
    Time.current # Более короткая форма записи.

Bundler

  • Держите гемы, которые используются только для разработки и/или тестирования в соответствующих группах вашего Gemfile. [ссылка]

  • Применяйте в своих проектах только проверенные временем библиотеки. Если вы задумываетесь, не включить ли в проект малоизвестный гем, вам следует сперва внимательно просмотреть его исходных код. [ссылка]

  • Системозависимые библиотеки в вашем проекте будут причиной постоянного изменения файла Gemfile.lock для проектов с несколькими разработчиками, работающими на разных операционных системах. Поэтому внесите все библиотеки, написанные для OS X, в группу darwin, а библиотеки, написанные для Linux, в группу linux. [ссылка]

    # Gemfile
    group :darwin do
      gem 'rb-fsevent'
      gem 'growl'
    end
    
    group :linux do
      gem 'rb-inotify'
    end

    Для включения нужных библиотек только в нужном окружении, добавьте в файл config/application.rb следующие строки:

    platform = RUBY_PLATFORM.match(/(linux|darwin)/)[0].to_sym
    Bundler.require(platform)
  • Сохраняйте файл Gemfile.lock в вашей системе управления версиями. Этот файл содержит неслучайную информацию. Так вы сможете удостовериться, что все члены вашей команды установят точно те же версии библиотек, что и вы, при помощи команды bundle install. [ссылка]

Управление процессами

  • Если в вашем проекте есть много зависимостей от внешних процессов, применяйте библиотеку foreman для управления ими. [ссылка]

Дополнительные источники

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

Сотрудничество

Ничто, описанное в этом руководстве, не высечено в камне. И я очень хотел бы сотрудничать со всеми, кто интересуется стилистикой оформления кода Rails, чтобы мы смогли вместе создать ресурс, который был бы полезен для всего сообщества программистов на Руби.

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

Вы можете поддержать проект (и РубоКоп) денежным взносом при помощи Patreon.

Как сотрудничать в проекте?

Это просто! Просто следуйте руководству по сотрудничеству.

Лицензирование

Creative Commons License Данная работа опубликована на условиях лицензии Creative Commons Attribution 3.0 Unported License

Расскажи другому

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

Всего,
Божидар