mybook

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(厳密にはパターンではなくイディオム)から始めるのが適切だ。

Loading diagram...

解決策: 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
end

INFO

コントローラは「どの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 FactoryFactory 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_createfind_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!

ActiveRecordpolymorphic 関連

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的な仕組みだ。

Loading diagram...

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.large

Auto 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パターンは相性がよく、よく組み合わせる