Decorator/Presenter

2013/10/20

記念すべき「Rails Tips」第1回は、DecoratorPresenter について書きます。

Rails で Decorator/Presenter というと draperactive_decorator などの実績のある有名な gem パッケージが存在していて、それらを導入すれば話は簡単なのですが、本稿ではあえて Rails 標準の機能を用いて Decorator/Presenter を実現する方法を説明します。「車輪の再発明!」と言わないでください。自分で作ってみることによって Ruby や Rails の知識が深まり、様々な応用が利くようになります。実際のところ、そんなに複雑なものではありません。

Decorator とは

「Decorator」はソフトウェアデザインパターンの1つで、継承(inheritance)に代わるクラスの拡張手段です。

具体例で説明しましょう。次のコードでは、クラス X と、それを継承するクラス Y を定義しています:

class X
  def initialize(name)
    @name = name
  end

  def name
    @name
  end
end

class Y < X
  def name
    'Mr. ' + super
  end
end

クラス Y は、次のように利用します:

y = Y.new('Smith')
puts y.name # => 'Mr. Smith'

Decorator パターンを使用した場合は、次のようなコードになります:

class X
  def initialize(name)
    @name = name
  end

  def name
    @name
  end
end

class Z
  attr_reader :object

  def initialize(object)
    @object = object
  end

  def name
    'Mr. ' + object.name
  end
end

クラス XZ は次のように利用されます:

x = X.new('Smith')
z = Z.new(x)
puts z.name # => 'Mr. Smith'

クラス Z のインスタンスはクラス X のインスタンスを変数として保持し、内部的に利用しています。別のオブジェクトを包み込むように見えるところから、Decorator には Wrapper という別名もあります。

委譲

さて、問題です。いま、クラス Xage というメソッドを追加することになりました:

class X
  def initialize(name, age)
    @name = name
    @age = age
  end

  def name
    @name
  end

  def age
    @age
  end
end

クラス Y およびクラス Z でもこの age メソッドをそのまま使いたいとすると、それらの定義はどうなるでしょうか。

継承を使っているクラス Y の場合は、修正の必要はありません。あるクラスで定義されていないメソッドが呼び出されると、その親クラスの同名のメソッドが呼び出されるからです。

他方、Decorator パターンを使用する場合は、次のように書かなければなりません:

class Z
  attr_reader :object

  def initialize(object)
    @object = object
  end

  def name
    'Mr. ' + object.name
  end

  def age
    object.age
  end
end

object.age を呼び出すメソッド age を定義しています。しかし、メソッドの数が増えてくると面倒ですね。幸いなことに Ruby には Forwardable という便利なモジュールが用意されており、次のように書き換えることができます:

require 'forwardable'

class Z
  extend Forwardable
  attr_reader :object
  def_delegator :object, :age

  def initialize(object)
    @object = object
  end

  def name
    'Mr. ' + object.name
  end
end

この結果、age メソッドが呼び出されると、object メソッドが返すオブジェクトの age メソッドが呼び出されることになります。このような仕組みを委譲(delegation)と呼びます。

実は、Rails には Forwardable モジュールとは別の委譲メカニズムが存在します。Active Support の中で Module クラスに delegate メソッドが追加されているのです。Rails の場合、先ほどのクラス Z のコードは次のように書けます:

class Z
  attr_reader :object
  delegate :age, to: :object

  def initialize(object)
    @object = object
  end

  def name
    'Mr. ' + object.name
  end
end

ActiveRecord モデルクラスの Decorator

Rails 開発で Decorator パターンを使用したくなる状況の筆頭は、ActiveRecord モデルクラスにメソッドを追加する必要に迫られたときです。

例えば、family_namegiven_name という2つの文字列型属性を持つ User モデルクラスがあるとします。すると、こんな風に書きたくなりますね:

class User < ActiveRecord::Base
  def full_name
    family_name + ' ' + given_name
  end
end

