- 自分用のメモを公開したものです。ドキュメント/ソースコード等をまとめただけで試していないコードも多いので、信頼性は低いです。
感想とか
感想
- ヘルパーをオブジェクト指向的に書けるようになる
- 個人的には類似gemのActiveDecoratorのがシンプルで好みかも。でもDraperも十分良さそう
pros
- 高機能
- 関連先もデコれる
- Decorator gemでは一番利用者が多いっぽい
cons
- 毎回明示的にデコらないといけない(prosと考えることもできる)
- どこでもデコれるのは自由度がありすぎてカオスになりそう ->
decorates_assigned
を使えば回避できるかも
基本的な使い方
導入
1. インストール
gem 'draper'
$ budle install
2. セットアップ
$ rails generate draper:install
3. デコレータを作成して、使用する
$ rails generate decorator Article
class ArticleDecorator < Draper::Decorator
delegate_all
def publication_status
if published?
"Published at #{published_at.strftime("%A, %B %e")}"
else
"Unpublished"
end
end
end
# ビュー
<%= @article.publication_status %>
ヘルパー/ビュー/モデルにロジックを置く場合との比較
ヘルパーにロジックを置く場合
- 手続き的で、オブジェクト指向的でない
- 名前空間がグローバル(ファイルは分けらる)
- 名前衝突の可能性
- どこに定義されているかわからない
def publication_status(article)
if article.published?
"Published at #{article.published_at.strftime('%A, %B %e')}"
else
"Unpublished"
end
end
<%= publication_status(@article) %>
ビューにロジックを置く場合
- 手続き的で、オブジェクト指向的でない
- メンテナンスしにくい
- 読みにくい
<% if @article.published? %>
<%= "Published at #{@article.published_at.strftime('%A, %B %e')}" %>
<% else %>
Unpublished
<% end %>
モデルに置く場合
- モデルにはドメインロジックを置きたい。プレゼンテーションロジックがモデルにあるのは微妙
- HTMLを描画するための機能(helper等)が利用できない
- fat model
class Article < ApplicationRecord
def publication_status
if published?
"Published at #{published_at.strftime("%A, %B %e")}"
else
"Unpublished"
end
end
end
# ビュー
<%= @article.publication_status %>
デコレータにロジックを置く場合
- オブジェクト指向的
- プレゼンテーションロジックをモデルから分けられる
class ArticleDecorator < Draper::Decorator
delegate_all
def publication_status
if published?
"Published at #{published_at.strftime("%A, %B %e")}"
else
"Unpublished"
end
end
end
# ビュー
<%= @article.publication_status %>
ヘルパーメソッドを使う*2
hメソッド: ヘルパーにアクセス
h
メソッドを通して、ヘルパーにアクセスできる
- Railsのヘルパー、自作ヘルパー両方にアクセスできる
helpers
でもok
class ArticleDecorator < Draper::Decorator
def emphatic
h.content_tag(:strong, "Awesome")
end
end
include Draper::LazyHelpers: ヘルパーを直接include
- hを介さず、ヘルパーを直接include
- でも大量のヘルパーが定義されちゃって、デコレーター/モデル/ヘルパーのメソッドがごっちゃになるから、いまいちっぽい
class ArticleDecorator < Draper::Decorator
include Draper::LazyHelpers
end
モデルのメソッドを使う*3
object: モデルインスタンスにアクセス
object.published_at.strftime("%A, %B %e")
model.published_at.strftime("%A, %B %e")
delegate_all: 全てのメソッドを委譲
- 内部的には
method_missing
を使い委譲している
- メソッド探索の順番: デコレータ -> 親デコレータ -> モデル
class ArticleDecorator < Draper::Decorator
delegate_all
end
delegate: 指定のメソッドを委譲
- deleageメソッドはActive Supportのdelegateとほぼ同じ。ただし
to: :object
を省略できる。
class ArticleDecorator < Draper::Decorator
delegate :title, :body
delegate :name, :title, to: :author, prefix: true
end
オブジェクトをデコる*3
基本
def show
@article = Article.find(params[:id]).decorate
end
デコる方法は3つ
- 一番オススメ
- デコレータをモデル名から推測してくれる(そのためデコレータ名がモデル名から推測できないといけない)
@article = Article.find(params[:id]).decorate
@article = ArticleDecorator.new(Article.find(params[:id]))
@article = ArticleDecorator.decorate(Article.find(params[:id]))
@widget = ProductDecorator.new(Widget.first)
@widget = ProductDecorator.decorate(Widget.first)
.decorates_finders
を使うと、デコレータがfind系(find/all/first/...)のメソッドが利用できるようになる。これでfindしつつデコることが可能
- でもいまいちな気がする。Draperのデコレータはビューで使うことを想定している。DBアクセスはモデルがやるべきで、デコレータの仕事じゃない
class ArticleDecorator < Draper::Decorator
decorates_finders
end
@article = ArticleDecorator.find(params[:id])
コレクションの各要素をデコる*2
- コレクションを操作すると、各要素は一気にデコられる
@articles = Article.recent.decorate
@articles = ArticleDecorator.decorate_collection(Article.all)
コレクション自体をデコる
- モデルのクラスメソッドを生やすイメージ
- Draper::CollectionDecoratorを継承
- 使わなそう -> patinationのメソッド生やすのに使える
class ArticlesDecorator < Draper::CollectionDecorator
def page_number
42
end
end
@articles = Article.all.decorate
@articles.page_number
関連先をデコる*2
メソッド
decorates_association: 関連先をデコる
class ArticleDecorator < Draper::Decorator
decorates_association :author
end
class AuthorDecorator < Draper::Decorator
def hoge
model.name + "hogehoge"
end
end
article = Article.first.decorate
article.author.hoge
decorates_associations: 関連先をデコる(複数)
class ArticleDecorator < Draper::Decorator
decorates_associations :author, :comments
end
オプション
with: 関連先のデコレーターを指定する
class ArticleDecorator < Draper::Decorator
decorates_association :author, with: FancyPersonDecorator
end
scope: 関連先をscopeで絞り込む
- decorate対象の絞り込ではなく、レコード自体の絞り込み
class ArticleDecorator < Draper::Decorator
decorates_association :comments, scope: :recent
end
context: 関連先にcontextを渡す
class ArticleDecorator < Draper::Decorator
decorates_association :author, context: {foo: "bar"}
end
class ArticleDecorator < Draper::Decorator
decorates_association :author,
context: ->(parent_context){ parent_context.merge(foo: "bar") }
end
POROをデコる
- ActiveRecord::BaseにはDraper::Decoratableモジュールがincludeされている。これにより
Article.first.decorate
のような処理が可能になる
- POROにもDraper::Decoratableモジュールをincludeすれば同じことが可能になる
class Cat
include Draper::Decoratable
end
class CatDecorator < ApplicationDecorator
def nyaa
"nyaa"
end
end
Cat.new.decorate.nyaa
- ActiveModel::Modelとかのformで使うようなオブジェクトをデコレートする時に便利そう
class ArticleForm
include ActiveModel::Model
include Draper::Decoratable
attr_accessor :field1, ...
end
自動でデコる
- デコレーターはモデルのように振る舞えるので、actionの開始時にデコレートして、ずっとデコレータを使いたくなる。でもコレは非推奨。ビューで使うように設計されているので、ビュー以外で使うべきでない。そのためコントローラーではモデルを使い、render直前にデコって、ビューではデコレータを使う
decorates_assigned
を使うとこれを自動化できる。コントローラーでは@article
をモデルとして扱い、ビューではarticle
をデコレーターとして扱える。
decorates_assigned
はGoRailsでもおすすめしてた。draperは結構自由にデコレートできてしまいカオスになりそうなので、コレを使って自動化しておくとよさそう(参考: https://gorails.com/episodes/decorators-with-draper)
class ArticlesController < ApplicationController
decorates_assigned :article
def show
@article = Article.find(params[:id])
end
end
# app/views/articles/show.html.erb
<%= article.decorated_title %>
def article
@decorated_article ||= @article.decorate
end
helper_method :article
decorates_assigned :article, :articles
ページネーションと一緒に使う
問題点
- kaminari等のページネーションgemと一緒に使うには一工夫必要
- ページネーションgemはRelationにcurrent_page等のメソッドを追加する。そのままデコレートしてしまうとcurrent_page等のメソッドにアクセスできなくなってしまう。
cats = Cat.all
cats = cats.page
cats.current_page
cats = cats.decorate
cats.current_page
解決方法1: Draper::CollectionDecorator
でdelegate
- 解決方法は色々あるっぽいが、デコレータコレクションの親クラスである
Draper::CollectionDecorator
でdelegate
してしまうのが簡単そう。
- 参考
Draper::CollectionDecorator.delegate :current_page, :total_pages, :limit_value, :total_count
解決方法2: Draper::CollectionDecorator
のサブクラスをコレクションにする
class PaginatingDecorator < Draper::CollectionDecorator
delegate :current_page, :total_pages, :limit_value, :total_count
end
class ApplicationDecorator < Draper::Decorator
def self.collection_decorator_class
PaginatingDecorator
end
end
class AnimalDecorator < ApplicationDecorator
end
その他メモ
Decorator/Presenter/ViewModel/Exibitの違い
- Decorator/Presenter/ViewModel/Exibitの違いがわからん
- いくつか記事を見た感じ、どうも文脈/人によって同じ言葉でも意味が変わるっぽい
- draperの文脈では全て同じもの(ビューヘルパーをオブジェクト指向的に扱うための、モデルのデコレータ)を指すことが多い気がする
- draperのDecoratorはGoFのDecoratorパターンに由来すると思われるが、ビュー以外での使用は想定していない(はず)。あんまり名前にこだわりすぎないほうがいいかもしれない
参考サイトのまとめ
- チーム内で認識を統一して、用語を使い分けてる
- 良さそう。でも難しい。
- 用法
- Decorator: 単一のモデルクラスに対応する ViewModel
- Presenter: 複数のモデルクラスにまたがる ViewModel、永続化されたモデルと一致しない ViewModel
- ViewModel: Decorator 、Presenter の上位概念。ビューに関連するロジックをまとめるレイヤーを指す。
- 全体的に難しい。あんまり理解できてない
- 用法
- Decorator: Presenter、Exibitの上位概念。GofのDecoratorと同じで、オブジェクトに機能/振る舞いを追加するもの。サンプルではSimpleDelegatorやDecoratorを使い実装
- Presenter: Decoratorの一種。モデルをデコレートするプレゼンテーションロジック置き場?(自信なし)
- Exibit: Decoratorの一種。モデルをビューに繋げる。Exibit自体はビューの機能を持たず、contextを持ち、contextにrenderさせる
- Presenter + Exhibit: PresenterとExhibitは排他的な関係ではない。同時に実現可能。draperはこれ?
- 用法
- Presenter: 一つのView につき一つの Decorator
- ActiveDecorator(gem): 一つの model につき一つの Decorator(たぶんdraperもコレに該当)
- Exhibit: View x Model の組み合わせにつき一つの Decorator
デコレータのクラス*2
Draper::Decorator: モデルインスタンスのデコレータ
- article.decorateの戻り値は、このクラスのサブクラス
- 基本コレをつかう
class 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
# ビュー
<%= @article.publication_status %>
Draper::CollectionDecorator: コレクションのデコレータ
- articles.decorateの戻り値は、このクラスのサブクラス
- 使わなそう
class ArticlesDecorator < Draper::CollectionDecorator
def page_number
42
end
end
@articles = Article.all.delegate
@articles = ArticlesDecorator.new(Article.all)
@articles = ArticlesDecorator.decorate(Article.all)
contextで追加データを渡す
Article.first.decorate(context: {role: :admin})
デコレータでHTMLをレンダリング
def emphatic
h.content_tag(:strong, "Awesome")
end
- 長い場合は、
h.render
で部分テンプレートを使って組み立てる
def sub_view
h.render 'sub_view', title: model.title
end
デコレータはモデルのように振る舞う
@person = Person.first
@decorated_person = @person.decorate
@decorated_person.is_a?(Person)
@person == @decorated_person
モデルメソッドをオーバーライドする
def created_at
article.created_at.strftime("%m/%d/%Y - %H:%M")
end
@article.created_at
ApplicationDecorator
- ApplicationRecordに合わせて、Draper::DecoratorではなくApplicationDecoratorを継承すると吉
class ApplicationDecorator < Draper::Decorator
end
class ArticleDecorator < ApplicationDecorator
end
ApplicationControllerの名前変更
- ベースとなるコントローラーはApplicationControllerで、draperもこれを仮定している
- 変更したい場合は以下の通り
Draper.configure do |config|
config.default_controller = BaseController
end
Active Jobとの統合
- デコレートされたオブジェクトをJobとしてシリアライズできるが、デシリアライズ時にはデコレートは解除されている。モデルオブジェクトのGlobal IDを使ってシリアライズをするため。
テスト
- RSpec, MiniTest::Rails, Test::Unitの3つに対応
- decoratorをgenerateした場合、テストも追加してくれる
RSpec
- デフォルトのパスはspec/decorators。パスを変えるなら、
type: :decorator
が必要
RSpecのコントローラーテスト
assigns(:article).should be_decorated
assigns(:article).should be_decorated_with ArticleDecorator
ApplicationController以外を使う場合はコレが必要
config.before(:each, type: :decorator) do |example|
Draper::ViewContext.controller = ExampleEngine::CustomRootController.new
end
isolated test
- Draperはヘルパーメソッドにアクセスするために、view contextを必要とする
- デフォルトではApplicationControllerのview contextを使う
- この依存を取り除いて、テストをスピードアップしたい場合は以下の設定をする。ただしデコレーターはヘルパーにアクセスできなくなる
Draper::ViewContext.test_strategy :fast
Draper::ViewContext.test_strategy :fast do
include ApplicationHelper
end
1.0へのアップデート
- 2013に1.0へのアップデートがあって、かなり変更が入ったらしい
- 主要なものだけ
Draper::Base -> Draper::Decorator
Draper::DecoratorEnumerableProxy -> Draper::CollectionDecorator
Draper::ModelSupport -> Draper::Decoratable
自動的に委譲 -> delegate_allで明示的に委譲
委譲の制御allows/denies/denies_allは削除
delegate_allでクラスメソッドも委譲する
デフォでfind系メソッドは追加されない
その他の使いそうなメソッド
l: デコレータでlocalizeメソッド
- よく使うので
helpers.localize
はl
で使える
localize
でもok
decorator.decorated_with?: 指定クラスでデコレートされていればtrue
Cat.first.decorate.decorated_with?(CatDecorator)
decorator.decorated?: オブジェクトがデコレートされていればtrue
Article.first.decorate.decorated?
Article.first.decorated?