KitchHike Tech Blog

KitchHike Product, Design and Engineering Teams

Decorator と Presenter を使い分けて、 Rails を ViewModel ですっきりさせよう

はじめに

こんにちは。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 は、いろいろと関連用語の多い概念です。ちょっと調べるだけでも DecoratorViewObjectPresenter などが出てきます。「あれ、これって同じ意味じゃない?どう違うの?」と混乱することもしばしば・・・。

さらに ViewModel 自体も使われるアーキテクチャなどの文脈によって、同じ用語でも前提や役割が異なってきます。たとえば MVVM(Model-View-ViewModel) での ViewModel はデータバインディングが前提となっていますし、その役割も今回説明している ViewModel とは異なってきます。

そんなわけで色々と混乱しがちな ViewModel 関連ですが、大事なことは開発メンバーの間で認識が統一されていて、同じ用語を別の意味で使っているといった齟齬がなくなることだと思います。

なのでKitchHikeでは ViewModel およびその関連用語について、以下のような用法で使い分けるようにしています。

  • Decorator
    • 単一のモデルクラスに対応する ViewModel
  • Presenter
    • 複数のモデルクラスにまたがる ViewModel
    • 永続化されたモデルと一致しない ViewModel
  • ViewModel
    • DecoratorPresenter の上位概念。ビューに関連するロジックをまとめるレイヤーを指す。

用法としてはかなりシンプルといっていいと思います。とはいえ、先述の 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を使いましたが DecoratorPORO で簡単に代替することができます。

それだけではありません。シンプルな手法ゆえにRubyでなくては、あるいはRailsでなくてはできない手法ではありません。たいていの言語、Webアプリケーションフレームワークであれば、明日から取り入れられるものです。

というわけで、あなたがもしロジックだらけのビューファイルを前にして途方に暮れていたのなら、明日から ViewModel を使ってみましょう!

まとめ

以上、KitchHikeでの ViewModel の使い方についてご紹介しました。

  • ViewModel は表示ロジックをすっきりさせ、メンテナンス性を向上させるためのテクニック。
  • 単一のモデルの表示ロジックを扱う場合は Decorator を使う。支援するgemも多いので便利。
  • 複数のモデルにまたがる表示ロジックを扱う場合は Presenter を使う。Decorator では扱いにくいクラス間共通の表示ロジックを分散させずに扱うことができる。
  • ViewModelPORO でも書ける、非常に簡単なしくみの割に効果的。たいていの言語やフレームワークで適用できる汎用的なパターン。

最後にコードの実装時および本記事を書くにあたって参考にさせていただいた記事の一覧です。こちらも是非ご覧くださいね。

参考

RailsでDraperを使ってプレゼンテーション層(デコレーター)を実装する

Decorator Pattern in Ruby

Rails: Presenter Pattern

Cleaning Up Your Rails Views With View Objects


キッチハイクでは、一緒に ViewModel を書いてくれるRailsエンジニア & React Nativeエンジニアを絶賛募集中です!

www.wantedly.com

www.wantedly.com

www.wantedly.com