HTML テンプレートの中にユーザーの氏名を埋め込むときに便利です。

しかし、この種のメソッドを必要に応じて次々とモデルクラスに追加するのは考え物です。モデルクラスがどんどん肥大化して、ソースコードの見通しが悪くなります。

とは言え、ヘルパーに次のようなメソッドを追加するのは、さらに状況を悪化させそうです:

def user_full_name(user)
  user.family_name + ' ' + user.given_name
end

こんな時に Decorator パターンを活用します。まず、最初に app ディレクトリの下に decorators サブディレクトリを作成します。サブディレクトリの名前は何でも構いませんが、分かりやすいものにしましょう。そして、(もしすでに起動していれば)Rails アプリケーションを停止して、起動し直します。app ディレクトリの下に標準的でないサブディレクトリを作ったときは、開発モードでも再起動が必要です。

続いて、app/decoraors ディレクトリに次のような内容で新規ファイル user_decorator.rb を作成します:

class UserDecorator
  attr_accessor :user
  delegate :family_name, :given_name, to: :user

  def initialize(user)
    self.user = user
    yield(self) if block_given?
  end

  def full_name
    family_name + ' ' + given_name
  end
end

3行目の delegate メソッドがポイントです。family_name メソッドと given_name メソッドを user に委譲しています。

また、7行目の yield(self) if block_given? にも注目してください。ブロック付きでインスタンス化された場合には、自分自身をブロックパラメータとして渡して yield しています。

この UserDecorator は、HTML テンプレートの中で次のように使用します:

<% UserDecorator.new(@user) do |u| %>
  <span class="name"><%= u.full_name %></span>
<% end %>

あるいは、ブロックを使わずに次のように書くことも可能です:

<% u = UserDecorator.new(@user) %>
<span class="name"><%= u.full_name %></span>

配列の要素全部を decorate する

先ほどの例では1個の User オブジェクトの Decorator を作成しましたが、コントローラからオブジェクトの配列が渡ってきた場合には、どうすればいいでしょうか。特に工夫しないのであれば、次のように書けます:

<ul>
<% @users.each do |user| %>
  <% UserDecorator.new(user) do |u| %>
    <li><%= u.full_name %></li>
  <% end %>
<% end %>
</ul>

しかし、次のように書けると嬉しいですよね:

<ul>
<% UserDecorator.build(@users) do |u| %>
  <li><%= u.full_name %></li>
<% end %>
</ul>

そのためには、次のようにクラスメソッド build を定義します:

class UserDecorator
  attr_accessor :user
  delegate :family_name, :given_name, to: :user

  def initialize(user)
    self.user = user
    yield(self) if block_given?
  end

  def full_name
    family_name + ' ' + given_name
  end

  class << self
    def build(users)
      users.each { |user| yield new(user) }
    end
  end
end

Presenter

以上のように定義した UserDecorator クラスを実際に ERB テンプレートで利用しようとすると大きな問題が現れます。Rails のヘルパーメソッドが使えないということです。例えば、image_tag メソッドや link_to メソッドが使えないのです。しかし、Decorator を Presenter に書き換えることでこの限界を打ち破ることができます。

「Presenter」という言葉は、ギャング・オブ・フォー(GoF)の定義した23のデザインパターンには含まれません。

残念ながらこの言葉の用法には若干の混乱があって、使う人によって定義が微妙に異なるのですが、本稿では John Athayde と Bruce Williams の『The Rails View: Create a Beautiful and Maintainable User Experience』(2012年)の定義に沿ってこの言葉を使うことにします。

彼らは、Rails のモデルオブジェクトとビューコンテキストを内包するクラスを「Presenter」と呼んでいます。「ビューコンテキスト」というのは、ERB テンプレートやヘルパー上で self によって参照されるオブジェクトのことです。その実体は ActionView::Base クラスのインスタンスです。

