LoginSignup
31
30

More than 1 year has passed since last update.

【初学者向け】ファットコントローラーを解消!

Last updated at Posted at 2023-05-05

はじめに

プログラミングを始めたばかりの方々、ファットコントローラーをリファクタリングするように言われたことはありませんか?この記事では、初学者の私がファットコントローラーの解消方法の1つ、「モデルに寄せる」についてまとめてみました。

モデルにロジックを寄せることは、よく勧められるアプローチですが、具体的にどのようにモデルに寄せるかがわからない!!!と初学者の私は思いました。そこで、この記事では同じくモデルに寄せる方法がわからないよ!という初学者向けにモデルへの寄せ方を紹介します。

私自身も初学者なので、間違いがあるかもしれません。また、コード例はChat GPTに考えてもらって、正しいか検証して、書いています。できるだけ正確な情報を提供することを心掛けていますが、間違いがあった場合はコメントで教えていただけると嬉しいです🙇‍♀️🙇‍♀️

また、さらに良いリファクタリング方法があるかもしれませんが、今回はモデルへの寄せ方の例としてコードをリファクタリングしました。このコードをそのまま使用すると、さらなるリファクタリングが求められる可能性があるので、ご注意ください。

ファットコントローラーとは

ファットコントローラーとは、コントローラーが肥大化している、つまり、コントローラーが多くのロジックや責任を持ちすぎている状態を指します。これは、コードの再利用性や可読性が低下し、メンテナンスが困難になるため問題とされています。

ファットモデルという問題もありますが、初学者はまずファットモデルを目指すと良いと言われています。つまり、コントローラーに書かれたロジックをモデルに寄せることが目標です!

コントローラーに書くべきコードは、scaffoldで自動生成されるコードを参考にすると良いそうです。あの状態に近づけるよう、一緒に頑張りましょう!

基本的なパターン

ファットコントローラーのロジックをモデルに寄せる方法には、以下の基本的なパターンがあります。

  1. クエリメソッドをクラスメソッドに置き換える

  2. クエリメソッドをスコープに置き換える

  3. モデルにインスタンスメソッドを追加する

では、実際にモデルに寄せてみましょう!

1. クエリメソッドをクラスメソッドに置き換える

クラスメソッドは、モデルクラス自体に関連するロジックを扱います。例えば、ある条件に合致するレコードを取得する処理などが該当します。

例えば、以下のファットコントローラーの例を見てみましょう。

app/controllers/article_controller.rb
class ArticlesController < ApplicationController
  def show
    @articles = Article.includes(:images).where(user_id: current_user.id).order(id: desc) if user_signed_in?
  end
end

このコードは、Articleモデルを使ってデータベースから情報を取得するためのActiveRecordのクエリを組み立てています。

これをモデルに寄せるにはまず、Articleモデルを使って情報を取得しているので、Articleモデルにクラスメソッドを追加します。メソッド内でArticleを参照している部分をselfに置き換えるだけでOKです。selfは、クラスメソッド内で使用されると、そのクラス自体(=Article)を指します。
クラスメソッドは定義する時self.メソッド名と書くので注意してください。

app/models/article.rb
class Article < ApplicationRecord
  def self.articles_get(user_id)
    self.includes(:images).where(user_id: user_id).order(id: desc)

    # self.includesのselfは省略可能ですが、わかりやすくするために、書いたままにしています。
    # includes(:images).where(user_id: user_id).order(id: desc)
  end
end

コントローラーでモデルのクラスメソッドを呼び出す際は、モデル名を使ってクラスメソッドを呼び出します。

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def show
    @articles = Article.articles_get(current_user.id) if user_signed_in?
  end
end

ですが、簡潔なクエリの場合は、scopeを使うことで、簡単にモデルに寄せることができます!

2. クエリメソッドをスコープに置き換える

scopeとは以下のように書きます。

class モデル名 < ApplicationRecord
  scope :スコープの名前, -> { 条件式 }
end

条件式の前に引数を書くこともできます。

scope機能のメリット

1.条件式に名前を付けられるので、直感的なコードになる
 scope :published, -> { where(published: true) }

 Blog.publishedで呼び出せる=現在より過去に公開されたブログを取得

2.修正箇所を限定することが出来る
…条件式に変更が出た場合に、修正がscopeメソッドで定義した箇所だけで済む

