Decorator パターン — 機能を動的に追加する
「モデルが700行になってしまいました…」
ある月曜の朝、ケンタはチームの朝会でついに白旗を上げた。
「えっと…Userモデルが700行を超えてしまいました。スクロールしても全体が見えなくて、どこに何が書いてあるかわからない状態です」
チームの沈黙が重かった。山田さんが静かにコードを開いた。
# app/models/user.rb(一部抜粋)
class User < ApplicationRecord
# 260行目あたりから表示系メソッドが始まる
def display_name
name.presence || email.split("@").first
end
def formatted_joined_at
joined_at.strftime("%Y年%m月%d日")
end
def avatar_url
if avatar.attached?
Rails.application.routes.url_helpers.rails_blob_url(avatar)
else
"https://via.placeholder.com/100?text=#{display_name[0]}"
end
end
def membership_badge
case membership_type
when "premium" then "👑 プレミアム会員"
when "corporate" then "🏢 法人会員"
else "一般会員"
end
end
def profile_completion_rate
fields = [:name, :bio, :phone_number, :avatar]
filled = fields.count { |f| send(f).present? }
(filled.to_f / fields.length * 100).to_i
end
# さらに続く...
end「ケンタ君、これはよくある問題だよ」と山田さんは言った。「Fat Model問題と呼ばれる。そして解決策はDecoratorパターンだ。」
ケンタは「Decorator?」と首をかしげた。「デコレーター、ケーキを飾り付けるやつですか?」
「まさにその感覚が正しい。今日はその話をしよう。」
「表示ロジックはどこに書くの?」
その日の夕方、ケンタはユーザープロフィールページをリファクタリングしていた。
「ユーザーの表示名って、登録名があれば登録名、なければメールアドレスの@より前を使うんですが、これってモデルのどこに書くべきでしたか?」
山田さんは答えた。「表示用のロジックをモデルに書くのは避けたい。モデルはデータと振る舞いを管理するもの。表示(View)の関心事は別のクラスに分けよう。それがDecoratorパターンだ。」
Decorator パターンとは
Decorator パターンは、既存のオブジェクトをラップし、元のオブジェクトを変更せずに機能を動的に追加するパターンだ。
比喩1:コーヒーのカスタマイズ — コーヒー(基本オブジェクト)に、ミルク(Decorator)、シロップ(Decorator)、ホイップクリーム(Decorator)を重ねていく。各トッピングは「コーヒーとして振る舞いつつ、何かを追加する」。
比喩2:スマートフォンのケース — 本体(iPhone)はそのままで、耐衝撃ケース、リングホルダー、財布機能を重ねていける。本体は一切変わらない。
比喩3:クリーニング後の服 — シャツにアイロン、のり付け、丁寧な折り畳みを施す。素材や形は変わらないが「付加価値」がついていく。
継承との違い: 継承はコンパイル時(クラス定義時)に決まるが、Decoratorは実行時に動的に機能を追加できる。また、複数のDecoratorを重ね合わせることができる。
INFO
Decoratorパターンのポイントは「元のオブジェクトを変えない」こと。UserモデルはDBとのやり取りに集中し、表示の責任はDecoratorが担う。これが単一責任の原則(SRP)だ。
問題のあるコード(Before)
Fat Modelの典型的なパターンを見てみよう。
# app/models/user.rb — モデルに表示ロジックが混入している
class User < ApplicationRecord
validates :email, presence: true
# ドメインロジック(正しい)
def premium?
membership_type == "premium"
end
def can_access_premium_content?
premium? && !subscription_expired?
end
# 表示ロジック(モデルに書くべきでない)
def display_name
name.presence || email.split("@").first
end
def avatar_url
if avatar.attached?
Rails.application.routes.url_helpers.rails_blob_url(avatar)
else
"https://via.placeholder.com/100?text=#{display_name[0]}"
end
end
def formatted_joined_at
joined_at.strftime("%Y年%m月%d日")
end
def membership_badge
case membership_type
when "premium" then "👑 プレミアム会員"
when "corporate" then "🏢 法人会員"
else "一般会員"
end
end
def profile_completion_rate
fields = [:name, :bio, :phone_number, :avatar]
filled = fields.count { |f| send(f).present? }
(filled.to_f / fields.length * 100).to_i
end
end問題点:
- モデルがどんどん太る(Fat Model問題)
- 表示形式が変わるたびにモデルのテストが必要
- 絵文字や文字列フォーマットがビジネスロジックと混在
- モデルをRails以外で使うとき(バッチ処理など)に不要なメソッドが邪魔
WARNING
Fat Modelは「技術的負債の温床」だ。今日1つの表示メソッドを追加するのに10分かかる。半年後は同じ作業に1時間かかる。700行のモデルは全員の生産性を下げる。
Draper Gem によるDecoratorの実装
Draper はRailsでDecoratorパターンを実現するための定番gemだ。
# Gemfile
gem "draper"bundle install
rails generate decorator User
# app/decorators/user_decorator.rb が生成される# app/decorators/user_decorator.rb
class UserDecorator < Draper::Decorator
delegate_all # 元のUserモデルのメソッドは全て委譲
# 表示ロジックだけここに書く
def display_name
object.name.presence || object.email.split("@").first
end
def avatar_url
if object.avatar.attached?
h.rails_blob_url(object.avatar)
else
h.image_url("default_avatar.png")
end
end
def formatted_joined_at
object.joined_at.strftime("%Y年%m月%d日")
end
def membership_badge
case object.membership_type
when "premium" then "プレミアム会員"
when "corporate" then "法人会員"
else "一般会員"
end
end
def profile_completion_rate
fields = [:name, :bio, :phone_number, :avatar]
filled = fields.count { |f| object.send(f).present? }
(filled.to_f / fields.length * 100).to_i
end
endINFO
h はビューヘルパーへのアクセスを提供する。h.image_url、h.link_toなどが使える。Decoratorはビューとモデルの橋渡し役なので、ヘルパーを使えることが重要。
コントローラでDecoratorを適用する
# app/controllers/users_controller.rb
class UsersController < ApplicationController
def show
@user = User.find(params[:id]).decorate
# または: @user = UserDecorator.decorate(User.find(params[:id]))
end
def index
@users = User.all.decorate
# コレクション全体を一括でデコレートできる
end
endViewでそのまま使う
<%# app/views/users/show.html.erb %>
<div class="profile">
<img src="<%= @user.avatar_url %>" alt="<%= @user.display_name %>">
<h1><%= @user.display_name %></h1>
<span class="badge"><%= @user.membership_badge %></span>
<p>登録日: <%= @user.formatted_joined_at %></p>
<div class="progress" style="width: <%= @user.profile_completion_rate %>%">
プロフィール完成度: <%= @user.profile_completion_rate %>%
</div>
<%# モデルのメソッドも委譲されるのでそのまま使える %>
<% if @user.premium? %>
<span>プレミアムコンテンツ利用中</span>
<% end %>
</div>INFO
モデルのメソッド(premium?, emailなど)も delegate_all のおかげでそのまま使える。ViewはDecoratorが本物のUserかどうか気にしなくていい。
WARNING
コレクションをデコレートするとき User.all.decorate は問題ないが、User.all.map(&:decorate) は遅い。Draperは decorate_collection という専用メソッドを持っている。大量データには注意。
Draperを使わない純粋Rubyの実装
Draperなしでも同じことができる。SimpleDelegator を使うと委譲が楽だ。
# app/decorators/user_decorator.rb
require "delegate"
class UserDecorator < SimpleDelegator
# SimpleDelegatorを継承すると、元オブジェクトの全メソッドが自動委譲される
# def_delegatorsを明示的に書く必要がない
def display_name
name.presence || email.split("@").first
# SimpleDelegatorのおかげで name, email が直接呼べる
end
def membership_badge
case membership_type
when "premium" then "プレミアム会員"
when "corporate" then "法人会員"
else "一般会員"
end
end
def formatted_joined_at
joined_at.strftime("%Y年%m月%d日")
end
# 元のオブジェクトへのアクセスは __getobj__ で
def original_user
__getobj__
end
end# 使う側
user = User.find(params[:id])
decorated = UserDecorator.new(user)
decorated.display_name # => "田中ケンタ"
decorated.premium? # => true(SimpleDelegatorが委譲)
decorated.email # => "kenta@example.com"
decorated.is_a?(User) # => false(注意!)
decorated.__getobj__.is_a?(User) # => trueWARNING
SimpleDelegator で作ったDecoratorは is_a?(User) が false を返す。ActiveRecordのポリモーフィズムやSTIを使っている箇所でこれが問題になることがある。型チェックが必要な場所では Draper の方が安全だ。
Decoratorを積み重ねる
複数のDecoratorを重ねて、段階的に機能を追加できる。
# 管理者向けには追加情報を表示
class AdminUserDecorator < UserDecorator
def display_name
"#{super} (ID: #{id})" # UserDecoratorのdisplay_nameを拡張
end
def admin_notes
internal_notes.presence || "ノートなし"
end
def last_login_summary
return "未ログイン" unless last_login_at
days_ago = ((Time.current - last_login_at) / 1.day).to_i
"#{days_ago}日前(#{last_login_at.strftime('%m/%d %H:%M')})"
end
end# コントローラでの使い分け
class UsersController < ApplicationController
def show
user = User.find(params[:id])
@user = if current_user.admin?
AdminUserDecorator.new(user)
else
UserDecorator.new(user)
end
end
end<%# テンプレートは同じものを使える %>
<h1><%= @user.display_name %></h1>
<%# 管理者なら "田中ケンタ (ID: 42)"、一般なら "田中ケンタ" %>
<% if @user.respond_to?(:admin_notes) %>
<p>管理メモ: <%= @user.admin_notes %></p>
<% end %>WARNING
Decoratorを深く積み重ねすぎると、「どのDecoratorがこのメソッドを提供しているか」がわかりにくくなる。3層以上のDecorator継承は避け、その場合はFacadeパターンか、Decoratorの合成を検討しよう。
APIレスポンスのDecorator — Serializerとの比較
Decoratorはビュー層だけでなく、APIレスポンスの整形にも使える。
# app/decorators/user_api_decorator.rb
class UserApiDecorator < SimpleDelegator
def as_json(options = {})
{
id: id,
display_name: name.presence || email.split("@").first,
email: email,
membership_label: membership_label,
joined_at: joined_at.iso8601
}
end
private
def membership_label
case membership_type
when "premium" then "プレミアム会員"
when "corporate" then "法人会員"
else "一般会員"
end
end
end# コントローラ
class Api::V1::UsersController < ApplicationController
def show
user = User.find(params[:id])
render json: UserApiDecorator.new(user).as_json
end
endDecoratorとSerializerの使い分け:
| 観点 | Decorator | Serializer(ActiveModelSerializers等) |
|---|---|---|
| 目的 | 表示ロジックの追加 | APIシリアライズに特化 |
| 用途 | HTML View + API 両対応 | API専用 |
| 学習コスト | 低い(Rubyの知識だけ) | 高い(DSL習得が必要) |
| 柔軟性 | 高い | 制約あり |
INFO
小規模APIならDecoratorで十分。APIが成長して複数バージョンが必要になったり、スパースフィールドセットが必要になったりしたら、dedicated Serializerライブラリへの移行を検討しよう。
ログ出力・キャッシュをDecoratorで追加する
Decoratorはビジネスロジックを変えずに横断的な関心事を追加できる。
# ログ機能をDecoratorで追加
class LoggingUserDecorator < SimpleDelegator
def initialize(user, logger: Rails.logger)
super(user)
@logger = logger
end
def update_profile(attributes)
@logger.info("[User] プロフィール更新開始 user_id=#{id}")
result = __getobj__.update(attributes)
if result
@logger.info("[User] プロフィール更新成功 user_id=#{id}")
else
@logger.warn("[User] プロフィール更新失敗 user_id=#{id} errors=#{errors.full_messages}")
end
result
end
endINFO
ログ出力やキャッシュをDecoratorで追加することで、本来のビジネスロジック(Userモデル)を一切変えずに横断的な関心事を追加できる。これはDecoratorパターンの真骨頂だ。
Rack MiddlewareはDecoratorである
「実はケンタ君、Railsの中核にもDecoratorパターンが使われていることを知っているか?」
「えっ、どこですか?」
「Rack Middlewareだよ。」
# config/application.rb
module MyApp
class Application < Rails::Application
# Middlewareスタックは、リクエスト/レスポンスを包むDecoratorの連鎖
config.middleware.use Rack::Attack # レート制限Decorator
config.middleware.use Rack::Cors # CORSヘッダーDecorator
config.middleware.insert_before 0, Rack::Deflater # 圧縮Decorator
end
end# カスタムMiddlewareを作ると、Decoratorパターンそのもの
class RequestTimingMiddleware
def initialize(app)
@app = app # 次のMiddleware(またはアプリ本体)を「包む」
end
def call(env)
started_at = Time.current
status, headers, body = @app.call(env) # 本体への委譲
elapsed = Time.current - started_at
Rails.logger.info("Request took #{(elapsed * 1000).round}ms")
headers["X-Request-Time"] = elapsed.to_s
[status, headers, body]
end
end「Middlewareは全部このパターンです。@app.call(env) で本体に委譲して、前後に処理を追加している。まさにDecoratorだ。」
ケンタは目を丸くした。「普段使っているRailsの中身がこんな風になっているとは思いませんでした!」
INFO
Rack Middlewareを理解するとDecoratorパターンが直感的に掴める。before/afterの処理をラップして委譲する構造は、UserDecoratorがUserモデルをラップする構造と全く同じだ。
Decorator vs Concern vs Helper
「どれを使えばいいか混乱します」とケンタが言った。
| Decorator | Concern | Helper | |
|---|---|---|---|
| 用途 | 特定オブジェクトの表示 | モデル横断の共有ロジック | Viewの汎用ヘルパー |
| テスト | 単体テストしやすい | モデルのテストに含む | ヘルパーのテスト |
| 依存 | モデルに依存 | モデルにinclude | ビューに依存 |
| 例 | user.display_name | has_notifications | format_price(1000) |
| ビューヘルパー | 使える(Draper) | 使えない | そのもの |
WARNING
Concern(ActiveSupport::Concern)はモデルのビジネスロジックを分割するためのもの。表示ロジックをConcernに書くのはDecoratorと同じ問題を引き起こす。Concernは「関心の共有」、Decoratorは「表示の追加」と使い分けよう。
どんなときにDecoratorを使うか — 判断フロー
「結局、迷ったときはどう判断すればいいですか?」とケンタが聞いた。
具体的な判断基準:
joined_at.strftime("%Y年%m月%d日")→ Decorator(表示専用)membership_type == "premium"→ Model(ビジネスルール)- 複数のモデルで使う
notifiable?→ Concern number_to_currency(price)→ Helper(汎用フォーマット)
INFO
迷ったら「これはDBの値や状態を判断するロジックか?」と聞いてみよう。答えがYesならModel。「これはViewでどう見せるかだけの話か?」ならDecorator。
テストの書き方
Decoratorはビューから切り離されているため、単体テストが書きやすい。
# spec/decorators/user_decorator_spec.rb
require "rails_helper"
RSpec.describe UserDecorator do
# Draperのspec用ヘルパーを使う
include Draper::ViewContext
let(:user) { create(:user, name: "田中ケンタ", membership_type: "premium") }
let(:decorated) { user.decorate }
describe "#display_name" do
context "名前が登録されている場合" do
it "登録名を返す" do
expect(decorated.display_name).to eq("田中ケンタ")
end
end
context "名前が未登録の場合" do
let(:user) { create(:user, name: nil, email: "kenta@example.com") }
it "メールアドレスの@より前を返す" do
expect(decorated.display_name).to eq("kenta")
end
end
end
describe "#membership_badge" do
context "プレミアム会員の場合" do
it "プレミアム会員を返す" do
expect(decorated.membership_badge).to eq("プレミアム会員")
end
end
context "一般会員の場合" do
let(:user) { create(:user, membership_type: "general") }
it "一般会員を返す" do
expect(decorated.membership_badge).to eq("一般会員")
end
end
end
describe "#profile_completion_rate" do
context "全フィールドが埋まっている場合" do
let(:user) { create(:user, :with_full_profile) }
it "100を返す" do
expect(decorated.profile_completion_rate).to eq(100)
end
end
end
endINFO
instance_double を使えば実際のDBアクセスなしに高速テストが書ける。Decoratorが表示ロジックのみに集中しているなら、DB不要のテストが自然に書ける。これもDecoratorの利点だ。
AWSでのDecorator的発想
AWSでも「既存のリソースに機能を追加する」パターンがある。
EC2インスタンス自体を変えずに、IAMロールで権限を、Security Groupでファイアウォールを、CloudWatchエージェントで監視を追加している。これはDecoratorパターンと同じ考え方だ。
AWSサービスとDecoratorの対応:
| AWSサービス | Decoratorとしての役割 |
|---|---|
| IAM Role | 権限の付与(元リソースは変わらない) |
| Security Group | ネットワークアクセス制御を追加 |
| Lambda Layer | 共通コードを関数に追加 |
| ECS サイドカー | ログ・監視をメインコンテナに追加 |
Lambda Layer は関数コードを変えずに共通ライブラリを追加。ECS サイドカー はメインコンテナを変えずに Fluentd や Datadog Agent を付加する。どちらもDecoratorの発想そのものだ。
INFO
インフラのDecoratorパターンはImmutable Infrastructureと相性がいい。「EC2インスタンスを変えずに機能を追加する」というアプローチは、コードの「元のクラスを変えずに機能を追加する」Decoratorと完全に一致している。
Decorator が深くなりすぎる問題
「Decoratorって便利ですね。全部Decoratorにすればいいんじゃないですか?」
山田さんは笑った。「その考えは危険だよ。」
# アンチパターン:深すぎるDecorator
user = LoggingDecorator.new(
CachingDecorator.new(
AdminUserDecorator.new(User.find(1))
)
)
# どのDecoratorがどのメソッドを持っているか追えない深いDecoratorの問題:
- デバッグが難しい(どのレイヤーでエラーが出たかわかりにくい)
is_a?チェックが失敗する- メソッドの名前が衝突したとき、どちらが呼ばれるかわかりにくい
改善策は「責任を明確に分けて、必要なDecoratorだけを選ぶ」こと。3層以上は設計を見直すサインだ。
WARNING
Decoratorは2層以内に収める。それ以上になったら設計を見直すサインだ。責任が増えすぎているなら、PresenterパターンやFacadeパターンへの移行を検討しよう。
ケンタの成長と次章への橋渡し
翌週、ケンタはリファクタリングを終えた。
「山田さん、Userモデルが700行から340行になりました!表示ロジックを全部UserDecoratorに移したんです。」
山田さんは画面を見て、うなずいた。「よくできた。テストは?」
「Decoratorのspecを書きました。モデルのテストより速く動くし、viewのセットアップも不要で、書きやすかったです。」
「継承は縦に積む、Decoratorは横から包む。これが今日の一番のポイントだ」と山田さんは言った。
ケンタはノートを開いて書いた:
Decoratorパターン = 元のオブジェクトをラップして機能を追加。継承より柔軟で組み合わせ自在。Railsでは表示ロジックをモデルから分離するのに使う。Rack Middlewareも同じ構造だった。
「ちなみに」と山田さんが続けた。「今日学んだデコレーター的な発想、つまり『元を変えずに動作を制御・追加する』という発想、次章のProxyパターンとも深く関係がある。Decoratorが機能の追加なら、Proxyはアクセス制御や遅延ロードだ。楽しみにしておきなさい。」
INFO
この章のまとめ
- Decoratorはオブジェクトをラップして機能を追加する。元のクラスは変わらない
- 継承より柔軟。複数のDecoratorを重ねたり、動的に適用できる
- Railsでは表示ロジックをモデルから分離するためによく使う(Fat Model解消)
- Draper gemがRailsでのDecorator実装を簡単にする
SimpleDelegatorを使うと依存なしに実装できる- Rack Middlewareはアプリ全体のDecoratorパターン
- Decorator・Concern・Helperはそれぞれ異なる用途で使い分ける
- AWSではIAMロール・SecurityGroupが同じ発想
- Decoratorは2層以内に収め、深くなりすぎないよう注意