猫Rails

ねこー🐈

Draperの使い方 まとめ

  • 自分用のメモを公開したものです。ドキュメント/ソースコード等をまとめただけで試していないコードも多いので、信頼性は低いです。

感想とか

感想

  • ヘルパーをオブジェクト指向的に書けるようになる
  • 個人的には類似gemのActiveDecoratorのがシンプルで好みかも。でもDraperも十分良さそう

pros

  • 高機能
  • 関連先もデコれる
  • Decorator gemでは一番利用者が多いっぽい

cons

  • 毎回明示的にデコらないといけない(prosと考えることもできる)
  • どこでもデコれるのは自由度がありすぎてカオスになりそう -> decorates_assignedを使えば回避できるかも

基本的な使い方

導入

1. インストール

# Gemfile
gem 'draper'
$ budle install

2. セットアップ

# app/decorators/application_decorator.rb を作成
# ApplicationDecoratorは全てのDecoratorの親クラスになる
$ rails generate draper:install

3. デコレータを作成して、使用する

# app/decorators/article_decorator.rb を作成
$ rails generate decorator Article
# app/decorators/article_decorator.rb

class ArticleDecorator < Draper::Decorator
  # Articleにのメソッドが使えるようになる。published?がそれ
  delegate_all

  # ビューで`@article.publication_status`のように、@articleのメソッドとして利用できる
  def publication_status
    # published?はArticleのメソッドだけど、ArticleDecoratorでもそのまま使える
    if published?
      "Published at #{published_at.strftime("%A, %B %e")}"
    else
      "Unpublished"
    end
  end

end
# ビュー

<%= @article.publication_status %>

ヘルパー/ビュー/モデルにロジックを置く場合との比較

ヘルパーにロジックを置く場合

  • 手続き的で、オブジェクト指向的でない
  • 名前空間がグローバル(ファイルは分けらる)
    • 名前衝突の可能性
    • どこに定義されているかわからない
# app/helpers/articles_helper.rb
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
# app/decorators/article_decorator.rb

class Article < ApplicationRecord
  def publication_status
    if published?
      "Published at #{published_at.strftime("%A, %B %e")}"
    else
      "Unpublished"
    end
  end
end
# ビュー

<%= @article.publication_status %>

デコレータにロジックを置く場合

  • オブジェクト指向的
  • プレゼンテーションロジックをモデルから分けられる
# app/decorators/article_decorator.rb

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
    # helpers.content_tag(:strong, "Awesome") でもok
    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") # modelはobjectのalias

delegate_all: 全てのメソッドを委譲

  • 内部的にはmethod_missingを使い委譲している
  • メソッド探索の順番: デコレータ -> 親デコレータ -> モデル
class ArticleDecorator < Draper::Decorator
  delegate_all
end

delegate: 指定のメソッドを委譲

  • deleageメソッドはActive Supportのdelegateとほぼ同じ。ただしto: :objectを省略できる。
class ArticleDecorator < Draper::Decorator
  # 指定のメソッドのみ委譲する(toオプションを指定しないとデフォルトでobjectに委譲する)
  # titleで、object.titleにアクセスできるようになる
  delegate :title, :body

  # 指定のメソッドを指定したオブジェクトに委譲する
  delegate :name, :title, to: :author, prefix: true
end

オブジェクトをデコる*3

基本

# app/controllers/articles_controller.rb
def show
  # articleをデコる
  @article = Article.find(params[:id]).decorate
end

デコる方法は3つ

  • 一番オススメ
  • デコレータをモデル名から推測してくれる(そのためデコレータ名がモデル名から推測できないといけない)
@article = Article.find(params[:id]).decorate
  • こちらならデコレータの名前が違う場合でもOK
# 名前同じ場合
@article = ArticleDecorator.new(Article.find(params[:id]))
@article = ArticleDecorator.decorate(Article.find(params[:id])) # newと同等

# 名前違ってもOK
@widget = ProductDecorator.new(Widget.first)
@widget = ProductDecorator.decorate(Widget.first)
  • .decorates_findersを使うと、デコレータがfind系(find/all/first/...)のメソッドが利用できるようになる。これでfindしつつデコることが可能
  • でもいまいちな気がする。Draperのデコレータはビューで使うことを想定している。DBアクセスはモデルがやるべきで、デコレータの仕事じゃない
# app/decorators/articl_decorator.rb
class ArticleDecorator < Draper::Decorator
  decorates_finders
end

# コントローラー
@article = ArticleDecorator.find(params[:id])

コレクションの各要素をデコる*2

  • コレクションを操作すると、各要素は一気にデコられる
# オススメ
# Relationの場合、そのままチェーンできる
@articles = Article.recent.decorate

