ボジダル・バツォフ
ロールモデルは重要です。
— アレックス・J・マーフィー巡査 / ロボコップ
ヒント | https://rails.rubystyle.guide で、ナビゲーションが大幅に改善されたこのガイドの美しいバージョンを見つけることができます。 |
このガイドの目的は、Ruby on Rails 開発のための一連のベスト プラクティスとスタイルの処方箋を提示することです。これは、既存のコミュニティ主導の Ruby コーディング スタイル ガイドを補完するガイドです。
この Rails スタイル ガイドでは、実際の Rails プログラマーが、他の現実の Rails プログラマーが保守できるコードを作成できるように、ベスト プラクティスを推奨しています。現実世界での使用法を反映したスタイルガイドが使用され、役立つはずだった人々に拒否された理想に固執したスタイルガイドは、それがどれほど優れていてもまったく使用されないリスクがあります。
このガイドは、関連するルールのいくつかのセクションに分かれています。ルールの背後にある理論的根拠を追加しようとしました(省略した場合でも、それは非常に明白であると想定しています)。
すべてのルールを突然思いついたわけではありません。これらのルールは主に、プロのソフトウェア エンジニアとしての私の広範なキャリア、Rails コミュニティのメンバーからのフィードバックや提案、およびさまざまな高く評価されている Rails プログラミング リソースに基づいています。
注記 | ここでのアドバイスの一部は、Rails の最新バージョンにのみ適用されます。 |
次のコマンドを使用して、AsciiDoctor PDF を使用してこのガイドの PDF コピーを生成し、AsciiDoctor で HTML コピーを生成できます。
# 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
に配置します。イニシャライザ内のコードはアプリケーションの起動時に実行されます。
各 gem の初期化コードを、gem と同じ名前の別のファイル (たとえば、 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
のデフォルト以外の追加の環境構成を作成しないでください。ステージングなどの実稼働環境に似た環境が必要な場合は、構成オプションに環境変数を使用します。
追加の設定は、 config/
ディレクトリの下の YAML ファイルに保存します。
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)))'
:via
オプションを使用して、 [:get, :post, :patch, :put, :delete]
間の複数のリクエスト タイプを 1 つのアクションにマップする必要がない限り、 match
使用してルートを定義しないでください。
コントローラーはスリムなままにしておきます。コントローラーはビュー レイヤーのデータのみを取得する必要があり、ビジネス ロジックを含めるべきではありません (すべてのビジネス ロジックは当然モデル内に存在する必要があります)。
各コントローラー アクションは、(理想的には) 最初の find または new 以外のメソッドを 1 つだけ呼び出す必要があります。
コントローラーとビューの間で渡されるインスタンス変数の数を最小限に抑えます。
アクション フィルターのオプションで指定されたコントローラー アクションは、字句スコープ内にある必要があります。継承されたアクションに指定された 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 text:
よりもrender plain:
を優先します。
# 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
...
非アクティブ レコード モデル クラスを自由に導入します。
モデルには、略語を使用せずに意味のある (ただし短い) 名前を付けます。
データベース機能を使用せずに 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 以降では、 ActiveModel::Attributes
を使用して ActiveRecord から属性 API を拡張することもできます。
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_and_belongs_to_many
よりもhas_many :through
優先します。 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
read_attribute(:attribute)
よりもself[:attribute]
を優先します。
# bad
def amount
read_attribute ( :amount ) * 100
end
# good
def amount
self [ :amount ] * 100
end
write_attribute(:attribute, value)
よりもself[: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
の下に保持します。
複数の関連アプリを保守している場合、またはバリデーターが十分に汎用的である場合は、カスタム バリデーターを共有 gem に抽出することを検討してください。
名前付きスコープを自由に使用します。
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 を使用してください。モデルのid
ではなく、URL 内のモデルの説明的な属性を表示します。これを達成する方法は複数あります。
to_param
メソッドをオーバーライドするこのメソッドは、オブジェクトへの URL を構築するために Rails によって使用されます。デフォルトの実装は、レコードのid
文字列として返します。これをオーバーライドして、人間が判読できる別の属性を含めることもできます。
class Person
def to_param
" #{ id } #{ name } " . parameterize
end
end
これを URL に適した値に変換するには、文字列に対してparameterize
呼び出す必要があります。 Active Record のfind
メソッドで見つけられるように、オブジェクトのid
先頭に置く必要があります。
friendly_id
ジェムid
代わりにモデルの説明的な属性を使用することで、人間が判読できる URL を作成できます。
class Person
extend FriendlyId
friendly_id :name , use : :slugged
end
使用法の詳細については、gem のドキュメントを確認してください。
find_each
find_each
使用して AR オブジェクトのコレクションを反復処理します。データベースからのレコードのコレクションをループする (たとえば、 all
メソッドを使用する) と、すべてのオブジェクトを一度にインスタンス化しようとするため、非常に非効率的です。その場合、バッチ処理方法を使用すると、レコードをバッチで処理できるため、メモリ消費が大幅に削減されます。
# bad
Person . all . each do | person |
person . do_awesome_stuff
end
Person . where ( 'age > 21' ) . each do |