Божидар Бацов.
Образцы для подражания важны.
— Офицер Алекс Дж. Мерфи / Робокоп
Кончик | Вы можете найти красивую версию этого руководства с значительно улучшенной навигацией по адресу https://rails.rubystyle.guide. |
Цель этого руководства — представить набор лучших практик и рецептов стиля для разработки Ruby on Rails. Это дополнительное руководство к уже существующему руководству по стилю кодирования Ruby, созданному сообществом.
Это руководство по стилю Rails рекомендует лучшие практики, позволяющие реальным программистам Rails писать код, который смогут поддерживать другие реальные программисты Rails. Руководство по стилю, отражающее реальное использование, привыкает, а руководство по стилю, которое придерживается идеала, отвергнутого людьми, которым оно должно помочь, рискует вообще не быть использованным - каким бы хорошим оно ни было.
Руководство разделено на несколько разделов связанных правил. Я попытался добавить обоснование правил (если оно опущено, я предполагал, что это довольно очевидно).
Я не придумал все правила на пустом месте — они в основном основаны на моей обширной карьере профессионального инженера-программиста, отзывах и предложениях членов сообщества Rails и различных высоко оцененных ресурсах по программированию Rails.
Примечание | Некоторые советы здесь применимы только к последним версиям Rails. |
Вы можете создать копию этого руководства в формате PDF с помощью AsciiDoctor PDF и копию HTML с помощью AsciiDoctor, используя следующие команды:
# Generates README.pdf
asciidoctor-pdf -a allow-uri-read README.adoc
# Generates README.html
asciidoctor README.adoc
Кончик | Установите gem install rouge |
Переводы руководства доступны на следующих языках:
японский
Русский
Кончик | RuboCop, статический анализатор кода (линтер) и форматтер, имеет расширение rubocop-rails , основанное на этом руководстве по стилю. |
Поместите собственный код инициализации в config/initializers
. Код в инициализаторах выполняется при запуске приложения.
Храните код инициализации для каждого гема в отдельном файле с тем же именем, что и у гема, например, carrierwave.rb
, active_admin.rb
и т. д.
Отрегулируйте соответствующим образом настройки среды разработки, тестирования и производственной среды (в соответствующих файлах в разделе config/environments/
).
Отметьте дополнительные ресурсы для предварительной компиляции (если есть):
# config/environments/production.rb
# Precompile additional assets (application.js, application.css,
#and all non-JS/CSS are already added)
config . assets . precompile += %w( rails_admin/rails_admin.css rails_admin/rails_admin.js )
Сохраните конфигурацию, применимую ко всем средам, в файле config/application.rb
.
При обновлении до более новой версии Rails настройки конфигурации вашего приложения останутся прежними. Чтобы воспользоваться последними рекомендуемыми практиками Rails, параметр config.load_defaults
должен соответствовать вашей версии Rails.
# good
config . load_defaults 6.1
Избегайте создания дополнительных конфигураций среды, отличных от настроек по умолчанию для development
, test
и production
. Если вам нужна среда, подобная производственной, например промежуточная, используйте переменные среды для параметров конфигурации.
Любую дополнительную конфигурацию храните в файлах YAML в каталоге config/
.
Начиная с Rails 4.2 файлы конфигурации YAML можно легко загрузить с помощью нового метода config_for
:
Rails :: Application . config_for ( :yaml_file )
Когда вам нужно добавить дополнительные действия к ресурсу RESTful (они вам вообще нужны?), используйте маршруты member
и collection
.
# bad
get 'subscriptions/:id/unsubscribe'
resources :subscriptions
# good
resources :subscriptions do
get 'unsubscribe' , on : :member
end
# bad
get 'photos/search'
resources :photos
# good
resources :photos do
get 'search' , on : :collection
end
Если вам нужно определить несколько маршрутов member/collection
используйте альтернативный синтаксис блоков.
resources :subscriptions do
member do
get 'unsubscribe'
# more routes
end
end
resources :photos do
collection do
get 'search'
# more routes
end
end
Используйте вложенные маршруты, чтобы лучше выразить связь между моделями Active Record.
class Post < ApplicationRecord
has_many :comments
end
class Comment < ApplicationRecord
belongs_to :post
end
# routes.rb
resources :posts do
resources :comments
end
Если вам нужно вкладывать маршруты глубиной более 1 уровня, используйте опцию shallow: true
. Это избавит пользователя от длинных URL-адресов posts/1/comments/5/versions/7/edit
а вас от помощников по длинным URL-адресам edit_post_comment_version
.
resources :posts , shallow : true do
resources :comments do
resources :versions
end
end
Используйте маршруты в пространстве имен для группировки связанных действий.
namespace :admin do
# Directs /admin/products/* to Admin::ProductsController
# (app/controllers/admin/products_controller.rb)
resources :products
end
Никогда не используйте устаревший маршрут дикого контроллера. Этот маршрут сделает все действия в каждом контроллере доступными через запросы GET.
# very bad
match ':controller(/:action(/:id(.:format)))'
Не используйте match
для определения каких-либо маршрутов, если только нет необходимости сопоставить несколько типов запросов среди [:get, :post, :patch, :put, :delete]
с одним действием, используя опцию :via
.
Сохраняйте контроллеры «тонкими» — они должны получать данные только для уровня представления и не должны содержать никакой бизнес-логики (вся бизнес-логика, естественно, должна находиться в модели).
Каждое действие контроллера должно (в идеале) вызывать только один метод, кроме начального поиска или нового.
Минимизируйте количество переменных экземпляра, передаваемых между контроллером и представлением.
Действия контроллера, указанные в опции «Фильтр действий», должны находиться в лексической области видимости. ActionFilter, указанный для унаследованного действия, затрудняет понимание масштаба его влияния на это действие.
# bad
class UsersController < ApplicationController
before_action :require_login , only : :export
end
# good
class UsersController < ApplicationController
before_action :require_login , only : :export
def export
end
end
Предпочитайте использование шаблона, а не встроенный рендеринг.
# 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/products_controller.rb
class ProductsController < ApplicationController
def index
render :index
end
end
Предпочитайте render plain:
вместо render text:
.
# 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!'
...
Предпочитайте соответствующие символы числовым кодам состояния HTTP. Они имеют смысл и не похожи на «магические» числа для менее известных кодов состояния HTTP.
# bad
...
render status : 403
...
# good
...
render status : :forbidden
...
Свободно внедряйте классы моделей, не относящиеся к Active Record.
Называйте модели осмысленными (но короткими) названиями без сокращений.
Если вам нужны объекты, поддерживающие поведение, подобное ActiveRecord (например, проверки), без функциональности базы данных, используйте ActiveModel::Model
.
class Message
include ActiveModel :: Model
attr_accessor :name , :email , :content , :priority
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
Начиная с Rails 6.1, вы также можете расширить API атрибутов из ActiveRecord, используя ActiveModel::Attributes
.
class Message
include ActiveModel :: Model
include ActiveModel :: Attributes
attribute :name , :string
attribute :email , :string
attribute :content , :string
attribute :priority , :integer
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
Если они не имеют какого-либо значения в бизнес-сфере, не добавляйте в свою модель методы, которые просто форматируют ваши данные (например, генерацию кода HTML). Эти методы, скорее всего, будут вызываться только из уровня представления, поэтому их место — в помощниках. Сохраняйте свои модели только для бизнес-логики и сохранения данных.
Избегайте изменения значений Active Record по умолчанию (имена таблиц, первичный ключ и т. д.), если у вас нет веских причин (например, база данных, которая не находится под вашим контролем).
# bad - don't do this if you can modify the schema
class Transaction < ApplicationRecord
self . table_name = 'order'
...
end
ignored_columns
Избегайте установки ignored_columns
. Он может перезаписать предыдущие назначения, и это почти всегда ошибка. Вместо этого предпочитайте добавление в список.
class Transaction < ApplicationRecord
# bad - it may overwrite previous assignments
self . ignored_columns = %i[ legacy ]
# good - the value is appended to the list
self . ignored_columns += %i[ legacy ]
...
end
Предпочитайте использовать синтаксис хэша для enum
. Массив делает значения базы данных неявными, и любая вставка/удаление/перестановка значений в середине, скорее всего, приведет к неработоспособности кода.
class Transaction < ApplicationRecord
# bad - implicit values - ordering matters
enum type : %i[ credit debit ]
# good - explicit values - ordering does not matter
enum type : {
credit : 0 ,
debit : 1
}
end
Группируйте методы в стиле макроса ( has_many
, validates
и т. д.) в начале определения класса.
class User < ApplicationRecord
# keep the default scope first (if any)
default_scope { where ( active : true ) }
# constants come up next
COLORS = %w( red green blue )
# afterwards we put attr related macros
attr_accessor :formatted_date_of_birth
attr_accessible :login , :first_name , :last_name , :email , :password
# Rails 4+ enums after attr macros
enum role : { user : 0 , moderator : 1 , admin : 2 }
# followed by association macros
belongs_to :country
has_many :authentications , dependent : :destroy
# and validation macros
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 }
# next we have callbacks
before_save :cook
before_save :update_username_lower
# other macros (like devise's) should be placed after the callbacks
...
end
has_many :through
Предпочитайте has_many :through
вместо has_and_belongs_to_many
. Использование has_many :through
позволяет использовать дополнительные атрибуты и проверки модели соединения.
# not so good - using has_and_belongs_to_many
class User < ApplicationRecord
has_and_belongs_to_many :groups
end
class Group < ApplicationRecord
has_and_belongs_to_many :users
end
# preferred way - using has_many :through
class User < ApplicationRecord
has_many :memberships
has_many :groups , through : :memberships
end
class Membership < ApplicationRecord
belongs_to :user
belongs_to :group
end
class Group < ApplicationRecord
has_many :memberships
has_many :users , through : :memberships
end
Предпочитайте self[:attribute]
вместо read_attribute(:attribute)
.
# bad
def amount
read_attribute ( :amount ) * 100
end
# good
def amount
self [ :amount ] * 100
end
Предпочитайте self[:attribute] = value
вместо write_attribute(:attribute, value)
.
# bad
def amount
write_attribute ( :amount , 100 )
end
# good
def amount
self [ :amount ] = 100
end
Всегда используйте проверки «нового стиля».
# bad
validates_presence_of :email
validates_length_of :email , maximum : 100
# good
validates :email , presence : true , length : { maximum : 100 }
При именовании пользовательских методов проверки придерживайтесь простых правил:
validate :method_name
читается как естественный оператор
имя метода объясняет, что он проверяет
метод распознается как метод проверки по его имени, а не как метод предиката
# good
validate :expiration_date_cannot_be_in_the_past
validate :discount_cannot_be_greater_than_total_value
validate :ensure_same_topic_is_chosen
# also good - explicit prefix
validate :validate_birthday_in_past
validate :validate_sufficient_quantity
validate :must_have_owner_with_no_other_items
validate :must_have_shipping_units
# bad
validate :birthday_in_past
validate :owner_has_no_other_items
Чтобы проверки было легко читать, не указывайте несколько атрибутов для каждой проверки.
# bad
validates :email , :password , presence : true
validates :email , length : { maximum : 100 }
# good
validates :email , presence : true , length : { maximum : 100 }
validates :password , presence : true
Если пользовательская проверка используется более одного раза или проверка представляет собой сопоставление с регулярным выражением, создайте собственный файл проверки.
# bad
class Person
validates :email , format : { with : / A ([^@ s ]+)@((?:[-a-z0-9]+ . )+[a-z]{2,}) z /i }
end
# good
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
.
Рассмотрите возможность извлечения пользовательских валидаторов в общий драгоценный камень, если вы поддерживаете несколько связанных приложений или валидаторы достаточно универсальны.
Свободно используйте именованные области.
class User < ApplicationRecord
scope :active , -> { where ( active : true ) }
scope :inactive , -> { where ( active : false ) }
scope :with_orders , -> { joins ( :orders ) . select ( 'distinct(users.id)' ) }
end
Когда именованная область, определенная с помощью лямбды и параметров, становится слишком сложной, вместо этого предпочтительнее создать метод класса, который служит той же цели, что и именованная область, и возвращает объект ActiveRecord::Relation
. Возможно, вы можете определить даже более простые области видимости, подобные этой.
class User < ApplicationRecord
def self . with_orders
joins ( :orders ) . select ( 'distinct(users.id)' )
end
end
Упорядочивайте объявления обратного вызова в том порядке, в котором они будут выполняться. Для справки см. Доступные обратные вызовы.
# bad
class Person
after_commit :after_commit_callback
before_validation :before_validation_callback
end
# good
class Person
before_validation :before_validation_callback
after_commit :after_commit_callback
end
Остерегайтесь поведения следующих методов. Они не проводят проверки модели и могут легко испортить состояние модели.
# bad
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
# good
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
Active Record.
friendly_id
драгоценный камень Он позволяет создавать удобочитаемые URL-адреса, используя некоторый описательный атрибут модели вместо ее id
.
class Person
extend FriendlyId
friendly_id :name , use : :slugged
end
Дополнительную информацию о его использовании можно найти в документации по драгоценному камню.
find_each
Используйте find_each
для перебора коллекции объектов AR. Циклический просмотр коллекции записей из базы данных (например, с использованием метода all
) очень неэффективен, поскольку он будет пытаться создать экземпляры всех объектов одновременно. В этом случае методы пакетной обработки позволяют работать с записями пакетно, тем самым значительно снижая потребление памяти.
# bad
Person . all . each do | person |
person . do_awesome_stuff
end
Person . where ( 'age > 21' ) . each do |