# こっちでもできる
# 配列の場合、`ArticleDecorator.decorate_collection`に食わせる
# Rails3だとallは配列を返すので、こっちを使う
@articles = ArticleDecorator.decorate_collection(Article.all)

コレクション自体をデコる

  • モデルのクラスメソッドを生やすイメージ
  • Draper::CollectionDecoratorを継承
  • 使わなそう -> patinationのメソッド生やすのに使える
# app/decorators/articles_decorator.rb

# ArticleDecoratorではない。複数形になるので注意。
class ArticlesDecorator < Draper::CollectionDecorator
  def page_number
    42
  end
end

@articles = Article.all.decorate # ArticlesDecoratorオブジェクト(ActiveRecordのRelation相当)
@articles.page_number # => 42

関連先をデコる*2

メソッド

decorates_association: 関連先をデコる

# app/decorators/article_decorator.rb
class ArticleDecorator < Draper::Decorator
  # ArticleDecoratorがArticleをデコレートした時に、自動でAuthorDecoratorがAuthorをデコレートする
  decorates_association :author
end


# app/decorators/author_decorator.rb
class AuthorDecorator < Draper::Decorator
  def hoge
    model.name + "hogehoge"
  end
end

# Articleしかデコレートしていないのに、authorまでデコレートできている
article = Article.first.decorate
article.author.hoge

decorates_associations: 関連先をデコる(複数)

# app/decorators/article_decorator.rb
class ArticleDecorator < Draper::Decorator
  decorates_associations :author, :comments
end

オプション

with: 関連先のデコレーターを指定する

class ArticleDecorator < Draper::Decorator
  # AuthorDecoratorではなく、FancyPersonDecoratorでデコレート
  decorates_association :author, with: FancyPersonDecorator
end

scope: 関連先をscopeで絞り込む

  • decorate対象の絞り込ではなく、レコード自体の絞り込み
class ArticleDecorator < Draper::Decorator
  # `article.comments`時に、`recent`で絞り込む
  decorates_association :comments, scope: :recent
end

context: 関連先にcontextを渡す

# 関連先にcontextを渡す
class ArticleDecorator < Draper::Decorator
  decorates_association :author, context: {foo: "bar"}
end


# ラムダで上書きすることも可能
# parent_contextはhashだよ
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 # => "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)
# app/controllers/articles_controller.rb
class ArticlesController < ApplicationController
  # `decorates_assigned`を使うと、
  # コントローラーでは`@article`をモデルとして扱い、
  # ビューでは`article`をデコレータとして扱える。
  #
  # 具体的には以下の2つをしてくれる
  # - @articleインスタンス変数をデコってくれる
  # - helper_methodを使い、ビューで`article`でアクセスできるようにする
  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
  • indexでも使う場合は複数形も必要
decorates_assigned :article, :articles

ページネーションと一緒に使う

問題点

  • kaminari等のページネーションgemと一緒に使うには一工夫必要
  • ページネーションgemはRelationにcurrent_page等のメソッドを追加する。そのままデコレートしてしまうとcurrent_page等のメソッドにアクセスできなくなってしまう。
cats = Cat.all    # Cat::ActiveRecord_Relationインスタンス
cats = cats.page  # Cat::ActiveRecord_Relationインスタンス
cats.current_page # => 1

cats = cats.decorate # CatsDecoratorインスタンス
cats.current_page    # NoMethodErrorになってしまう

解決方法1: Draper::CollectionDecoratordelegate

# config/initializers/draper.rb
Draper::CollectionDecorator.delegate :current_page, :total_pages, :limit_value, :total_count

解決方法2: Draper::CollectionDecoratorのサブクラスをコレクションにする

# Draper::CollectionDecoratorのサブクラスを作って、delegate
class PaginatingDecorator < Draper::CollectionDecorator
  delegate :current_page, :total_pages, :limit_value, :total_count
end

# コレクションがPaginatingDecoratorになるようにする
class ApplicationDecorator < Draper::Decorator
  def self.collection_decorator_class
    PaginatingDecorator
  end
end

# 各DecoratorはApplicationDecoratorを継承する
class AnimalDecorator < ApplicationDecorator
end

その他メモ

Decorator/Presenter/ViewModel/Exibitの違い

  • Decorator/Presenter/ViewModel/Exibitの違いがわからん
  • いくつか記事を見た感じ、どうも文脈/人によって同じ言葉でも意味が変わるっぽい
  • draperの文脈では全て同じもの(ビューヘルパーをオブジェクト指向的に扱うための、モデルのデコレータ)を指すことが多い気がする
  • draperのDecoratorはGoFのDecoratorパターンに由来すると思われるが、ビュー以外での使用は想定していない(はず)。あんまり名前にこだわりすぎないほうがいいかもしれない

