mcpField
R
ruby-on-rails-best-practices

ruby-on-rails-best-practices

github.com/midudev開発7.0k62 回インストール今日更新
リポジトリ

ruby-on-rails-best-practices について

Basecamp のアプリ Fizzy と Campfire のパターンに基づく Rails ガイダンススキルです。Rails 開発者向けに、コード、ジョブ、コントローラー、リアルタイム機能、テストの構成を支援します。

主な機能

  1. 01モデルと Concern の構成
  2. 02バックグラウンドジョブの規約
  3. 03リソース指向コントローラー
  4. 04リクエストコンテキストの管理
  5. 05Turbo ブロードキャストのパターン
  6. 06Fixture を使ったテスト

活用シーン

  1. 01モデル固有の Concern の構成
  2. 02コントローラーとジョブの責務確認
  3. 03Turbo Stream ブロードキャストの実装
  4. 04Rails のテスト手法の選択

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/midudev/autoskills --skill ruby-on-rails-best-practices

インストール後、Claude Code で/ruby-on-rails-best-practicesを入力して読み込まれたか確認します。

同梱ファイル

SKILL.md と一緒にインストールされる参考資料やスクリプト。

rules/
association-extensions.md
callbacks-patterns.md
code-style.md
concern-naming.md
current-attributes.md
current-in-other-contexts.md
fixtures-testing.md
model-scoped-concerns.md
nested-service-objects.md
paired-async-methods.md
resource-controllers.md
scoping-concerns.md
template-method-concerns.md
thin-controllers.md
thin-jobs.md
turbo-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」の考え方を体現しています。

  1. 機能豊富なモデル、薄いコントローラー - ドメインロジックはモデルとコンサーンに記述します
  2. サービスオブジェクト層を設けない - コントローラーからモデルを直接呼び出します
  3. コードを近くに配置する - コンサーン、ジョブ、サービスは、それらが担うモデルの近くに配置します
  4. 暗黙的な処理より明示的な処理 - コールバックではなく、ブロードキャストを明示的に呼び出します
  5. 設定より規約 - 予測可能性を高めるため、命名パターンに従います

コメント