【PHP デザインパターン実践】Factory Methodを深掘りする – よくある疑問への回答

以前、ZennのサイトでPHPにおけるFactory Method パターンの基本的な概念と実装例を紹介する記事を投稿しました。
PHP デザインパターン学習:Factory Method パターンの実践的な実装と理解

そちらの内容について、とある学習意欲の高いエンジニア仲間から率直で貴重な質問をいただいたので、本記事ではそちらの疑問の解消に焦点を当てつつ、Factory Method パターンの真価についてあらためて考えてみたいと思います。

Factory Method への疑問:分岐の増加問題

寄せられた疑問は以下のようなものです。

Factory Method パターンを使わないコードの場合:

function processOrder($paymentType, $amount) {
    if ($paymentType == 'credit_card') {
        new CreditCardProcessor()->processPayment($amount);
        return;
    }
    // ...他の決済方法
}

Factory Method パターンを使用した場合:

function processOrder($paymentType, $amount) {
    $factory = null;

    switch ($paymentType) {
        case 'credit_card':
            $factory = new CreditCardFactory();
            break;
        // ...他の決済方法
    }

    $factory->pay($amount);
}

質問: processOrder の処理において Factory パターンを適用した場合でも決済方法を追加すれば、switch 内の分岐が増えていくのではないでしょうか? この点を許容できる理由があれば教えてください。

これは非常に鋭い指摘です。確かに、基本的な実装では両者とも新しい決済方法を追加する際に条件分岐が増えていきます。では、Factory Method パターンの本当の価値はどこにあるのでしょうか?

基本実装でのメリット

まず、基本的な実装でも、Factory Method パターンには以下のメリットがあります:

  1. 具体的な実装の詳細からの分離:クライアントコードは決済処理の詳細(初期化方法など)を知る必要がありません。
  2. 変更の局所化:具体的な決済処理クラスの実装が変わっても、影響はファクトリー内に閉じ込められます。
  3. 一貫した生成ロジック:すべての決済処理オブジェクトが同じ方法で生成されることを保証します。

つまり、条件分岐が増える問題は残っていても、コードの品質や保守性は向上しています。

進化した Factory Method パターン

しかし、Factory Method パターンの真価は、より高度な実装によって発揮されます。以下の 2 つの実装方法を見てみましょう。

サンプルの完全版はGithubリポジトリを参照ください。

1. レジストリクラスを利用した実装

use_registry_example.phpを実行

// ファクトリーレジストリクラス
class PaymentFactoryRegistry {
    private static $factories = [];

    // ファクトリーを登録
    public static function register($type, PaymentFactory $factory) {
        self::$factories[$type] = $factory;
    }

    // ファクトリーを取得
    public static function getFactory($type): PaymentFactory {
        if (!isset(self::$factories[$type])) {
            throw new Exception("サポートされていない決済方法: {$type}");
        }
        return self::$factories[$type];
    }
}

// 抽象的なファクトリークラス
abstract class PaymentFactory {
    // ファクトリーメソッド
    abstract public function createProcessor(): PaymentProcessor;

    // テンプレートメソッド
    public function pay($amount) {
        $processor = $this->createProcessor();
        return $processor->processPayment($amount);
    }
}

// 具体的なファクトリークラスの一つ
class CreditCardFactory extends PaymentFactory {
    private $apiKey;

    public function __construct($apiKey = 'default_key') {
        $this->apiKey = $apiKey;
    }

    public function createProcessor(): PaymentProcessor {
        return new CreditCardProcessor($this->apiKey);
    }
}

// アプリケーション起動時に各種決済方法とそれに対応するFactoryクラスを登録する
PaymentFactoryRegistry::register('credit_card', new CreditCardFactory());
PaymentFactoryRegistry::register('paypal', new PayPalFactory());
// ... 他の決済方法も登録

// この関数は決済方法が増えても変更不要!
function processOrderWithRegistry($paymentType, $amount) {
    $factory = PaymentFactoryRegistry::getFactory($paymentType);
    $factory->pay($amount);
}

// 使用例
echo "
決済処理を実行します:
";
processOrderWithRegistry('credit_card', 5000);
processOrderWithRegistry('paypal', 3000);

2. 設定ベースの動的ファクトリー生成

use_provider_example.phpを実行

// 設定ベースの決済ファクトリープロバイダー
class PaymentFactoryProvider {
    private static $config = [
        // デフォルト (設定ファイルで上書き)
        // 'credit_card' => [
        //     'class' => CreditCardFactory::class,
        //     'params' => ['api_key' => 'production_key_123']
        // ],
        // ... 他の決済方法
    ];

    // 設定ファイルを読み込む
    public static function loadConfig($configFile) {
        // JSONファイルから設定を読み込む処理
    }

    // ファクトリーを取得
    public static function getFactory($type): PaymentFactory {
        // 設定に基づいてファクトリーを動的に生成
    }
}

// 抽象的なファクトリークラス
abstract class PaymentFactory {/** 前者と共通のため省略 */}

// 具体的なファクトリークラスの一つ
class CreditCardFactory extends PaymentFactory {/** 前者と共通のため省略 */}

// この関数は決済方法が増えても変更不要!
function processOrderWithProvider($paymentType, $amount) {
    $factory = PaymentFactoryProvider::getFactory($paymentType);
    $factory->pay($amount);
}

// 使用例
echo "
決済処理を実行します:
";
processOrderWithProvider('credit_card', 5000);
processOrderWithProvider('paypal', 3000);

これらの実装では、新しい決済方法を追加する際に processOrder 関数を変更する必要がなくなります。これこそが Factory Method パターンの本当の力です。

発展的な実装の利点

このような高度な実装では、以下の利点があります:

  1. コード変更なしでの拡張: 新しい決済方法を追加するために、既存のコードを変更する必要がありません。
  2. 設定による制御: コードをリリースせずに設定ファイルの変更だけで新しい決済方法を追加できます。
  3. プラグインアーキテクチャの実現: アプリケーションの実行時にプラグインとして新しい決済方法を追加できます。
  4. テスト容易性の向上: テスト用のモックファクトリーを簡単に登録できます。

実務での応用例

実際のプロジェクトでは、レジストリパターンや設定ベースの動的ファクトリー生成を採用することで、条件分岐の増加問題を解決できます。例えば:

  • 新しい決済方法が必要になったとき、設定ファイルを更新するだけで追加できる
  • サードパーティのプラグインとして新しい決済方法を追加できる
  • システム全体の構成を変更せずに、特定の顧客向けにカスタム決済方法を導入できる

まとめ

質問の回答として:

  • 基本的な Factory Method パターンの実装では、確かに決済方法の追加ごとに条件分岐が増えます
  • しかし、より高度な実装(レジストリクラスの採用や設定ベース)を採用することで、この問題を解決できます
  • 基本実装でもオブジェクト生成の抽象化というメリットはありますが、高度な実装によってパターンの真価が発揮されます

実際のプロジェクトでは、アプリケーションの規模や要件に応じて、基本実装と高度な実装を使い分けることが重要です。小規模なアプリケーションでは基本実装で十分かもしれませんが、拡張性が重要な大規模アプリケーションでは高度な実装を検討する価値があります。

Follow me!