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.comDecoratorの役割とDraperについて
そもそものDecoratorの役割等について、簡潔に記載されています。 qiita.comDraperの使い方 まとめ
上記公式ドキュメントの日本語訳的な位置づけかと思います。
公式ドキュメントに書かれている使用方法は大体書かれています。 nekorails.hatenablog.com