読者です 読者をやめる 読者になる 読者になる

最小のDIコンテナを使って疎結合にする例

前回のエントリの続き。
この記事では前回のエントリで書いたDIコンテナを使ってクラス間を疎結合にする例を書く。
クラス間を疎結合にすると言っても、クラスの内部で他のクラスに依存しているのではDIコンテナをどう使おうと疎結合にはならない。
以下のコードがそう。Loggerクラス内部にLogWriterクラスへの依存を抱えている。

<?php

// ごくごく単純なロガー
class Logger
{
    protected $writer;
    function __construct()
    {
        // LogWriterクラスがハードコーディングされている!
        $this->writer = new LogWriter;
    }
    function debug($msg)
    {
        $this->writer->write('[debug]' . date('c') . ' '. $msg . PHP_EOL);
    }
    function info($msg)
    { 
        $this->writer->write('[info]' . date('c') . ' '. $msg . PHP_EOL);
    }
}

// 実際にログを吐き出すクラス
class LogWriter
{
    protected $path;
    function __construct($path)
    {
        $this->path = $path;
    }

    function write($msg)
    {   
        // 渡された文字列をファイルに追記する
        file_put_contents($this->path, $msg, FILE_APPEND);
    }
}

ユーザーはLoggerクラスを用いてログを取るわけだが、内部的にはログの字面の整形をLoggerクラスが担当し、ログを実際にどこかに吐く仕事をLogWriterクラスが行っている。
Loggerクラス内部でLogWriterクラスに依存している以上、このままだと何をやっても疎結合にはできないので以下のように書き換える。

<?php

interface ILogwWriter
{
    function write($msg);
}

// ごくごく単純なロガー
class Logger
{
    protected $writer;
    function __construct(ILogWriter $writer)
    {
        // 外部から依存するオブジェクトを受け取る形になっている
        $this->writer = $writer;
    }
    function debug($msg)
    {
        $this->writer->write('[debug]' . date('c') . ' '. $msg . PHP_EOL);
    }
    function info($msg)
    { 
        $this->writer->write('[info]' . date('c') . ' '. $msg . PHP_EOL);
    }
}

// ファイルにログを吐き出すクラス
class FileLogWriter implements ILogWriter
{
    protected $path;
    function __construct($path)
    {
        $this->path = $path;
    }

    function write($msg)
    {   
        // 渡された文字列をファイルに追記する
        file_put_contents($this->path, $msg, FILE_APPEND);
    }
}

これでLoggerクラスはFileLogWriterクラスという具象クラスに依存しなくなり、ILogWriterインターフェイスに依存するようになったのがわかる。
ログをどう吐き出すかという実装をこれで入れ替えることができる。
ためしにILogWriterインターフェイスの別の実装を作ってみる。

<?php

class HTMLLogWriter implements ILogWriter
{
    function write($msg)
    {
        echo '<pre>' . htmlspecialchars($msg, ENT_QUOTES, 'utf-8') . '</pre>';
    }
}

んで下のように使えるようになる。

<?php

// オブジェクトの設定
$logger2 = new Logger(new HTMLLogWriter);

// オブジェクトの利用
$logger2->info('hoge-');
$logger2->debug('fuga-');

ログを吐き出すロジックが交換されているのがわかると思う。
これは典型的なStrategyパターンである。
んで、前のエントリで書いたDIコンテナを使ってオブジェクトの設定を管理する。

<?php

class MyComponenntFactory extends ComponentFactory
{
    function buildLogWriter()
    {
        return new FileLogWriter('./log.txt');
    }
    function buildLogger()
    {
        return new Logger($this->container->get('logWriter'));
    }
}

$container = new DIContainer(new MyComponentFactory);

// コンテナからオブジェクトを取り出す
$logger = $container->get('logger');

// オブジェクトを利用する
$logger->info('hoge-');

これで、オブジェクト間の依存がDIコンテナによって管理され、疎結合となった。
とはいっても、これだけだとあんまり実用的ではないし、まだ何が何だかわからないと思うのでもう少し例を発展させる。
例えば、Loggerインスタンスを内部で使うFooAppクラスがあるとする。

<?php

class FooApp
{
    protected $logger;
    function setLogger(Logger $obj)
    {
        $this->logger = $obj;
    }
    function execute()
    {
        /* 
         * なんらかの処理 。
         * $this->loggerが使われる。
         */
    }
}

Loggerインスタンスを外部から受け取る形になっているのがわかると思う。
んでこれもコンテナで管理するとこうなる。

<?php

class MyComponentFactory extends ComponentFactory
{
    function buildLogWriter()
    {
        return new FileLogWriter('./log.txt');
    }
    function buildLogger()
    {
        return new Logger($this->container->get('logWriter'));
    }
    function buildFooApp()
    {
        $obj = new FooApp;
        $obj->setLogger($this->container->get('logger'));
        return $obj;
    }
}

$container = new DIContainer(new MyComponentFactory);

// コンテナからオブジェクトを取り出す
$fooapp = $container->get('fooApp');

$fooapp->execute();

コンテナが全てのオブジェクト間の依存を解決してくれているのがわかる。
そして管理されるオブジェクトはコンテナに全く依存していない。
ここから例えばログを吐き出すロジックを交換したくなったら、このようにすれば良い。

<?php

class FugaComponentFactory extends MyComponentFactory
{
    function buildLogWriter()
    {
        return new HTMLLogWriter;
    }
}

$container = new DIContainer(new FugaComponentFactory);

$fooapp = $container->get('fooApp');

$fooapp->execute();

オブジェクトの設定と利用がきちんと分離されているので、DIコンテナ側をいじるだけでロジックの交換ができる、と。