1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonによるCommandパターンとFactory Methodの実践的活用法

Posted at

はじめに

Pythonでアプリケーションを開発していると、似たような処理フローだけど少しずつ異なる実装が必要なケースに遭遇することがあります。
例えば:

  • ユーザー種別によって異なる計算ロジック
  • 支払い方法ごとに異なる決済処理
  • データ形式に応じた異なる変換処理

このような場合、単純に条件分岐を書いていくと、コードが複雑化し保守性が低下していきます。
この記事では、CommandパターンとFactory Methodパターンを組み合わせることで、
どのように柔軟で保守性の高い実装を実現できるかをPythonのコードで解説します。

image.png

このパターンを使うべき状況

以下のような状況で特に有効です:

  1. 似たような処理フローが複数存在する
  2. 新しい処理バリエーションが追加される可能性が高い
  3. 処理の実行者が具体的な実装を知る必要がない
  4. 処理のパラメータ化や履歴管理が必要

実装例

具体例として、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()

パターンのメリット

  1. 拡張性が高い

    • Pythonの動的性を活かしつつ、型ヒントで安全性も確保
    • 新しい支払い方法の追加が容易(Open-Closed Principleの遵守)
    • ファクトリーのマッピング辞書に追加するだけで対応可能
  2. 責務の分離が明確

    • 各Commandクラスが特定の処理に特化
    • データクラスやEnum使用による明確なインターフェース
    • 型ヒントによる意図の明確化
  3. テストが容易

    • 各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
    
  4. エラーハンドリングが統一的

    • 例外処理が集中化され、管理が容易
    • 型ヒントによるバグの早期発見

実装の拡張例

たとえば、コンビニ決済を追加する場合:

# 支払い方法の列挙型に追加
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)

まとめ

image.png

PythonでのCommandパターンとFactory Methodパターンの組み合わせは、
特に以下の点で効果的です:

  1. コードの見通しの良さ
  2. 拡張性の高さ
  3. テストのしやすさ
  4. エラーハンドリングの統一性

これらのパターンは、アプリケーションの規模や要件に応じて適切に採用することが重要です。
小規模なプロジェクトでは、かえって複雑になる可能性があるため、
状況に応じて判断することをお勧めします。

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?