Decorator/Presenter
2013/10/20
記念すべき「Rails Tips」第1回は、Decorator と Presenter について書きます。
Rails で Decorator/Presenter というと draper や active_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
クラス X
と Z
は次のように利用されます:
x = X.new('Smith') z = Z.new(x) puts z.name # => 'Mr. Smith'
クラス Z
のインスタンスはクラス X
のインスタンスを変数として保持し、内部的に利用しています。別のオブジェクトを包み込むように見えるところから、Decorator には Wrapper という別名もあります。
委譲
さて、問題です。いま、クラス X
に age
というメソッドを追加することになりました:
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_name
と given_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
参考資料
- Jay Fields, Rails: Presenter Pattern, 2007-03-16
- John Athayde and Bruce Williams, The Rails View: Create a Beautiful and Maintainable User Experience, 2012
- Dan Croak, Decorators compared to Strategies, Composites, and Presenters, 2012-04-12
- Mike Pack, Exhibit vs Presenter, 2012-04-30
- Bryan Helmkamp, 7 Patterns to Refactor Fat ActiveRecord Models, 2012-10-17
- Ryo Nakamura, Viewの為に簡単なDecoratorをつくる, 2013
[更新] 「委譲」の項で使用されていた「属性」という表現(3カ所)を「メソッド」に変更しました。(2013-10-21)