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

# はじめに

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

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

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

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

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

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

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


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

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

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

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


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

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

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

```ruby: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.メソッド名`と書くので注意してください。

```ruby: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
```
コントローラーでモデルのクラスメソッドを呼び出す際は、モデル名を使ってクラスメソッドを呼び出します。

```ruby: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とは以下のように書きます。
```ruby
class モデル名 < ApplicationRecord
  scope :スコープの名前, -> { 条件式 }
end
```
条件式の前に引数を書くこともできます。

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

　`Blog.published`で呼び出せる＝現在より過去に公開されたブログを取得
  
2.修正箇所を限定することが出来る
…条件式に変更が出た場合に、修正がscopeメソッドで定義した箇所だけで済む

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


1のコードをスコープを用いて書いてみましょう！
<details><summary>１のコード</summary>

```ruby: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
```
</details>

scopeを使うと…

```ruby: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_images`と`by_user`のscopeを記載します。それにより、`Article.with_images`のように使うことができるようになります。
`by_user`の引数`user_id`に`current_user.id`を代入すればOKです。
`scope`を使うことで、コードがすっきりし、わかりやすくなったと思います。

```ruby: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`とは名前を整数の定数に割り当てるために使われるデータ型のことです。これにより、整数値を意味のある名前で表現することができます。

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

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

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

```ruby: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_user`は`User`モデルなので、`User`モデルに書きます。
`current_user`を`self`に置き換えます。

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

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

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

```ruby: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
```

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

```ruby: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は省略可能。わかりにくい時は書くと良い。

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

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

脱ファットコントローラーができたら、次は脱ファットモデルをしていかないといけないそうです…
詳細は[Railsのファットモデル 3つの対処法](https://engineer-first.net/fat-model-workarounds)、[Railsのファットモデル問題に対処する前に読んでほしい記事](https://qiita.com/klriutsa/items/8d7381f437c225c64a5f)にて書かれています。

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

## 追記

みなさま、こちらの[【ガチでコーディング添削】プロが未経験エンジニアのコードを美しくします！](https://youtu.be/JKCQn5gsMu4)の動画はご覧になられましたでしょうか？

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

[1.でクエリメソッドをクラスメソッドに変えよう](https://qiita.com/miyu-40/items/ef2ce957d837d4a862b7#1-%E3%82%AF%E3%82%A8%E3%83%AA%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%82%92%E3%82%AF%E3%83%A9%E3%82%B9%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%81%AB%E7%BD%AE%E3%81%8D%E6%8F%9B%E3%81%88%E3%82%8B)と書いたのですが、このままでは良くないようです！！同じコードを例にインスタンスメソッドを定義してみましょう。

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

```ruby: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`部分をまず変更してみましょう。
```ruby
@articles = current_user.articles.includes(:images).order(id: desc) if user_signed_in?
```
に変えることができます。

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

じゃあ、あとは簡単！[3. モデルにインスタンスメソッドを追加する](https://qiita.com/miyu-40/items/ef2ce957d837d4a862b7#3-%E3%83%A2%E3%83%87%E3%83%AB%E3%81%AB%E3%83%A1%E3%82%BD%E3%83%83%E3%83%89%E3%82%92%E8%BF%BD%E5%8A%A0%E3%81%99%E3%82%8B)で紹介したようにインスタンスメソッドを定義してみましょう。

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

```ruby:app/models/user.rb
class User < ApplicationRecord
  def articles_with_images
    self.articles.includes(:images).order(id: desc)
  end
end
```
これでインスタンスメソッドを定義することができました！
以下のように、Userモデルのオブジェクト`current_user`がこのメソッドを呼び出すことができます。

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

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

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

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

```ruby
# 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 クエリインターフェイス](https://railsguides.jp/active_record_querying.html)
[Railsのファットコントローラー 3つの対処法](https://engineer-first.net/fat-controller-workarounds)
[Railsのパターンとアンチパターン4: コントローラ編（翻訳）](https://techracho.bpsinc.jp/hachi8833/2021_10_05/112108)
[【新人プログラマ応援】Railsにおける良いコントローラ、悪いコントローラについて](https://qiita.com/jnchito/items/2b57316b39d92ca0e8dc)
[MVCモデルにおけるモデルとコントローラーの境界とその扱いを考える](https://qiita.com/MasashiFujiike/items/5c1c3e92c0289812a952)
[【Rails】 モデルのスコープ機能(scope)の使い方を１から理解する](https://pikawaka.com/rails/scope)
[ActiveRecord::Enum](https://api.rubyonrails.org/v5.1/classes/ActiveRecord/Enum.html)
[よく使うQueryメソッドはscopeで整頓しよう](https://norix.tokyo/rails-tips/408/)