はじめに
こんにちは。KitchHikeエンジニアの小川です。
Webアプリケーション開発において、コードベースが大きくなってくると、よく問題になるものの一つが表示ロジックの重複ではないでしょうか。知らず知らずのうちにビューに同じようなロジックが増えて、コードの見通しが悪くなってくるのです。
KitchHikeのコードにもその兆候が見られはじめたので、対策として ViewModel
パターンを取り入れています。このパターンを適用することで、表示ロジックを集約し、コードのメンテナンス性を向上させることができました。
ViewModel
は、そんなに凝ったことをしているわけではないのですが、コードをすっきりさせる上ではかなり強力なパターンです。
今回はこの ViewModel
について、実際の使い方を含めご紹介したいと思います。
ViewModelとは?
一言でいえばビューに関連するロジックを抜き出して、 ViewModel
というレイヤーにそれらを集約する実装のパターンです。
例えば以下のような日付時刻の表示ロジック、いかにも色んな所で使いそうですね。
<%= @event.start_at.strftime('%Y/%m/%d %H:%M') %>
これをビューにベタ書きしていくと、同じ表示ロジックが重複して、メンテナンス性が低下します。どこかにまとめたいものです。
しかし、こういったロジックをモデルクラスに定義するのも考えものです。これらはあくまで表示のためだけのロジックだからです。 数が少ないうちはいいですが、表示ロジックが増えてくると、モデルが肥大化し、本来のビジネスロジックの見通しが悪くなってきます。
この解決のために、表示ロジック用のクラスを作成して、そこにロジックを集約させるのが ViewModel
の発想です。
<%= @event_view_model.start_datetime %>
helperじゃダメなの?
ここまでご覧になって、「 Railsには helper
があるのに、それじゃダメなの?」と疑問に思われる方がいらっしゃるかもしれません。確かにその通りです。 helper
を使わずに、あえて ViewModel
を取り入れるメリットはなんでしょうか。
端的に言えば、helper
よりも ViewModel
の方が、どのドメインで使われるものなのかを安全かつ明確に記述できるからです。
まず、 helper
にはスコープの問題があります。名前空間がグローバルなので、メソッド名が衝突する危険性があります。
一方、 ViewModel
は関連するビューのドメインごとにクラスを分割するので、その危険性はありません。
また、アプリケーションが大規模になってくると、メソッドが思わぬ使い方をされてバグを生むことがあります。それを避けるには、そのメソッドがどのような場合に使われるものなのかを明確にすることが大事です。
helper
はそのドメインに応じてファイルを分割することができますが、実質はグローバル関数です。以下の例のように、本来想定されない使われ方もできてしまいます。
<%= format_event_datetime(@event.start_at) %> # 本当はEventモデル用なのに・・・ <%= format_event_datetime(@popup.start_at) %>
ViewModel
であれば、それがどのドメインに属するものなのかを明示的に書くことができます。想定外の使われ方もされづらくなります。
<%= @event_view_model.start_datetime %>
もちろん、小規模なアプリケーションであれば、 helper
で必要十分です。ただ、規模が大きくなってくると helper
では色々限界が見えてきます。ある程度の規模のアプリケーションにおいては、 ViewModel
の方がより良い選択肢となるのではないかと思います。
ViewModelの関連用語いろいろと、KitchHikeでの使い分け
ところで ViewModel
は、いろいろと関連用語の多い概念です。ちょっと調べるだけでも Decorator
、 ViewObject
、 Presenter
などが出てきます。「あれ、これって同じ意味じゃない?どう違うの?」と混乱することもしばしば・・・。
さらに ViewModel
自体も使われるアーキテクチャなどの文脈によって、同じ用語でも前提や役割が異なってきます。たとえば MVVM(Model-View-ViewModel)
での ViewModel
はデータバインディングが前提となっていますし、その役割も今回説明している ViewModel
とは異なってきます。
そんなわけで色々と混乱しがちな ViewModel
関連ですが、大事なことは開発メンバーの間で認識が統一されていて、同じ用語を別の意味で使っているといった齟齬がなくなることだと思います。
なのでKitchHikeでは ViewModel
およびその関連用語について、以下のような用法で使い分けるようにしています。
Decorator
- 単一のモデルクラスに対応する
ViewModel
- 単一のモデルクラスに対応する
Presenter
- 複数のモデルクラスにまたがる
ViewModel
- 永続化されたモデルと一致しない
ViewModel
- 複数のモデルクラスにまたがる
ViewModel
Decorator
、Presenter
の上位概念。ビューに関連するロジックをまとめるレイヤーを指す。
用法としてはかなりシンプルといっていいと思います。とはいえ、先述の MVVM(Model-View-ViewModel)
や、 MVP(Model-View-Presenter)
のアーキテクチャに慣れ親しんでいる方にとっては違和感があるかもしれませんね・・・。繰り返しになりますが、KitchHikeではこの用法で使用している、ということで見ていただければと思います。
ViewModelのケーススタディ
以降では、 ViewModel
をどのように使っているか、パターンごとにケーススタディを交えてみていきたいと思います。
KitchHikeでは大別して以下2つのパターンで ViewModel
を活用しています。
Decorator
による単一モデルの表示ロジックの整理Presenter
による複数モデルにまたがった表示ロジックの整理
それぞれ見ていきましょう。
Decoratorで単一モデルの表示ロジックを整理する
本記事の冒頭で紹介したのがこのパターンです。おそらく既に取り入れている方も多いのではないでしょうか。
ビューにベタ書きされた表示ロジックを Decorator
クラスに集約します。このパターンは、一番メジャーなパターンであるだけに、それを支援するGemも豊富です。 ActiveDecorator
や Draper
が定番といえるでしょう。
どのように Decorator
を適用するか以下のようなビューファイルのif文をお題に書き換えてみましょう。
- if @popup.max_capacity <= @popup.total_seats %span 満席 - else %span 空き席あり
KitchHikeでは Draper
を使っているので、 Draper
で書き換えてみます。
まずはこのような Draper::Decorator
を継承した Decorator
クラスを定義します。
# app/decorators/popup_decorator.rb class PopupDecorator < Draper::Decorator delegate_all end
ここにビューのロジックを書き加えていきます。
# app/decorators/popup_decorator.rb class PopupDecorator < Draper::Decorator delegate_all def seats_label if model.max_capacity <= model.total_seats "満席" else "空席あり" end end end
次は定義した Decorator
を呼び出せるようにしましょう。 コントローラで以下のようにモデルのオブジェクトをデコレートすると、 Decorator
に定義したメソッドが呼び出せるようになります。
class PopupsController < ApplicationController def show @popup = Popup.find_by(title: "foo").decorate end end
これで元のビューからif文を無くすことができました!だいぶシンプルになりましたね。
%span=@popup.seats_label
また、シンプルになるだけではありません。 helper
との比較で説明したように、名前衝突の危険もなくなりましたし、ドメインに対して表示ロジックが紐づく形となるので、よりオブジェクト指向なコードになるというメリットもあります。
Presenterで複数モデルにまたがった表示ロジックを整理する
このようにいいことずくめの Decorator
ですが、不得意なところもあります。
Decorator
による実装は、基本的に単一のモデルでの表示ロジックの集約というユースケースを想定しています。
したがって、複数モデルにまたがる表示ロジックがある場合や、たくさんのモデルをViewで扱う必要がある場合などは、 Decorator
だけでは対処が難しいのです。
そんな場合は Presenter
を使うのが効果的です。
Presenter
は、 Decorator
と比べるといまいち影の薄いパターンのように思うのですが、複雑なロジックを整理する上では非常に強力です。
単純な例ですが、キャンペーン対象のユーザーのみ見れるようなデータを考えてみましょう。 キャンペーン対象のユーザーのみのデータなので、それ以外のユーザーには表示しませんし、そもそもデータを取得する必要はありません。
class PopupsController < ApplicationController def index if current_user.campaign_user? @campaign_message = Message.where(...) @campaign_popups = Popup.where(...) end # ... end end
このような場合、Decorator
ではどうしたものかと考え込んでしまいます。というのもロジックを実装するにしても複数のクラスにまたがっているので、個々のクラスに実装すると表示ロジックが分散してしまうからです。
ここで Presenter
を使ってみましょう。
class PopupPresenter def initialize(user) @user = user end def campaign_user? @user.campaign_user? end def campaign_message @campaign_message = Message.where(...) if campaign_user? end def campaign_popups @campaign_popups = Popup.where(...) if campaign_user? end end
Presenter
は、同じ表示ロジックに関連するクラスを全て包含するようにして定義します。こうすることで同じ表示ロジックのまとまりを分散させることなく、一つのクラス内で扱うことができます。
そして、コントローラから Presenter
を呼び出すようにしてみます。
class PopupsController < ApplicationController def index @presenter = PopupPresenter.new(current_user) # ... end end
表示の出し分けロジックを Presenter
に寄せることで、 コントローラがすっきりしました! もちろんビューも Presenter
を呼び出すことでよりきれいに書くことができます。
さらに、表示ロジックをコントローラから引きはがすことでテストも書きやすくなります。シンプルだけど非常に効果的です。
明日からあなたのコードもViewModelですっきりさせてみよう
さて、ここまで実際の ViewModel
の使い方を見てきました。
「思ったより簡単!」と思われたのではないでしょうか。その通り、非常にシンプルな手法なのです。
コードを見ていただければ分かる通り、 Presenter
の実装はいたってシンプルに PORO ( Plain Old Ruby Object )
だけです。また、 今回の紹介ではGemを使いましたが Decorator
も PORO
で簡単に代替することができます。
それだけではありません。シンプルな手法ゆえにRubyでなくては、あるいはRailsでなくてはできない手法ではありません。たいていの言語、Webアプリケーションフレームワークであれば、明日から取り入れられるものです。
というわけで、あなたがもしロジックだらけのビューファイルを前にして途方に暮れていたのなら、明日から ViewModel
を使ってみましょう!
まとめ
以上、KitchHikeでの ViewModel
の使い方についてご紹介しました。
ViewModel
は表示ロジックをすっきりさせ、メンテナンス性を向上させるためのテクニック。- 単一のモデルの表示ロジックを扱う場合は
Decorator
を使う。支援するgemも多いので便利。 - 複数のモデルにまたがる表示ロジックを扱う場合は
Presenter
を使う。Decorator
では扱いにくいクラス間共通の表示ロジックを分散させずに扱うことができる。 ViewModel
はPORO
でも書ける、非常に簡単なしくみの割に効果的。たいていの言語やフレームワークで適用できる汎用的なパターン。
最後にコードの実装時および本記事を書くにあたって参考にさせていただいた記事の一覧です。こちらも是非ご覧くださいね。
参考
RailsでDraperを使ってプレゼンテーション層(デコレーター)を実装する
Cleaning Up Your Rails Views With View Objects
キッチハイクでは、一緒に ViewModel
を書いてくれるRailsエンジニア & React Nativeエンジニアを絶賛募集中です!