webエンジニアの日常

RubyやPython, JSなど、IT関連の記事を書いています

ヘルパーが太ってきたらデコレータでダイエット

画面へ表示するためだけのメソッドは、モデルには書かずに、ヘルパーに書く。というのはみなさんご存知のはずです。

ただ、そうすると今度はヘルパーが太ってくる。

最初はそこまで多くならないだろうと、一つのファイルに集めていたヘルパーメソッドたちが、徐々に増えてきて、どこにメソッドを書いたのかわからない、そんな経験ないでしょうか?

もし今その状況ならデコレータで解決できるかもしれません。

スポンサーリンク

デコレータとは

GoFのデザインパターンのひとつです。

増補改訂版Java言語で学ぶデザインパターン入門

増補改訂版Java言語で学ぶデザインパターン入門

既存のモデルを変更することなく、あとから機能を追加していくデザインパターンです。

名前の通り装飾するという感じですね。

rubyではデコレータを自分で実装しなくても、gemが公開されています。

有名なのはdraperとactive_decoratorですが、今回は主にdraperについて書きます。

どちらが良いとははっきり言えないですが、ベースとなるモデルの属性をそのままメソッド名にしたかったので、draperを選びました。

draperを使う

Gemfileに追加します。&bundle install

gem 'draper'

今回はタイトルと著者、値段カラムをもったBookモデルを考えます。

ただし、画面に表示するときは、必ずどの画面でも「タイトル(著作者)」と表示することとします。

これぐらいなら、

<%= @book.title %>(<%= @book.author) %>)

としてもいいのですが、今回は実装例なので、かっこよく

<%= @book.title_for_display %>

みたいにしてみます。

モデルと画面、コントローラが実装できたら、以下のコマンドでデコレータを作ります

rails generate decorator book

すると、appディレクトリ以下に、decoratorsというディレクトリが作られ、その中にbook_decorator.rbが作られます。

ではbook_decorator.rbを編集しましょう。

class BookDecorator < Draper::Decorator
  def title_for_display
    "#{object.title}(#{object.author})" 
  end
end

簡単ですが、以上です。

次に、画面側でこのデコレータを使うようにしましょう

<% decorated_book = ::BookDecorator.decorate(@book) %>
<p><%= decorated_book.title_for_display %>
<p><%= decorated_book.price %>

できたー

と、思ってしまいますが、残念。じつはこのままでは、エラーが出てしまいます。

エラーの内容はdecorated_bookはpriceなんて知らないよ、です。

どうすればいいかというと、先ほどのデコレータに一行追加します

class BookDecorator < Draper::Decorator

  delegate_all

  def title_for_display
    "#{object.title}(#{object.author})" 
  end
end

delegate_allとすることで、デコレータが知らないメソッドはデコレートした元のモデルに処理を丸投げ(委譲)できるようになります。

これでデコレートされたモデルからもpriceが呼べるようになりました。

書籍一覧など、複数のインスタンスを一気にデコレートするにはdecorate_collectionメソッドが使えます。

<% decorated_books = ::BookDecorator.decorate_collection(@book) %>
<% decorated_books .each do |decorated_book| %>
  <p><%= decorated_book.title_for_display %></p>
  <p><%= decorated_book.price %></p>
<% end %>

みたいな感じです。

最後に、上記では、title_for_dispalyというメソッドを定義しましたが、元のモデルのカラム名と同じメソッド名でもOKです

class BookDecorator < Draper::Decorator

  delegate_all

  def title
    "#{object.title}(#{object.author})" 
  end
end

読者登録はこちらからお願いします。