ActiveDecoratorについて調べた

先月、Railsコミッターの松田さんを招待して開催されたActiveDecoratorのコードリーディング会に参加しました。

当時の自分の理解度としてはパーフェクトRuby on Railsの該当の説明箇所を読んでサンプルコードを動かしたことがある程度で、あまり詳しくは分かっていなかった状態でした。

松田さんの解説を聞きながらコードリーディングを進める中でも理解が追いつかない箇所が結構あったのですが、FBCのチーム開発でActiveDecoratorを利用するissueに取り組む機会があり、改めて調べてみました。

ActiveDecoratorとは

ActiveDecoratorは冒頭でも触れたRailsコミッターの松田さんが作られたgemです。

その詳細に入る前にRailsのhelperメソッドについて整理しておきたいと思います。

Railsではユーザーに表示する画面の内容や、そのデータの処理結果をどう実装するかのロジックでビューヘルパーを用います。 ただ、ヘルパーは全てのコントローラのビューで利用が可能だったりで、サービスの規模が大きくなるにつれてどこでメソッドが使われているかがわかりにくくなっていったり、名前の重複がないように注意する必要があったり課題もあります。 (パーフェクトRuby on Railsより。自分はまだこの課題に直面したりした経験がなくふんわり理解です🐣)

speakerdeck.com

上記資料でも松田さんが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 がモジュールとして定義されていた場合に、インスタンス変数に対して Decoratorextend していて、それによってビューに渡したモデルのインスタンスをレシーバとしてメソッドを呼び出すことができます。

ここで、改めて上に例で書いた「江戸川コナン(コナン)」の流れを見るとなるほどとなるかもです。

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でお知らせいただけると幸いです。

参考資料