肥大化したRailsのリファクタリングをしていますが、一部のcontrollerやmodelに関しては処理が多くなり限界を感じつつありました。
そこでdecoratorやpresenterを活用してみようと思ったものの、この2つの違いなどがいまいち分かりづらく感じました。色々読んでみて知ったそれぞれの違いや役割について整理してみました。
decorator/presenterとは?
Decoratorはソフトウェアのデザインパターンの一つです。GoFによって定義されたそうです。
Presenterは元々はMVCから派生したアーキテクチャパターンのMVP(Model-View-Presenter)の考えからきた概念のようです。Controllerの肥大化防止、テスタビリティの向上が期待できるみたいです。
decoratorとpresenterの役割
下記の記事では、あくまでチーム内での定義を書いてくださってますが非常にわかりやすかったので引用させていただきました。
- Decorator
- 単一のモデルクラスに対応する ViewModel
- Presenter
- 複数のモデルクラスにまたがる ViewModel
- 永続化されたモデルと一致しない ViewModel
- ViewModel
Decorator
、Presenter
の上位概念。ビューに関連するロジックをまとめるレイヤーを指す。
Decorator
とPresenter
の違いは単一のモデルを扱うか複数のモデルを扱うかどうかです。また、永続化されないケースやActiveRecord化されていないモデルなんかもPresenterで扱うそうです。
ViewModel
についてはView Object
とかPresenter層
と表現されることも多いですが、ビューに関連するロジックをまとめるレイヤーと解釈すれば問題ないかと思います。
decoratorについて
decoratorは単一のモデルクラスに対応するものです。例えば、userモデルがあった場合、そのうちビューに関連するロジックをまとめる場合はuser_decorator
を作り、ここに寄せていくような形です。
以下はdraperからの引用です。
app/helpers/articles_helper.rb# articleインスタンスのpublishedに応じて表示の出し分けをするhelper def publication_status(article) if article.published? "Published at #{article.published_at.strftime('%A, %B %e')}" else "Unpublished" end end
上記のようなものがビューに関連するロジックといえます。こうした処理をモデルに書いているパターンもあるかもしれませんが、これらもdecoratorにまとめることができます。
app/decorators/article_decorator.rbclass ArticleDecorator < Draper::Decorator delegate_all def publication_status if published? "Published at #{published_at}" else "Unpublished" end end def published_at object.published_at.strftime("%A, %B %e") end end
あとはcontrollerでdecoratorを呼び出します。
app/controllers/articles_controller.rbdef show @article = Article.find(params[:id]).decorate end
このようにdecoratorを使うことでViewに関連したロジックをまとめることができます。
どうやって実装するのか?
draper
やactive_decarator
などのgemが有名です。また、かんたんなdecoratorであればPOROで定義している方もいるみたいです。
両方使っているわけではないのでかなり粗いですが、大まか違い下記です。
-
ActiveDecorator
はViewでActiveRecordを取り扱うときに暗黙的にデコレートしたオブジェクトが渡される -
Draper
は明示的にActiveRecordインスタンスのあとに.decorator
を宣言する必要がある
これらの詳細について他の記事もたくさんあるので詳細は割愛します。
ちなみに自分はプロジェクトの規模感や明示的にdecoratorを使っていることがわかりやすいという点からdraperを導入しました。プロジェクト規模やチームに合わせて導入してみてください。
helperじゃだめなのか?
viewのロジックを担うという点ではRailsではhelperの役割では?と思うかもしれません。
ただ、helperは名前空間がグローバルなため、他のhelperと名前が重複しないよう注意する必要があります。
class UserHelper
def full_name
#~~~
end
end
class ShopHelper
def full_name
#~~~
end
end
また、本来userインスタンスを想定しているところにshopインスタンスをいれてしまう、といったトリッキーなこともできてしまう恐れがあります。
モデルに依存しないような汎用的な処理であればhelperに持たせても良いですが、モデルに依存するような形なのであれば、decoratorにまとめるほうが安全かつ明確です。
decoratorを取り扱う上での注意事項
かといって、なんでもかんでもhelperのロジックをdecoratorに入れるのはアンチパターンです。オブジェクト指向の原則に従って適切に切り分けることが重要です。
Decoratorを従来のビューヘルパー代わりに用いることは避けましょう。Decoratorからコードの臭いが立ち昇ったら適切に処理し、メソッドや責務が複雑にならないようにしましょう。Decoratorクラスを新しく導入することを恐れてはいけませんが、Decoratorが存在してもよい正当な理由がある場合にのみ行いましょう。
presenterについて
decoratorに比べるとRailsで取り扱われている例が少ないかもしれませんが、contorollerを薄くしたい場合に重宝しています。
自分はPOROでpresenterクラスを作成し、こちらをcontrollerで生成する形で取り入れています。この方法であればgemなどを入れる必要もないので簡単に試すことができます。
まず、app/presenters
などをつくりpresenterクラスを定義します。
# POROで実装
class HogePresenter
def initialize(hoge_id)
@hoge = Hoge.find(hoge_id)
end
def fuga
@hoge.xxx
end
end
controllerでpresenterのみを呼び出します。
class HogeController < ActionController::Base
def index
@presenter = HogePresenter.new(params[:hoge_id])
end
end
下記のように@presenter
からViewのロジックを呼び出します
<%= @presenter.fuga %>
このようにすることで、controllerの処理をほとんどpresenterにまとめることができます。また、presenterに分離されたことで、テスタビティも向上します。
インスタンス変数やset_*メソッドを減らせる
Rails Wayに沿って愚直に開発していると場合よってはcontrollerにはインスタンス変数が増えていきます。また、set_xxx
といったprivateメソッドをbefore_actionで呼びまくるみたいなことにもなりがちです。
下記のように懐疑的に思う人もちらほら見受けられました。
自分たちのプロジェクトでもbefore_actionにset_
しまくって明らかに可読性が下がっている箇所があったりしました。(Rubocopに従っているとなりがちです)
しかし、presenterであれば上記のインスタンス変数も1つになり、可読性の向上につながります。
presenterの利点まとめ
- インスタンス変数やsetter系のメソッドが減り、controllerを薄くすることができる
- テスタビリティが高い
- POROでもはじめられるので参入障壁が低い
まとめ
以上、「decorator/presenterの概念を理解してRailsをスリムにする」でした!
- decoratorもpresenterもビューに関連するロジックをまとめるレイヤーを指す
- Decoratorはソフトウェアのデザインパターンの一つで単一のモデルクラスに対応し、ヘルパーやモデルの肥大化防止に役立つ
- helperよりも安全に扱いやすい
- Railsで扱いやすいgem(daraperやActiveDecorator)がある
- Presenterは複数のモデルクラスにまたがり、controllerの肥大化やテスタビティの向上が期待できる
- インスタンス変数やset_*メソッドが減らせる
- POROで簡単に始められるので参入障壁が低い
decoratorやpresenterの理解が少しでも進んだなら幸いです。