浜松のWEBシステム開発・スマートフォンアプリ開発・RTK-GNSS関連の開発はお任せください
株式会社シーポイントラボ
TEL:053-543-9889
営業時間:9:00~18:00(月〜金)
住所:静岡県浜松市中区富塚町1933-1 佐鳴湖パークタウンサウス2F

【PHP】Abstract Factoryパターンで元のコードを触らずに改造しやすいパッケージを作る

 パッケージとして公開したコードは呼ぶ側では直接改変しないのが原則です。composer patchesなどで一部できますが、バージョンアップ時に壊れやすく積極的に行うべきではありません。なるべく引数や用意されたオプションで問題を解決するのが望ましいです。とはいえコードの振る舞い自体を変更しなければならない時がしばしばあります。パッケージを使う側の開発者がそういった際に容易に振る舞いの変更をできる様にするための方法としてパッケージを作る側は AbstractFactory パターンを使えます。

 Abstract Factory パターンは抽象的なインスタンスの生成の仕方を記述するパターンです。これは例えば次の様なコードです。

<?php

/**
 * 以下の要素を組み合わせて扱う場合を考えます。
 */
// ログの表示先とロガーを扱うとします
interface LogDisplay {}
interface Logger {}
/**
 * Abstract Factory(抽象的なファクトリ)用のインターフェースを用意します。
 * このファクトリではファクトリを使う予定のクラス内で行われる全てのインスタンス生成を請け負います。
 */
interface LoggerFactory {
    public function createLogger(): Logger;
    public function createLogDisplay(): LogDisplay;
}

/**
 * デフォルトの実装です。このファクトリを使いまわして、
 * 元のソースコードを書き換えずに好き勝手インスタンス化部分を変更できるようにします。
 */
class BasicLoggerFactory implements LoggerFactory {
    public function createLogger(): Logger{
        return new BasicLogger($this);
    }
    public function createLogDisplay(): LogDisplay{
        return new BasicLogDisplay();
    }
}

/**
 * デフォルトの各要素の実装です
 */
class BasicLogger implements Logger {
    private LogDisplay $display;
    public function __construct(LoggerFactory $factory){
        $this->display = $factory->createLogDisplay();
    }
    public function dumpDisplay(){
        var_dump($this->display);
    }
}
class BasicLogDisplay implements LogDisplay {}

/**
 * 使用例です。
 */
// 普通に用意された物を使う場合
$factory = new BasicLoggerFactory();
$logger = $factory->createLogger();
$logger->dumpDisplay();
// object(BasicLogDisplay)#3 (0) {
// }

// LogDisplayだけ変更したい場合
class BitDiffLoggerFactory extends BasicLoggerFactory {
    public function createLogDisplay(): LogDisplay{
        return new class implements LogDisplay {
            // 自由なその場の実装を行うことができます
        };
    }
}
$factory = new BitDiffLoggerFactory();
$logger = $factory->createLogger();
$logger->dumpDisplay();
// object(LogDisplay@anonymous)#5 (0) {
// }
// ↑の様に Logger クラスを変えずに LogDisplay だけ変更できます。

 Abstract Factory でパッケージ内のインスタンス生成全てを定義する様に強制します。そしてその Abstract Factory のインスタンスをパッケージ内の各コードの中で呼び出して都度インスタンスを生成させます。こうすることによって new XXXX という記述を書き換える必要がなくなり、パッケージを使う側のユーザーがコードをピンポイントで変更してそれを適用できる様になります。

 例では Logger クラス内部で LogDisplay クラスをファクトリ経由でインスタンス化しています。Abstract Factory で作っているため LogDisplay と Factory の変更のみで LogDisplay の差し替えができました。これがもし new でインスタンス化していたならば BasicLogger クラス内に new LogDisplay と書かれることになり、LogDisplay を差し替えるためには Logger も差し替える必要が出てきます。これは次の様になります。

<?php

/**
 * newで直接インスタンスを作成する場合のLogger例
 */
class BasicLogger implements Logger {
    protected LogDisplay $display;
    public function __construct(){
        $this->display = new BasicLogDisplay();
    }
    public function dumpDisplay(){
        var_dump($this->display);
    }
}
$logger = new DirectLogger();
$logger->dumpDisplay();
// object(BasicLogDisplay)#4 (0) {
// }

// ↑で使われているLogDisplayを差し替える場合
// 以下のように新たなLoggerも作成する必要があります。
class CustomLogDisplay implements LogDisplay {
    // 自由な実装を行うことができます
}

class CustomLogger extends BasicLogger {
    protected LogDisplay $display;
    public function __construct(){
        $this->display = new CustomLogDisplay();
    }
}
$logger = new CustomLogger();
$logger->dumpDisplay();
// object(CustomLogDisplay)#6 (0) {
// }

 Abstract Factory パターンを使うことで new の連鎖をショートカットして部分的にコードを適用できます。もしこういったショートカットができないのであれば何か差し替えたいクラスを作った後に、そのクラスを呼ぶクラス、そのクラスを呼ぶクラスを呼ぶクラスと連続して改造しなければいけない部分が増えていきます(一応Abstract Factory抜きでもReflectionでできますが、大分荒業でどこの何に何が依存しているのかわかりにくくなります)。

 このようにAbstract Factory パターンを導入し全てのインスタンス化をそこにまとめることでパッケージの利用者は必要に応じて特定の部分だけをカスタマイズすることが可能となります。これによりパッケージ全体を把握せずインターフェースに沿って問題となる部分だけ、変更したい部分だけを改修できパッケージの規模によっては手間が大きく減ります。

  • この記事いいね! (0)