「Presenter」を「Decorator」のサブカテゴリと捉える見方(Exhibit vs Presenter)もありますが、継承の代替手段である Decorator パターンは定義上、1個のオブジェクトしか内包しないので、「Presenter」のように2個のオブジェクトに委譲を行うような設計は厳密には Decorator パターンではありません。

2007年に Jay Fields が書いたブログ記事 Rails: Presenter Pattern における「Presenter パターン」は、引数として params を取るようなクラスを定義して、フォームのレンダリング(プレゼンテーション)に利用するような設計を指しています。これは明らかに『The Rails View』における「Presenter」の語法とは異なります。こういう事情があるので、Bryan Helmkamp は 7 Patterns to Refactor Fat ActiveRecord Models で、「Presenter」という言葉の代わりに「View Object」という用語を使用しています。また、draper は「View Model」という用語で自らを定義しています。しかし、世の中の趨勢としては、Rails において「モデルとビューの架け橋」となるようなクラスを一般に「Presenter」と呼ぶという方向で落ち着きつつあるようです。最近 Jay Fields の「Presenter」のような設計は「Form Object」という名前で呼ばれているので、混同される可能性は減ってきたと思われます。

Presenter の実装

次に示すのは、UserDecorator クラスを『The Rails View』流の Presenter に書き換えたものです:

class UserPresenter
  attr_accessor :user, :view_context
  delegate :family_name, :given_name, to: :user

  def initialize(user, view_context)
    self.user = user
    self.view_context = view_context
    yield(self) if block_given?
  end

  def full_name
    family_name + ' ' + given_name
  end

  def full_name_with_hyperlink
    view_context.link_to(full_name, user)
  end

  class << self
    def build(users, view_context)
      users.each { |user| yield new(user, view_context) }
    end
  end
end

ファイル名は user_presenter.rb に変更してください。設置するディレクトリはどこでもいいのですが app/presenters ディレクトリを作って、そこに移動すると分かりやすいのではないでしょうか。Rails アプリケーションの再起動をお忘れなく。

view_context という属性が追加され、コンストラクタの引数の数が2に増えています。full_name_with_hyper_link メソッドの中では、Rails ヘルパーメソッドの1つである link_to が使われています。

UserPresenter の使用例は以下の通りです:

<% UserPresenter.new(@user, self) do |u| %>
  <div><%= u.full_name_with_hyper_link %></div>
<% end %>

<ul>
<% UserPresenter.build(@users, self) do |u| %>
  <li><%= u.full_name_with_hyper_link %></li>
<% end %>
</ul>

ModelPresenter

最後に、User クラス以外のクラスのための Presenter を実装しやすくするため、汎用的な部分を抜き出して ModelPresenter クラスを定義しましょう:

class ModelPresenter
  attr_accessor :object, :view_context

  def initialize(object, view_context)
    self.object = object
    self.view_context = view_context
    yield(self) if block_given?
  end

  class << self
    def build(objects, view_context)
      objects.each { |object| yield new(object, view_context) }
    end
  end
end

この結果、UserPresenter クラスの定義はこんなに簡潔になります:

class UserPresenter < ModelPresenter
  delegate :family_name, :given_name, to: :object

  def full_name
    family_name + ' ' + given_name
  end

  def full_name_with_hyperlink
    view_context.link_to(full_name, object)
  end
end

参考資料

  1. Jay Fields, Rails: Presenter Pattern, 2007-03-16
  2. John Athayde and Bruce Williams, The Rails View: Create a Beautiful and Maintainable User Experience, 2012
  3. Dan Croak, Decorators compared to Strategies, Composites, and Presenters, 2012-04-12
  4. Mike Pack, Exhibit vs Presenter, 2012-04-30
  5. Bryan Helmkamp, 7 Patterns to Refactor Fat ActiveRecord Models, 2012-10-17
  6. Ryo Nakamura, Viewの為に簡単なDecoratorをつくる, 2013

[更新] 「委譲」の項で使用されていた「属性」という表現(3カ所)を「メソッド」に変更しました。(2013-10-21)