すでにメンバーの場合は

無料会員登録

GitHubアカウントで登録 Pikawakaが許可なくTwitterやFacebookに投稿することはありません。

登録がまだの方はこちらから

Pikawakaにログイン

GitHubアカウントでログイン Pikawakaが許可なくTwitterやFacebookに投稿することはありません。

Rails

更新日:

【Rails】 Draperを使ってデコレーターを導入しよう!

ぴっかちゃん
ぴっかちゃん

DraperはRuby on Railsにデコレーターを導入することができるgemです。

デコレーターとは

デコレーターは、プログラミングにおいて「オブジェクトに新しい機能を追加する」ためのデザインパターンの一つです。デコレーターの役割は、以下の通りです。

名前 役割
モデル データベースにアクセスする処理を担当します。
ヘルパー データベースにアクセスする処理は含まず、ビューに関するロジックを扱います。
デコレーター 特定のモデルに関連したビューに関するロジックを扱います。

モデルに関連性のある表示ロジックをデコレーターに記載することで、 モデル、ヘルパー、 デコレーターの責務が明確になり、全体的に処理の内容が読み取りやすいディレクトリ設計となります。
draperというgemを使うことでデコレーターに表示ロジックを持たせることができます。

Draperとは

Draperは、Railsアプリケーションにおいてオブジェクトの表示ロジックを扱うためのデコレーターを導入するためのgemです。Draperを使うことで、モデルやヘルパーに直接表示ロジックを追加せず、デコレーターを通じて表示のカスタマイズができます。

Draperを導入しよう

Draperを導入するにはGemfileに以下の行を追加します。

Gemfile
1
gem 'draper'

そして、bundle installコマンドでインストールします。
これで導入は完了です。

デコレーターの生成

この章ではデコレーターの生成方法を解説していきます。

rails generate draper:install

生成されるすべてのデコレーターが継承するApplicationDecoratorを作成するには、以下のコマンドを実行します。

ターミナル
1
rails generate draper:install

コマンドを実行するとappディレクトリ内にdecoratorsが作成され、このディレクトリ内にapplication_decorator.rbというファイルが作成されます。

app/decorators/application_decorator.rb
1
2
3
4
5
6
7
8
class ApplicationDecorator < Draper::Decorator
  # Define methods for all decorated objects.
  # Helpers are accessed through `helpers` (aka `h`). For example:
  #
  #   def percent_amount
  #     h.number_to_percentage object.amount, precision: 2
  #   end
end

このファイルにはこれから作成するデコレーターファイルで共通して定義したいコードを記述します。

このコマンドを実行後、コントローラーを作成すると自動でそのコントローラーに紐づいたモデルに対するデコレーターファイルがappディレクトリ内のdecoratorsディレクトリに内に作成されます。

デコレーターの自動作成

作成されたファイルは以下のように記述されています。

app/decorators/article_decorator.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
class ArticleDecorator < ApplicationDecorator
  delegate_all

  # Define presentation-specific methods here. Helpers are accessed through
  # `helpers` (aka `h`). You can override attributes, for example:
  #
  #   def created_at
  #     helpers.content_tag :span, class: 'time' do
  #       object.created_at.strftime("%a %m/%d/%y")
  #     end
  #   end

end

コントローラー作成時に生成されるデコレーターファイルは、ApplicationDecoratorを継承しています。このように、作成されたすべてのデコレーターファイルがApplicationDecoratorを継承するため、共通化したい処理がある場合は、ApplicationDecoratorファイルに記述するようにしましょう。

新しいコントローラ作成時のデコレーターファイルに関する設定

新しいコントローラ作成時に自動でデコレーターファイルを生成しないようにするには、config/application.rbファイルに次の設定を追加します。

config/application.rb
1
2
3
4
5
6
7
# 略〜
class Application < Rails::Application
config.generators do |g|
g.decorator false
end
end # 略〜

rails generate decorator モデル名

既存のモデルに対してデコレーターを作成するには以下のコマンドを実行します。

ターミナル
1
2
3
4
rails generate decorator モデル名

# Userモデルに作成する例
rails generate decorator User

