Zenn
📖

フロントでのStrategyパターンの使い方

2025/03/22に公開

最近Reactアプリケーションでユーザーの支払い手段(例:creditpaypaybankなど)によって処理を変える機能を作る時 if 文を多用してるのを見た。

これだとコードがどんどん煩雑になるので、Strategy(ストラテジー)パターンを使って、支払いごとの処理を整理する方法をメモする。


❌ よくある悪い例:条件分岐だらけの関数

まずは、よくある if 文だらけの例。

function pay(method, amount) {
  if (method === 'credit') {
    return fetch('/api/pay/credit', {
      method: 'POST',
      body: JSON.stringify({ amount })
    }).then(res => res.json())
  } else if (method === 'paypay') {
    return fetch('/api/pay/paypay', {
      method: 'POST',
      body: JSON.stringify({ amount })
    }).then(res => res.json())
  } else if (method === 'bank') {
    return fetch('/api/pay/bank', {
      method: 'POST',
      body: JSON.stringify({ amount })
    }).then(res => res.json())
  } else {
    throw new Error('Unknown method')
  }
}

⚠️ 問題点

  • 条件が増えるたびに if が増殖
  • 1つの関数が複数の責務を持ってしまっている
  • テストしづらく、拡張性も低い
  • コードの重複が多く、メンテナンスしにくい

✅ 解決策1:関数として分離する(ただし限界あり)

function payByCredit(amount) {
  return fetch('/api/pay/credit', {
    method: 'POST',
    body: JSON.stringify({ amount })
  }).then(res => res.json())
}

function payByPayPay(amount) {
  return fetch('/api/pay/paypay', {
    method: 'POST',
    body: JSON.stringify({ amount })
  }).then(res => res.json())
}

function payByBank(amount) {
  return fetch('/api/pay/bank', {
    method: 'POST',
    body: JSON.stringify({ amount })
  }).then(res => res.json())
}

function pay(method, amount) {
  switch (method) {
    case 'credit':
      return payByCredit(amount)
    case 'paypay':
      return payByPayPay(amount)
    case 'bank':
      return payByBank(amount)
    default:
      throw new Error('Unknown method')
  }
}

この方法の問題点

デメリット 説明
条件分岐が残る switch のメンテが必要
責任が分散する 「支払いごとの振る舞い」が1つのまとまりとして扱いづらい
拡張性が低い 新しい支払い手段追加時に関数+switch文修正が必要
テストしにくい コンテキストごとにまとめてモックしにくい

✅ 解決策2:ストラテジーパターンを使ったオブジェクト設計

イメージ

+--------------------+
|  PaymentContext    |   ← 支払いの呼び出し側
+--------------------+
| - strategy         |   ← 戦略オブジェクトを保持
| + setStrategy()    |
| + checkout()       |
+---------+----------+
          |
          | calls
          v
+---------------------+
|  PaymentStrategy     |   ← インターフェース / 抽象クラス
+---------------------+
| + pay(amount)        |
+----+--------+--------+
     |        |        
     |        |        
     v        v        
+------------+      +------------------+      +------------------------+
| CreditCard |      |   PayPayPayment  |      |   BankTransferPayment  |
+------------+      +------------------+      +------------------------+
| + pay()    |      | + pay()          |      | + pay()                |
+------------+      +------------------+      +------------------------+

共通の戦略インターフェース

// strategies/PaymentStrategy.js
export class PaymentStrategy {
  async pay(amount) {
    throw new Error('pay() must be implemented')
  }
}

各支払い戦略の実装

// strategies/CreditCardPayment.js
import { PaymentStrategy } from './PaymentStrategy'

export class CreditCardPayment extends PaymentStrategy {
  async pay(amount) {
    const res = await fetch('/api/pay/credit', {
      method: 'POST',
      body: JSON.stringify({ amount })
    })
    return res.json()
  }
}
// strategies/PayPayPayment.js
import { PaymentStrategy } from './PaymentStrategy'

export class PayPayPayment extends PaymentStrategy {
  async pay(amount) {
    const res = await fetch('/api/pay/paypay', {
      method: 'POST',
      body: JSON.stringify({ amount })
    })
    return res.json()
  }
}
// strategies/BankTransferPayment.js
import { PaymentStrategy } from './PaymentStrategy'

export class BankTransferPayment extends PaymentStrategy {
  async pay(amount) {
    const res = await fetch('/api/pay/bank', {
      method: 'POST',
      body: JSON.stringify({ amount })
    })
    return res.json()
  }
}

支払いを管理するコンテキスト

// PaymentContext.js
export class PaymentContext {
  constructor(strategy) {
    this.strategy = strategy
  }

  setStrategy(strategy) {
    this.strategy = strategy
  }

  async checkout(amount) {
    return this.strategy.pay(amount)
  }
}

Reactで使ってみる

// App.jsx
import React, { useState } from 'react'
import { PaymentContext } from './PaymentContext'
import { CreditCardPayment } from './strategies/CreditCardPayment'
import { PayPayPayment } from './strategies/PayPayPayment'
import { BankTransferPayment } from './strategies/BankTransferPayment'

function App() {
  const [paymentContext] = useState(() => new PaymentContext(new CreditCardPayment()))
  const [message, setMessage] = useState('')

  const handleChangeMethod = (method) => {
    switch (method) {
      case 'credit':
        paymentContext.setStrategy(new CreditCardPayment())
        break
      case 'paypay':
        paymentContext.setStrategy(new PayPayPayment())
        break
      case 'bank':
        paymentContext.setStrategy(new BankTransferPayment())
        break
    }
  }

  const handleCheckout = async () => {
    const result = await paymentContext.checkout(3000)
    setMessage(`支払い結果: ${result.status}`)
  }

  return (
    <div>
      <h1>支払いデモ(Strategyパターン)</h1>
      <select onChange={(e) => handleChangeMethod(e.target.value)}>
        <option value="credit">クレジットカード</option>
        <option value="paypay">PayPay</option>
        <option value="bank">銀行振込</option>
      </select>
      <button onClick={handleCheckout}>¥3,000 支払う</button>
      {message && <p>{message}</p>}
    </div>
  )
}

export default App

ストラテジーパターンのメリット

たとえば、新しく コンビニ支払い を追加したい場合、必要なのは以下の2つだけ。

  1. ConvenienceStorePayment クラスを作成
  2. App.jsx に選択肢とstrategyの追加

既存の支払いUIやロジックには一切手を加える必要がない

さらにもし checkout() を他の画面やサービス層でも使いたくなった場合、ストラテジーパターンを使っていなければ、その都度 ifswitch を追加する必要がある。

// 非ストラテジーパターンの例
function payElsewhere(method, amount) {
  if (method === 'credit') {
    return payByCredit(amount)
  } else if (method === 'paypay') {
    return payByPayPay(amount)
  }
  // どんどん増える…
}
// ストラテジーパターンの例
function payElsewhere(paymentContext) {
  return paymentContext.checkout(3000)
}

まとめると

メリット 説明
条件分岐が不要 if / switch の多用を避けられる
責任分離 各支払い手段の処理は独立したクラスに閉じ込められる
拡張しやすい 新しい支払い方法を追加するのが簡単
コンポーネントがスリムに UI 側のコードがシンプルになる
テストがしやすい 各手段の挙動を個別にテストできる

終わりに

ストラテジーパターンを活用することで、Reactアプリの支払い処理をきれいに分離し、保守性・拡張性を大きく向上させることができる。
小さなアプリでは関数分割でも十分かもしれないが、スケーラブルな設計を目指すなら、ストラテジーパターンは非常に有効。

Discussion

ログインするとコメントできます