PHPによるデザインパターン2 FactoryMethod

デザインパターン勉強会の内容。第二回目。


参考にした書籍は以下。(すでに絶版)
http://www.amazon.co.jp/PHP%E3%81%AB%E3%82%88%E3%82%8B%E3%83%87%E3%82%B6%E3%82%A4%E3%83%B3%E3%83%91%E3%82%BF%E3%83%BC%E3%83%B3%E5%85%A5%E9%96%80-%E4%B8%8B%E5%B2%A1-%E7%A7%80%E5%B9%B8/dp/4798015164/ref=sr_1_1?ie=UTF8&s=books&qid=1265290118&sr=1-1

FactoryMethodパターン

【目的】
使いたいオブジェクトを生成する為の窓口となるAPIを作成する事で、クライアント(呼び出し)側とオブジェクト側を疎結合に保つ。

インスタンスを生成する工場」のようなパターンなので、Factory Method と呼ばれる。

【具体的な実装内容】
「窓口となるクラス」、「実際に使いたいクラスのインターフェース」、「実際使う(インスタンスが返される)クラス」を実装する。

コード例

あるcsvファイルから一行ずつ読み込んで表示するプログラム

<?php

    $fileName = $_POST['fileName'];    // 'sample.csv' が入っているとする

    if (is_readable($fileName)) {
        $handle = fopen($fileName);
    } else {
        die();
    }

    while ($row = fgetcsv($handle, 4096, ',')) {
        echo $row;
 }


上記内容は 「csvファイル」を読み込む事しか想定していないが、例えば渡されるファイル名が xxxx.xml だった場合、パースするという処理も行いたい。
この場合、上記スクリプトを拡張するとしたら…

<?php

    $fileName = $_POST['fileName'];    // 'sample.xml' が入っているとする

    if (is_readable($fileName)) {
        if (false !== strpos($fileName, '.xml')) {    // ファイル名に.xml があったら…の分岐を追加
            $xml = simplexml_load_file($fileName);
        } else {
            $handle = fopen($fileName);
        }
        $handle = fopen($fileName);
    } else {
        die();
    }

    // ここも変えないと…
    while ($row = fgetcsv($handle, 4096, ',')) {
        echo $row;
 }


等と、ベタ書きで処理を追加していくしかない。これでは保守性も落ちるし、バグも内包しやすい。

そこでFactory Method。

使いたいクラスのインターフェースを実装

■reader.class.php

<?php

interface Reader
{
    /** 読み込み */
    public function read();
    /** 表示 */
    public function display();
}

ひとまず読み込みと表示さえ行えれば要求を満たすので、上記インターフェースを設計。

csvファイル対応クラスを実装

■CSVFileReader.class.php

<?php

class CSVFileReader implements Reader
{
    /** コンストラクタ */
    public function __construct($fileName)
    {
        if (!is_readable($fileName)) {
            throw new Exception($fileName . 'is not readable.');
        }
        $this->_fileName = $fileName;
    }

    /** 読み込み */
    public function read()
    {
        $this->_handler = fopen($this->_fileName, 'r');
    }

    /** 表示 */
    public function display()
    {
        while ($row = fgetcsv($this->_handle, 4096, ',')) {
            echo $row;
      }
    }
}

xmlファイル対応クラスを実装

■XMLFileReader .class.php

<?php

class XMLFileReader implements Reader
{
    /** コンストラクタ */
    public function __construct($fileName)
    {
        if (!is_readable($fileName)) {
            throw new Exception($fileName . 'is not readable.');
        }
        $this->_fileName = $fileName;
    }

    /** 読み込み */
    public function read()
    {
        $this->_handler = simplexml_load_file($this->_fileName);
    }

    /** 表示 */
    public function display()
    {
        foreach ($this->_handler->hoge as $hoge) {
            echo $hoge;
            foreach ($hoge->fuga as $fuga) {
                echo $fuga;
            }
        }
    }
}

インスタンス生成API (Factoryクラス) を実装

■ReaderFactory.class.php

<?php

class ReaderFactory
{
    require_once 'Reader.class.php';
    require_once '
CSVFileReader.class.php';

    require_once 'XMLFileReader.class.php'

    /** Readerクラスのインスタンスを生成するAPI */
    public function create($fileName)
    {
        $reader = $this->_createReader($fileName);

        return $reader;
    }

    /** 生成するReaderサブクラスを選定する */
    private function _createReader($fileName)
    {
        $poscsv = stripos($fileName, '.csv');
        $posxml = stripos($fileName, '.xml');

        if (false !== $poscsv) {
            $classPrefix = 'CSV';
        } elseif (false !== $posxml) {
            $classPrefix = 'XML';
        } else {
            throw new Exception($fileName . ' is not supported.');
        }

        return new {$classPrefix}FileReader($fileName);
    }
}

クライアント側コード

<?php

    require_once 'ReaderFactory.class.php';

    $fileName = $_POST['fileName'];

    $factory = new ReaderFactory();
    $obj = $factory->create($fileName);

    $obj->read();
    $obj->display();


