PHPのDIで動的にオブジェクトを確保する考察

Posted by Hiraku on 2012-12-02

Dependency InjectionがPHPでも流行っているそうです。が、未だによくわからないので、わからないところを自分なりに考察してみます。

※DIコンテナではなくデザインパターンとしてのDIを考えます。

Dependency Injectionとは

Dependency Injectionはデザインパターンの一種です。日本語なら依存性の注入と訳されます。「Inversion of Control コンテナと Dependency Injection パターン」が原典でしょうか。

ざっくり要約すると「クラスの中でnewしてはいけない。必要なインスタンスは外から突っ込むべし」というところかな。

class Y {
  private $x;

  function __construct() {
    $this->x = new X;
  }

  //...$xを使ったコード色々...
}

上記のYクラスではコンストラクタ内でXクラスのインスタンスを生成しています。これをやってしまうと、YクラスはXクラスに強く依存してしまうため、拡張性がダメダメです。

DIに従って「インスタンスを外から突っ込む」ようにすると、こうなります。

class Y {
  private $x;

  function __construct(X $x) {
    $this->x = $x;
  }

  //...$xを使ったコード色々...
}

ぱっと見あまり差はないのですが、こちらの方が便利です。$xはXクラスのインスタンスであれば何でもよい状態になったので、モックに差し替えてテストを行ったり、細かい修正を継承で書いたりと柔軟性が上がります。

//Xクラスを置き換え
class MockX extends X {
  //...
}

$y = new Y(new X);     //通常の使い方
$y = new Y(new MockX); //こうしてもいい

なお、通常の利用時にnew Xするのが面倒なら、デフォルト値を設定することも可能です。

class Y {
  private $x;

  function __construct(X $x=null) {
    $this->x = $x ?: new X;
  }

  //...$xを使ったコード色々...
}

$y = new Y; //こう書けるようになった

ここまで整備すれば、DI適用前と変わらないインターフェースで使えて便利!柔軟性も上がってみんなハッピー!な感じですね。めでたしめでたし。

いや待て、それだけじゃコード書けねえだろ!

というツッコミが頭をよぎりました。

うーん。確かに、DBやLoggerなど、クラスの中で一つだけあれば十分なものも多く、そういうオブジェクトはコンストラクタに渡す形で「外から突っ込む」ことができるでしょう。

しかし、動的にオブジェクトを作るケースだって山ほどあるはずです。「必要なオブジェクトはクラスの生成時に全部できあがっている」なんて状態はそうそうあるもんじゃありません。なんかこう、↓のようなコードを書くことって多いと思うのです。コンストラクタ内で必要な全オブジェクトを生成するなんて無理でしょう。

class C {
  function getConfig($filename) {
    $content = file_get_contents($filename);
    return new Config($content);
  }

  function getConfigs(array $filenames) {
    $configs = array();
    foreach ($filenames as $fn) {
      $content = file_get_contents($filename);
      $configs[] = new Config($content);
    }
    return $configs;
  }
}

DIではあらかじめ想定されうる個数のオブジェクトしか確保できません。「何個必要か実行してみないとわからない」「任意のデータでも対処可能にしたい」こういうケースは困るのではないかと思うわけです。

では、DIの思想を反映させつつ、動的にオブジェクトを取得できるようにするにはどうすればいいでしょうか? 思いつくものを挙げてみます。

factoryをinjectする

new Configというコードを何らかの関数でWrapし、その関数自体をinjectするパターン。

// Factoryをinjectする
class C_a {
    private $configFactory;

    function __construct(callable $factory) {
        $this->configFactory = $factory;
    }

    function getConfig($filename) {
        $content = file_get_contents($filename);
        return call_user_func($this->configFactory, $content);
    }
}

//インスタンス化サンプル
$c = new C_a(function($content){
  return new MockConfig($content);
});
$config = $c->getConfig('hoge.ini'); //MockConfigクラスが返る

サンプルではコールバックできるなら何でもOKにしてみました。これで「依存を外から突っ込んでいる」し、動的にオブジェクトを生成することができるようになりました。

しかし主観ですが、読みにくくなった気がします。コンストラクタで突っ込まれた$configFactoryは、本当にConfigクラスのインスタンスを返してくれるのか、どこにも保証されていません。そして保証することは不可能です。PHPは戻り値のタイプヒントができないからです。インターフェースを定義しようとこれは同じです。

