はじめに
Railsで学習開発を進める中で、Serviceオブジェクトを使ってビジネスロジックを切り出す構成にしました。
実装を進める中で、ディレクトリ構成と名前空間(module)の関係や、呼び出し方で少し詰まった部分があったため、実際に触って理解できた内容を学習記録としてまとめます。
まず理解したこと
今回触ってみて、自分の中では以下のように整理できました。
-
Serviceオブジェクト
- ビジネスロジックをコントローラーやモデルから切り出す
- 処理単位で責務を分ける
-
module(名前空間)
- クラスをグルーピングするための仕組み
- クラス名の衝突を防ぐ
-
ディレクトリ構成との関係
- Railsはファイルパスと定数名の一致を前提にしている
- 「フォルダを分けただけ」では名前空間は作られない
Serviceを分割したときの課題
Serviceを増やしていくと、以下の問題が出てきました。
- どのServiceがどの役割か分かりにくい
- ファイル数が増えて見通しが悪くなる
そのため、以下のようにディレクトリを分割しました。
app/services/rf/
├── calculators/
├── builders/
└── shared/
この時点では、**「フォルダで分ければ Rf::Calculators::RankCalculator で呼べる」**と思っていました。
名前空間とmoduleの関係
ここが今回一番理解できたポイントです。
NGパターン(最初の認識)
# app/services/rf/calculators/rank_calculator.rb
class RankCalculator
# ...
end
これだと定義されるのは単なる ::RankCalculator になってしまいます。
💡 Rails(Zeitwerk)が期待している形
ファイルパスが app/services/rf/calculators/rank_calculator.rb の場合、Railsは以下の定数を期待します。
Rf::Calculators::RankCalculator
正しい書き方
module Rf
module Calculators
class RankCalculator
def self.call
# 処理
end
end
end
end
学んだこと
-
::はディレクトリの区切りではなく名前空間の区切り。 - 名前空間は
moduleで明示的に定義する必要がある。 - フォルダ分けだけでは不十分。
エラーの原因(実際に詰まったところ)
実装中、以下のようなエラーに遭遇しました。
Zeitwerk::NameError:
expected file .../app/services/rf/shared/base_date.rb to define constant Rf::Shared::BaseDate, but didn't
原因:
- ファイルパスと定数名が一致していない
- 内部で
moduleが適切に定義されていない
Service呼び出し時の悩み
Serviceを呼び出すときに、毎回フルパスで書くのが少し冗長に感じました。
Rf::Calculators::RankCalculator.call
インスタンス変数で解決できるか?
最初は以下のように考えました。
@calculator = Rf::Calculators::RankCalculator
しかし、これは単なるクラスの参照を保持しているだけで、名前空間の解決そのものには関係ありません。
根本的な解決にはなりませんでした。
実際の対処方法
1. エイリアスを使う
RankCalculator = Rf::Calculators::RankCalculator
RankCalculator.call
2. includeを使う
include Rf::Calculators
RankCalculator.call
変数の使い分け(ローカル vs インスタンス)
ここも今回整理できたポイントです。
これまでは「コントローラーからViewにデータを渡すもの」という、いわばRailsのレールに乗ったお作法としてなんとなくインスタンス変数(@)を使っていました。
しかし、APIモードでの開発やServiceオブジェクトの利用を考えると、以下のように使い分けるのが適切だと整理できました。
| 変数種別 | 使い時 | 特徴 |
|---|---|---|
| ローカル変数 | result = RankCalculator.call |
一時的な値。スコープ(メソッド内)が限定される。 |
| インスタンス変数 | @result = RankCalculator.call |
限定的。状態を保持したいとき。クラス内(Viewなど)で共有したいとき。 |
今回の結論:
Serviceの戻り値は基本ローカル変数で扱い、Viewへ渡すなど状態を持つ必要があるときのみインスタンス変数を使用する。
今回の学び
今回の学習で、以下のように整理できました。
- Serviceは責務ごとに分割する。
- ディレクトリ構成と名前空間は別物。
-
moduleを使って名前空間を正しく定義する必要がある。 - Railsはファイルパスと定数名の一致を前提としている(Zeitwerkの仕組み)。
- インスタンス変数は万能ではなく、用途を明確に分ける。
まとめ
今回の内容はRailsの基礎的な部分ではありますが、実際にServiceを分割して実装する中で理解が深まった部分でした。
特に**「名前空間の考え方」「moduleの役割」「変数の使い分け」**あたりは、今後の設計にも関わる重要なポイントだと感じています。
今後やりたいこと
- Serviceオブジェクトの設計パターン整理
-
callメソッドの統一 - moduleの責務設計
- Zeitwerkのオートロードの仕組みを深掘りする