rails generate draper:installコマンドは、Draperの初期設定を行うために使われますが、必ずしも実行する必要はありません。単純なアプリケーションで、デコレーターの共通ロジックが不要な場合はこの初期設定コマンドを実行しなくても、既存のモデルに対してデコレーターを作成するコマンドを実行すればデコレーターファイルを作成することができます。

その際は以下のようにDraper::Decoratorを継承した形でファイルが作成されます。

初期化しないとき
1
2
3
4
class ArticleDecorator < Draper::Decorator
delegate_all end

複数のデコレーターで共通するメソッドやロジックを持ちたい場合は、初期設定コマンドでapplication_decorator.rbを作成すると便利です。

次に作成されたデコレーターファイルに、ビューで使用する表示ロジックを追加します。例えばArticleモデルのpublished_atという日時属性を特定のフォーマットで表示するロジックを追加するには以下のように記述します。

app/decorators/user_decorator.rb
1
2
3
4
5
6
7
class ArticleDecorator < ApplicationDecorator
  delegate_all

def published_at
object.published_at.strftime("%A, %B %e")
end
end

ハイライトが当たっていない部分はデフォルトで記述されているコードです。

デコレーターのメソッド

Draperには便利なメソッドが用意されています。この章ではデコレーターのメソッドについて解説していきます。

delegate_allメソッド

delegate_allメソッドは元のモデルで定義されている全てのメソッドをデコレーター内でも使えるようになるメソッドです。このメソッドを使うとデコレーターを通して元のモデルのメソッドやプロパティにアクセスすることができるようになります。

例えば以下のファイルにdelegate_allメソッドを書いたとします。

app/decorators/user_decorator.rb
1
2
3
4
class UserDecorator < ApplicationDecorator
delegate_all
end

このデコレーターファイルはUserモデルに対するデコレーターなので、元のモデルはUserモデルになります。Userモデルに以下のメソッドが定義されていたら、delegate_allメソッドを記述したファイルでもこれらのメソッドが使えるようになります。

Userモデル
1
2
3
4
5
class User < ApplicationRecord
  def display_name_and_age
    "#{name} (#{age}歳)"
  end
end

例えば以下のようにdelegate_allメソッドを記述せずに、Userモデルで定義したdisplay_name_and_ageメソッドをデコレーターファイル内で使用します。

app/decorators/user_decorator.rb
1
2
3
4
5
6
class UserDecorator < ApplicationDecorator

 def name_with_age
  display_name_and_age
 end
end

すると以下のようにメソッドが定義されていないというエラーが発生することがわかります。

エラーが発生

以下のようにdelegate_allを記述します。

app/decorators/user_decorator.rb
1
2
3
4
5
6
7
class UserDecorator < ApplicationDecorator
delegate_all
def name_with_age display_name_and_age end end

するとUserモデルで定義したメソッドが呼び出され、エラーなく表示されます。

エラーなく表示

object

4行目で使われているobjectはデコレーターが元にしているモデルのインスタンスを指します。objectを使うことで、元のモデルの情報にデコレーターの中からアクセスできるようになります。つまり、この例ではArticleモデルで定義されているメソッドや、プロパティにアクセスできます。

上のコードではdelegate_allを使っているため、objectを使わず、以下のように省略して書くことも可能です。

objectを省略した場合
1
2
3
4
5
6
7
class ArticleDecorator < Draper::Decorator
  delegate_all

  def published_at
   published_at.strftime("%A, %B %e")
  end
end

delegate_allをデコレーター内で使用していない場合はobjectを使って元のモデルのプロパティを呼び出す必要があります。

delegate_allを使わない場合
1
2
3
4
5
class ArticleDecorator < Draper::Decorator
  def published_at
    object.published_at.strftime("%A, %B %e")
  end
end

hエイリアス

また、デコレーター内でrailsのビュー用のヘルパーメソッドにアクセスするにはview_contextをメソッドの先頭につける必要がありますが、Draperにはhというヘルパーエイリアスが用意されていて、view_contextの代わりとして使うことができます。以下はリンクを作成するlink_toと、任意のタグを生成するcontent_tagを使った時の例です。