【特徴】
クライアント側コードでは、扱いたいReaderクラスを直接 new せず、Factoryクラスのメソッドから生成している。
この事から、FactoryMethodパターンは「VirtualConstructor」とも呼ばれる。

実際扱いたいクラスとクライアントの間にFactoryクラスというレイヤーを一枚噛ませる事で、クラス間の結合をゆるく(疎結合化)できる為、保守性が向上する。

もう一つのOOP的特徴は、ポリモーフィズム多態性)を利用した実装となっている事。
interfaceを設計しサブクラスを実装する事で、クライアント側からは同じメソッドを呼んでいるが、ファイル名によって実際の動作が変わっている。

もっとコードを短く

■ReaderFactory.class.php

<?php

class ReaderFactory
{
    require_once 'Reader.class.php';
    require_once 'CSVFileReader.class.php';
    require_once 'XMLFileReader.class.php';

    /** Readerクラスのインスタンスを生成するAPI */
    public static function create($fileName)
    {
        $poscsv = stripos($fileName, '.csv');
        $posxml = stripos($fileName, '.xml');

        if (false !== $poscsv) {
            $classPrefix = 'CSV';
        } elseif (false !== $posxml) {
            $classPrefix = 'XML';
        } else {
            throw new Exception($fileName . ' is not supported.');
        }

        return new {$classPrefix}FileReader($fileName);
    }

}

クライアント側コード

<?php

    require_once 'ReaderFactory.class.php';

    $fileName = $_POST['fileName'];

    // $factory = new ReaderFactory();
    // $obj = $factory->create($fileName);

    $obj = ReaderFactory::create($fileName);

    $obj->read();
    $obj->display();


staticメソッドに変えてスッキリ(保守性は落ちるかも)!

さらに…

メソッドチェーンにしてもうちょっと短く

■CSVFileReader.class.php

<?php

class CSVFileReader implements Reader
{
    /** コンストラクタ */
    public function __construct($fileName)
    {
        if (!is_readable($fileName)) {
            throw new Exception($fileName . 'is not readable.');
        }
        $this->_fileName = $fileName;
    }

    /** 読み込み */
    public function read()
    {
        $this->_handler = fopen($this->_fileName, 'r');

        return $this;
    }

    /** 表示 */
    public function display()
    {
        while ($row = fgetcsv($this->_handle, 4096, ',')) {
            echo $row;
      }

        return $this;
    }
}

上記のように、各メソッドの返り値にとして自分自身を返す事で、メソッドチェーンが実装できる!

クライアント側コード

<?php

    require_once 'ReaderFactory.class.php';

    $fileName = $_POST['fileName'];

    // $factory = new ReaderFactory();
    // $obj = $factory->create($fileName);

    $obj = ReaderFactory::create($fileName);

    // $obj->read();
    // $obj->display();

    $obj->read()->display();

これで4行のコードが2行に(保守性は(ry)!
メソッドチェーン気持ちいいですよね!

ZendFrameworkでの利用例

■Pagenator.php より抜粋

<?php

    /**
     * Specifies that the factory should try to detect the proper adapter type first
     *
     * @var string
     */
    const INTERNAL_ADAPTER = 'Zend_Paginator_Adapter_Internal';

    /**
     * Factory.
     *
     * @param  mixed $data
     * @param  string $adapter
     * @param  array $prefixPaths
     * @return Zend_Paginator
     */
    public static function factory($data, $adapter = self::INTERNAL_ADAPTER,
                                   array $prefixPaths = null)
    {
        if ($adapter == self::INTERNAL_ADAPTER) {
            if (is_array($data)) {
                $adapter = 'Array';
            } else if ($data instanceof Zend_Db_Table_Select) {
                $adapter = 'DbTableSelect';
            } else if ($data instanceof Zend_Db_Select) {
                $adapter = 'DbSelect';
            } else if ($data instanceof Iterator) {
                $adapter = 'Iterator';
            } else if (is_integer($data)) {
                $adapter = 'Null';
            } else {
                $type = (is_object($data)) ? get_class($data) : gettype($data);
                
                /**
                 * @see Zend_Paginator_Exception
                 */
                require_once 'Zend/Paginator/Exception.php';
                
                throw new Zend_Paginator_Exception('No adapter for type ' . $type);
            }
        }
        
        $pluginLoader = self::getAdapterLoader();
        
        if (null !== $prefixPaths) {
            foreach ($prefixPaths as $prefix => $path) {
                $pluginLoader->addPrefixPath($prefix, $path);
            }
        }
        
        $adapterClassName = $pluginLoader->load($adapter);
        
        return new self(new $adapterClassName($data));
    }

    /**
     * Returns the adapter loader.  If it doesn't exist it's created.
     *
     * @return Zend_Loader_PluginLoader
     */
    public static function getAdapterLoader()
    {
        if (self::$_adapterLoader === null) {
            self::$_adapterLoader = new Zend_Loader_PluginLoader(
                array('Zend_Paginator_Adapter' => 'Zend/Paginator/Adapter')
            );
        }
        
        return self::$_adapterLoader;
    }


今回はこんな感じでした。