これが動的型付けの特徴と言われると黙るしかないですが、オブジェクト生成するたびにinstanceofで型チェックするというのも面倒くさいし、今の段階だとこのパターンは使いにくい気がします。

メリット

  • 柔軟性はもっとも高い。factory関数は呼ばれる毎にnewしてもいいし、毎回同じインスタンスを返してシングルトン風にすることも可能。「newの抽象化」というレベル。

デメリット

  • 型の保証が取れない。injectされたfactoryが正当なものか検証するすべがない
  • オブジェクト生成のコードがダサい。PHPはプロパティ中のコールバックを呼ぶのに一度変数に代入するか、call_user_func()を使う必要があり、こう書くしかない
  • 全てのオブジェクト生成をfactory関数経由で行うので、パフォーマンス面で不利

クラス名をinjectする

PHPは文字列をクラス名と見なしてnewできるという特性があります。そのため、クラス名さえinjectできればクラスが固定になってしまう状況を回避できます。


// クラス名をinjectする
class C_b {
    private $configClass;

    function __construct($class='Config') {
        if (!is_string($class)) throw new InvalidArgumentException;
        if (!is_a($class, 'Config', true)) throw new InvalidArgumentException;
        $this->configClass = $class;
    }

    function getConfig($filename) {
        $content = file_get_contents($filename);
        return new $this->configClass($content);
    }
}

//インスタンス化サンプル
$c = new C_b('MockConfig');
$config = $c->getConfig('foo.ini');

割とすっきり書けました。オブジェクト生成のコードも、普通にnew Config($content)と書くのとあまり変わらない構造で書けて、まあ読める範囲じゃないでしょうか。

is_a()関数で型のチェックを前もって行うことができるので、factoryと違って型を保証できます。

難点としてはあくまでクラス名しかinjectしないので、newした後にsetHogehoge()を呼ぶようなことはできません。

メリット

  • 型の保証が可能
  • 手軽
  • パフォーマンスも良好
  • 書き方もそれほど複雑ではない

デメリット

  • 「newでインスタンスを生成する」こと自体は固定化される。factoryほどの柔軟性はない
  • 状態を持てないのでnewの後に何か呼ぶ必要があったとすると対応できない。
  • コンストラクタ引数の加工も無理

プロトタイプをinjectする

クラス名のinjectとよく似ているのですが、いったん適当な雛形のインスタンスを生成し、それをinjectしておく方式です。オブジェクトの生成にはclone演算子を使い、雛形からコピーを作ることで対処します。

面倒くさくしただけに見えますが、型チェックが楽に書けるのと、色々設定を行った後の状態をinjectできるので、単にクラス名だけinjectするよりも幅が広がります。

// プロトタイプをinjectする
class C_c {
    private $configPrototype;

    function __construct(Config $prototype) {
        $this->configPrototype = $prototype;
    }

    function getConfig($filename) {
        $content = file_get_contents($filename);
        $config = clone $this->configPrototype;
        $config->setContent($content);
        return $config;
    }
}

//インスタンス化サンプル
$c = new C_c(new MockConfig);
$config = $c->getConfig('foo.ini');

メリット

  • 型の保証が可能。しかもタイプヒントを使った書き方ができるのですっきりする
  • パフォーマンスはクラス名の場合と同等
  • 書き方もそれほど複雑ではない
  • いろいろinjectされた後のオブジェクトをプロトタイプとして使うことも可能。

デメリット

  • 「cloneでインスタンスを生成する」こと自体は固定化される。factoryほどの柔軟性はない
  • 本来必要でない余分なインスタンスを作らなければならない
  • クラスをclone可能にしておかなければならない
  • cloneはコンストラクタのように引数を渡せないため、cloneした後でsetterを呼んで、必要な値をsetしなければならない。setterを意識して作っておく必要があるし、使う方も少し面倒。

まとめ

思いついたやり方はまとめきりましたが、どれも決定打ではない気がするなあ…。適宜使い分けるしかないのか。

PHPは引数だけタイプヒントが使えたり、インターフェースがあったりして少し硬めの書き方ができるのだけど、結局は動的型付けの言語なので、あまり型っぽく書くのは向いていないのかもしれないですね。

関数の戻り値にもタイプヒントが使えるようになれば、factoryが使いやすくなるんですけどね。

他にいいやり方があったら教えてください。

keyword: DI

PHPの最新記事