scope :published, -> { where(published: true).limit(2) }

1のコードをスコープを用いて書いてみましょう!

1のコード
app/controllers/article_controller.rb
class ArticlesController < ApplicationController
  def show
    @articles = Article.includes(:images).where(user_id: current_user.id).order(id: desc) if user_signed_in?
  end
end

scopeを使うと…

app/models/article.rb
class Article < ApplicationRecord
  scope :with_images, -> { includes(:images) }
  scope :by_user, ->(user_id) { where(user_id: user_id).order(id: desc) }
end

Articleの絞り込みなので、Articleモデルにwith_imagesby_userのscopeを記載します。それにより、Article.with_imagesのように使うことができるようになります。
by_userの引数user_idcurrent_user.idを代入すればOKです。
scopeを使うことで、コードがすっきりし、わかりやすくなったと思います。

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def show
    @articles = Article.with_images.by_user(current_user.id) if user_signed_in?
  end
end

enum使用時

また、enumを使用している場合はenumのスコープを自動的に作成され、以下のようにstatusの特定の値を持つ(または持たない)すべてのオブジェクトを検索できるようになります。

enumとは名前を整数の定数に割り当てるために使われるデータ型のことです。これにより、整数値を意味のある名前で表現することができます。

app/models/conversation.rb
class Conversation < ActiveRecord::Base
  enum :status, { active: 0, archived: 1 }
end

# Conversation.where(status: :active)と同じ
Conversation.active
# Conversation.where(status: :actived)と同じ
Conversation.archived

3. モデルにインスタンスメソッドを追加する

app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  def create
    @product = Product.find(params[:product_id])
    if current_user.can_purchase?(@product)
      order = current_user.purchase(@product)
      redirect_to orders_path, success: '商品を購入しました'
    else
      redirect_to products_path, alert: '購入条件に満たしていません。'
    end
  end
end

if current_user.can_purchase?(@product)のブロックをモデルに寄せてみましょう。
current_userUserモデルなので、Userモデルに書きます。
current_userselfに置き換えます。

if文でtruefalseかを判定するので、まずreturnを使ってfalseを早期に返しましょう。

order = current_user.purchase(@product)current_userselfに置き換えるだけです。
あとは引数をproductにすると、モデルに寄せることができました。

if文のメソッドを定義する場合、truefalseかを判定する必要があることに気をつけましょう。

app/models/user.rb
class User < ApplicationRecord
  def purchase_product(product)
    return false unless self.can_purchase?(product)
    
    order = self.purchase(product)
    true
  end
end

コントローラーはモデルに定義したメソッドにもともと何が入っていたのか思い出したら大丈夫です。
selfcurrent_userproduct@productです。

app/controllers/orders_controller.rb
class OrdersController < ApplicationController
  def create
    @product = Product.find(params[:product_id])
    if current_user.purchase_product(@product)
      redirect_to orders_path, success: '商品を購入しました'
    else
      edirect_to products_path, alert: '購入条件に満たしていません。'
    end
  end
end

まとめ

クラスメソッドとかインスタンスメソッドとか書きましたが、メソッドをモデルに書くことがわかったら、大丈夫です。
一応説明すると、クラスメソッドはモデルクラス全体で使われる汎用的な処理を定義するために使用され、インスタンスメソッドは特定のレコードに対して操作するメソッドを定義するために使用されます。

つまり、Articleモデルにメソッドを記載すると、クラスメソッドであればArticleというクラス(例: Article.with_images)で使うことができ、インスタンスメソッドであれば@articlesというArticleモデルのインスタンス(例: @articles.with_images)で使うことができるようになります。

メソッドを作る手順としては、

  1. コントローラーのコードの中から、モデルに関連するロジックを見つけて、それをモデルのメソッドにまとめる
  2. その際、クラスメソッドを作成する場合は、モデル名をselfに置き換えて、インスタンスメソッドを作成する場合は、モデルのインスタンスに関連するオブジェクトをselfに置き換える
    ※selfは省略可能。わかりにくい時は書くと良い。

です。メソッドをモデルに寄せて、脱ファットコントローラーを一緒に目指しましょう!

クラスメソッドよりインスタンスメソッドを定義しよう!
インスタンスメソッドを使ってメソッドチェーンを綺麗に繋いだ方がいいとのことです。
追記で詳細書かせていただきました!