デコレーター
1
2
3
4
5
6
7
8
h.link_to 'クリック', root_path
# => <a href="/">クリック</a>

h.content_tag :p, "段落です"
# => <p>段落です</p>

h.content_tag(:div, content_tag(:p, "Hello world!"), class: "strong")
# => <div class="strong"><p>Hello world!</p></div>

以下が使用例です。

使用例
1
2
3
4
5
6
7
8
9
10
11
12
class UserDecorator < Draper::Decorator
  delegate_all

  def posts_list
    # `h`を使って、`content_tag`ヘルパーを呼び出し
h.content_tag(:ul) do
object.posts.map do |post|
h.content_tag(:li, post.title)
end.join.html_safe end end end

この例の場合、7行目でmapメソッドを使用しているため、返り値は文字列が入った配列になります。このままではcontent_tagタグで作成されたliタグの文字列が入った配列がビューに表示されてしまいます。そのためまずは9行目のjoin.html_safejoinメソッドでmapメソッドから返された配列を1つの文字列に結合しています。

返り値の確認
1
2
3
4
5
6
7
8
9
10
11
object.posts.map do |post|
  h.content_tag(:li, post.title)
end
#mapの返り値の例
=> ["<li>タイトル1</li>", "<li>タイトル2</li>", "<li>タイトル3</li>"]

object.posts.map do |post|
  h.content_tag(:li, post.title)
end.join
#joinを使った返り値の例
=> "<li>タイトル1</li><li>タイトル2</li><li>タイトル3</li>"

このようにjoinで結合された文字列には複数の<li>タグが含まれていますが、Railsでは安全性を確保するために、文字列の中に含まれる特殊な文字やコードはそのまま実行されず、文字列として表示されます。したがって、これらの<li>タグはHTMLタグとして解釈されず、<li>タイトル</li>といったように単なるテキストとして表示されてしまいます。そのため、この結合された文字列に対してhtml_safeメソッドを使うことで、文字列がそのままHTMLとして認識されるようなり、ブラウザに正しくHTMLとして解釈させています。

使用例
1
2
3
4
5
6
7
8
9
10
11
12
class UserDecorator < Draper::Decorator
  delegate_all

  def posts_list
    # `h`を使って、`content_tag`ヘルパーを呼び出し
    h.content_tag(:ul) do
      object.posts.map do |post|
        h.content_tag(:li, post.title)
end.join.html_safe
end end end

link_toのようなRailsのビューヘルパーメソッドは、内部で適切な処理を行っているため、追加でhtml_safeを使う必要はありません。

デコレーターファイル内でhを頻繁に使用する場合は、以下のようにクラス内にinclude Draper::LazyHelpersを記述することで、デコレーター内でhを省略して、Railsのビュー用ヘルパーメソッドを直接使用できるようになります。

hを省略
1
2
3
4
5
6
7
8
9
class UserDecorator < ApplicationDecorator
include Draper::LazyHelpers
delegate_all def create_p
# `h`を使和なくてもヘルパーを呼び出せる
content_tag :p, "段落です"
end end

初期設定コマンドで作成されるapplication_decorator.rbinclude Draper::LazyHelpersを追加すれば、全てのデコレーターファイル内でhを省略してヘルパーメソッドを使えるようになるので便利です。

app/decorators/application_decorator.rb
1
2
3
class ApplicationDecorator < Draper::Decorator
include Draper::LazyHelpers
end

decorate

作成したデコレーターを使用するには元のモデルクラスのインスタンスをデコレートする必要があります。その時に使うのがdecorateメソッドです。

メソッドを使う場所はコントローラでも良いですし、ビューファイルでも良いですが、コントローラーでdecorateを使用するのが一番良いです。これにより、ビュー内のコードをシンプルに保ち、デコレーションの処理が統一されて、すべての表示が同じスタイルで揃うようになります。

以下の例のようにデコレートファイルに定義したメソッドを使うビューファイルに対応したコントローラのアクション内で使用します。

