📖
フロントでのStrategyパターンの使い方
最近Reactアプリケーションでユーザーの支払い手段(例:credit
、paypay
、bank
など)によって処理を変える機能を作る時 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つだけ。
-
ConvenienceStorePayment
クラスを作成 -
App.jsx
に選択肢とstrategyの追加
既存の支払いUIやロジックには一切手を加える必要がない。
さらにもし checkout()
を他の画面やサービス層でも使いたくなった場合、ストラテジーパターンを使っていなければ、その都度 if
や switch
を追加する必要がある。
// 非ストラテジーパターンの例
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