更新日:
【Rails】 Draperを使ってデコレーターを導入しよう!
DraperはRuby on Railsにデコレーターを導入することができるgemです。
デコレーターとは
デコレーターは、プログラミングにおいて「オブジェクトに新しい機能を追加する」ためのデザインパターンの一つです。デコレーターの役割は、以下の通りです。
名前 | 役割 |
---|---|
モデル | データベースにアクセスする処理を担当します。 |
ヘルパー | データベースにアクセスする処理は含まず、ビューに関するロジックを扱います。 |
デコレーター | 特定のモデルに関連したビューに関するロジックを扱います。 |
モデルに関連性のある表示ロジックをデコレーターに記載することで、 モデル、ヘルパー、 デコレーターの責務が明確になり、全体的に処理の内容が読み取りやすいディレクトリ設計となります。
draper
というgemを使うことでデコレーターに表示ロジックを持たせることができます。
Draperとは
Draper
は、Railsアプリケーションにおいてオブジェクトの表示ロジックを扱うためのデコレーターを導入するためのgemです。Draper
を使うことで、モデルやヘルパーに直接表示ロジックを追加せず、デコレーターを通じて表示のカスタマイズができます。
Draperを導入しよう
Draper
を導入するにはGemfile
に以下の行を追加します。
1
gem 'draper'
そして、bundle install
コマンドでインストールします。
これで導入は完了です。
デコレーターの生成
この章ではデコレーターの生成方法を解説していきます。
rails generate draper:install
生成されるすべてのデコレーターが継承するApplicationDecorator
を作成するには、以下のコマンドを実行します。
1
rails generate draper:install
コマンドを実行すると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
ディレクトリに内に作成されます。
作成されたファイルは以下のように記述されています。
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
ファイルに次の設定を追加します。
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
という日時属性を特定のフォーマットで表示するロジックを追加するには以下のように記述します。
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
メソッドを書いたとします。
1
2
3
4
class UserDecorator < ApplicationDecorator
delegate_all
end
このデコレーターファイルはUser
モデルに対するデコレーターなので、元のモデルはUser
モデルになります。User
モデルに以下のメソッドが定義されていたら、delegate_all
メソッドを記述したファイルでもこれらのメソッドが使えるようになります。
1
2
3
4
5
class User < ApplicationRecord
def display_name_and_age
"#{name} (#{age}歳)"
end
end
例えば以下のようにdelegate_all
メソッドを記述せずに、User
モデルで定義したdisplay_name_and_age
メソッドをデコレーターファイル内で使用します。
1
2
3
4
5
6
class UserDecorator < ApplicationDecorator
def name_with_age
display_name_and_age
end
end
すると以下のようにメソッドが定義されていないというエラーが発生することがわかります。
以下のようにdelegate_all
を記述します。
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
を使わず、以下のように省略して書くことも可能です。
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
を使って元のモデルのプロパティを呼び出す必要があります。
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_safe
のjoin
メソッドで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のビュー用ヘルパーメソッドを直接使用できるようになります。
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.rb
にinclude Draper::LazyHelpers
を追加すれば、全てのデコレーターファイル内でh
を省略してヘルパーメソッドを使えるようになるので便利です。
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モデルの間にアソシエーションを使って関連付けをしています。
1
2
3
class Article < ApplicationRecord
belongs_to :author
end
1
2
3
class Author < ApplicationRecord
has_many :articles
end
そしてそれぞれのモデルのデコレーターを定義します。AuthorDecorator
と ArticleDecorator
に以下のようにメソッドを定義します
1
2
3
4
5
6
7
class AuthorDecorator < Draper::Decorator
delegate_all
def full_name
"#{first_name} #{last_name}"
end
end
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
を使用します
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
オブジェクトも自動的にデコレートされます。
実際に使用してみます。
まずはコントローラーでインスタンスをデコレートします。
1
2
3
def show
@article = Article.find(params[:id]).decorate
end
上のコードを実行すると、@article
はArticleDecorator
でデコレートされ、さらに@article.author
は自動的にAuthorDecorator
でデコレートされます。
ビューでは、デコレートされた関連オブジェクトのメソッドを直接使用できます。
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 })
そして、デコレーター内でそのコンテキストデータにアクセスします。
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つに分けることができます。それぞれの役割を明確にすることで、コードの可読性や保守性を向上させることができます。この章では、それぞれの使い分けについて具体例を挙げながら解説します。
モデルで使うべき処理
モデルは、データの保存や更新、バリデーションなど、アプリケーションのビジネスロジックを担う部分です。データベースと直接やりとりする役割があり、以下のような処理をモデルに持たせるのが適切です。
例えば、ユーザーの年齢を計算する処理はデータそのものに直結するため、モデルに定義します。
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です。
- デコレーターは、モデルのデータをビューで表示するためのロジックをまとめたものです。
- デコレーターを使うことで、モデルのビジネスロジックとビューの表示ロジックを分離できるため、コードの整理がしやすくなります。