コントローラーで使用する例
1
2
3
4
5
6
7
# app/controllers/articles_controller.rb
def show
@article = Article.find(params[:id]).decorate
end # app/views/articles/show.html.erb <%= @article.publication_status %>

ビューファイルで使う場合は以下のように記述します。コントローラで作成したモデルクラスのインスタンスに対してdecorateメソッドでデコレートすることで、デコレーターファイルで定義したメソッドを使うことができます。

ビューファイルで使用する例
1
2
3
4
5
6
7
# app/controllers/articles_controller.rb
def show
  @article = Article.find(params[:id])
end

# app/views/articles/show.html.erb
<%= @article.decorate.publication_status %>

高度な使い方

この章ではDraperの高度な使い方を解説していきます。

関連オブジェクトのデコレーションの使い方

Draperのdecorates_associationメソッドを使用すると、あるモデル(例えばArticle)に関連する別のモデル(例えばAuthor)も自動的にデコレートすることができます。

関連オブジェクトのデコレーションを行うには、デコレータークラス内でdecorates_associationメソッドを使用します。

具体的なコード例を使って、関連オブジェクトのデコレーションについて解説します。以下の例では、ArticleモデルとAuthorモデルの間にアソシエーションを使って関連付けをしています。

app/models/article.rb
1
2
3
class Article < ApplicationRecord
  belongs_to :author
end
app/models/author.rb
1
2
3
class Author < ApplicationRecord
  has_many :articles
end

そしてそれぞれのモデルのデコレーターを定義します。AuthorDecoratorArticleDecoratorに以下のようにメソッドを定義します

app/decorators/author_decorator.rb
1
2
3
4
5
6
7
class AuthorDecorator < Draper::Decorator
  delegate_all

def full_name
"#{first_name} #{last_name}"
end
end
app/decorators/article_decorator.rb
1
2
3
4
5
6
7
class ArticleDecorator < Draper::Decorator
  delegate_all

def formatted_publication_date
object.published_at.strftime("%B %d, %Y")
end
end

そしてArticleDecoratorからAuthorモデルを自動的にデコレートするために、decorates_association を使用します

app/decorators/article_decorator.rb
1
2
3
4
5
6
7
8
9
10
11
class ArticleDecorator < Draper::Decorator
  delegate_all

  # Authorモデルを自動的にデコレート
decorates_association :author
def formatted_publication_date object.published_at.strftime("%B %d, %Y") end end

このようにすることで関連するAuthorオブジェクトも自動的にデコレートされます。
実際に使用してみます。
まずはコントローラーでインスタンスをデコレートします。

app/controllers/articles_controller.rb
1
2
3
def show
  @article = Article.find(params[:id]).decorate
end

上のコードを実行すると、@articleArticleDecoratorでデコレートされ、さらに@article.authorは自動的にAuthorDecoratorでデコレートされます。
ビューでは、デコレートされた関連オブジェクトのメソッドを直接使用できます。

app/views/articles/show.html.erb
1
2
3
4
<!-- ArticleDecoratorのメソッドを使用 -->
<p>公開日 on: <%= @article.formatted_publication_date %></p>
<!-- AuthorDecoratorのメソッドを使用 -->
<p>著者: <%= @article.author.full_name %></p>

カスタムコンテキストの利用

Draperのデコレーターでは、コンテキスト(context)を使用してデコレーターに追加のデータを渡すことができます。これにより、デコレーターがビューや他の要素に依存しない状態で動作するように設定できます。

デコレーターでコンテキストを使用する場合、decorateメソッドのオプションとしてcontextを渡します。

コントローラー内での使用例
1
@article = Article.find(params[:id]).decorate(context: { user: current_user })

そして、デコレーター内でそのコンテキストデータにアクセスします。

app/decorators/article_decorator.rb
1
2
3
4
5
6
7
class ArticleDecorator < Draper::Decorator
  delegate_all

  def editable_by_user?
    context[:user].admin? || object.user == context[:user]
  end
end

コントローラーで渡されたデータはcontext[:user]で取得することができます。
この例では、editable_by_user?メソッドが、デコレータのコンテキストに渡されたcurrent_userオブジェクトを利用して、記事が現在のユーザーによって編集可能かどうかを判断しています。