脱ファットコントローラーができたら、次は脱ファットモデルをしていかないといけないそうです…
詳細はRailsのファットモデル 3つの対処法Railsのファットモデル問題に対処する前に読んでほしい記事にて書かれています。

以上駄文でしたが、読んでくださり、ありがとうございました。

追記

みなさま、こちらの【ガチでコーディング添削】プロが未経験エンジニアのコードを美しくします!の動画はご覧になられましたでしょうか?

私はたった今見たのですが、クラスメソッドではなく、インスタンスメソッドを使い、綺麗に繋いで文章のように読めるメソッドチェーンにした方がいいとのこと!

1.でクエリメソッドをクラスメソッドに変えようと書いたのですが、このままでは良くないようです!!同じコードを例にインスタンスメソッドを定義してみましょう。

では、クラスメソッドをどうインスタンスメソッドに変えていったらいいのでしょうか。それは、まずコントローラー部分でメソッドチェーンにしてみることです!

app/controllers/article_controller.rb
class ArticlesController < ApplicationController
  def show
    @articles = Article.includes(:images).where(user_id: current_user.id).order(id: desc) if user_signed_in?
  end
end

この@articles部分をまず変更してみましょう。

@articles = current_user.articles.includes(:images).order(id: desc) if user_signed_in?

に変えることができます。

これでcurrent_userが関連づけられたarticlesを絞り込むことができました。
ちなみに、articlesメソッドはUserモデルにhas_many :articlesとArticleモデルとの関連付けを行うことで、使うことができるメソッドです。

じゃあ、あとは簡単!3. モデルにインスタンスメソッドを追加するで紹介したようにインスタンスメソッドを定義してみましょう。

current_userなので、Userモデルにメソッドを作成します。

app/models/user.rb
class User < ApplicationRecord
  def articles_with_images
    self.articles.includes(:images).order(id: desc)
  end
end

これでインスタンスメソッドを定義することができました!
以下のように、Userモデルのオブジェクトcurrent_userがこのメソッドを呼び出すことができます。

app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def show
    @articles = current_user.articles_with_images if user_signed_in?
  end
end

わからないメモ🗒️
.where(user_id: current_user.id)のようにwhereを使っているクエリメソッドは`current_user'みたいに、先にオブジェクトを作っておくと、いいのかな🤔
先にオブジェクトを作ってたら、インスタンスメソッドになるよね。

current_userのような絞り込みをしないときとかは、クラスメソッドじゃなくて、scopeを使うようにしたらいいのかなと思いました!

もし、current_userで絞り込んだ記事をインスタンスメソッドではなく、scopeで定義するなら、以下のようになります。

# app/controllers/article_controller.rb
class ArticlesController < ApplicationController
  def show
    @articles = current_user.articles.includes(:images).order(id: desc) if user_signed_in?
  end
end

# scopeを定義して、モデルに寄せると…

# app/models/article.rb
class Article < ApplicationRecord
  scope :with_images, -> { includes(:images) }
  scope :ordered_desc, -> { order(id: desc) }
end

#app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  def show
    @articles = current_user.articles.with_images.ordered_desc if user_signed_in?
  end
end

scopeはモデルに定義するため、Articleモデルにscopeを定義して使うことができます。
Articleの絞り込みなので、インスタンスメソッドとは違い、Userモデルに書くことはできません!
そのため、articles分長くなっちゃうので、current_userのように記事の絞り込みをしてから、さらに絞り込むにはscopeは向いてないのかなと思いました🤔

scopeはクラスメソッドの代わりに使うイメージで、モデルクラス全体の絞り込みをしたい時に使ったら、コードが見やすくなって、綺麗なメソッドチェーンを作れるのかなと思いました!

参考サイト

Active Record クエリインターフェイス
Railsのファットコントローラー 3つの対処法
Railsのパターンとアンチパターン4: コントローラ編(翻訳)
【新人プログラマ応援】Railsにおける良いコントローラ、悪いコントローラについて
MVCモデルにおけるモデルとコントローラーの境界とその扱いを考える
【Rails】 モデルのスコープ機能(scope)の使い方を1から理解する
ActiveRecord::Enum
よく使うQueryメソッドはscopeで整頓しよう

31
30
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
31
30