【Ruby on Rails】プロジェクトのディレクトリ構成のベストプラクティス

Ruby

こんにちは、かつコーチです。

前回まで、Service ObjectやConcernといった設計パターンを解説してきました。

これらのクラスを実際に配置する際、標準のappディレクトリだけでは受け皿が足りなくなります。

今回は、大規模なRailsプロジェクトで役立つカスタムディレクトリの追加方法と、整理方針を解説します。

カスタムディレクトリの追加方法

標準のappディレクトリ構成の限界

rails new直後のappディレクトリには、modelsviewscontrollersなど、MVCに対応したフォルダしか用意されていません。

Service Object・フォームオブジェクト・検索用のクエリオブジェクトなど、MVCに収まらないクラスが増えると、置き場所に困ります。

app/modelsに何でも置いてしまうと、Active Recordのモデルと、それ以外のPORO(Plain Old Ruby Object)が混在し、ファイル一覧から意図が読み取れなくなります。

app/services・app/queriesの追加

Railsはapp配下の任意のディレクトリを自動でオートロード対象(クラス名からファイルを自動で読み込む仕組み)に含めてくれます。

そのため、フォルダを作ってファイルを置くだけで、requireなしに新しいクラス群を使い始められます。

app/
  services/
    order_creation_service.rb
  queries/
    published_articles_query.rb
  forms/
    signup_form.rb
  decorators/
    article_decorator.rb

クエリオブジェクトの例です。

# app/queries/published_articles_query.rb
class PublishedArticlesQuery
  def initialize(relation = Article.all)
    @relation = relation
  end

  def call
    @relation.where(published: true).order(published_at: :desc)
  end
end
# 呼び出し側
@articles = PublishedArticlesQuery.new.call.page(params[:page])

Rails 7以降のデフォルトであるZeitwerkというオートローダーは、ファイルパスとクラス名(この場合はapp/queries/published_articles_query.rbPublishedArticlesQuery)を一致させることを厳密に要求します。

ディレクトリを増やす際は、命名規則をこのルールに合わせておくことが重要です。

eager_load_pathsを意識する

開発環境ではファイルを保存するたびに自動で再読み込みされるため、多くの場合は追加設定なしに動きます。

一方で本番環境では、config.eager_loadが有効な場合にapp配下のすべてのディレクトリが起動時に一括読み込みされます。

app直下に作ったディレクトリであれば標準で対象に含まれるため、特別な設定は基本的に不要です。

lib配下にクラスを置く場合は、config/application.rbconfig.autoload_pathsに明示的にパスを追加する必要がある点に注意してください。

# config/application.rb
module MyApp
  class Application < Rails::Application
    config.autoload_paths += %W(#{config.root}/lib)
  end
end

大規模プロジェクトでの整理方針

機能単位でディレクトリを切る

サービスやコントローラの数が増えてくると、フラットなapp/servicesの中にファイルが数十個並ぶ状態になりがちです。

こうなったら、機能ドメインごとにサブディレクトリで整理する方針に切り替えます。

app/services/
  orders/
    order_creation_service.rb
    order_cancellation_service.rb
  notifications/
    push_notification_service.rb
    email_notification_service.rb

Zeitwerkの規約に合わせるため、この場合はモジュールでネームスペースを切る必要があります。

# app/services/orders/order_creation_service.rb
module Orders
  class OrderCreationService
    def call
      # ...
    end
  end
end

呼び出し側もOrders::OrderCreationService.new.callのように、ネームスペース付きで参照します。

ディレクトリ構成表で全体像を示す

チームで開発する場合、README等に構成表を残しておくと、新規メンバーのオンボーディングがスムーズになります。

ディレクトリ役割
app/services複数モデルにまたがるユースケースの実行
app/queries複雑な検索条件をカプセル化したオブジェクト
app/forms複数モデルへの入力をまとめて扱うフォームオブジェクト
app/decoratorsビュー表示用の整形ロジック
app/validators再利用可能なカスタムバリデータ

よくあるつまずきポイント・エラー対処

ネームスペースを切ったのにNameErrorが出る

私が実際にハマったのが、ディレクトリにネームスペース用フォルダを作ったのに、クラス側でモジュールを書き忘れたケースです。

❌ Before:ディレクトリはネームスペースを切っているのにクラスにモジュールがない

# app/services/orders/order_creation_service.rb
class OrderCreationService
  def call
    # ...
  end
end

このファイルをOrders::OrderCreationServiceとして呼び出したところ、以下のエラーが出ました。

NameError: uninitialized constant Orders::OrderCreationService

Zeitwerkは「ファイルパスの各階層」と「モジュール・クラスの入れ子構造」が一致していることを前提にオートロードするため、パスと定義がずれるとこのエラーになります。

✅ After:ファイルパスに合わせてmoduleで囲む

# app/services/orders/order_creation_service.rb
module Orders
  class OrderCreationService
    def call
      # ...
    end
  end
end

bin/rails zeitwerk:checkコマンドを実行すると、オートロードの設定に問題がないかを事前に確認できます。

bin/rails zeitwerk:check
# => All is good!

デプロイ前にこのコマンドを実行しておくと、本番環境でだけeager_loadによってエラーが顕在化する事故を防げます。

まとめ

この記事のポイント

  • app配下にservicesqueriesformsなどのカスタムディレクトリを作れば、Zeitwerkが自動でオートロードしてくれる
  • lib配下にクラスを置く場合はconfig.autoload_pathsへの追加設定が必要
  • サービスの数が増えたら、機能ドメインごとにサブディレクトリとネームスペースで整理する
  • ネームスペースを切る際は、ファイルパスとmodule/classの入れ子構造を必ず一致させる
  • bin/rails zeitwerk:checkでオートロードの整合性を事前に確認できる

次に読むべき記事

→ 次の記事:RailsでシンプルなTodoアプリを作ってみる

タグ: Ruby on Rails, 上級者向け, 設計

タイトルとURLをコピーしました