実際の使用例

この章では実際にどのような時にデコレーターを使ったら良いかを解説していきます。

条件によって表示が異なるとき

ある条件の時に表示を変えたいときは条件分岐などのコードを記述する必要があります。ビューファイル内でそのようなロジックを書いてしまうとコードが複雑化し、可読性が落ちてしまいます。

例としてユーザーのアカウント状態に応じて、適切なアイコンやメッセージを表示する場合を考えてみましょう。以下のコードはデコレーターに切り出す前のビューファイルです。

デコレーター使用前
1
2
3
4
5
6
7
8
9
10
<div>
  <% if @user.status == "active" %>
    <i class="fa fa-check-circle text-success"></i>
  <% elsif @user.status == "inactive" %>
    <i class="fa fa-times-circle text-danger"></i>
  <% else %>
    <i class="fa fa-question-circle text-warning"></i>
  <% end %>
  <%= @user.name %>
</div>

ビューファイルの中に上のような条件分岐のロジックを書いてしまうとコードが冗長になり、読みにくいです。そのため、この部分をデコレーターを使用して表示するためのロジックを簡潔にしてみます。

デコレーター
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# app/decorators/user_decorator.rb
class UserDecorator < Draper::Decorator
  delegate_all

  def status_icon
    case object.status
    when "active"
      h.content_tag(:i, "", class: "fa fa-check-circle text-success")
    when "inactive"
      h.content_tag(:i, "", class: "fa fa-times-circle text-danger")
    else
      h.content_tag(:i, "", class: "fa fa-question-circle text-warning")
    end
  end
end

このようにロジックの部分を切り出します。そしてビューファイルで使用します。

デコレーター使用後
1
2
3
<div>
  <%= @user.status_icon %> <%= @user.name %>
</div>

このようにかなりスッキリしてみやすくなりました。

繰り返し処理があるとき

複数のデータを扱う際、繰り返し処理のコードを記述する必要があります。先ほどの例と同じようにビューファイル内でそのようなロジックを書いてしまうとコードが複雑化し、可読性が落ちてしまいます。

例としてユーザーの最近の行動履歴(最後に訪問したページやクリックしたリンクなど)に基づいて、動的にナビゲーションメニューを生成する場合を考えてみましょう。

以下のような繰り返し処理のロジックをビューファイルに書いてしまうとコードが冗長になり、読みにくいです。

デコレーター使用前
1
2
3
4
5
6
7
8
9
<nav>
  <ul class="nav">
    <% @user.recent_pages.each do |page| %>
      <li class="nav-item">
        <%= link_to page.name, page.path %>
      </li>
    <% end %>
  </ul>
</nav>

そのため、表示ロジックをデコレーターを使って簡潔にしてみます。

デコレーター
1
2
3
4
5
6
7
8
9
10
11
12
# app/decorators/user_decorator.rb
class UserDecorator < Draper::Decorator
  delegate_all

  def dynamic_navigation_menu
    menu = h.content_tag(:ul, class: "nav")
    object.recent_pages.each do |page|
      menu += h.content_tag(:li, h.link_to(page.name, page.path), class: "nav-item")
    end
    h.raw(menu)
  end
end

このようにロジックの部分をデコレーター切り出します。そしてビューファイルで使用します。

デコレーター使用後
1
<nav><%= @user.decorate.dynamic_navigation_menu %></nav>

このようにかなりスッキリしてみやすくなりました。

このようにビューファイルの中に長い表示ロジックがある場合はデコレーターを使用することで、ビューのコードが簡潔になり可読性が向上することがわかります。

また、デコレーターにメソッドを定義することで、同じようなHTMLメニューを作成する処理を共通化できます。例えば、他のページでユーザーメニューを表示する際に、このメソッドを呼び出すことで、HTMLメニューの表示をさまざまな場所で再利用できるようになり、非常に便利です。

モデル・ヘルパー・デコレーターの使い分け

