💎

DHHのRailsコーディングスタイル

バニラRailsで十分 — 37signalsの哲学

DHH(David Heinemeier Hansson)はRailsの創始者であり、37signals(Basecamp、HEY)のCTOです。数十年間プロダクションRailsコードを書きながら確立したコーディングスタイルは「シンプルさ」と「フレームワークへの信頼」に基づいています。


1. バニラRailsで十分

習慣的にインストールする外部ライブラリ(Devise、Redis、Sidekiq等)を最小化し、フレームワーク内蔵の基本機能を最大限活用するという原則です。

実際に37signalsは外部依存性を減らし、データベースだけでバックグラウンドジョブやキャッシュ等を処理し、デプロイと運用をシンプルにしています。

# ❌ 習慣的に追加する外部gem
gem 'devise'        # → has_secure_passwordで十分な場合が多い
gem 'sidekiq'       # → Rails 8のSolid Queueで代替可能
gem 'redis'         # → Solid Cache、Solid CableでDB基盤代替
gem 'pundit'        # → シンプルなbefore_actionコールバックで十分な場合も

# ✅ Rails内蔵機能をまず検討
has_secure_password          # 認証
ActiveJob + Solid Queue      # バックグラウンドジョブ
Rails.cache + Solid Cache    # キャッシュ
Action Cable + Solid Cable   # WebSocket

核心: gemを追加する前に「Rails内蔵機能で解決できないか?」をまず自問しましょう。


2. 全てをCRUDにマッピングせよ

カスタムアクションを作る代わりに、新しいリソースを作って標準CRUD(Create/Read/Update/Delete)にマッピングします。

# ❌ カスタムアクション — コントローラが肥大化
resources :cards do
  member do
    patch :close      # CardsController#close
    patch :reopen     # CardsController#reopen
    patch :archive    # CardsController#archive
  end
end

# ✅ 新リソースでCRUDマッピング — 一貫したパターン
resources :cards do
  resource :closure, only: [:create, :destroy]   # create=閉じる、destroy=再開
  resource :archival, only: [:create, :destroy]  # create=アーカイブ、destroy=復元
end

こうすると:

  • ClosuresController#create = カード閉じる

  • ClosuresController#destroy = カード再開

  • 全コントローラが標準7アクション(index/show/new/create/edit/update/destroy)のみ使用

  • ルーティングがRESTfulで予測可能


3. 状態をブーリアンではなくレコードで管理せよ

特定の状態にtrue/falseカラムを追加する代わりに、別のレコードを作成して管理します。

# ❌ ブーリアンカラム — 履歴なし
class Card < ApplicationRecord
  # closed: boolean(カラム)
  # いつ閉じたか?誰が閉じたか?分からない
end

# ✅ レコードで管理 — 自然な履歴
class Closure < ApplicationRecord
  belongs_to :card
  belongs_to :user     # 誰が閉じたか
  # created_at          # いつ閉じたか(自動)
end

class Card < ApplicationRecord
  has_many :closures

  def closed?
    closures.exists?
  end

  def closed_by
    closures.last&.user
  end
end

利点

  • いつ、誰が閉じたか自然に記録

  • 閉じる/開く履歴全体を照会可能

  • 状態変更の監査(audit)ログ自動生成

  • destroyで状態取消可能(ブーリアントグルより明確)


学習方法

37signalsのオープンソースコードをGitHubで直接見てみましょう:

  • Campfire — 37signalsのチャットアプリ(Rails基本機能のみで実装)

  • ONCE — 一度買えば永遠に使えるアプリ(外部依存性最小化哲学)

  • Basecamp — DHHのコアプロダクト

実践課題: 次のプロジェクトで習慣的に使っていた外部ライブラリを一つ外して自分で実装してみましょう。has_secure_passwordでDeviseなしの認証を作ったり、Solid QueueでSidekiqを代替することから始めます。

キーポイント

1

新機能追加前 → 「Rails内蔵で対応できるか?」をまず確認

2

カスタムアクションが必要な時 → 新リソース(Controller)に分離してCRUDマッピング

3

状態変更機能 → ブーリアンカラムの代わりに別モデル(レコード)を作成

4

Gemfileにgem追加前 → Rails 8 Solidシリーズ(Queue/Cache/Cable)を検討

5

37signalsオープンソース(Campfire、ONCE)コードを読んでパターン学習

6

次のプロジェクトで外部gemを一つ外して自分で実装してみる

メリット

  • デプロイ・運用がシンプルに(Redis、別ワーカー不要)
  • 依存性が減りセキュリティ脆弱性も減少
  • コントローラが一貫した7アクションパターンを維持
  • 状態変更履歴が自動的に残る
  • RESTfulルーティングでAPI予測可能
  • Railsアップグレード時の破損を最小化

デメリット

  • 初期は自分で実装するのが遅い場合がある(学習コスト)
  • リソース分離が過度だとコントローラファイル数が増加
  • チームメンバーが慣習的Rails(Deviseなど)に慣れていると混乱する可能性
  • 大規模サービスではRedis/Sidekiqが性能上必要な場合がある

ユースケース

認証: Devise → has_secure_password(Rails内蔵) バックグラウンドジョブ: Sidekiq → Solid Queue(Rails 8) キャッシュ: Redis → Solid Cache(DB基盤) カード閉じる: patch :close → ClosuresController#create 購読解除: patch :unsubscribe → CancellationsController#create 記事公開: update(published: true) → PublicationsController#create 注文完了: update(completed: true) → CompletionsController#create