レベル: 中級 Jack Herrington (jherr@pobox.com), Senior Software Engineer, Leverage Software
2006年 7月 18日 デザイン・パターンは、JavaTM 設計者たちだけのものである - 今までは、このように信じさせられてきたのではないでしょうか。実のところ、デザイン・パターンは誰にとっても役立つものです。では、これらのツールが設計のエリート専用のものでないとしたら、どんなもので、なぜPHP アプリケーションに役立つのでしょうか。そんな疑問に、この記事でお答えしましょう。
デザイン・パターンがソフトウェア・コミュニティーに発表されたのは、「ギャング・オブ・フォー」として知られるErich Gamma、Richard Helm、Ralph Johnson、そして John Vlissides 共著の『DesignPatterns』のなかです。序文で紹介されている、デザイン・パターンの背後にある中心的概念は単純なものです。何年にもわたるソフトウェア開発のなかで、Gammaら 4 人は確かな設計には特定のパターンがあることを発見しました。それは、家やビルを設計する建築家がバスルームの配置やキッチンの構成方法のテンプレートを開発するのとまったく同じことです。そのようなテンプレート、すなわちデザイン・パターンがあれば、建築家はより優れた建造物をより短時間で設計できます。同じことが、ソフトウェアにも当てはまります。
デザイン・パターンは堅固なソフトウェアをより早く開発するための便利な方法であるだけでなく、大規模な構想を親しみやすい言葉に要約する方法にもなります。例えば、疎結合を提供するメッセージング・システムを作成していると言う代わりに、単にパターンの名前、「オブザーバー」を作成していると言うことができます。
ちょっとした例を用いてパターンの価値を説明するのは困難です。パターンは大規模なコードでこそ真価を発揮するため、小さな例では大げさに見えてしまいがちです。この記事では大掛かりなアプリケーションを提示できないため、ここに記載する例の原則(コードそのものというわけではなく) が実際の大きなアプリケーションでどのように適用されるかについて考えてください。小さなアプリケーションでパターンを使用してはならないと言っているわけではありません。優れたアプリケーションのほとんどは小さなものから始まって、それから規模を大きくしていくため、ここで紹介する堅実なコーディング演習から始めない理由は何もありません。
デザイン・パターンとは何であるか、なぜ役立つのかがわかったとろころで、早速PHP V5 に共通の 5 つのパターンを見てみましょう。
ファクトリー・パターン
『Design Patterns』に記載されたデザイン・パターンのほとんどは、疎結合を奨励しています。この概念を理解する最短の道は、多くの開発者が大規模なシステムで経験する苦戦を語ることです。この苦戦は、コードの一部分を変更したことによって、まったく関係のないように思えたシステムの他の部分が次々に破壊されてしまったときに始まります。
問題は密結合にあります。つまり、システムの一部の関数とクラスが、他の部分にある関数とクラスの振る舞いと構造に依存しすぎているということです。これらのクラスが互いに対話できるようにする一連のパターンは必要ですが、クラスが連動してしまうほど密接に結合させるのは理想ではありません。
大規模なシステムでは、多数のコードが少数のキーとなるクラスに依存します。そのためこれらのクラスを変更するとなると、難問が持ち上がります。例えば、ファイルから読み込むUser クラスがあるとします。このクラスをデータベースから読み込む別のクラスに変更したいのですが、すべてのコードはファイルからの読み込みを行う元のクラスを参照しています。そんな場合に役に立つのが、ファクトリー・パターンです。
ファクトリー・パターンは、オブジェクトを生成するメソッドを持つクラスです。newを直接使う代わりに、factory クラスを使ってオブジェクトを生成します。こうすると、factoryを変更するだけで、生成されるオブジェクトのタイプを変更できます。factoryを使用するすべてのコードは自動的に変更されます。
リスト 1 に、factory クラスの例を示します。式のサーバー側は、2 つの部分で構成されています。1つはデータベース、もう 1 つは、フィードの追加、フィード・リストの要求、そして特定のフィードに関連付けられたアーティクルの取得といったことを実行できる一連のPHP ページです。
リスト 1. Factory1.php
<?php
interface IUser
{
function getName();
}
class User implements IUser
{
public function __construct( $id ) { }
public function getName()
{
return "Jack";
}
}
class UserFactory
{
public static function Create( $id )
{
return new User( $id );
}
}
$uo = UserFactory::Create( 1 );
echo( $uo->getName()."\n" );
?>
|
IUser というインターフェースは、ユーザー・オブジェクトの実行内容を定義します。IUserの実装は User と呼ばれ、UserFactory という名前の factory クラスは IUserオブジェクトを生成します。図 1 に、その関係を UML で示します。
図 1. factory クラスと関連 IUser インターフェースおよび User クラス
このコードを PHP インタープリターを使ってコマンドラインで実行すると、その結果は以下のようになります。
% php factory1.php
Jack
% |
このテスト・コードは、ファクトリーに User オブジェクトを要求し、getNameメソッドの結果をプリントします。
ファクトリー・パターンには、ファクトリー・メソッドを使ったバリエーションがあります。このクラスの公開静的メソッドは、特定タイプのオブジェクトを構成します。この方法は、特定タイプのオブジェクトを生成することが重要である場合に役立ちます。例えば、まずオブジェクトを生成してから、多数の属性を設定する必要があるとします。このバージョンのファクトリー・パターンはプロセスを一箇所にカプセル化するため、複雑な初期化コードがコード・ベースの至るところにコピー・ペーストされることがなくなります。
リスト 2 に、ファクトリー・メソッドを使った例を示します。
リスト 2. Factory2.php
<?php
interface IUser
{
function getName();
}
class User implements IUser
{
public static function Load( $id )
{
return new User( $id );
}
public static function Create( )
{
return new User( null );
}
public function __construct( $id ) { }
public function getName()
{
return "Jack";
}
}
$uo = User::Load( 1 );
echo( $uo->getName()."\n" );
?>
|
このコードは一層単純になっています。インターフェースは IUser のみで、このインターフェースを実装するクラスもUser というクラスが 1 つあるだけです。User クラスにはオブジェクトを生成する2 つの静的メソッドがあります。図 2 の UML にその関係を示します。
図 2. ファクトリー・メソッドをもつ IUser インターフェースと User クラス
このスクリプトをコマンドラインで実行すると、以下に示すように、リスト 1のコードと同じ結果になります。
% php factory2.php
Jack
%
|
前にも言ったように、このようなパターンは小さなアプリケーションでは大げさに見えるかもしれません。それでもなお、どんなサイズのプロジェクトで使用するためにも、このような確実なコーディング形式を学んでおくことには意義があります。
シングルトン・パターン
一部のアプリケーション・リソースはそれ自体に 1 つのタイプのリソースだけしかない排他的なものです。例えば、データベース・ハンドルによるデータベースへの接続は排他的です。特に単一ページのフェッチを実行しているときなど、接続を開いたり閉じたりし続けるのは、オーバー・ヘッドになるため、アプリケーション内でデータベース・ハンドルを共有するとよいでしょう。
そんなニーズに対応するのが、シングルトン・パターンです。アプリケーションが一度に1 つしかオブジェクトを含められない場合、そのオブジェクトがシングルトンとなります。リスト3 のコードは、PHP V5 でのデータベース接続のシングルトンを示しています。
リスト 3. Singleton.php
<?php
require_once("DB.php");
class DatabaseConnection
{
public static function get()
{
static $db = null;
if ( $db == null )
$db = new DatabaseConnection();
return $db;
}
private $_handle = null;
private function __construct()
{
$dsn = 'mysql://root:password@localhost/photos';
$this->_handle =& DB::Connect( $dsn, array() );
}
public function handle()
{
return $this->_handle;
}
}
print( "Handle = ".DatabaseConnection::get()->handle()."\n" );
print( "Handle = ".DatabaseConnection::get()->handle()."\n" );
?>
|
このコードには、DatabaseConnection という単一のクラスがあります。コンストラクターはprivate であるため、独自の DatabaseConnection を作成することはできませんが、静的get メソッドを使って唯一の DatabaseConnection オブジェクトを取得できます。図3 に、このコードの UML を示します。
図 3. データベース接続シングルトン
コードの内容が正しい証拠は、handle メソッドによって返されるデータベース・ハンドルが2 つの呼び出しで共通しているということです。コマンドラインでこのコードを実行してみるとわかります。
% php singleton.php
Handle = Object id #3
Handle = Object id #3
%
|
返された 2 つのハンドルは、同じオブジェクトです。アプリケーション全体でデータベース接続シングルトンを使用すれば、同じハンドルをどこででも再使用できます。
グローバル変数を使ってデータベース・ハンドルを格納することもできますが、その方法は小規模なアプリケーションでしかうまくいきません。大規模なアプリケーションではグローバル変数を使わずに、オブジェクトとメソッドを使ってリソースにアクセスしてください。
オブザーバー・パターン
コンポーネント間の密結合を避ける別の手段として、オブザーバー・パターンがあります。このパターンは単純で、一方のオブジェクトがもう一方のオブジェクト(オブザーバー) を登録するメソッドを追加することによって、監視対象となります。監視対象オブジェクトはその変更時に登録オブザーバーにメッセージを送信します。オブザーバーがその情報をどう処理するかは、監視対象オブジェクトにとっては関係なく、重要でもありません。オブジェクトは必ずしも理由を理解することなく、その結果が、オブジェクト同士が対話する方法になります。
単純な例は、システム内のユーザー・リストです。リスト 4 に、ユーザーが追加されたときにメッセージを送信するユーザー・リストを示します。このリストはロギングを行うオブザーバーによって監視され、ユーザーが追加されるとオブザーバーがメッセージを発行します。
リスト 4. Observer.php
<?php
interface IObserver
{
function onChanged( $sender, $args );
}
interface IObservable
{
function addObserver( $observer );
}
class UserList implements IObservable
{
private $_observers = array();
public function addCustomer( $name )
{
foreach( $this->_observers as $obs )
$obs->onChanged( $this, $name );
}
public function addObserver( $observer )
{
$this->_observers []= $observer;
}
}
class UserListLogger implements IObserver
{
public function onChanged( $sender, $args )
{
echo( "'$args' added to user list\n" );
}
}
$ul = new UserList();
$ul->addObserver( new UserListLogger() );
$ul->addCustomer( "Jack" );
?>
; |
このコードは、2 つのインターフェースと 2 つのクラス、合計 4 つの要素を定義しています。IObservableインターフェースは監視可能なオブジェクトを定義し、UserList はこのインターフェースを実装して自身を監視対象として登録します。IObserverリストはオブザーバーとなるための条件を定義し、この IObserver インターフェースはUserListLogger によって実装されます。この関係を図 4 の UML に示します。
図 4. 監視対象ユーザー・リストとユーザー・リスト・イベント・ロガー
これをコマンドラインで実行すると、以下のような出力になります。
% php observer.php
'Jack' added to user list
%
|
このテスト・コードは UserList を作成し、これに UserListLogger オブザーバーを追加します。次に、カスタマーを追加し、この変更がUserListLogger に通知されます。
ここで非常に重要な点は、UserList はロガーが実行する内容を認識しないということです。別のことを行う1 つ以上のリスナーが存在する場合もあります。例えば、新しいユーザーにシステムへの参加を歓迎するメッセージを送信するオブザーバーを持つことができます。この方法の意義は、UserListは UserList に依存するオブジェクトすべてを意識することなく、ジョブに専念できるということ、つまりユーザー・リストを保守し、リストが変更されたときにメッセージを送信できるということにあります。
このパターンはメモリー内のオブジェクトに限ったものではなく、大規模なアプリケーションで使用されるデータベース駆動型メッセージ・キューイング・システムの基盤となります。
コマンド・チェーン・パターン
疎結合をテーマとして構築されたコマンド・チェーン・パターンは、メッセージ、コマンド、要求、あるいは必要なすべてのものを一連のハンドラーによってルーティングします。各ハンドラーは、要求を処理できるかどうかを自己決定します。処理できる場合、要求は処理され、プロセスが停止します。システムに対するハンドラーの追加や削除は、他のハンドラーに影響を与えずに行うことができます。リスト5 に、このパターンの例を示します。
リスト 5. Chain.php
<?php
interface ICommand
{
function onCommand( $name, $args );
}
class CommandChain
{
private $_commands = array();
public function addCommand( $cmd )
{
$this->_commands []= $cmd;
}
public function runCommand( $name, $args )
{
foreach( $this->_commands as $cmd )
{
if ( $cmd->onCommand( $name, $args ) )
return;
}
}
}
class UserCommand implements ICommand
{
public function onCommand( $name, $args )
{
if ( $name != 'addUser' ) return false;
echo( "UserCommand handling 'addUser'\n" );
return true;
}
}
class MailCommand implements ICommand
{
public function onCommand( $name, $args )
{
if ( $name != 'mail' ) return false;
echo( "MailCommand handling 'mail'\n" );
return true;
}
}
$cc = new CommandChain();
$cc->addCommand( new UserCommand() );
$cc->addCommand( new MailCommand() );
$cc->runCommand( 'addUser', null );
$cc->runCommand( 'mail', null );
?> |
このコードは、ICommand オブジェクトのリストを保守する CommandChain クラスを定義しています。ICommandインターフェースを実装するクラスは 2 つあり、1 つはメールの要求に応答し、もう1 つはユーザーの追加に応答します。図 5 に UML を示します。
図 5. コマンド・チェーンと関連コマンド
テスト・コードが含まれるこのスクリプトを実行すると、以下のような出力になります。
% php chain.php
UserCommand handling 'addUser'
MailCommand handling 'mail'
% |
コードはまず CommandChain オブジェクトを生成し、このオブジェクトに 2 つのコマンド・オブジェクトのインスタンスを追加します。次に、2つのコマンドを実行して、何がこれらのコマンドに応答するかを調べます。コマンドの名前がUserCommand または MailCommand のいずれかと一致すると、コードは無視され、何も起こりません。
コマンド・チェーン・パターンは、要求を処理するために拡張可能なアーキテクチャーを作成する場合に重宝し、多くの問題を対処することができます。
ストラテジー・パターン
最後に紹介するデザイン・パターンはストラテジー・パターンです。このパターンでは、複雑なクラスからアルゴリズムを抽出し、アルゴリズムを簡単に交換できるようにします。ストラテジー・パターンは例えば、検索エンジンでのページのランク付け方法を変更する場合に選択できます。検索エンジンについて、次のようないくつかのパーツに分けて考えてみましょう。すべてのページで繰り返すパーツ、各ページをランク付けするパーツ、そしてランクに基づいて結果を並べるパーツです。複雑な例では、これらのパーツのクラスはすべて同じになってしまうでしょう。ストラテジー・パターンを使えば、ランク付けの部分を取り出して別のクラスにすることができるため、検索エンジン・コードの残りの部分に影響を与えることなく、ページのランク付け方法を変更することができます。
わかりやすい例として、リスト 6 にユーザー・リスト・クラスを示します。このクラスは、ストラテジーのプラグ・アンド・プレイ・セットに基づいてユーザーを検索するメソッドを提供するものです。
リスト 6. Strategy.php
<?php
interface IStrategy
{
function filter( $record );
}
class FindAfterStrategy implements IStrategy
{
private $_name;
public function __construct( $name )
{
$this->_name = $name;
}
public function filter( $record )
{
return strcmp( $this->_name, $record ) <= 0;
}
}
class RandomStrategy implements IStrategy
{
public function filter( $record )
{
return rand( 0, 1 ) >= 0.5;
}
}
class UserList
{
private $_list = array();
public function __construct( $names )
{
if ( $names != null )
{
foreach( $names as $name )
{
$this->_list []= $name;
}
}
}
public function add( $name )
{
$this->_list []= $name;
}
public function find( $filter )
{
$recs = array();
foreach( $this->_list as $user )
{
if ( $filter->filter( $user ) )
$recs []= $user;
}
return $recs;
}
}
$ul = new UserList( array( "Andy", "Jack", "Lori", "Megan" ) );
$f1 = $ul->find( new FindAfterStrategy( "J" ) );
print_r( $f1 );
$f2 = $ul->find( new RandomStrategy() );
print_r( $f2 );
?>
|
図 6 に、このコードの UML を示します。
図 6. ユーザー・リストとユーザー選択ストラテジー
UserList クラスは、名前配列のラッパーです。このクラスは、名前のサブセットを選択するストラテジーのうちのいずれかを使用するfind メソッドを実装します。これらのストラテジーは、IStrategy インターフェースで定義されます。このインターフェースには、2つの実装があります。1 つはユーザーを無作為に選択し、もう 1 つは指定された名前の後にあるすべての名前を選択します。このテスト・コードを実行すると、以下のような出力になります。
% php strategy.php
Array
(
[0] => Jack
[1] => Lori
[2] => Megan
)
Array
(
[0] => Andy
[1] => Megan
)
%
|
このテスト・コードは 2 つのストラテジーに対して同じユーザー・リストを実行した結果を示します。最初のストラテジーでは、Jから後にソートされるすべての名前を検索します。その結果、Jack、Lori、Meganが検出されます。次のストラテジーでは名前を無作為に選択するため、結果は毎回違うものになります。上記の結果では、Andyと Megan となっています。
ストラテジー・パターンは、データのフィルタリング、検索、あるいは処理方法に高い柔軟性が要求される、複雑なデータ管理システムやデータ処理システムに最適なパターンです。
まとめ
この記事では、PHP アプリケーションで最も一般的に使わされるデザイン・パターンのうちのいくつかを紹介しました。『DesignPatterns』には、それ以外にも多くのパターンが紹介されています。アーキテクチャーが持つ難解な雰囲気に押し流されないでください。あなたには、どんなプログラミング言語でも、どんな技術レベルでも使うことが6++できるパターンという素晴らしいアイデアがあります。
参考文献 学ぶために
製品や技術を入手するために
議論するために
著者について  | |  | Jack D. Herringtonは、20 年以上の経験を持つシニア・ソフトウェア・エンジニアです。彼の著書には、『Code
Generation in Action』、『Podcasting Hacks』、『PHP Hacks』の 3 冊があります。また、30
を超える記事も執筆しています。 |
記事の評価
|