0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Rails初学者】Fat Controllerの解消でService層を検討したが、Railsらしい設計を選んだ話

Last updated at Posted at 2025-07-27

Fat Controllerの解消でService層を検討したが、Railsらしい設計を選んだ話

はじめに

Railsを学び始めて間もない私ですが、アンケート選択機能を実装していたら、コントローラーがどんどん長くなってしまいました。気がつくと100行を超えるようなメソッドができてしまい、読みにくさを感じ修正をしたいと思いました。
「Fat Controllerは良くない」ということは知っていたので、調べてみると「Service層を使えば解決可能」という記事をたくさん見つけました。早速実装を検討していたのですが、色々と調べているうちに、本当にService層が正解なのかいう疑問が湧いてきました。

問題の発生:コントローラーの肥大化

当初のコード(一部)

class SurveySelectionsController < ApplicationController
  def index
    @page = params[:page] || "select"

    if @page == "select"
      # アンケート選択画面用の変数セット
      @categories = Category.all
      @survey_types = SurveyType.all
      @surveys = build_surveys_query
      @selected_survey_ids = @event.survey_selections.pluck(:survey_id)
      # ... 大量の変数定義
    else
      # 管理画面用の変数セット
      @survey_selections = @event.survey_selections.includes(:survey, :event)
      # ... さらに大量の変数定義
    end
  end

  def create
    survey_ids = selection_params[:survey_ids] || []
    survey_ids.reject!(&:blank?)
    existing_survey_ids = @event.survey_selections.pluck(:survey_id)
    new_survey_ids = survey_ids.map(&:to_i) - existing_survey_ids
    # ... 複雑な登録処理
  end

  private

  def build_surveys_query
    surveys = Survey.all
    # カテゴリ・タイプ・キーワードでの絞り込み処理
    # ... 長い条件分岐
  end
end

感じていた課題

  • 各アクションが長すぎる:1つのアクションに複数の責務が混在
  • 条件分岐の複雑化@pageによる画面切り替えロジック
  • 重複コード:似たような変数定義が複数箇所に
  • テストしにくい:プライベートメソッドの単体テストが困難

最初のアプローチ:Service層の検討

考えていたService層設計

# 検討していたService層の分割案
class SurveySelectionCreator
  def self.call(event, survey_ids)
    # アンケート選択の登録処理
  end
end

class SurveySelectionUpdater
  def self.call(survey_selection, params)
    # 状態更新処理
  end
end

class SurveysQueryService
  def self.call(params)
    # アンケートの絞り込み処理
  end
end

一見すると責務が分離され、テストもしやすそうに見えます。しかし、Rails設計について調べていく中で「Service層はRailsにおいてはアンチパターンになりうる」という考え方があることを知り、非常に驚きました。

Service層が適さない理由

調べてみると、以下のような理由でService層の導入には慎重になるべきことがわかりました。

Railsの設計思想との乖離

Railsは層(レイヤー)分割を減らして、書くコード量を減らす設計になっています。モデルを例に挙げると、ORマッパー、バリデーション、コールバック、フォームオブジェクト、ビジネスロジック置き場といろんな機能をあわせ持っています。

結合による生産性向上

  • 適切な結合:重複コードを削減し、コード量を減らす
  • 登場人物の削減:クラス・ファイル数を減らして全体把握を容易に
  • モデルの概念拡張:1つのクラスに関連機能を集約

Service層の具体的な問題

  1. Serviceオブジェクト間でのメソッド共有が困難
  2. 設計方針の統一が難しい:チーム内で「Service」の認識がバラバラとなりやすいらしい
  3. 責務の境界が曖昧:どこに何を書くべきか迷いやすいらしい
  4. 「私の考える最強のService対決」:チーム内で設計方針について議論に時間がかかり着地点を見つけるのが困難になりがち

Railsらしい解決アプローチ

1. モデルへの責務移動

