Decorator を試す

最近気が付くと Model に何かと集中させてしまいがち。
このままいくと「Let’s go fat Model!」です。

解決策について書いてあるものを見つけたので、何度かに分けて試してみたいと考えています。
今回は Decorator を試します。

参考

Decorator

調べた限り、Decorator は Model(View) へ書きがちな表示にまつわる実装を持たせるためのデザインパターンに当たるものだそう。
特定の日付の表示パターンであったり、数字のスコアを A とか B と表示させたり、姓と名を合わせたフルネームを返す実装を寄せるためのもの。
これで、表示だけにかかわる Model の実装を減らすことができる。

ただし、Rails の文脈で書かれている Decorator と、Ruby の文脈で書かれている Decorator は異なっているような記述が多いので注意すべきものだと感じます。
少なくとも Ruby の文脈で書かれている時、「表示に関連した実装を寄せる」みたいなことは関係ない。

Decorator を試す

参考にしたものを見ると、だいたい (drapper)[https://github.com/drapergem/draper] を利用しているパターンが多いようです。
とりあえず今回は使わないことにします。

UserDecorator を作る

first_name last_name ageを持つ users テーブル User モデルがすでにあるものとする。
UserDecorator を実装して、full_namegeneration を返すようにする。

app/decorators/user_decorator.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
require 'delegate'

class UserDecorator < SimpleDelegator
def full_name
@full_name ||= "#{first_name} #{lastname}"
end

def generation
# 値の埋め方自体は別の方法は十分あると思うが一旦これで。
@generation ||=
if age<20
"10代以下"
elsif (20..29).cover?(age)
"20代"
elsif (30..39).cover?(age)
"30代"
elsif (40..49).cover?(age)
"40代"
elsif (50..59).cover?(age)
"50代"
elsif (60..69).cover?(age)
"60代"
else
"70代以上"
end
end
end

SimpleDelegator を継承したクラスを用意する。
SimpleDelegator は引数に与えたオブジェクトへメソッドの呼び出しをすべて移譲する。

UserDecorator を使う

用意した UserDecorator を使ってみる。

コントローラー

app/controllers/users_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
class UsersController < ApplicationController

def index
@users = User.all
end

def show
user = User.find(params[:id])

# UserDecorator を使う
@user = UserDecorator.new(user)
end
end

View テンプレート

一覧を表示するindex.html.erbでは、
取得できているのはUser::ActiveRecord_Relationなので、each の中でUserDecoratorを使う。

app/views/users/index.html.erb
1
2
3
4
5
6
7
8
9
10
<h1>Index</h1>

<% @users.each do |u| %>
<!-- UserDecorator をここで使う -->
<% user = UserDecorator.new(u) %>
<%= user.first_name %> -
<%= user.last_name %> -
<%= user.full_name %> -
<%= user.generation %>
<% end %>

表示すると以下のようになる。

1
2
Index
First - Last - First Last - 20代

一覧を表示する show.html.erb では、
コントローラで UserDecorator を使用したインスタンスを取得済みなので素直にメソッドを呼び出す。

app/views/users/show.html.erb
1
2
3
4
5
<h1>Show</h1>
<%= @user.first_name %></br>
<%= @user.last_name %></br>
<%= @user.full_name %></br>
<%= @user.generation %></br>

表示すると以下のようになる。

1
2
3
4
5
Show
First
Last
First Last
20代

それぞれ、UserDecorator で拡張したメソッドが使えた。

UserDecorator を Module で作ってみる

Module で拡張してみる。といってもただの Module の使い方というだけの気もする。
UserDecoratorModuleを作る。

app/decorators/user_decorator_module.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
module UserDecoratorModule
def full_name
@full_name ||= "#{first_name} #{lastname}"
end

def generation
@generation ||=
if age<20
"10代以下"
elsif (20..29).cover?(age)
"20代"
elsif (30..39).cover?(age)
"30代"
elsif (40..49).cover?(age)
"40代"
elsif (50..59).cover?(age)
"50代"
elsif (60..69).cover?(age)
"60代"
else
"70代以上"
end
end
end

UserDecorator を Module で使う 1

素直にモデルで include する。

app/models/user.rb
1
2
3
class User < ApplicationRecord
include UserDecoratorModule
end

使う側は User を呼び出せば、もちろん UserDecoratorModule の機能を使える。

app/controllers/users_controller.rb
1
2
3
4
5
6
7
8
9
10
class UsersController < ApplicationController

def index
@users = User.all
end

def show
@user = User.find(params[:id])
end
end

UserDecorator を Module で使う 2

インスタンスに extend するパターン。

app/controllers/users_controller.rb
1
2
3
4
5
6
7
8
9
10
11
class UsersController < ApplicationController

def index
@users = User.all
end

def show
@user = User.find(params[:id])
@user.extend UserDecoratorModule
end
end

index.html.erbでは、
コントローラで取得できているのはUser::ActiveRecord_Relationなので、each の中でUserDecoratorModuleを使う。

app/views/users/index.html.erb
1
2
3
4
5
6
7
8
9
<h1>Index</h1>

<% @users.each do |user|%>
<% user.extend UserDecoratorModule %>
<%= user.first_name %> -
<%= user.lastname %> -
<%= user.full_name %> -
<%= user.generation %>
<% end %>

一覧を表示するshow.html.erbでは、コントローラで extend 済みなので素直にメソッドを呼び出す。

app/views/users/show.html.erb
1
2
3
4
5
<h1>Show</h1>
<%= @user.first_name %></br>
<%= @user.lastname %></br>
<%= @user.full_name %></br>
<%= @user.generation %></br>

draper を使ってみる

導入

Gemfile に以下を記述してインストール。

Gemfile
1
gem 'draper'

続けて以下コマンドを実行します。

1
bundle exec rails generate draper:install

実装

UserDecoratorDraper::Decorator の継承として準備します。

app/decorators/user_decorator.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class UserDecorator < Draper::Decorator
def full_name
@full_name ||= "#{first_name} #{lastname}"
end

def generation
@generation ||=
if age<20
"10代以下"
elsif (20..29).cover?(age)
"20代"
elsif (30..39).cover?(age)
"30代"
elsif (40..49).cover?(age)
"40代"
elsif (50..59).cover?(age)
"50代"
elsif (60..69).cover?(age)
"60代"
else
"70代以上"
end
end
end

.decorate を呼び出すと Model 名と対応したデコレーターを付与してくれる。
.allの呼び出し時点(User::ActiveRecord_Relationの時点)でデコレーターでの拡張をできるのは便利。
もちろんdraperを使わなかったときみたいなUserDecorator.decorate(User.find(params[:id]))という呼び出しができる。
好みだけ言うなら後者がいい。

1
2
3
4
5
6
7
8
9
10
11
12
13
class UsersController < ApplicationController
def index
# 便利
@users = User.all.decorate
end

def show
 # draper 使わなかったときみたいな呼び出しもできる
@user = UserDecorator.decorate(User.find(params[:id]))

# @user = UserDecorator.new(User.find(params[:id])) も可
end
end

今回は Decorator を試してみました。
先人たちの Fat ~~ を避けるための知恵をまだまだ吸収したいところです。

ではでは。