はじめに
Pythonでアプリケーションを開発していると、似たような処理フローだけど少しずつ異なる実装が必要なケースに遭遇することがあります。
例えば:
- ユーザー種別によって異なる計算ロジック
- 支払い方法ごとに異なる決済処理
- データ形式に応じた異なる変換処理
このような場合、単純に条件分岐を書いていくと、コードが複雑化し保守性が低下していきます。
この記事では、CommandパターンとFactory Methodパターンを組み合わせることで、
どのように柔軟で保守性の高い実装を実現できるかをPythonのコードで解説します。
このパターンを使うべき状況
以下のような状況で特に有効です:
- 似たような処理フローが複数存在する
- 新しい処理バリエーションが追加される可能性が高い
- 処理の実行者が具体的な実装を知る必要がない
- 処理のパラメータ化や履歴管理が必要
実装例
具体例として、ECサイトでの商品注文処理を実装してみましょう。
支払い方法によって処理が異なる状況を想定します。
from abc import ABC, abstractmethod
from dataclasses import dataclass
from enum import Enum, auto
from typing import Optional
# 支払い方法の列挙型
class PaymentMethod(Enum):
CREDIT_CARD = auto()
BANK_TRANSFER = auto()
# 注文詳細のデータクラス
@dataclass
class OrderDetails:
amount: int
customer_id: str
# 実行結果のデータクラス
@dataclass
class OrderResult:
success: bool
message: str
transaction_id: Optional[str] = None
# 基本となるコマンドの抽象クラス
class OrderCommand(ABC):
def __init__(self, order_details: OrderDetails):
self.order_details = order_details
self._result: Optional[OrderResult] = None
@abstractmethod
def execute(self) -> None:
pass
@property
def result(self) -> Optional[OrderResult]:
return self._result
# クレジットカード決済用のコマンド
class CreditCardOrderCommand(OrderCommand):
def execute(self) -> None:
# クレジットカード決済の実装
print(f"クレジットカードで決済を実行: ¥{self.order_details.amount}")
# 実際の決済処理をここで実装
self._result = OrderResult(
success=True,
message="クレジットカード決済が完了しました",
transaction_id="CC-2024-001"
)
# 銀行振込用のコマンド
class BankTransferOrderCommand(OrderCommand):
def execute(self) -> None:
# 銀行振込用の実装
print(f"銀行振込の受付を実行: ¥{self.order_details.amount}")
# 振込先情報の生成などの処理をここで実装
self._result = OrderResult(
success=True,
message="銀行振込の受付が完了しました",
transaction_id="BT-2024-001"
)
# コマンドのファクトリークラス
class OrderCommandFactory:
@staticmethod
def create_command(payment_method: PaymentMethod, order_details: OrderDetails) -> OrderCommand:
command_map = {
PaymentMethod.CREDIT_CARD: CreditCardOrderCommand,
PaymentMethod.BANK_TRANSFER: BankTransferOrderCommand,
}
command_class = command_map.get(payment_method)
if command_class is None:
raise ValueError(f"Unsupported payment method: {payment_method}")
return command_class(order_details)
# 使用例
def process_order(payment_method: PaymentMethod, order_details: OrderDetails) -> OrderResult:
try:
# コマンドの生成
command = OrderCommandFactory.create_command(payment_method, order_details)
# コマンドの実行
command.execute()
if command.result is None:
raise RuntimeError("Command execution did not produce a result")
return command.result
except Exception as e:
return OrderResult(success=False, message=f"Error processing order: {str(e)}")
# メイン処理
def main():
# 注文情報の作成
order_details = OrderDetails(amount=10000, customer_id="CUST-001")
# クレジットカード決済の実行
print("\n=== クレジットカード決済のテスト ===")
credit_result = process_order(PaymentMethod.CREDIT_CARD, order_details)
print(f"結果: {credit_result}")
# 銀行振込の実行
print("\n=== 銀行振込決済のテスト ===")
bank_result = process_order(PaymentMethod.BANK_TRANSFER, order_details)
print(f"結果: {bank_result}")
if __name__ == "__main__":
main()
パターンのメリット
-
拡張性が高い
- Pythonの動的性を活かしつつ、型ヒントで安全性も確保
- 新しい支払い方法の追加が容易(Open-Closed Principleの遵守)
- ファクトリーのマッピング辞書に追加するだけで対応可能
-
責務の分離が明確
- 各Commandクラスが特定の処理に特化
- データクラスやEnum使用による明確なインターフェース
- 型ヒントによる意図の明確化
-
テストが容易
- 各Commandクラスを個別にテスト可能
- モックやスタブの作成が容易
# テスト例 def test_credit_card_command(): order_details = OrderDetails(amount=1000, customer_id="TEST-001") command = CreditCardOrderCommand(order_details) command.execute() assert command.result is not None assert command.result.success assert command.result.transaction_id is not None
-
エラーハンドリングが統一的
- 例外処理が集中化され、管理が容易
- 型ヒントによるバグの早期発見
実装の拡張例
たとえば、コンビニ決済を追加する場合:
# 支払い方法の列挙型に追加
class PaymentMethod(Enum):
CREDIT_CARD = auto()
BANK_TRANSFER = auto()
CONVENIENCE_STORE = auto() # 追加
# コンビニ決済用のコマンドを実装
class ConvenienceStoreOrderCommand(OrderCommand):
def execute(self) -> None:
print(f"コンビニ決済の受付を実行: ¥{self.order_details.amount}")
# 支払い番号の生成など
payment_number = "12345678" # 実際は適切な番号生成ロジックを実装
self._result = OrderResult(
success=True,
message=f"コンビニ決済の受付が完了しました。支払い番号: {payment_number}",
transaction_id=f"CV-2024-{payment_number}"
)
# ファクトリーのマッピングに追加
class OrderCommandFactory:
@staticmethod
def create_command(payment_method: PaymentMethod, order_details: OrderDetails) -> OrderCommand:
command_map = {
PaymentMethod.CREDIT_CARD: CreditCardOrderCommand,
PaymentMethod.BANK_TRANSFER: BankTransferOrderCommand,
PaymentMethod.CONVENIENCE_STORE: ConvenienceStoreOrderCommand, # 追加
}
command_class = command_map.get(payment_method)
if command_class is None:
raise ValueError(f"Unsupported payment method: {payment_method}")
return command_class(order_details)
まとめ
PythonでのCommandパターンとFactory Methodパターンの組み合わせは、
特に以下の点で効果的です:
- コードの見通しの良さ
- 拡張性の高さ
- テストのしやすさ
- エラーハンドリングの統一性
これらのパターンは、アプリケーションの規模や要件に応じて適切に採用することが重要です。
小規模なプロジェクトでは、かえって複雑になる可能性があるため、
状況に応じて判断することをお勧めします。