Railsアプリケーションの中で、データの処理や表示に関わる役割は、モデル・ヘルパー・デコレーターの3つに分けることができます。それぞれの役割を明確にすることで、コードの可読性や保守性を向上させることができます。この章では、それぞれの使い分けについて具体例を挙げながら解説します。

モデルで使うべき処理

モデルは、データの保存や更新、バリデーションなど、アプリケーションのビジネスロジックを担う部分です。データベースと直接やりとりする役割があり、以下のような処理をモデルに持たせるのが適切です。

例えば、ユーザーの年齢を計算する処理はデータそのものに直結するため、モデルに定義します。

Userモデル
1
2
3
4
5
6
7
class User < ApplicationRecord
  def age
    return unless birthday

    ((Time.zone.now - birthday.to_time) / 1.year.seconds).floor
  end
end

このように、Userモデル内でageメソッドを定義し、誕生日から年齢を計算する処理を持たせています。この処理はデータの特性に基づくものであり、モデルが適切な場所です。

ヘルパーで使うべき処理

ヘルパーは、主にビューでの表示に関する処理をサポートします。ビューで使われる複雑なロジックを整理し、HTMLの生成やフォーマットを行う役割を持っています。ユーザーの情報を装飾するような処理は、ヘルパーで定義するのが一般的です。

例えば、金額を渡したら¥を付ける処理をヘルパーに持たせる場合は以下のようになります。

ヘルパー
1
2
3
4
5
module ApplicationHelper
  def format_currency(price, currency = "¥")
    "#{currency}#{number_to_currency(price)}"
  end
end

このような表示に関する処理はヘルパーに書くことで、ビューがスッキリし、再利用性が高まります。

デコレーターで使うべき処理

デコレーターは、主にオブジェクトの振る舞いや表示を変更するために使用されます。モデルのロジックやビューの複雑さを避けつつ、柔軟にデータの表現を拡張する役割を果たします。ビジネスロジックをモデルに持たせすぎず、ビューやプレゼンテーションロジックと切り分けるためにデコレーターを使います。

例えば、ユーザーのアカウントがアクティブかどうか、またはプレミアムユーザーかどうかを判断し、ビューで特定の表示を行う必要があるとします。このような場合、ビジネスロジックはモデルで管理し、表示に関するロジックはデコレーターに任せるのが適切です。

モデル側のロジック
1
2
3
4
5
6
7
8
9
class User < ApplicationRecord
  def premium?
    plan == 'premium'
  end

  def active?
    status == 'active'
  end
end

デコレーターでは、Userモデルのデータを基に、UIでの表示に特化した処理を追加します。例えば、ユーザーのアカウントステータスに応じた表示ラベルやスタイルを追加する処理をデコレーターに定義します。

デコレーター
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class UserDecorator < Draper::Decorator
  delegate_all

  def status_label
    if object.active?
      h.content_tag(:span, "Active", class: "label label-success")
    else
      h.content_tag(:span, "Inactive", class: "label label-default")
    end
  end

  def premium_badge
    if object.premium?
      h.content_tag(:span, "Premium", class: "badge badge-warning")
    else
      h.content_tag(:span, "Free", class: "badge badge-secondary")
    end
  end
end

ビューでは以下のように呼び出します。

ビューファイル
1
2
<%= @user.decorate.status_label %>
<%= @user.decorate.premium_badge %>

まとめ

モデル、ヘルパー、デコレーターはそれぞれ異なる役割を持ち、適切に使い分けることでアプリケーションの保守性を高めることができます。モデルにはビジネスロジック、ヘルパーには表示に関わるロジック、そしてデコレーターには表示の装飾や振る舞いの変更を持たせるという基本的な考え方を押さえておくことが重要です。

RailsではDraperというgemを導入することで、簡単にデコレーターを使用することができます。ビューファイルが複雑化しているアプリではぜひ導入してみましょう。

この記事のまとめ

  • DraperはRuby on Railsにデコレーターを導入することができるgemです。
  • デコレーターは、モデルのデータをビューで表示するためのロジックをまとめたものです。
  • デコレーターを使うことで、モデルのビジネスロジックとビューの表示ロジックを分離できるため、コードの整理がしやすくなります。