こんにちは、かつコーチです。
前回まで、Service ObjectやConcernといった設計パターンを解説してきました。
これらのクラスを実際に配置する際、標準のappディレクトリだけでは受け皿が足りなくなります。
今回は、大規模なRailsプロジェクトで役立つカスタムディレクトリの追加方法と、整理方針を解説します。
カスタムディレクトリの追加方法
標準のappディレクトリ構成の限界
rails new直後のappディレクトリには、models・views・controllersなど、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.rb → PublishedArticlesQuery)を一致させることを厳密に要求します。
ディレクトリを増やす際は、命名規則をこのルールに合わせておくことが重要です。
eager_load_pathsを意識する
開発環境ではファイルを保存するたびに自動で再読み込みされるため、多くの場合は追加設定なしに動きます。
一方で本番環境では、config.eager_loadが有効な場合にapp配下のすべてのディレクトリが起動時に一括読み込みされます。
app直下に作ったディレクトリであれば標準で対象に含まれるため、特別な設定は基本的に不要です。
lib配下にクラスを置く場合は、config/application.rbのconfig.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配下にservices・queries・formsなどのカスタムディレクトリを作れば、Zeitwerkが自動でオートロードしてくれるlib配下にクラスを置く場合はconfig.autoload_pathsへの追加設定が必要- サービスの数が増えたら、機能ドメインごとにサブディレクトリとネームスペースで整理する
- ネームスペースを切る際は、ファイルパスとmodule/classの入れ子構造を必ず一致させる
bin/rails zeitwerk:checkでオートロードの整合性を事前に確認できる
次に読むべき記事
→ 次の記事:RailsでシンプルなTodoアプリを作ってみる
タグ: Ruby on Rails, 上級者向け, 設計