ruby-on-rails-best-practices について
ruby-on-rails-best-practicesは、Basecampの本番Railsアプリからまとめた設計パターンとコーディング規約で、Railsプロジェクトの開発やレビューに携わるエンジニア向けです。モデル構成からリアルタイム機能、テストまで6分野・16項目を扱い、Railsの慣例に沿って追加レイヤーを抑える方針です。
主な機能
- 016分野にわたる16のルール
- 02モデルとconcernの配置規約
- 03ジョブとコントローラー設計の指針
- 04Currentとコールバックのパターン
- 05Turboのリアルタイム機能とテストの指針
活用シーン
- 01Railsのモデルとconcernの配置を決める
- 02コントローラーとバックグラウンドジョブの責務を見直す
- 03Turboのリアルタイム機能でブロードキャスト方式を選ぶ
- 04Railsプロジェクトのテストとコード構成を統一する
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ sergiodxa/ agent-skills --skill ruby-on-rails-best-practicesインストール後、Claude Code で/ruby-on-rails-best-practicesを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
rules/association-extensions.mdcallbacks-patterns.mdcode-style.mdconcern-naming.mdcurrent-attributes.mdcurrent-in-other-contexts.mdfixtures-testing.mdmodel-scoped-concerns.mdnested-service-objects.mdpaired-async-methods.mdresource-controllers.mdscoping-concerns.mdtemplate-method-concerns.mdthin-controllers.mdthin-jobs.mdturbo-broadcasts.md詳細
Ruby on Rails のベストプラクティス
Basecamp の本番環境の Rails アプリケーション(Fizzy と Campfire)から抽出したアーキテクチャパターンとコーディング規約です。Basecamp 独自の工夫を加えた「Rails Way」に沿って、コードの整理、保守性、規約の遵守に焦点を当てた 6 つのカテゴリ、16 のルールをまとめています。
適用する場面
次のような場面で、これらのガイドラインを参照してください。
- モデル、コンサーン、コントローラーを整理するとき
- バックグラウンドジョブを作成するとき
- Turbo Streams でリアルタイム機能を実装するとき
- コードの配置場所を決めるとき
- Rails アプリケーションのテストを書くとき
- Rails コードのアーキテクチャの一貫性をレビューするとき
ルールの概要
モデルの整理(HIGH)
model-scoped-concerns - @rules/model-scoped-concerns.md
モデル固有のコンサーンは app/models/concerns/ ではなく、app/models/model_name/ に配置します。
# Directory structure
app/models/
├── card.rb
├── card/
│ ├── closeable.rb # Card::Closeable
│ ├── searchable.rb # Card::Searchable
│ └── assignable.rb # Card::Assignable
# app/models/card.rb
class Card < ApplicationRecord
include Closeable, Searchable, Assignable
# Ruby resolves from Card:: namespace first
end
concern-naming - @rules/concern-naming.md
振る舞いを表すコンサーンには -able の接尾辞を使い、機能を表すコンサーンには名詞を使います。
# Behaviors: -able suffix
module Card::Closeable # Can be closed
module Card::Searchable # Can be searched
module User::Mentionable # Can be mentioned
# Features: nouns
module User::Avatar # Has avatar
module User::Role # Has role
module Card::Mentions # Has @mentions
template-method-concerns - @rules/template-method-concerns.md
カスタマイズ可能な振る舞いには、共有コンサーンでテンプレートメソッドを使います。
# app/models/concerns/searchable.rb (shared)
module Searchable
def search_title
raise NotImplementedError
end
end
# app/models/card/searchable.rb (model-specific)
module Card::Searchable
include ::Searchable
def search_title
title # Implement the hook
end
end
バックグラウンドジョブ(HIGH)
paired-async-methods - @rules/paired-async-methods.md
同期メソッドには、ジョブをエンキューする _later バリアントを組み合わせます。
# app/models/card/readable.rb
def remove_inaccessible_notifications
# Sync implementation
end
private
def remove_inaccessible_notifications_later
Card::RemoveInaccessibleNotificationsJob.perform_later(self)
end
# app/jobs/card/remove_inaccessible_notifications_job.rb
class Card::RemoveInaccessibleNotificationsJob < ApplicationJob
def perform(card)
card.remove_inaccessible_notifications
end
end
thin-jobs - @rules/thin-jobs.md
ジョブからモデルメソッドを呼び出します。ロジックはすべてモデルに記述します。
# Bad: Logic in job
class ProcessOrderJob < ApplicationJob
def perform(order)
order.items.each { |i| i.product.decrement!(:stock) }
order.update!(status: :processing)
end
end
# Good: Job delegates to model
class ProcessOrderJob < ApplicationJob
def perform(order)
order.process # Single method call
end
end
コントローラー(HIGH)
resource-controllers - @rules/resource-controllers.md
状態変更にはカスタムアクションではなく、リソースコントローラーを作成します。
# Bad: Custom actions
resources :cards do
post :close
post :reopen
end
# Good: Resource controllers
resources :cards do
resource :closure, only: [:create, :destroy]
end
# app/controllers/cards/closures_controller.rb
class Cards::ClosuresController < ApplicationController
def create
@card.close
end
def destroy
@card.reopen
end
end
scoping-concerns - @rules/scoping-concerns.md
ネストされたリソースのセットアップには、CardScoped のようなコンサーンを使います。
# app/controllers/concerns/card_scoped.rb
module CardScoped
extend ActiveSupport::Concern
included do
before_action :set_card
end
private
def set_card
@card = Current.user.accessible_cards.find_by!(number: params[:card_id])
end
end
# Usage
class Cards::CommentsController < ApplicationController
include CardScoped
end
thin-controllers - @rules/thin-controllers.md
コントローラーから、機能豊富なモデル API を直接呼び出します。サービスオブジェクトは使いません。
# Good: Thin controller, rich model
class Cards::ClosuresController < ApplicationController
include CardScoped
def create
@card.close # All logic in model
end
end
リクエストコンテキスト(MEDIUM)
current-attributes - @rules/current-attributes.md
リクエストスコープのデータには、連鎖するセッターを備えた Current を使います。
class Current < ActiveSupport::CurrentAttributes
attribute :session, :user, :account
def session=(value)
super(value)
self.user = session&.user
end
end
current-in-other-contexts - @rules/current-in-other-contexts.md
Current が自動的に設定されるのは Web リクエスト内だけです。ジョブ、メーラー、チャネルでは明示的なセットアップが必要です。
# Jobs: extend ActiveJob to serialize/restore Current.account
# Mailers from jobs: wrap in Current.with_account { mailer.deliver }
# Channels: set Current in Connection#connect
アソシエーションとコールバック(MEDIUM)
association-extensions - @rules/association-extensions.md
必要なコンテキストに応じて、アソシエーション拡張とモデルクラスメソッドを使い分けます。
# Use extension when you need parent context (proxy_association.owner)
has_many :accesses do
def grant_to(users)
board = proxy_association.owner
Access.insert_all(users.map { |u| { user_id: u.id, board_id: board.id, account_id: board.account_id } })
end
end
# Use class method when operation is independent
class Access
def self.grant(board:, users:)
insert_all(users.map { |u| { user_id: u.id, board_id: board.id } })
end
end
callbacks-patterns - @rules/callbacks-patterns.md
ジョブには after_commit を使い、単純な処理にはインラインのラムダを使います。
# Jobs: after_commit
after_create_commit :notify_recipients_later
# Simple ops: inline lambda
after_save -> { board.touch }, if: :published?
# Conditional: remember and check pattern
before_update :remember_changes
after_update_commit :process_changes, if: :should_process?
Turbo とリアルタイム機能(MEDIUM)
turbo-broadcasts - @rules/turbo-broadcasts.md
コールバックではなく、コントローラーから明示的にブロードキャストします。
# app/models/message/broadcasts.rb
module Message::Broadcasts
def broadcast_create
broadcast_append_to room, :messages, target: [room, :messages]
end
end
# Controller calls explicitly
def create
@message = @room.messages.create!(message_params)
@message.broadcast_create
end
テスト(MEDIUM)
fixtures-testing - @rules/fixtures-testing.md
ファクトリーではなくフィクスチャを使います。テスト内でもコンサーンの構造を再現します。
# test/fixtures/cards.yml
logo:
title: The logo isn't big enough
board: writebook
creator: david
# test/models/card/closeable_test.rb
class Card::CloseableTest < ActiveSupport::TestCase
test "close creates closure" do
card = cards(:logo)
assert_difference -> { Closure.count } do
card.close
end
end
end
コードの整理(LOW-MEDIUM)
nested-service-objects - @rules/nested-service-objects.md
サービスオブジェクトは app/services ではなく、モデルの名前空間の下に配置します。
# Good: app/models/card/activity_spike/detector.rb
class Card::ActivitySpike::Detector
def initialize(card)
@card = card
end
def detect
# ...
end
end
code-style - @rules/code-style.md
条件分岐は展開した形式を優先し、メソッドは呼び出し順に並べます。
# Expanded conditionals
def find_record
if record = find_by_id(id)
record
else
NullRecord.new
end
end
# Method ordering: caller before callees
def process
step_one
step_two
end
private
def step_one; end
def step_two; end
方針
これらのパターンは、追加要素を最小限に抑えて Rails の規約を活用する「Vanilla Rails」の考え方を体現しています。
- 機能豊富なモデル、薄いコントローラー - ドメインロジックはモデルとコンサーンに記述します
- サービスオブジェクト層を設けない - コントローラーからモデルを直接呼び出します
- コードを近くに配置する - コンサーン、ジョブ、サービスは、それらが担うモデルの近くに配置します
- 暗黙的な処理より明示的な処理 - コールバックではなく、ブロードキャストを明示的に呼び出します
- 設定より規約 - 予測可能性を高めるため、命名パターンに従います