はじめに
現在、プログラミングスクールでRuby on Rails を学習中です。
カリキュラムの課題をクリアしたあとに、自分の書いたコードと模範解答を見比べてみたところ、あることに気づきました。
自分のコントローラーは長い。
模範解答はスッキリしている。
処理の内容は同じなのに、なぜこんなにも違うのか。
そこで出てきたのが「Fat Controller(太ったコントローラー)」という考え方でした。
この記事では、
- Fat Controllerとは
- なぜ問題なのか
- どう改善すればよいのか
を、Rails初学者の視点で整理してみます。
Fat Controllerとは
Railsは MVC(Model-View-Controller) という設計パターンを採用しています。
それぞれの役割はざっくり言うと次の通りです。
| 層 | 役割 |
|---|---|
| Model | データやビジネスロジック |
| View | 表示 |
| Controller | リクエストを受け取り、処理を振り分ける |
Controllerは本来、「交通整理役」です。
しかし、Controllerに
- 複雑な条件分岐
- データ加工処理
- 外部API連携
- ビジネスロジック
- メール送信処理
などをどんどん書いていくと、ファイルは肥大化していきます。
これが Fat Controller と呼ばれる状態です。
Rails界隈では
Fat Model, Skinny Controller
という言葉もあります。
つまり「Controllerはできるだけ薄く保とう」という考え方です。
なにが問題なのか
Fat Controllerの問題は、単に「コードが長いこと」ではありません。
本質は、責務が集中していることです。
① 単一責任の原則に違反する
Controllerが
- ビジネスルール
- データ保存
- 条件分岐
- 外部連携
などを同時に担うと、
仕様変更があるたびにControllerを修正することになり、壊れやすいコードになります。
② テストがしづらい
Controllerにロジックを書くと、テストはHTTPリクエスト前提になります。
処理の中身だけを確認したい場合も、
HTTPリクエストを通してテストする必要があります。
ロジックをModelやServiceに切り出せば、単体テストが可能になります。
③ 再利用できない
Controllerに書いたロジックは、Webリクエストに強く依存します。
- バッチ処理で使いたい
- 別APIからも使いたい
- コンソールから呼び出したい
といった場合に再利用しづらくなります。
④ 変更に弱い
責務が集中していると影響範囲も広がります。
結果として、
- コンフリクトが増える
- バグが混入しやすい
- 可読性が下がる
といった問題が発生します。
どう改善するか
では実際に、記事投稿アプリの例で見てみます。
想定機能
- ユーザーが記事を投稿する
- 文字数が100文字以上なら「公開(published)」にする
- 100文字未満なら「下書き(draft)」にする
- 公開された場合は通知メールを送る
Fat Controllerの例
def create
@post = Post.new(post_params)
# 文字数によってステータスを決定
if @post.content.length >= 100
@post.status = "published"
else
@post.status = "draft"
end
if @post.save
# 公開された場合のみ通知メール送信
if @post.status == "published"
NotificationMailer.post_published(@post).deliver_now
end
redirect_to @post
else
render :new
end
end
このControllerは、
-
文字数判定ロジック
-
ステータス決定
-
メール送信処理
-
保存処理
-
画面遷移制御
をすべて担当しています。
責務が集中している状態です。
改善例①(Modelにロジックを移す)
まずはビジネスロジックをModelに移してみます。
# app/models/post.rb
class Post < ApplicationRecord
def assign_status
self.status = content.length >= 100 ? "published" : "draft"
end
def publish_notification
return unless status == "published"
NotificationMailer.post_published(self).deliver_now
end
end
Controllerは次のようになります。
def create
@post = Post.new(post_params)
@post.assign_status
if @post.save
@post.publish_notification
redirect_to @post
else
render :new
end
end
Controllerが少しスッキリしました!
改善例②(Serviceオブジェクトに切り出す)
さらに責務を整理して、投稿処理全体をServiceにまとめます。
# app/services/post_creator.rb
class PostCreator
def self.call(params)
post = Post.new(params)
post.assign_status
if post.save && post.status == "published"
NotificationMailer.post_published(post).deliver_now
end
post
end
end
controllerはこうなります。
def create
@post = PostCreator.call(post_params)
if @post.persisted?
redirect_to @post
else
render :new
end
end
めちゃくちゃスッキリしました!
改善後のポイント
Controllerは、
-
パラメータを受け取る
-
処理を呼び出す
-
レスポンスを決める
だけになりました。
ビジネスロジックはControllerから分離され、
テストや再利用もしやすい構造になります。
これが「Skinny Controller」に近い形です。
まとめ
今回の学びは、
「動くコード」と「設計されたコード」は違う
ということでした。
最初は「とりあえず動けばOK」と思いながら、
ほとんどの処理をControllerに書いていました。
しかし、
-
責務を分ける
-
ロジックを適切な場所に置く
-
Controllerは薄く保つ
という考え方を知ってから、コードの見え方が大きく変わりました。
まだまだ難しい部分はありますが、意識していきたいと思いました。
参考記事