Viewの為に簡単なDecoratorをつくる

最近久しぶりに1からRailsアプリをつくる機会があって、 手製で簡単なDecoratorを導入してみたら結構良かったという話です。

サンプル

ブログ記事を描画するViewをつくる例で説明する。

ブログ記事を表すEntryというModel用クラスがあり(ActiveRecord::Baseは使っていない)、このインスタンスがMVCにおけるcontrollerからviewに受け渡される。Entryクラスは、自身の持つデータの読み書きに関する責務だけ負ったクラスである。データは実際にはGithub上にファイルとして保存されており、content(ファイルの中身)、filename(ファイル名)、time(最終編集時刻)を持つ。Viewでは以下のようなものを描画したい。

  • Markdownで書かれたファイルの中身を変換したHTML
  • 拡張子を取り除いたファイル名(=タイトル)
  • 変更した日付
  • 記事ページへのリンク
  • 記事編集ページへのリンク
  • Github上のファイルへのリンク

実装

Decorator層をつくり、Modelに依存した描画を行う責務を負わせる。DecoratorはEntryのプロキシで、描画に特化したメソッドを持ち、自身の知らないメソッドはEntryに委譲する。標準ライブラリのSimpleDelegatorを継承して使うと簡単にこの機能を実現できる。

```ruby class EntryDecorator < SimpleDelegator include Rails.application.routes.url_helpers

def self.renderer @renderer ||= Redcarpet::Markdown.new(Redcarpet::Render::HTML, :fencedcodeblocks => true) end

def title File.basename(filename, ".*") end

def date time.to_date end

def showpath entrypath(filename) end

def editpath editentry_path(filename) end

def fullpath "#{Settings.github.entries_path}/#{filename}" end

def githubpath "#{Settings.repositoryfiles_url}/#{fullpath}" end

def renderedcontent self.class.renderer.render(content).htmlsafe end end ```

View

結果的にViewがこういう風に書ける。 helperだけで実現するよりOOPっぽいコードになると思う。

``` -# app/views/entries/show.html.slim - entry = decorate(@entry)

article h1.title= entry.title .content= entry.rendered_content

  • if loggedin? .buttons = linktowithicon "pencil", entry.editpath, :class => "btn" = linktowithicon "trash", entry, :method => :delete, :class => "btn" = linktowithicon "github", entry.githubpath, :class => "btn" ```

-# app/views/entries/index.html.slim ul.entries - decorate(@entries).each do |entry| li.entry = link_to entry.show_path do .title i.icon-file-alt = entry.title .date= entry.date

```ruby

app/helpers/entries_helper.rb

module EntriesHelper def decorate(args) case args when Enumerable args.map {|e| EntryDecorator.new(e) } else EntryDecorator.new(args) end end end ```

Testing

テストも簡潔に書けて良い。

```ruby require "spec_helper"

describe EntryDecorator do let(:instance) do described_class.new(entry) end

let(:entry) do mock( :name => "title.md", :path => "title.md", :content => "# title", :time => Time.utc(2000, 1, 1) ) end

describe "#title" do it "returns path without its extension part" do instance.title.should == "title" end end

describe "#date" do it "returns a date of its time" do instance.date.should == Date.new(2000, 1, 1) end end

describe "#showpath" do it "returns a path to show entry" do instance.showpath.should == "/entries/title.md" end end

describe "#editpath" do it "returns a path to edit entry" do instance.editpath.should == "/entries/title.md/edit" end end

describe "#githubpath" do it "returns a path to show entry on github" do instance.githubpath.should == "https://github.com/owner/repo/tree/master/entries/title.md" end end

describe "#fullpath" do it "returns a relative path of entry from repository root" do instance.fullpath.should == "entries/title.md" end end

describe "#renderedcontent" do it "returns a content rendered as Markdown format" do instance.renderedcontent.should == "

title

\n" end end end ```

Decoratorの話は、Objects on RailsやClean Ruby、The Rails View等にもっと詳しく載っていると思う。ActiveDecoratorを導入するというのもアリだと思う。