先月、Railsコミッターの松田さんを招待して開催されたActiveDecoratorのコードリーディング会に参加しました。
当時の自分の理解度としてはパーフェクトRuby on Railsの該当の説明箇所を読んでサンプルコードを動かしたことがある程度で、あまり詳しくは分かっていなかった状態でした。
松田さんの解説を聞きながらコードリーディングを進める中でも理解が追いつかない箇所が結構あったのですが、FBCのチーム開発でActiveDecoratorを利用するissueに取り組む機会があり、改めて調べてみました。
ActiveDecoratorとは
ActiveDecoratorは冒頭でも触れたRailsコミッターの松田さんが作られたgemです。
その詳細に入る前にRailsのhelperメソッドについて整理しておきたいと思います。
Railsではユーザーに表示する画面の内容や、そのデータの処理結果をどう実装するかのロジックでビューヘルパーを用います。 ただ、ヘルパーは全てのコントローラのビューで利用が可能だったりで、サービスの規模が大きくなるにつれてどこでメソッドが使われているかがわかりにくくなっていったり、名前の重複がないように注意する必要があったり課題もあります。 (パーフェクトRuby on Railsより。自分はまだこの課題に直面したりした経験がなくふんわり理解です🐣)
上記資料でも松田さんがRailsのhelperメソッドについて触れられているのですが、リーディング会ではgemの制作背景を伺うことができて興味深かったです。
この課題を解決するための方法の一つが、gemとして提供されているActiveDecoratorやDraperなどのプレゼンターというレイヤーを導入することです。
プレゼンターを導入することで、ビューにわたすオブジェクトの単位でユーザーに表示する画面のロジックを整理することが可能です。
ざっくりとした使い方はこんな感じです。
- Model
class User < ActiveRecord::Base # full_name:string # nick_name:string end
- Controller
class UsersController < ApplicationController def show @user = User.find(params[:id]) end end
- Decorator
module UserDecorator def full_name_with_nick_name "#{full_name} (#{nick_name})" end end
- View
<p><%= @user.full_name_with_nick_name %></p>
- 出力例
江戸川コナン(コナン)
ActiveDecoratorの仕組み
# active_decorator/lib/active_decorator/monkey/abstract_controller/rendering.rb module ActiveDecorator module Monkey module AbstractController module Rendering def view_assigns hash = super hash.each_value do |v| ActiveDecorator::Decorator.instance.decorate v end hash end end end end end
AbstractController::Rendering
を上記のように拡張し、ControllerからViewに渡されるインスタンス変数に decorate
が適用されるようになっています。
# active_decorator/lib/active_decorator/decorator.rb # 54行目 d = decorator_for obj.class return obj unless d obj.extend d unless obj.is_a? d
インスタンス変数のモデルクラスに Decorator
がモジュールとして定義されていた場合に、インスタンス変数に対して Decorator
を extend
していて、それによってビューに渡したモデルのインスタンスをレシーバとしてメソッドを呼び出すことができます。
ここで、改めて上に例で書いた「江戸川コナン(コナン)」の流れを見るとなるほどとなるかもです。
ActiveDecoratorでは、デコレーターに実装したメソッドは基本的にビューの中でしか利用できませんが、以下のようにするとテストでも利用が可能です。
describe '#full_name_with_nick_name' do it 'returns the full name and nick name' do user = User.new(full_name: '江戸川コナン', nick_name: 'コナン') decorated_user_name = ActiveDecorator::Decorator.instance.decorate(user) expect(decorated_user_name.full_name_with_nick_name).to eq('江戸川コナン(コナン)') end end
まとめ
コードリーディング会に参加した際にはついていくのがやっと感があったのですが、自分でも使ってその上でgemのソースコードを読むとそういうことだったのかとなる部分がたくさんありました。
偶然ではありましたがチーム開発のissueで調べる機会に恵まれて良かったです。
サンプルコード、説明ともにおかしな点がありましたら、コメントやTwitterでお知らせいただけると幸いです。