参考サイトのまとめ

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

  • チーム内で認識を統一して、用語を使い分けてる
  • 良さそう。でも難しい。
  • 用法
    • Decorator: 単一のモデルクラスに対応する ViewModel
    • Presenter: 複数のモデルクラスにまたがる ViewModel、永続化されたモデルと一致しない ViewModel
    • ViewModel: Decorator 、Presenter の上位概念。ビューに関連するロジックをまとめるレイヤーを指す。

Exhibit vs Presenter

  • 全体的に難しい。あんまり理解できてない
  • 用法
    • Decorator: Presenter、Exibitの上位概念。GofのDecoratorと同じで、オブジェクトに機能/振る舞いを追加するもの。サンプルではSimpleDelegatorやDecoratorを使い実装
    • Presenter: Decoratorの一種。モデルをデコレートするプレゼンテーションロジック置き場?(自信なし)
    • Exibit: Decoratorの一種。モデルをビューに繋げる。Exibit自体はビューの機能を持たず、contextを持ち、contextにrenderさせる
    • Presenter + Exhibit: PresenterとExhibitは排他的な関係ではない。同時に実現可能。draperはこれ?

model の decorator の話

  • 用法
    • Presenter: 一つのView につき一つの Decorator
    • ActiveDecorator(gem): 一つの model につき一つの Decorator(たぶんdraperもコレに該当)
    • Exhibit: View x Model の組み合わせにつき一つの Decorator

デコレータのクラス*2

Draper::Decorator: モデルインスタンスのデコレータ

  • article.decorateの戻り値は、このクラスのサブクラス
  • 基本コレをつかう
# app/decorators/article_decorator.rb

class ArticleDecorator < Draper::Decorator
  # Articleにのメソッドが使えるようになる。published?がそれ
  delegate_all

  # ビューで`@article.publication_status`のように、@articleのメソッドとして利用できる
  def publication_status
    # published?はArticleのメソッドだけど、ArticleDecoratorでもそのまま使える
    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の戻り値は、このクラスのサブクラス
  • 使わなそう
# app/decorators/articles_decorator.rb
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で追加データを渡す

# デコレータ内で、contextメソッドを通して利用できる
Article.first.decorate(context: {role: :admin})

デコレータでHTMLをレンダリング

# デコレータ
def emphatic
  h.content_tag(:strong, "Awesome") # => <strong>Awesome</strong>
end
  • 長い場合は、h.renderで部分テンプレートを使って組み立てる
# デコレータ
def sub_view
  h.render 'sub_view', title: model.title # => articles/sub_view.html.erb を表示する
end

デコレータはモデルのように振る舞う

@person = Person.first
@decorated_person = @person.decorate

@decorated_person.is_a?(Person) # true
@person == @decorated_person # true

モデルメソッドをオーバーライドする

  • 便利だけどわかりにくくなりそう
# デコレータ
def created_at
  article.created_at.strftime("%m/%d/%Y - %H:%M")
end

# ビュー
@article.created_at

ApplicationDecorator

  • ApplicationRecordに合わせて、Draper::DecoratorではなくApplicationDecoratorを継承すると吉
# app/decorators/application_decorator.rb
class ApplicationDecorator < Draper::Decorator
  # 全てのデコレータで共有する処理はここに置く
end

# app/decorators/article_decorator.rb
# Draper::Decoratorではなく、ApplicationDecoratorを継承する
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のコントローラーテスト

# デコられているかテストできる述語マッチャ(predicate matchers)
assigns(:article).should be_decorated

# デコレーターを指定できる
assigns(:article).should be_decorated_with ArticleDecorator

ApplicationController以外を使う場合はコレが必要

# spec_helper.rb
config.before(:each, type: :decorator) do |example|
  Draper::ViewContext.controller = ExampleEngine::CustomRootController.new
end

isolated test

  • Draperはヘルパーメソッドにアクセスするために、view contextを必要とする
  • デフォルトではApplicationControllerのview contextを使う
  • この依存を取り除いて、テストをスピードアップしたい場合は以下の設定をする。ただしデコレーターはヘルパーにアクセスできなくなる
# spec_helper

# 依存を取り除く
Draper::ViewContext.test_strategy :fast

# 個別にHelperをinclude
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.localizelで使える
  • localizeでもok

decorator.decorated_with?: 指定クラスでデコレートされていればtrue

Cat.first.decorate.decorated_with?(CatDecorator) # => true

decorator.decorated?: オブジェクトがデコレートされていればtrue

Article.first.decorate.decorated? # => true
Article.first.decorated? # => false