この記事はZeals Advent Calendar2020の9日目の記事です。見ていただきありがとうございます!
今回はRailsでよく話題になるFatController,FatModelについて自分の考えを書いてみました。
Railsでの設計を考える上での何かしらの参考になれば幸いです。
かんたんな説明(忙しい人用)
- Railsの通常の実装だとControllerやModelがFatになってしまう
- Fatを防ぐために、以下のクラス設計をする
- ControllerのFat対策
- レスポンスを生成するSerializerクラスを作成する
- 複雑なビジネスロジックを実装するためのUseCaseクラスを作成する
- ModelのFat対策
- 複雑なクエリーを定義するためのQueryクラスを使用する
- ControllerのFat対策
上記のクラス設計を利用したアクションのメソッドは以下のようになります
また、Modelには処理を記述していないです。
def create
user = UserQuery.new.search_from_task_group_id(@group_id)
tasks = UseCase::CreateTaskList.execute(@task_group, @task_names, user)
render json: tasks
end
そもそもFatとは
Fatな状態とははソースコードの行数が多すぎてしまい、ソースコードがわかりづらくなってしまっている状態です。
ここでのFatはRubyのソースコード解析ツール(Lint)のRubcopのClassLengthのデフォルト値の100行を超えているものをFatなものとします。
肥大化(Fat)することの問題点
個人的には肥大化してしまうことにより以下の問題が発生してしまっていると思います。
- たくさんのソースコードが記述されているため、すぐに内容の理解ができない
- 実装をしているときに、他の部分がきになってしまい、実装が難しくなってしまったり、ストレスになってしまう
- 一つの肥大化しているクラスはたくさんのことをしているため、多くの機能修正や機能追加をするときに変更をする必要があるため、変更作業のコンフリクトが発生する可能性があります
- 肥大化しているクラスに実装をしたため、次の実装も肥大化しているクラスに実装してしまい、ますます肥大化していってしまいます
- このようなクラスはGod Objectと呼ばれます
Railsは肥大化しやすいです
すでにいろいろなところで語られているように、Railsは肥大化しやすいです。
例えば、データを登録するAPIの主な処理の流れを実装すると以下のフローになります。
- APIのクライアントからパラメーターを受け取る
- パラメーターをもとにModelをDBから取得する
- Modelを操作をしてビジネスロジックを実行する
- APIのクライアントにレスポンスを返す
これを、ControllerとModelだけで実装するため肥大化してしまいます。
肥大化を回避するために記述するコードを、分割する必要があります。
どのように分割するか
処理の流れに応じてクラスを作成する
コードを分割するには責務に応じてクラスを作成して、役割ごとにコードを実装します。
先程のデータを登録するAPIの処理の流れを責務に応じてわけると以下のようになります。
- APIのクライアントからパラメーターを受け取る : リクエストを受け取るクラスの役割
- パラメーターをもとにModelを取得する : DBからModel(Record)を取得するクラスの役割
- Modelを操作をしてビジネスロジックを実行する : Modelを操作してビジネスロジックを実行するクラスの役割
- APIのクライアントにレスポンスを返す : レスポンスを返すクラスの役割
リクエストとレスポンスに関係するクラスについて
Rails(Webフレームワーク)はリクエストとレスポンスに関する処理はControllerで実行することになりますので、Controllerの責務になります。
レスポンスの詳細はコントローラーでは扱わない
レスポンスはModelのリレーション次第でデータをクライアントに返すためのに生成するコードを多くのコードを記述することになってしまうことが考えられます
そのため、active_model_serializersなどのライブラリーを使用してレスポンスを返すコードが肥大化しないようにする必要があります。
active_model_serializersを使用すると、レスポンスを返すコードはが以下のようにすごく短い記述になりますので、レスポンスコードのによるにコントローラーの肥大化を防ぐことができます。
render json: @model
DBからModel(Record)を取得するクラスについて
複数のテーブルからデータを取得するには、ActiveRecordのクラスではなくQueryクラスを使用する
Railsの標準的な実装方法だと、ActiveRecordのクラスでscopeで実装になります。
しかし、scopeを複数実装するとすぐに肥大化してしまいます。
そのために、scopeに定義する処理は1,2行でかつ他のTabelが関係しないものなどのシンプルなものだけにして、複数のテーブルが関係するSQLは、Queryクラスに記述したほうがいいです
Queryクラスについて
QueryクラスはActiveRecord::Relationクラスを引数に受け取って、受け取ったRelationクラスにwhereやjoinなどのSQL文を追記していきます。
Queryクラスを使用することにより、肥大化してしまう原因の一つである複雑なSQLの実装部分を専用のクラスに処理を実装して、ActiveRecordのクラスは肥大化しないですみます。
Queryクラスは以下のような実装になります
class UserQuery
attr_reader :relation
def initialize(relation = User.all)
@relation = relation
end
def join_task_group
relation.joins(
task_comment:
{task: :task_group}
}
)
end
def search_from_task_group_id(group_id)
join_task_group.where('task_group.id = ?' , group_id)
end
end
詳細は、以下の記事などを参照してください
https://qiita.com/furaji/items/12cef3ec4d092865af88
ビジネスロジックを実行するクラスについて
ビジネスロジックは、Controllerに実装するケースが多いと思います。
その場合にActionに記述するコードの行数が多い場合は、privateメソッドを定義するケースが多いと思います。
アクションが1,2個のうちは、privateメソッドを定義していてもわかりやすくていいです。
それが、アクションが4,5個あるとどのprivateメソッドが、どのアクションに関係するのかをすぐに理解するのが難しくなってしまいます。
アクション内で実行するビジネスロジックを実装するだけのおこなう専用のクラスを作成すると、コントローラーにはprivateメソッドを実装する必要がないため、アクションが増えてもコントローラー自体は見やすいままですみます。
ビジネスロジックを実装するクラスについて
このようなクラスは、UseCaseやApplicationServiceクラスなどと呼ばれています。
クラスは、以下のようになります。
- 実行したいビジネスロジックをクラス名にする
- ビジネスロジックを実行するために必要となる処理をprivateメソッドで構成する
実装するコードのサンプル例
module UseCase
class CreateTaskList
def execute(task_group_id, task_names, user)
task_group = find_task_group(task_group_id)
tasks = create_tasks(task_group, user, task_names,)
send_the_completion_to_the_user(tasks, user)
end
private
def find_task_group(task_group_id)
# TaskGroupを取得する処理時を実行する
end
def create_tasks(task_group, user, task_names)
# Taskを作成する処理
end
def send_the_completion_to_the_user(tasks, user)
# 完了したことをユーザーに送信する処理
end
end
end
UseCaseの詳細については以下のURLを参照してください。
https://webuild.envato.com/blog/a-case-for-use-cases/
全体のサンプルコード
今まで説明したクラス設計を利用して、アクションのメソッドを定義すると以下のようになります1
def create_tasks
user = UserQuery.new.search_from_task_group_id(@group_id)
tasks = UseCase::CreateTaskList.execute(@task_group, @task_names, user)
render json: tasks
end
この行数だけで以下のことを行っています
- パラメーターからuserを取得する
- Taskリストを作成する
- そのなかで、ユーザーに通知する
- 作成したtasksをAPIのクライアントにレスポンとして返す
注意事項
- QueryクラスやUseCaseクラスは非常に便利ですが、単純なところでもQueryクラスやUseCaseクラスを使用すると、かえって複雑になってしまいますので、過度に使用すぎないようにしたほうがいいです
- 自分の中では以下のような基準を設けています2ので、一つの参考例にしてもらえたらと思います。
- Serializeクラス : ActiveModelのクラスをSerializeするとき
* 配列の場合は、Serializeクラスを使用しなくていい - Queryクラス : 複数のテーブルを使用したSQLを実行する
- 他のテーブルが関係していない場合は、Queryクラスを使用しなくて良い
- UseCaseクラス : アクションのメソッドが10行以上かプライベートメソッドを複数定義している場合
- アクションのメソッドが10行以内で、プライベートメソッドを定義していない場合はUseCaseクラスにしないでよい
- Serializeクラス : ActiveModelのクラスをSerializeするとき
最後に
今回ご紹介したのを参考にしていただき、Fatなクラスが少なくなればと思います。
Zealsでは、Rails以外にもたくさんの技術について記事を書いていますので、そちらも読んでいただけたらと思います
日本語のテックブログ : https://tech.zeals.co.jp/
英語のテックブログ : https://medium.com/zeals-tech-blog