ラッパってなに?
プロキシそのものの話に入る前に関連項目としてラッパーの話をします。普通の話過ぎてガッカリかも知れませんが、ちょっと覚悟を決めて読んで欲しいのです。PHPはなんでもかんでも関数一発だったりする部分がありますので、アプリの規模がリロードデバッグでは十分に確認出来ない程度まで複雑度が上がってくると低レベル層との密結合が癌になることが多いのです。故にぺちぱにとって、最も重要なパターンのひとつになります。
<?php
class Blog {
private $_DB = null;
public function __construct ( $db ) {
$this->_DB = $db;
}
public function getContent ( $id ) {
$str = null;
if ( !preg_match ( '#^[-_0-9A-Za-z]$#', $id ) )
throw new Exception ( 'IDやばくね?' . $id );
// 本当はこんなに無防備にパスにアクセスしちゃ駄目
if ( is_file ( '/path/to/file/' . $id ) ){
$str = file_get_contents ( '/path/to/file/' . $id );
}
else {
$res = $this->_DB->executeQuery (
"select * from article where id = '" . $this->_DB->escape ( $id ) . "';"
);
$str = $res['body'];
file_put_contents ( '/path/to/file/' . $id, $str );
}
return $str;
}
}
みたいなコードがあったとします。
もう、これディクレトリ構成とか全部再現して手動でファイル作ったりファイルの中を確認しながら動かさないと動作確認できませんね。まだまだ条件が単純なので良いですが、もっと条件が複雑に絡んできたら動作確認しきれずにリリースして事故る可能性が大になってきます。このサンプルコードでは、まだクラスにまとまっているから良いですが、全部HTMLを返すコードと一緒に書かれているPHP的なPHPならば、もっと厄介になってきます。
そこで以下のようにコードを直すとどうでしょう?
<?php
class Blog {
private $_DB = null;
private $_FileSystem = null;
public function __construct ( $db, $f ) {
$this->_DB = $db;
$this->_FileSystem = $f;
}
public function getContent ( $id ) {
$str = null;
if ( !preg_match ( '#^[-_0-9A-Za-z]$#', $id ) )
throw new Exception ( 'IDやばくね?' . $id );
// 本当はこんなに無防備にパスにアクセスしちゃ駄目
if ( $this->_FileSystem->isFile ( '/path/to/file/' . $id ) ){
$str = $this->_FileSystem->read ( '/path/to/file/' . $id );
}
else {
$res = $this->_DB->executeQuery (
"select * from article where id = '" . $this->_DB->escape ( $id ) . "';"
);
$str = $res['body'];
$this->_FileSystem->write ( '/path/to/file/' . $id, $str );
}
return $str;
}
}
class FileSystem {
public function isFile ( $path ) { return is_file ( $path ); }
public function write ( $path, $str ) { return file_put_contents ( $path, $str ); }
public function read ( $path ) { return file_get_contents ( $path ); }
}
これで、ここで使っているファイル操作についてはラップできました。ここで得られる大きなメリットではコンストラクタで渡す引数の "$f" をダミーに変えることが出来るという点です。また、元からDBはオブジェクトを渡す構造になっているので、いっそ$dbもダミーにしてしまいましょう。
<?php
class Blog {
private $_DB = null;
private $_FileSystem = null;
public function __construct ( $db, $f ) {
$this->_DB = $db;
$this->_FileSystem = $f;
}
public function getContent ( $id ) {
$str = null;
if ( !preg_match ( '#^[-_0-9A-Za-z]$#', $id ) )
throw new Exception ( 'IDやばくね?' . $id );
// 本当はこんなに無防備にパスにアクセスしちゃ駄目
if ( $this->_FileSystem->isFile ( '/path/to/file/' . $id ) ){
$str = $this->_FileSystem->read ( '/path/to/file/' . $id );
}
else {
$res = $this->_DB->executeQuery (
"select * from article where id = '" . $this->_DB->escape ( $id ) . "';"
);
$str = $res['body'];
$this->_FileSystem->write ( '/path/to/file/' . $id, $str );
}
return $str;
}
}
class FileSystem {
public function isFile ( $path ) { return is_file ( $path ); }
public function write ( $path, $str ) { return file_put_contents ( $path, $str ); }
public function read ( $path ) { return file_get_contents ( $path ); }
}
class DummyFileSystem1 extends FileSystem {
public function isFile ( $path ) { return true; }
public function write ( $path, $str ) { throw new Exception ( '呼ばれないはずだよ?' ); }
public function read ( $path ) {
if ( $path != '/path/to/file/1' ) throw new Exception ( '$pathがおかしいよ' );
return 'piyo';
}
}
class DummyFileSystem2 extends FileSystem {
public function isFile ( $path ) { return false; }
public function write ( $path, $str ) {
if ( $str != 'hoge' ) throw new Exception ( '$strがおかしいよ' );
if ( $path != '/path/to/file/1' ) throw new Exception ( '$pathがおかしいよ' );
return true;
}
public function read ( $path ) { throw new Exception ( '呼ばれないはずだよ?' ); }
}
class DBDummy {
public function executeQuery ( $str ) {
if ( $str != "select * from article where id = '1';" )
throw new Exception ( 'SQL文がおかしいよ?' );
return array ( 'body' => 'hoge' );
}
public function escape ( $str ) {
return str_replace ( "'", "''", $str );
}
}
$b1 = new Blog ( new DBDummy(), new DummyFileSystem1() );
$b2 = new Blog ( new DBDummy(), new DummyFileSystem2() );
print ( $b1->getContent ( 1 ) == 'piyo' ? 'ok' : 'ng' ) . "\n";
print ( $b2->getContent ( 1 ) == 'hoge' ? 'ok' : 'ng' ) . "\n";
と、することでDBにも実ファイルにもアクセスすることなくBlogのgetContentメソッド内のコードカバレッジを100%にすることが出来ました。(通常はこういうことをする場合、ダミークラスを定義するのではなくモッキングフレームワークを使います) PHPは低レベル層へ関数一発の気軽なアクセスが出来るのがとても大きな魅力ではありますが、それ故にある程度以上の規模のアプリを組む時には、このラッパという考え方が非常に重要になるのです。是非、ご一考ください。
次回について
引き続きプロキシパターンについてです。プロキシとはラッパのことではありますが「低レベル層のラッパを作りましょう!」ということよりは「複雑な生成をしなければならないオブジェクトを包含して簡単にアクセス出来るプロキシを作りましょう。さらにFlyWeightと組み合わせるとメモリの節約になるよ」というパターンです。次回は、そちらについて説明します。