Rubyのデザインパターンを調べると、GoFの23パターンを解説した記事と、RailsのService objectやForm objectを列挙した記事のどちらかに行き着きます。困るのは、その中間にある「どのパターンを、どのgemで、どこまで自前で書くか」が書かれていない点です。本記事はRuby 4.0.6とRails 8.1系を前提に、Interactor・Factory・Decorator・Strategyの4つを実装コードで整理します。掲載したコードはすべてRuby 4.0.6(x86_64-darwin)とinteractor 3.2.0で実行し、出力を確認したものです。
まとめ:Railsで使うパターンの選び分けと標準ライブラリで足りる範囲
複数モデルにまたがる更新手続きを1クラスに閉じたいならInteractorパターン、生成する具象クラスの分岐を集約したいならFactoryパターン、表示専用のメソッドをモデルから追い出したいならDecoratorパターン、実行時に振る舞いを差し替えたいならStrategyパターンが受け皿になります。ここでgemが要るのはInteractor(interactor 3.2.0)と、Rails文脈のDecorator(draper 4.0.6)だけです。FactoryとStrategyはRubyの言語機能だけで書けます。
注意が要るのは標準添付ライブラリの扱いです。Singletonは現在も標準添付(default gem)ですが、Observerパターンで使うobserverはRuby 3.4.0でbundled gemへ移され、Bundler管理下のアプリでは Gemfile に書かないとrequireが失敗します。以降で、各パターンの実装と、採用しない方がよい条件を順に見ていきます。
GoFの23パターンとRails流オブジェクトの守備範囲の違い
Railsのアプリケーションコードでは、GoFのパターン名がそのまま登場する場面はそれほど多くありません。Active RecordがTemplate MethodやObserverに相当する仕組みをフレームワーク側で持ち、コールバックやActiveSupport::Notificationsとして提供済みだからです。アプリ側で明示的に書くのは、フレームワークが用意していない層――複数モデルをまたぐ手続き、表示専用のロジック、実行時に差し替えたい処理――に限られます。
GoFパターンがRailsのコードに出てこない理由
GoFのパターンは静的型付け言語での制約を前提に組み立てられています。Abstract Factoryが必要になるのは、生成するクラスをコンパイル時に決められないからです。Rubyではクラス自体がオブジェクトなので、クラスを変数に入れて渡せば同じ目的を達成できます。同様にStrategyは、インターフェイスを宣言しなくてもcallに応答するオブジェクトを渡せば成立します。GoF側の分類と用語を確認したい場合はGoFのデザインパターンとは?23種類の一覧と代表パターンをコードで解説を先に読むと、以降の話が接続しやすくなります。
したがってRailsで問題になるのは「どのGoFパターンを使うか」ではなく、肥大したモデルやコントローラのどこを、どの単位で切り出すかです。切り出し先の選定基準はRailsのFat Model対策|1500行model.rbを分割する4手法と判断基準で扱っています。本記事は切り出した後の「実装の型」を担当します。
Ruby 3.4でobserverが標準添付から外れた影響
Rubyの標準添付ライブラリは段階的に切り離されています。ruby/rubyのNEWS.mdによると、Ruby 3.4.0でobserver 0.1.2がdefault gemからbundled gemへ移されました。bundled gemはRuby本体と一緒にインストールされますが、Bundlerの管理下では Gemfile に書かれていないgemを読めません。Ruby 4.0.6のBundler配下でrequire "observer"を実行すると、次のように停止します(3行目のファイルパスは環境により異なるため先頭を省略し、以降のバックトレース2行も省略しています)。
obs.rb:1: warning: observer used to be loaded from the standard library, but is not part of the default gems since Ruby 3.4.0.
You can add observer to your Gemfile or gemspec to fix this error.
bundled_gems.rb:60:in 'Kernel.require': cannot load such file -- observer (LoadError)
対処は Gemfile にgem "observer"を追記するだけです。一方、Singletonパターンで使うsingletonはRuby 4.0.6でもdefault gemのままで(gem list -d singletonの出力が「Installed at (default)」)、Gemfileへの追記なしにrequireできます。同じ「標準添付」でも扱いが分かれているため、Ruby 3.3以前で書かれたパターン解説をそのまま持ち込むと、アップグレード時に読み込みエラーとして表面化します。
Interactorパターンの実装:interactor gem 3.2.0のcontextとfail!
Interactorパターンは、アプリケーションが「行うこと」を1クラス1責務で表現する設計です。Railsではcollectiveideaのinteractor gemが事実上の共通実装で、GitHubスター3,460・RubyGems累計ダウンロード約2,668万(いずれも2026年8月19日時点)と、この用途では突出した採用数です。最新版の3.2.0は2025年7月10日公開。前版の3.1.2が2019年12月29日なので、5年半ぶりの更新です。3.2.0の変更はostructへの明示的な依存追加、Contextのパターンマッチ対応、ネストしたcontextから失敗を送出するバグ修正の3点で、APIの互換性は保たれています。
gem "interactor", "~> 3.0"
include Interactorとcontextの基本形
クラスにinclude Interactorしてcallを定義すると、そのクラスはInteractor::Contextを1つ受け取って処理する部品になります。呼び出し側はクラスメソッドのcallを使い、戻り値としてcontextを受け取ります。インスタンスを作ってcallを呼ぶ書き方は、後述する例外の扱いが変わるため使いません。
class SendWelcomeMail
include Interactor
def call
user = User.find(context.user_id)
UserMailer.welcome(user).deliver_later
context.delivered_at = Time.current
end
end
result = SendWelcomeMail.call(user_id: params[:id])
contextへの値の出し入れはセッターとゲッターで行います。3.2.0からはパターンマッチにも対応し、case result in {user_id: Integer => id}のように戻り値を分解できます。contextはto_hでハッシュにもできるため、ログ出力やテストの検証で扱いやすい形になっています。
context.fail!の挙動とコントローラでの分岐
処理を失敗として打ち切るときはcontext.fail!を呼びます。引数にハッシュを渡すと、contextの更新と失敗フラグの設定を同時に行えます。呼び出し側はsuccess?で分岐します。
class ChargePayment
include Interactor
def call
context.fail!(error: "card_declined") if gateway.declined?
context.charge_id = gateway.charge_id
end
end
def create
result = ChargePayment.call(amount: params[:amount])
if result.success?
redirect_to order_path(result.charge_id)
else
flash.now[:alert] = result.error
render :new, status: :unprocessable_entity
end
end
fail!の実体はInteractor::Failure例外の送出です。クラスメソッドのcallがこの例外を捕捉するため、コントローラ側には例外が飛んできません。逆に、テストでインスタンスのcallを直接呼ぶとこの例外がそのまま送出されます。もう1点、afterフックは成功時にしか実行されません。失敗時にも必ず通したい後始末は、afterではなく次に述べるrollbackか、呼び出し側に書きます。
Organizerで複数Interactorを連結したときのロールバック順
複数の処理を順に流すときはInteractor::Organizerを使い、organizeに実行順で並べます。途中で失敗すると、それより前に成功したInteractorのrollbackが逆順で呼ばれます。次のコードはローカルのRuby 4.0.6とinteractor 3.2.0でそのまま実行できます。
require "interactor"
class ReserveStock
include Interactor
def call
context.reservation_id = "RSV-#{context.item_id}"
end
def rollback
puts "rollback: #{context.reservation_id} を解放"
end
end
class ChargePayment
include Interactor
def call
context.fail!(error: "card_declined") if context.amount > 10_000
context.charge_id = "CH-001"
end
end
class PlaceOrder
include Interactor::Organizer
organize ReserveStock, ChargePayment
end
result = PlaceOrder.call(item_id: 7, amount: 50_000)
puts "success?=#{result.success?}"
puts "error=#{result.error}"
puts "reservation_id=#{result.reservation_id}"
実行結果は次のとおりです。
rollback: RSV-7 を解放
success?=false
error=card_declined
reservation_id=RSV-7
読み取っておきたい点が2つあります。1つは、rollbackが呼び出し側へ制御が戻る前に完了していること。もう1つは、rollbackが走った後もcontextのreservation_idが残っていることです。contextは失敗しても巻き戻りません。ログや画面表示で失敗後のcontextを読むときは、「その値が指す実体はすでに取り消されている」前提で扱う必要があります。rollbackはDBのトランザクション境界を持たないため、rollback自体の実行中にプロセスが落ちれば取り消しは途中で止まります。DBの更新を伴う場合は、rollbackメソッドに任せきりにせずOrganizer全体をActiveRecord::Base.transactionで囲みます。
Interactorを採用すべきでない場面
Interactorは万能の受け皿ではありません。次の3条件のいずれかに当てはまるなら、導入しない方がコードは読みやすくなります。
- 単一モデルの属性更新しかしない処理:
user.update(profile_params)で終わる処理をInteractorに包むと、contextという間接層が1つ増えるだけで、テストも呼び出し側も長くなります。 - 戻り値を型付きで返したい処理:contextはOpenStructを継承した動的なオブジェクトで、タイポは実行時まで見つかりません。返す値が決まっているならStructや素のPOROの方が壊れにくくなります。
- 別の結果オブジェクト規約が先に入っている場合:dry-monadsやActiveInteractionと併存させると失敗表現が二重になり、呼び出し側が
success?とSuccess()のどちらを見るべきか判断できなくなります。
いずれも「Interactorが提供する規約より、その規約を維持するコストの方が大きい」ケースです。
逆に採用が効くのは、決済・在庫・通知のように「途中で失敗したら前の処理を取り消す」順序性がある手続きです。この条件を満たさないなら、Interactorではなく素のservice objectで十分です。
Factoryパターン:生成の分岐をcase文から表引きへ
Factoryパターンは、どの具象クラスを作るかの判断を1か所へ集約する設計です。Rubyの解説ではcaseで分岐する実装がよく紹介されますが、種類が増えるたびに条件節を足すことになります。クラスがオブジェクトであることを使い、定数ハッシュの表引きに置き換える方が拡張時の差分が小さくなります。
class BankTransfer
def self.label = "銀行振込"
end
class CreditCard
def self.label = "クレジットカード"
end
class PaymentFactory
HANDLERS = {
"bank_transfer" => BankTransfer,
"credit_card" => CreditCard
}.freeze
def self.build(kind)
HANDLERS.fetch(kind) { raise ArgumentError, "unknown payment: #{kind}" }
end
end
puts PaymentFactory.build("credit_card").label
出力は「クレジットカード」です。fetchにブロックを渡しているので、未知のキーはその場でArgumentErrorになります。[]で引くとnilが返り、エラーが出るのは呼び出しの数行あとになるため、原因の特定が遅れます。パターンそのものの意図や他言語での書き方はファクトリパターンとは?Factoryパターンの仕組み・使いどころとJava/Pythonの実装例で整理しています。
factory_botのfactoryとGoF Factoryの守備範囲の違い
Railsで「factory」と言うと、テストデータ生成のfactory_bot(現行6.6.0、2026年5月4日公開)を指すことが多く、検索結果でも両者が混在します。factory_botのfactoryはテスト用の属性テンプレートで、生成する具象クラスを実行時に選ぶGoFのFactoryとは目的が違います。設計の話をしているのかテストデータの話をしているのかで語の指す対象が変わるため、チーム内のレビューでは「生成の分岐を集約するFactory」と「factory_botのファクトリ定義」を明示的に呼び分けると議論が噛み合います。
Decoratorパターン:SimpleDelegatorとDraper 4.0.6の使い分け
SimpleDelegatorによる委譲とis_a?の落とし穴
表示専用のメソッドをモデルに置くと、ビューの都合がドメインの層に染み出します。Decoratorパターンはこれを外側のオブジェクトへ移す方法で、Rubyでは標準添付のdelegateライブラリにあるSimpleDelegatorだけで書けます。
require "delegate"
User = Struct.new(:family_name, :given_name, :created_at)
class UserDecorator < SimpleDelegator
def full_name
"#{family_name} #{given_name}"
end
def joined_on
created_at.strftime("%Y年%m月%d日")
end
end
user = UserDecorator.new(User.new("山田", "太郎", Time.new(2026, 4, 1)))
puts user.full_name
puts user.joined_on
puts user.family_name
puts "is_a?(User) => #{user.is_a?(User)}"
実行結果は次のとおりです。
山田 太郎
2026年04月01日
山田
is_a?(User) => false
4行目に落とし穴があります。未定義のメソッドは元オブジェクトへ委譲されるのに、is_a?はSimpleDelegator自身が答えるためfalseになります。caseとクラスで分岐している箇所や、instance_of?・kind_of?で型を確かめている既存コードは、デコレータを通した瞬間に分岐が外れます。元のオブジェクトが要る箇所では__getobj__で取り出します。なおto_partial_pathのように元オブジェクトが定義したメソッドは委譲されるため、renderのパーシャル名推論は通ります。
Draper 4.0.6(2025年11月16日公開)は、この委譲に加えてビューヘルパーへのアクセスとコレクションのデコレートを用意したgemです。
Draperを入れる判断基準とRails 8.1系での依存
Rails本体側の依存はactionpack・activemodel・activesupportがいずれも5.0以上(ほかにactivemodel-serializers-xml・request_store・ruby2_keywordsを含む計6件)で、上限指定がないためRails 8.1系にもそのまま入ります。判断基準は単純で、デコレータの中でlink_toやnumber_to_currencyといったビューヘルパーを呼びたいならDraper、日付や氏名の整形だけならSimpleDelegatorで足ります。
プラガブルなオブジェクト:Strategyパターンによる差し替え設計
プラガブルなオブジェクトとは、同じ呼び出し口を持つ部品を実行時に差し替えられる設計を指します。Rubyではインターフェイスの宣言が不要なので、同じメソッド名に応答するクラスを用意し、コンストラクタで受け取るだけで成立します。これがStrategyパターンのRubyでの書き方です。
class FlatRate
def fee(_amount) = 500
end
class RateByAmount
def fee(amount) = amount < 10_000 ? 500 : 0
end
class Checkout
def initialize(shipping: FlatRate.new)
@shipping = shipping
end
def total(amount)
amount + @shipping.fee(amount)
end
end
puts Checkout.new.total(12_000)
puts Checkout.new(shipping: RateByAmount.new).total(12_000)
出力は順に12500、12000です。差し替えの口をキーワード引数の既定値として開けておくと、既存の呼び出しを壊さずに新しい料金体系を後から挿せます。テストでも、送料計算だけを固定値のダブルに置き換えられます。継承でFlatRateCheckoutとRateByAmountCheckoutを作ると組み合わせのたびにクラスが増えますが、差し替えなら送料と決済の2軸が独立したままです。判断としては、変化する軸が2つ以上あるなら継承を選ばず差し替えにします。他言語での実装例はStrategyパターンとは?PHP・Java・TypeScriptの実装例と使いどころにまとめています。
よくある質問
Interactorとは何ですか?
アプリケーションが行う操作を1つだけ担当するオブジェクトです。「注文を確定する」「請求する」といった単位でクラスを作り、入出力をcontextという1つのオブジェクトにまとめてやり取りします。Railsではcollectiveideaのinteractor gemがこの規約を提供しており、include Interactorとcallの定義だけで使えます。
Service ObjectとInteractorはどう違いますか?
service objectは「1クラス1操作」という命名と配置の慣習で、戻り値やエラー表現の決まりはありません。Interactorはそこにcontextという共通の入出力、success?とfail!による成否表現、Organizerによる連結とrollbackという規約を足したものです。複数の処理を順に流して失敗時に巻き戻す必要がなければ、規約のない素のservice objectで十分です。
RubyでObserverパターンを使うには何が必要ですか?
Ruby 3.4.0以降、observerはdefault gemからbundled gemへ移されました。Bundler管理下のアプリでは Gemfile にgem "observer"を書かないとrequire "observer"がLoadErrorになります。Railsアプリでモデルの状態変化を他へ通知したいだけなら、標準添付のObservableを持ち込むよりActiveSupport::Notificationsを使う方が、購読解除やテストの取り回しが楽になります。
factory_botのfactoryとFactoryパターンは同じものですか?
別物です。factory_botのfactoryはテスト用データの属性テンプレートで、生成対象のクラスは定義時に決まっています。Factoryパターンは、生成する具象クラスを実行時の条件で選ぶための設計です。両方が同じ「factory」という語を使うため、レビューでは呼び分けを決めておくと誤解を避けられます。
DraperとSimpleDelegatorのどちらを選ぶべきですか?
デコレータの中でビューヘルパーを呼ぶ必要があるかどうかで決めます。link_toやnumber_to_currencyを使いたいならDraper 4.0.6、日付や氏名の整形だけなら標準添付のSimpleDelegatorで足ります。どちらの場合もis_a?が元クラスに対してfalseを返す点は共通なので、クラスで分岐している既存コードがないか確認してから導入してください。
関連記事
- RailsのFat Model対策|1500行model.rbを分割する4手法と判断基準:Interactorやservice objectへ何を切り出すかの選定基準をまとめた記事
- GoFのデザインパターンとは?23種類の一覧と代表パターンをコードで解説:本記事で扱った4パターンの出典であるGoF分類の全体像
- ファクトリパターンとは?Factoryパターンの仕組み・使いどころとJava/Pythonの実装例:Factoryパターンを他言語の実装で比較した記事
- Strategyパターンとは?PHP・Java・TypeScriptの実装例と使いどころ:差し替え設計を静的型付け言語で書いた場合の違い
- RailsのConcernはアンチパターンか?正しい使い方と乱用を避ける代替設計:パターン適用前に検討すべきconcernの適否を扱った記事