PHPによるデザインパターン2 FactoryMethod
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; }
今回はこんな感じでした。