Draper導入しました

背景、経緯、目的

業務でRailsを使ってWebサービスを開発しているのですが、
一部Modelにおいて、徐々にメソッドが増え肥大化してきたため、チームでDraperの導入を決めました。
以下のようなメソッドをModelからDecoratorへ移せたらいいなというイメージでした。

  • 表示に関するメソッドだが、オブジェクトに依存しているメソッド
  • 多くのページで使用するメソッド
  • 以下例
# app/models/book.rb
# 本の在庫状況を表示
def display_status
  sold_out? ? '売り切れ' : '在庫有り'
end

主な使用方法

  • gemを入れる
gem 'draper'
  • 以下コマンド等で既存モデルに対して、Decoratorファイルを生成
rails generate decorator Book
  • Decorator側に移すメソッド等を記載
# app/decorators/book_decorator.rb
class BookDecorator < Draper::Decorator
  # 本の在庫状況を表示
  def display_status
    sold_out? ? '売り切れ' : '在庫有り'
  end
end
  • delegete_all呼び出しによって、BookModel内のメソッドを利用可能にします。
    以下例のようにsold_out?メソッドがModel内にあっても、Decorator側で利用できます。
    (上記コマンドで生成した場合、初めからdelegete_allは記載されているかも。)
# app/decorators/book_decorator.rb
class BookDecorator < Draper::Decorator
  delegate_all

  # 本の在庫状況を表示
  def display_status
    sold_out? ? '売り切れ' : '在庫有り'
  end
end
  • Controller側からViewにインスタンス変数を渡す直前に.decorateでデコレート
    View側でDecorateファイルに記載したメソッド等が使えます。
# app/controllers/books_controller.rb
@book = Book.first.decorate
-# app/views/books/show.html.haml
%dl
  %dt 在庫状況
  %dd= @book.display_status
  • ActiveRecord_Relationに対しても、まとめてデコレートし、View側で個別にメソッドを使用可能です。
# app/controllers/books_controller.rb
@books = Book.all.decorate
-# app/views/books/index.html.haml
- @books.each do |book|
  %dl
    %dt 在庫状況
    %dd= book.display_status

その他、Tips

  • association

    associationを記載しておくと、関連するModelも合わせてデコレートできる。

# app/decorators/book_decorator.rb
class BookDecorator < Draper::Decorator
  # 対1
  decorates_association :author
  # 対複数
  decorates_associations :comments
end
  • ActiveAdmin

    ActiveAdminを使用した管理画面で、Decoratorメソッドを使用する場合、
    そのままでは使えず、decorate_withで渡してあげる必要があります。

# app/admin/book.rb
ActiveAdmin.register Book do
  decorate_with BookDecorator
end
  • kaminari

    ページネーションでkaminari等を使用している場合、
    ActiveRecord_Relationにメソッドを追加されているので、
    そのままだと、ActiveRecord_Relationをデコレート後には使用できません。
    該当メソッド類をdelegeteして使用できるようにする必要があります。

# app/decorators/collection_decorator.rb
class CollectionDecorator < Draper::CollectionDecorator
  delegate :current_page, :total_pages, :limit_value, :entry_name, :total_count, :offset_value, :last_page?
end

特定のModel等ではなく、多くのModel等で使用する場合、まとめてしまって良いかも。
(各DecoratorはApplicationDecoratorを引き継ぐようにする)

# app/decorators/application_decorator.rb
class ApplicationDecorator < Draper::Decorator
  def self.collection_decorator_class
    CollectionDecorator
  end
end
# app/decorators/book_decorator.rb
class BookDecorator < ApplicationDecorator
end
  • CollectionDecorator

    基本的に使うケースは少ないかつ、あまり推奨されないかと思いますが、
    デコレートした後のCollectionDecoratorに対して、
    どうしてもビュー側でfind_by等を使いたくなるケースもあります。
    CollectionDecoratorにそういったメソッドは用意されていないので、自分で追加する必要があります。

# app/decorators/collection_decorator.rb
class CollectionDecorator < Draper::CollectionDecorator
  def find_by(attribute = {})
    object.find_by(attribute)&.decorate
  end
end
  • spec

    上記のコマンドで生成した場合、specファイル等も合わせて作成されます。
    特に変わった点はないが、当然オブジェクト生成時にデコレートする必要があります。

# spec/decorators/book_decorator_spec.rb
RSpec.describe BookDecorator do
  describe '#display_status' do
    subject { book.display_status }
    let(:book) { build(:book).decorate }
    context '...' do
    end
  end
end

感想

  • お試し的に、associationが複雑でないModel等から導入しましたが、
    インスタンスメソッドが24個→11個になったModel等もあったので、
    当初のFatModelの回避という点では目的を達成できていそう。
  • 1つのModelにインスタンスメソッドが20個以上ある等が無ければ、
    個人で開発する小規模なサービス等ではメリットを感じる場面が少なさそう。
  • 途中から導入すると移行がそれなりに大変なので、
    規模が大きくなるとあらかじめ分かっているサービスであれば、最初から導入したい
    (現在も関連箇所を触る際等に、適宜移行中)

参考記事

  • Draper公式ドキュメント
    READMEが丁寧に書いてあり、かつそんなに文量も多くないので、
    基本的に公式ドキュメントで事足りるかと思います。
    github.com

  • Decoratorの役割とDraperについて
    そもそものDecoratorの役割等について、簡潔に記載されています。 qiita.com

  • Draperの使い方 まとめ
    上記公式ドキュメントの日本語訳的な位置づけかと思います。
    公式ドキュメントに書かれている使用方法は大体書かれています。 nekorails.hatenablog.com