はじめに
個人開発でRailsアプリを作成する中で、コントローラーが肥大化していくことに悩んでいました。1つのアクションにロジックが詰め込まれ、「動くけど読みにくい」状態になっていました。この記事では、Fat Controllerをどう設計改善したか、UseCaseクラス導入のプロセスと学びをまとめます。
Fat Controllerとは
- 処理をコントローラ内で引き受けすぎてしまっている
- コントローラ内のコードの量も、ロジックの量も増えている
- コントローラ内にあらゆる実装を書き込んでいる
必要以上にコントローラ内のコードがある状態をFatControllerと言います。
Fat Controllerはなぜよくないのか
- コード量が増えすぎて、可読性が下がる
- ロジックが埋もれて、再利用しづらい
- テストがしにくく、保守性が悪化
Fat Controllerについては下記の記事がわかりやすく書いてありました。
解消方法
ここで必要となってくるのが単一責任の原則とDRY原則という考え方です。
単一責任の原則(SRP: Single Responsibility Principle)
定義
「ソフトウェアの各コンポーネント(クラス、モジュールなど)は、それぞれ1つの責任のみを持つべきだという原則」
つまり、1つの役割・目的だけを持つようにすることで、責務が明確になり、他のコードへの影響が最小限になる。
DRY原則(Don't Repeat Yourself)
定義
「同じロジック・知識を複数箇所に書かないこと」
同じコードを何度も書くことを避け、1か所にまとめて記述することで、保守性や可読性を向上させる。
実際のコード
Before
#app/controllers/users_controller.rb
class UsersController < ApplicationController
def update_password
@user = current_user
if @user.authenticate(params[:user][:current_password])
if @user.update(params.require(:user).permit(:password, :password_confirmation))
flash[:notice] = "パスワードを変更しました"
redirect_to edit_password_user_path
else
flash.now[:alert] = "パスワードの更新に失敗しました"
render :edit_password, status: :unprocessable_entity
end
else
@user.errors.add(:current_password, "が正しくありません")
flash.now[:alert] = "変更に失敗しました"
render :edit_password, status: :unprocessable_entity
end
end
end
この書き方の問題点
- 認証・更新、エラー処理、画面遷移が1アクションに纏められている
- コントローラのコード量が増え、可読性が下がる
- テストもしづらく、変更時の影響範囲が広い
After
UseCaseクラスによる責務分散
#app/controllers/users_controller.rb
def update_password
service = UpdatePassword.new(
current_user,
current_password: params[:user][:current_password],
new_params: password_params
)
if service.call
flash[:notice] = "パスワードを変更しました"
redirect_to edit_password_user_path
else
flash.now[:alert] = "変更に失敗しました"
render :edit_password, status: :unprocessable_entity
end
end
private
def password_params
params.require(:user).permit(:password, :password_confirmation)
end
#app/usecases/update_password.rb
class UpdatePassword
def initialize(user, current_password:, new_params:)
@user = user
@current_password = current_password
@new_params = new_params
end
def call
if @user.authenticate(@current_password)
@user.update(@new_params)
else
@user.errors.add(:current_password, "が正しくありません")
false
end
end
end
設計改善ポイント
単一責任の原則(SRP)を守れる
以前のFat Controllerでは、1つのメソッドが「認証」「更新」「フラッシュメッセージの出力」「画面遷移」など、複数の責務を抱えていました。
UseCaseを導入したことで、認証・更新処理はUseCaseに委譲
フラッシュや画面遷移はControllerに限定と、役割を明確に分けられ、どこを変更すればよいかもはっきりしました。
Fat Controller を避けられる
責務の分離により、コントローラーの処理が短く、読みやすくなりました。update_password の中身が「処理を呼び出す」だけになったことで、流れが明確に把握でき、保守性が格段に上がりました。
DRY原則の実現
同じような処理(バリデーションやパラメータ処理)が複数箇所に分散していると、修正漏れやバグの温床になります。
UseCase導入によって、重複しやすいロジック(認証や更新)を1か所にまとめられたことで、変更時の影響範囲を最小限にできました。
テストがしやすくなる
UseCaseクラスはコントローラーから切り離されているため、RSpecでの単体テストが可能になりました。
たとえば、認証失敗や更新失敗のケースを明示的にテストできます。
得られた学び
今回の改善を通して、設計や保守性に対する考え方が大きく変わりました。
まず、Fat Controllerがなぜ問題なのかを実感しました。認証や更新、フラッシュメッセージの制御といった処理を1つのアクションに詰め込むことで、コードの見通しが悪くなり、バグの温床になりやすいことを痛感しました。
そこでUseCaseクラスを導入することで、パスワード変更という単一の責任を切り出し、コントローラーでは処理の流れだけに集中できるようになりました。結果として、単一責任の原則(SRP)を意識した保守しやすい設計へと改善されました。
また、重複しやすいロジックをUseCaseに集約したことで、DRY原則(Don't Repeat Yourself)も実践できました。1か所を修正すれば全体が改善される設計になり、将来的な仕様変更にも柔軟に対応できます。
さらに、UseCaseは外部依存が少なく、RSpecでの単体テストが非常にしやすくなりました。認証失敗やバリデーション失敗といったシナリオを分離して検証できることで、品質保証にもつながりました。
コードの可読性、保守性、再利用性、テストのしやすさという点で、UseCaseパターンは非常に有効であると体感しました。
「とりあえず動く」から一歩進み、コードの役割を明確に分離する設計を目指しました。UseCaseクラスによってコードの見通しが良くなり、保守や機能追加のしやすさにもつながっています。