Factory パターン — オブジェクト生成を委譲する
「支払い方法が3つに増えました」
入社から4ヶ月が経ち、ケンタはチームの中で少しずつ存在感を発揮し始めていた。毎朝のスタンドアップでは発言できるようになり、先週からコードレビューのコメントを書くことにも挑戦している。
そんなある月曜日の朝、プロダクトマネージャーの田中さんから新しいタスクが届いた。
「ケンタくん、決済システムを実装してほしいんだけど、クレジットカード、コンビニ払い、銀行振込の3種類に対応してほしい。来月にはPayPayも追加予定。その次の月には楽天ペイも。」
ケンタは少し身構えた。「来月もその次の月も追加される予定があるんですね……」
「そう。あとで追加しやすいように設計してほしい。よろしく!」
田中さんは笑顔で去っていった。ケンタは山田さんに相談することにした。
「それぞれの決済方法で処理が全然違うから、条件分岐で……」とケンタが言いかけると、山田さんは静かに首を振った。「まずどう書こうとしているか、見せて。」
ケンタが書きかけたコードはこうだった。
# app/controllers/payments_controller.rb
class PaymentsController < ApplicationController
def create
order = Order.find(params[:order_id])
method = params[:payment_method]
# またif-else地獄になりそうな予感
if method == "credit_card"
processor = CreditCardProcessor.new(
order: order,
card_token: params[:card_token],
amount: order.total_price
)
elsif method == "convenience_store"
processor = ConvenienceStoreProcessor.new(
order: order,
store_code: params[:store_code],
amount: order.total_price
)
elsif method == "bank_transfer"
processor = BankTransferProcessor.new(
order: order,
bank_code: params[:bank_code],
branch_code: params[:branch_code],
amount: order.total_price
)
end
result = processor.process
render json: result
end
end「またここに来た」と山田さんは言った。「コントローラがオブジェクトの作り方を全部知っている。Factoryパターンを使おう。」
Factory パターンとは
Factory パターンは、オブジェクトの生成を専用のクラスに委譲するパターンだ。
日常の比喩は寿司職人(板前) だ。「マグロをください」と言えば、板前(Factory)が適切な切り方、盛り付けで寿司(Product)を作ってくれる。お客さん(クライアント)は包丁の使い方も、マグロのどこを切るかも知らなくていい。「何が欲しいか」だけを伝えれば、「どうやって作るか」は職人に任せられる。
もう一つの比喩:建設会社 だ。「3LDKの家を建ててください」と言えば、建設会社(Factory)が基礎工事、大工工事、設備工事などを組み合わせて家(Product)を作る。依頼主は各工事の順番や詳細を知らなくていい。
GoFでは「Factory Method」と「Abstract Factory」の2つが定義されているが、実務ではSimple Factory(厳密にはパターンではなくイディオム)から始めるのが適切だ。
解決策: Simple Factory から始める
ステップ1: 共通インターフェースを定義
すべての決済処理が実装すべき「共通の契約」を定義する。これがあることで、コントローラはどの決済方法でも同じインターフェースで扱える。
# app/services/payment_processors/base_processor.rb
module PaymentProcessors
class BaseProcessor
attr_reader :order
def initialize(order:, **options)
@order = order
@options = options
end
# すべてのProcessorが実装しなければならないメソッド
def process
raise NotImplementedError, "#{self.class}#process を実装してください"
end
def cancel
raise NotImplementedError, "#{self.class}#cancel を実装してください"
end
def refund(amount: nil)
raise NotImplementedError, "#{self.class}#refund を実装してください"
end
protected
def amount
order.total_price
end
def log_transaction(action, result)
Rails.logger.info(
"[PaymentProcessor] action=#{action} processor=#{self.class.name} " \
"order_id=#{order.id} amount=#{amount} result=#{result}"
)
end
end
endステップ2: 具体的なProcessorを実装
各決済方法の処理を独立したクラスに封じ込める。
# app/services/payment_processors/credit_card_processor.rb
module PaymentProcessors
class CreditCardProcessor < BaseProcessor
def initialize(order:, card_token:, **options)
super(order: order, **options)
@card_token = card_token
end
def process
charge = Stripe::Charge.create(
amount: amount,
currency: "jpy",
source: @card_token,
metadata: {
order_id: order.id,
user_id: order.user_id
}
)
order.update!(
payment_transaction_id: charge.id,
paid_at: Time.current
)
log_transaction("charge", "success")
{ success: true, transaction_id: charge.id }
rescue Stripe::CardError => e
log_transaction("charge", "failed: #{e.message}")
{ success: false, error: e.message, code: e.code }
rescue Stripe::StripeError => e
ErrorTracker.notify(e, order_id: order.id)
{ success: false, error: "決済処理に失敗しました" }
end
def cancel
return { success: false, error: "取引IDがありません" } unless order.payment_transaction_id
Stripe::Refund.create(charge: order.payment_transaction_id)
{ success: true }
end
def refund(amount: nil)
refund_amount = amount || self.amount
Stripe::Refund.create(
charge: order.payment_transaction_id,
amount: refund_amount
)
{ success: true, refunded_amount: refund_amount }
end
end
end# app/services/payment_processors/convenience_store_processor.rb
module PaymentProcessors
class ConvenienceStoreProcessor < BaseProcessor
STORE_CODES = %w[7eleven familymart lawson].freeze
PAYMENT_EXPIRY_DAYS = 3
def initialize(order:, store_code:, **options)
super(order: order, **options)
@store_code = store_code
end
def process
unless STORE_CODES.include?(@store_code)
return { success: false, error: "対応していないコンビニです" }
end
payment_number = generate_payment_number
order.update!(
payment_number: payment_number,
payment_expires_at: PAYMENT_EXPIRY_DAYS.days.from_now,
payment_store: @store_code
)
log_transaction("issue_number", "success")
{
success: true,
payment_number: payment_number,
expires_at: order.payment_expires_at,
store: @store_code
}
end
def cancel
order.update!(payment_number: nil, payment_expires_at: nil)
{ success: true }
end
def refund(amount: nil)
# コンビニ払いは返金処理が銀行振込になるケースが多い
{ success: false, error: "コンビニ払いの返金は管理画面から手動対応してください" }
end
private
def generate_payment_number
# ハイフン区切りの読みやすい番号を生成
"#{@store_code.upcase}-#{order.id.to_s.rjust(8, '0')}-#{SecureRandom.hex(4).upcase}"
end
end
end# app/services/payment_processors/bank_transfer_processor.rb
module PaymentProcessors
class BankTransferProcessor < BaseProcessor
TRANSFER_EXPIRY_DAYS = 7
VIRTUAL_ACCOUNT_PREFIX = "012"
def initialize(order:, bank_code: nil, branch_code: nil, **options)
super(order: order, **options)
@bank_code = bank_code
@branch_code = branch_code
end
def process
virtual_account = generate_virtual_account
order.update!(
virtual_account_number: virtual_account,
payment_expires_at: TRANSFER_EXPIRY_DAYS.days.from_now
)
log_transaction("issue_virtual_account", "success")
{
success: true,
bank_name: "ジャパンバンク",
branch_code: "001",
account_number: virtual_account,
account_name: "カ)マイショップ",
expires_at: order.payment_expires_at
}
end
def cancel
order.update!(virtual_account_number: nil)
{ success: true }
end
def refund(amount: nil)
{ success: false, error: "銀行振込の返金は振込先口座を確認の上、手動対応してください" }
end
private
def generate_virtual_account
"#{VIRTUAL_ACCOUNT_PREFIX}#{order.id.to_s.rjust(10, '0')}"
end
end
endステップ3: Factoryを作る
各Processorクラスのマッピングを1か所に集める。これがFactoryだ。
# app/services/payment_processors/factory.rb
module PaymentProcessors
class Factory
# 支払い方法と処理クラスのマッピング
PROCESSORS = {
"credit_card" => CreditCardProcessor,
"convenience_store" => ConvenienceStoreProcessor,
"bank_transfer" => BankTransferProcessor,
}.freeze
# メインのファクトリメソッド
def self.create(payment_method:, order:, **options)
processor_class = PROCESSORS[payment_method]
unless processor_class
raise UnknownPaymentMethodError,
"不明な支払い方法: #{payment_method}。" \
"利用可能な方法: #{PROCESSORS.keys.join(', ')}"
end
processor_class.new(order: order, **options)
end
# 利用可能な支払い方法の一覧
def self.available_methods
PROCESSORS.keys
end
# 特定の支払い方法が利用可能かチェック
def self.supported?(payment_method)
PROCESSORS.key?(payment_method)
end
end
class UnknownPaymentMethodError < StandardError; end
endステップ4: コントローラからFactoryを使う
# app/controllers/payments_controller.rb
class PaymentsController < ApplicationController
def create
order = Order.find(params[:order_id])
processor = PaymentProcessors::Factory.create(
payment_method: params[:payment_method],
order: order,
**payment_params
)
result = processor.process
if result[:success]
render json: result, status: :created
else
render json: { error: result[:error] }, status: :unprocessable_entity
end
rescue PaymentProcessors::UnknownPaymentMethodError => e
render json: { error: e.message }, status: :bad_request
end
private
def payment_params
params.permit(:card_token, :store_code, :bank_code, :branch_code)
.to_h
.symbolize_keys
end
endINFO
コントローラは「どのProcessorを使うか」の詳細を知らなくなった。Factoryに「支払い方法」を渡すだけで、適切なProcessorが返ってくる。PayPayが追加されても、コントローラのコードは変わらない。変更箇所は PROCESSORS ハッシュへの1行追加と、新しいProcessorクラスの追加だけだ。
新しい支払い方法の追加が簡単
来月追加予定のPayPayは、既存コードを一切変えずに追加できる。
# app/services/payment_processors/paypay_processor.rb
module PaymentProcessors
class PayPayProcessor < BaseProcessor
def process
response = PayPay::Payment.create(
merchant_payment_id: order.id.to_s,
amount: { amount: amount, currency: "JPY" },
redirect_url: Rails.application.routes.url_helpers.payment_callback_url
)
order.update!(paypay_payment_id: response.body[:result][:merchant_payment_id])
{
success: true,
redirect_url: response.body[:result][:url]
}
rescue PayPay::Error => e
{ success: false, error: e.message }
end
def cancel
PayPay::Payment.cancel(order.paypay_payment_id)
{ success: true }
end
def refund(amount: nil)
PayPay::Payment.refund(
merchant_payment_id: order.paypay_payment_id,
amount: amount || self.amount
)
{ success: true }
end
end
end# PROCESSORS に1行追加するだけ
PROCESSORS = {
"credit_card" => CreditCardProcessor,
"convenience_store" => ConvenienceStoreProcessor,
"bank_transfer" => BankTransferProcessor,
"paypay" => PayPayProcessor, # ← 追加
}.freeze「すごい!新しいProcessorクラスを作って、マッピングに1行追加するだけ!コントローラもOrderモデルも何も変えなくていいんですね!」
Factory Method パターン(GoFの本来の形)
GoFの「Factory Method」は、生成メソッドをサブクラスにオーバーライドさせる形だ。通知送信を例に見てみよう。
# app/services/notifier_service.rb
class NotifierService
# Template Method Pattern と組み合わせた Factory Method
def notify(user, message)
notifier = create_notifier(user) # Factory Method
notifier.send_message(message)
end
private
# サブクラスがオーバーライドする「Factory Method」
def create_notifier(user)
raise NotImplementedError, "create_notifier を実装してください"
end
end
class EmailNotifierService < NotifierService
private
def create_notifier(user)
EmailNotifier.new(
to: user.email,
from: "noreply@myapp.com"
)
end
end
class SmsNotifierService < NotifierService
private
def create_notifier(user)
SmsNotifier.new(
phone_number: user.phone_number,
sender_id: "MyApp"
)
end
end
class LineNotifierService < NotifierService
private
def create_notifier(user)
LineNotifier.new(
line_user_id: user.line_user_id,
access_token: Rails.application.credentials.line[:access_token]
)
end
end
# 使う側
notifier = case user.preferred_channel
when "email" then EmailNotifierService.new
when "sms" then SmsNotifierService.new
when "line" then LineNotifierService.new
end
notifier.notify(user, "注文が完了しました")Simple FactoryとFactory Methodの使い分け:
| Simple Factory | Factory Method |
|---|---|
| 生成ロジックが単純 | 生成に複雑なロジックが必要 |
| 生成クラスが固定的 | 生成をサブクラスで変えたい |
| 早く実装したい | 継承で拡張したい |
| テストが書きやすい | 柔軟性が高い |
FactoryBot との関係
テストでよく使う FactoryBot も「Factoryパターン」の実装だ。名前の通り、Factoryを自動化したgemだ。
# spec/factories/orders.rb
FactoryBot.define do
factory :order do
association :user
status { :pending }
total_price { Faker::Commerce.price(range: 1000..50_000) }
# アイテム付きの注文
trait :with_items do
after(:create) do |order|
create_list(:order_item, 3, order: order)
end
end
# 完了済みの注文
trait :completed do
status { :completed }
completed_at { 1.day.ago }
payment_transaction_id { "ch_#{SecureRandom.hex(16)}" }
end
# キャンセル済みの注文
trait :cancelled do
status { :cancelled }
end
# 高額注文(プレミアム送料無料の境界値テスト用)
trait :high_value do
total_price { 15_000 }
end
end
end
# テストでの使い方
let(:pending_order) { create(:order) }
let(:completed_order) { create(:order, :completed) }
let(:large_order_with_items) { create(:order, :with_items, :high_value) }WARNING
FactoryBotのtraitsを組み合わせすぎると、どんなオブジェクトが生成されるか把握しにくくなる。「FactoryはDBに保存される最小限のデータだけを定義し、テストケースで必要な追加データは明示的に渡す」という方針を持つと、Factory定義が肥大化しにくい。create(:order, total_price: 8000) のように、明示的に上書きする。
Rails標準のFactory的なパターン
Railsには標準でFactory的な機能が組み込まれている。
first_or_create と find_or_initialize_by
# 条件に合うレコードを取得、なければ作成する
user = User.find_or_create_by(email: "test@example.com") do |u|
u.name = "テストユーザー"
u.role = "member"
end
# 保存しない版(バリデーションを先に実行したい場合)
user = User.find_or_initialize_by(email: "test@example.com")
user.name = "テストユーザー" unless user.persisted?
user.save!ActiveRecord の polymorphic 関連
Polymorphicはある意味でFactoryパターンを使っている。notifiable_type に応じて適切なクラスのインスタンスを返す。
# app/models/notification.rb
class Notification < ApplicationRecord
belongs_to :notifiable, polymorphic: true
belongs_to :user
end
# 使う側
comment = Comment.create!(body: "いいね!", post: post)
like = Like.create!(post: post, user: current_user)
# 両方ともNotificationに保存できる
Notification.create!(notifiable: comment, user: post.author)
Notification.create!(notifiable: like, user: post.author)
# 取得時に自動的に正しいクラスでインスタンス化される(Factory的動作)
notification = Notification.find(1)
notification.notifiable # => #<Comment ...> または #<Like ...>polymorphic: true は内部でFactoryパターンを使っており、notifiable_type の値に応じて適切なクラスの find を呼び出している。
AWSでのFactory的発想
AWSのCloudFormationやTerraformはまさにFactory的な仕組みだ。
CloudFormationテンプレートに「どんなリソースが必要か」を定義すると、AWS(Factory)が適切なリソースを生成してくれる。開発環境と本番環境で同じテンプレートから異なるサイズのリソースを生成できる(パラメータを変えるだけ)。
# cloudformation/template.yml
Parameters:
Environment:
Type: String
AllowedValues: [development, staging, production]
Mappings:
# 環境ごとのインスタンスサイズ(Factory的な設定)
InstanceTypeMap:
development:
EC2: t3.micro
RDS: db.t3.micro
production:
EC2: c5.xlarge
RDS: db.r5.largeAuto Scaling Group も同じだ。「このLaunch Templateからインスタンスを作れ」と定義しておくと、需要に応じてEC2インスタンスを自動生成する。Auto Scaling GroupはEC2 Factoryだと言える。
AWS CDK はさらにFactoryパターンに近い。TypeScriptやPythonのコードでAWSリソースを定義する。
// CDKでのFactory的パターン(TypeScript)
class PaymentStack extends cdk.Stack {
constructor(scope: cdk.App, id: string) {
super(scope, id);
// Factoryを呼ぶように、必要なリソースを「宣言」するだけ
const api = new apigateway.RestApi(this, 'PaymentApi');
const lambda = new lambda.Function(this, 'PaymentProcessor', {
runtime: lambda.Runtime.RUBY_3_2,
handler: 'handler.process'
});
}
}テストの書き方
Factoryパターンの各クラスを独立してテストできる。
# spec/services/payment_processors/credit_card_processor_spec.rb
RSpec.describe PaymentProcessors::CreditCardProcessor do
let(:user) { create(:user) }
let(:order) { create(:order, user: user, total_price: 5000) }
let(:processor) { described_class.new(order: order, card_token: "tok_test") }
describe "#process" do
context "正常な決済" do
before do
stub_request(:post, "https://api.stripe.com/v1/charges")
.to_return(
status: 200,
body: { id: "ch_test123", status: "succeeded" }.to_json
)
end
it "orderのpayment_transaction_idを更新する" do
processor.process
expect(order.reload.payment_transaction_id).to eq("ch_test123")
end
it "成功レスポンスを返す" do
result = processor.process
expect(result[:success]).to be true
expect(result[:transaction_id]).to eq("ch_test123")
end
end
context "カードエラー" do
before do
allow(Stripe::Charge).to receive(:create)
.and_raise(Stripe::CardError.new("Your card was declined", nil, code: "card_declined"))
end
it "失敗レスポンスを返す" do
result = processor.process
expect(result[:success]).to be false
expect(result[:error]).to include("declined")
end
end
end
end# spec/services/payment_processors/factory_spec.rb
RSpec.describe PaymentProcessors::Factory do
let(:order) { create(:order) }
describe ".create" do
it "credit_card でCreditCardProcessorを返す" do
processor = described_class.create(
payment_method: "credit_card",
order: order,
card_token: "tok_test"
)
expect(processor).to be_a(PaymentProcessors::CreditCardProcessor)
end
it "不明な支払い方法でエラーを発生させる" do
expect {
described_class.create(payment_method: "bitcoin", order: order)
}.to raise_error(PaymentProcessors::UnknownPaymentMethodError)
end
end
endケンタの気づき
「Factoryパターンって、『オブジェクトの作り方を別の場所に集める』ことなんですね。コントローラは『何を使いたいか』だけを伝えて、『どう作るか』はFactoryが知っている。」
「そう」と山田さんは言った。「生成のロジックと使用のロジックを分離することだ。コントローラは『決済Processorを使う』だけで、『どう作るか』はFactoryが知っている。新しい決済方法が増えても、コントローラは触らない。」
「StrategyパターンとFactoryパターン、組み合わせることが多そうですね。」
「まさに。Strategyパターンの戦略クラスを生成するのがFactoryの役割であることが多い。前章のShippingStrategies::StrategySelectorもFactoryの一種だった。」
ケンタはノートにメモした。
Factoryパターン = オブジェクトの生成を専門クラスに委ねる。クライアントは「何が欲しいか」だけを伝え、「どう作るか」は知らなくていい。StrategyパターンとFactoryパターンはセットで使うことが多い。
INFO
この章のまとめ
- Factoryパターンはオブジェクト生成を専用クラスに委譲する
- クライアントは具体的なクラスを知らず、Factoryに「種類」だけ渡す
- Simple Factory(最も実用的)、Factory Method(継承で拡張)の2種類を状況で使い分ける
- 新しい種類を追加するとき、Factoryのマッピングへの1行追加と新クラスの作成だけで済む
- FactoryBotはFactoryパターンをテストに適用したライブラリ——名前の通り
- RailsのPolymorphicアソシエーションやfind_or_create_byもFactory的な動作をする
- CloudFormation・Auto Scaling GroupなどAWSでも同じ考え方
- StrategyパターンとFactoryパターンは相性がよく、よく組み合わせる