# Survey モデルに絞り込みロジックを移動
class Survey < ApplicationRecord
  scope :by_category, ->(category_id) {
    category_id == "none" ? where(category_id: nil) : where(category_id: category_id)
  }
  
  scope :by_type, ->(type_id) {
    type_id == "none" ? where(survey_type_id: nil) : where(survey_type_id: type_id)
  }
  
  scope :by_keyword, ->(keyword) {
    where("content ILIKE ?", "%#{keyword}%") if keyword.present?
  }
  
  def self.filter_by_params(params)
    surveys = includes(:category, :survey_type)
    surveys = surveys.by_category(params[:category_id]) if params[:category_id].present?
    surveys = surveys.by_type(params[:survey_type_id]) if params[:survey_type_id].present?
    surveys = surveys.by_keyword(params[:keyword]) if params[:keyword].present?
    surveys
  end
end

2. コントローラーの整理・分割

class SurveysController < ApplicationController
  before_action :authenticate_user!
  before_action :set_event, only: [:select, :search, :create]

  def select
    setup_survey_selection_data
  end

  def search
    setup_survey_selection_data
    respond_to { |format| format.js }
  end

  def create
    result = create_survey_selections
    redirect_with_result(result)
  end

  private

  def setup_survey_selection_data
    @categories = Category.all
    @survey_types = SurveyType.all
    @surveys = Survey.filter_by_params(search_params)
    @selected_survey_ids = @event.survey_selections.pluck(:survey_id)
    setup_filter_options
  end

  def create_survey_selections
    survey_ids = extract_survey_ids
    return { success: false, message: "アンケートを選択してください" } if survey_ids.empty?

    new_survey_ids = survey_ids - existing_survey_ids
    @event.add_survey_selections(new_survey_ids, current_user)
  end

  # その他のプライベートメソッド...
end

3. Event モデルでの関連処理

class Event < ApplicationRecord
  has_many :survey_selections, dependent: :destroy
  
  def add_survey_selections(survey_ids, user)
    success_count = 0
    errors = []
    
    survey_ids.each do |survey_id|
      survey = Survey.find_by(id: survey_id)
      next unless survey
      
      selection = survey_selections.build(
        survey_id: survey_id,
        selected_at: Time.current,
        user_id: user.id,
        completed: false
      )
      
      if selection.save
        success_count += 1
      else
        errors << "アンケートID #{survey_id}: #{selection.errors.full_messages.join(', ')}"
      end
    end
    
    { success_count: success_count, errors: errors }
  end
end

改善後の効果

コードの改善点

  • 責務の明確化:各モデルが自身に関連するロジックを持つ
  • 再利用性の向上:モデルメソッドは他の箇所でも利用可能
  • テストの簡素化:モデルメソッドの単体テストが容易
  • Railsらしさの維持:フレームワークの思想に沿った設計

パフォーマンスの向上

# 改善前:N+1問題の可能性
@surveys = build_surveys_query

# 改善後:eager loading
@surveys = Survey.filter_by_params(search_params)
# includes(:category, :survey_type) をモデル内で実行

学んだこと

1. フレームワークの思想を理解する重要性

「Fat Controller解消 = Service層」という固定観念を持っていましたが、調べれば調べるほどRailsの設計思想を理解した上でのアプローチが重要だと学びました。

2. 「層を増やすことの覚悟」

新しい層を入れるのは相当の覚悟を持って挑まねばならない作業

この考え方は多くの記事で言及されており、非常に印象的でした。安易に層を増やすのではなく、既存の仕組みを最大限活用することの大切さを実感しました。

3. チーム開発での設計統一

Service層を導入すると「私の考える最強のService対決」になりがちという話は、実際の開発でよく起こりうる課題を表現しているようです。まだ、学習中なので実際はわかりませんが、非常に参考になりました。

まとめ

Fat Controllerの解消にService層を検討することは一般的ですが、Railsにおいては必ずしも最適解ではないことを学びました。

推奨アプローチの順序

  1. モデルのメソッドとして切り出せないか?
  2. Viewに依存したロジックはHelperに切り出せないか?
  3. そもそもControllerを分割できないか?
  4. それでも困難な場合は、モデルの分割や独自クラスの検討

Railsの設計思想を理解し、フレームワークの長所を活かした設計を心がけることで、より保守性の高いアプリケーションを作ることができます。

「Service層ありき」ではなく、「Railsらしさ」を軸にした設計判断の重要性を改めて実感した貴重な経験でした。


参考記事


初学者のため、間違えていたらすいません。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?