[Ruby]VCにビジネスロジックを書いてはいけない?[Helper/Decorator/ViewObject/N+1クエリ問題]
昨日の記事の追記について、簡単にしかまとめられてなかったので、詳しくまとめます!
なぜビューに「ビジネスロジック」を書いてはいけない?
そもそも「ビジネスロジック」とは?
ビジネスロジックとは、アプリケーションの「業務のルール」や「処理の流れ」に関するロジックのこと。
前回書いた下記コードは、プレゼンテーション層(View)にビジネスロジックが含まれているのが問題。
管理者: <%= user.owned_groups.pluck(:name).join(", ") %>
「Viewにロジックを書かず、できるだけプレゼンテーションのみに集中させる」
「プレゼンテーション」とは?⇒3層アーキテクチャに関係してくる。
3層アーキテクチャとは?
3層アーキテクチャ(Three-Tier Architecture)とは、アプリケーションを以下の3つの層に分ける設計思想。基本情報でも出てきたような🤔
-
プレゼンテーション層(Presentation Layer)
-
役割: ユーザーとのやり取りを担当する(UI・ビュー)
ユーザーとの対話=プレゼンテーションと覚えた -
具体例:
View
(HTML, CSS, JavaScript, ERB) - MVCでの対応: V(View)
- 重要な点: ここには「データ取得」や「ビジネスロジック」を書かない(表示のみに専念)
-
役割: ユーザーとのやり取りを担当する(UI・ビュー)
-
アプリケーション層(Application Layer)
-
役割: アプリケーションのロジック(ビジネスロジック)を処理する
しかし、Fat Controller(太ったコントローラ)を防ぐためモデルにビジネスロジックを書くことが基本。 -
具体例:
Controller
(RailsのController, Service, ViewObject) - MVCでの対応: C(Controller)
- 重要な点: プレゼンテーション層とデータ層の橋渡しをするが、できるだけ薄くする(Fat Controllerを防ぐ)
-
役割: アプリケーションのロジック(ビジネスロジック)を処理する
-
データ層(Data Layer)
- 役割: データベースとのやり取りを担当する
-
具体例:
Model
(ActiveRecord, Repositoryパターン) - MVCでの対応: M(Model)
- 重要な点: データの取得・保存・更新を担当する。ビジネスロジックが入ることもあるが、Modelが肥大化しやすいので適度に分割する。
MVCと3層アーキテクチャの対応関係
MVCは「3層アーキテクチャ」にほぼ対応している(似ている)けれど、アプリケーション層(コントローラ)にはビジネスロジックを書かず、別の場所(Service, Decorator, ViewObject)に分離するのが理想とする。
3層アーキテクチャ | MVCの対応 | 役割 |
---|---|---|
プレゼンテーション層 | View (V) | ユーザーにデータを表示する |
アプリケーション層 | Controller (C) | 入力の処理・モデルとViewの橋渡し |
データ層 | Model (M) | データの管理・ビジネスロジック |
Fat Controllerを避けるため、「ビジネスロジックはModelに書く」という考え方が強い。
しかし、その分モデルに全部詰め込み、Fat Model(太ったモデル)になり、それもそれで管理が難しくなってしまう。
⇒Fat Model(太ったモデル)を防ぐために、ビジネスロジックを分離する
代わりにHelper / Decorator / ViewObject を使用する。
この3つのやり方どれを使うかは、チームの文化によって異なる。
Helper / Decorator / ViewObject
(1)Helper(ヘルパー)
app/helpers/user_helper.rb にメソッドを定義し、View側ではメソッドを呼ぶだけにする方法。
module UserHelper
def user_owned_group_names(user:)
user.owned_groups.pluck(:name).join(", ")
end
end
コントローラ
def index
# N+1発生しないようにjoins&preloadしている。eager_loadやincludesでもOK
@users = User.joins(:owned_groups).preload(:owned_groups)
end
ビューはこれだけで済む。
<% @users.each do |user| %>
<span class="task-meta">
管理者: <%= user_owned_group_names(user:) %> # helperのメソッドを呼び出すだけ
</span>
<% end %>
N+1クエリ問題とは
データの取得時に不必要に多くのクエリ(問い合わせ)が発行される問題。
<% @users.each do |user| %>
<%= user.owned_groups.pluck(:name).join(", ") %>
<% end %>
@users = User.all
上記コードだと、発生するクエリは以下。
SELECT * FROM users;
SELECT * FROM groups WHERE user_id = 1;
SELECT * FROM groups WHERE user_id = 2;
SELECT * FROM groups WHERE user_id = 3;
...
最初のUserを取得するクエリ(1回) + それぞれのユーザーの owned_groups を取得するクエリ(N回) = 「N+1クエリ」が発生する。
ユーザーが100人いる状況だと、全ユーザの取得(1回)+各ユーザの owned_groups を取得(100回)で、計101回クエリが発生することになる。
問題になる点
- クエリの数が増えると、データベースへの負荷が高まる
- パフォーマンスが悪化する(特にデータ量が多い場合)
解決策
- preloadメソッド
- eager_loadメソッド
- includesメソッド
の使用で解決できる。
(2)Decorator(デコレーター)
ModelとViewの中間にデータの加工の役割を担うもの。gemをインストールが必要となる。
app/decorators/user_decorator.rb を作成し、そこに処理をまとめる。
class UserDecorator < SimpleDelegator
def owned_group_names
owned_groups.pluck(:name).join(", ")
end
end
コントローラにdecorateしてからViewに渡す。
# Controller
def index
@users = User.includes(:owned_groups).map { |user| UserDecorator.new(user) }
end
ビューはこんな感じ。
管理者: <%= user.owned_group_names %>
(3)ViewObject(ビューモデル)
複数のデータを組み合わせて表示したい時に使う。(ユーザーの情報+その人の投稿数などを1つのオブジェクトとして扱いたい場合)
app/view_objects/user_profile_view.rb を作成する。
class UserProfileView
def initialize(user)
@user = user
end
def owned_group_names
@user.owned_groups.pluck(:name).join(", ")
end
def task_count
@user.tasks.count
end
end
コントローラでViewObjectに変換。
def index
@users = User.includes(:owned_groups, :tasks).map { |user| UserProfileView.new(user) }
end
ビューで呼び出す。
管理者: <%= user.owned_group_names %> /
合計投稿件数: <%= user.task_count %>
参考文献
ビジネスロジック
Helper / Decorator / ViewObject
N+1クエリ問題
Discussion