PHPによるデザインパターン入門 - Bridge〜実装と機能の架け橋
このエントリは、Do You PHP?(www.doyouphp.jp)で公開していたコンテンツを移行/加筆/修正したものです。公開の経緯はこちらをどうぞ。目次はこちらです。サンプルコードを手直ししたものをgithubに上げてありますのでそちらもどうぞ。
GoF本における分類
構造+オブジェクト
はじめに
ここではBridgeパターンについて説明します。
「bridge」とは「橋」の意味ですね。橋は川の両岸や島と島など、ある点とある点を結ぶ役割を持っています。
では、Bridgeパターンは、何と何を結ぶ橋なのでしょうか?早速、見てみましょう。
たとえば
何らかの処理を実装する場面を考えてみましょう。当然ですが、「何をするのか」は決まっていますね?しかし、「どうやって実現するのか」が色々考えられる場合があります。
たとえば、データのソート処理はその代表例と言えるでしょう。「ソートをする」という「何をするのか」は分かっていても、「どうやって実装するのか」には、バブルソートやヒープソート、クイックソートなど様々なロジックが存在します。
このような場合、それぞれの実装ごとにクラスを用意してやることが最も単純になるでしょう。しかし、クラスの数があっという間に増えてしまいます。
また、機能を拡張しようとした場合、すべてのクラスを変更する必要がでてきてしまいます。クラスの数が多ければ多いほど、修正に要する作業は大きくなってしまいます。
もうひとつ、何らかのデータソースからデータを読み込んで一覧表示するアプリケーションを考えてみましょう。このアプリケーションの機能、つまり「何をするのか」は、以下のようにまとめられると思います。
- データソースを開く
- データを読み込む
- データを一覧表示する
この程度のアプリケーションであれば、みなさんも簡単に作成してしまうことでしょう。
ここで、データソースへのアクセス方法やデータの取得方法、データの表示形式が色々考えられる場合はどうでしょうか?if文やswitch文を使って処理を分岐させることになるでしょう。しかし、新しい機能の追加や実装の変更があるたび、コードの修正をおこなっていてはテストをやり直す必要がありますし、if文やswitch文も複雑になり、メンテナンスビリティの悪いコードになってしまいます。
これは、「何をするのか」と「どうやって実現するのか」を一緒に考えてしまっているから起こっている問題です。ここで「何をするのか」と「どうやって実現するのか」を分けて考えてみると、機能を拡張する場合は「何をするのか」側を変えることになりますし、実装の方法を変える場合は「どうやって実現するのか」側を変えれば良いことになります。
「何をするのか」と「どうやって実現するのか」分けて考え、これらを結びつけるための橋を用意するパターン、それがBridgeパターンです。
Bridgeパターンとは?
Bridgeパターンはオブジェクトの構造に注目したパターンで、「機能を提供するクラス群」と「実装を提供するクラス群」を分けることを目的としています。
GoF本では、Bridgeパターンの目的は次のように定義されています。
抽出されたクラスと実装を分離して、それらを独立に変更できるようにする。
「分ける」と言っても「インターフェース」と「実装クラス」を分けることを言っているのではなく、「何をするのか」と「どうやって実現するのか」を分けるということです。また、「Bridge」とは「橋」の意味ですが、委譲を使うことで「機能を提供するクラス群」と「実装を提供するクラス群」を橋渡ししているように見えることから名付けられています。
Bridgeパターンの構造
Bridgeパターンのクラス図と構成要素は、次のようになります。
- Abstractionクラス
「何をするのか」を実現するクラス群で最上位に位置するクラスです。内部には、Implementorオブジェクトを保持していて、Implementorクラスが提供する機能をclientに提供します。
- RefinedAbstractionクラス
Abstractionクラスで提供される機能を拡張するサブクラスです。
- Implementorクラス
「どうやってするのか」を実現するクラス群で最上位に位置するクラスです。クラスではなく、インターフェースとして実装される場合もあります。また、Abstractionクラスで提供しているAPIに一致する必要はありません。
- ConcreteImplementorAクラス、ConcreteImplementorBクラス
Implementorクラスを継承したサブクラスです。このクラスに具体的な実装をおこないます。
Bridgeパターンのメリット
Bridgeパターンのメリットとしては、以下のものが挙げられます。
- クラス階層の見通しが良くなる
「機能」と「実装」を提供するクラス群が分けられているので、クラス階層を理解しやすく、見通しが良くなります。つまり、保守性が高くなると言えます。機能と実装が1つのクラスで実装されていると、概してクラス階層が複雑になり、どの部分が機能なのか実装なのかが分かりづらくなるため、保守性が低くなりがちです。
- 最終的に作成すべきクラス数を抑えることができる
継承を使った単純な多態性を使った場合と比べ最終的に作成すべきクラス数を抑えることができます。例として、機能の種類が4つ、実装の種類が3つある場合を考えてみます。継承を使った単純な多態性のみの場合ですと、
親クラス:1 + サブクラス:12(4×3)= 13
のクラスを作成する必要があります。一方、Bridgeパターンを使うと、
親クラス:2 + サブクラス:7(4+3)= 9
のクラスを作成することになります。当然、機能機能や実装が増えるたびに修正しなければならないクラスの数に開きが出てくることは明らかです。
- 機能の拡張と実装の切り替えが容易
「機能」と「実装」を分けることで、お互いに影響することなく拡張や切り替えが可能になります。機能を拡張したい場合、機能を提供するクラス側のみ変更することになります。また、実装を追加したい場合も同様のことが言えます。
Bridgeパターンの適用例
Bridgeパターンの適用例を見てみましょう。
ここでは、データを取得して表示するアプリケーションにBridgeパターンを適用してみます。このアプリケーションはかなり単純ですのでBridgeパターンを適用するほどではありませんが、Bridgeパターンを適用すると設計がスマートになります。
まずは「どうやって実現するのか」側から見ていきましょう。
DataSourceインターフェースは「どうやって実現するのか」側の最上位に位置するクラスで、Implementorクラスに相当します。今回はインターフェースとして実装しています。また、3つのメソッドopen、read、closeが定義されていますが、これは後ほど出てくるListingクラスが利用するAPIになります。
DataSourceインターフェース(DataSource.class.php)
<?php /** * Implementorに相当する * このサンプルでは、インターフェースとして実装 */ interface DataSource { public function open(); public function read(); public function close(); }
続けてConcreteImplementorクラスに相当するFileDataSourceクラスです。DataSourceインターフェースを実装し、定義された3メソッドを具体的に実装しています。今回は名前の通り、データソースとしてファイルを使用しています。このファイル名はコンストラクタの引数として指定するようになっています。
FileDataSourceクラス(FileDataSource.class.php)
<?php require_once 'DataSource.class.php'; /** * Implementorクラスで定義されている機能を実装する * ConcreteImplementorに相当する */ class FileDataSource implements DataSource { /** * ソース名 */ private $source_name; /** * ファイルハンドラ */ private $handler; /** * コンストラクタ * @param $source_name ファイル名 */ function __construct($source_name) { $this->source_name = $source_name; } /** * データソースを開く * @throws Exception */ function open() { if (!is_readable($this->source_name)) { throw new Exception('データソースが見つかりません'); } $this->handler = fopen($this->source_name, 'r'); if (!$this->handler) { throw new Exception('データソースのオープンに失敗しました'); } } /** * データソースからデータを取得する * @return string データ文字列 */ function read() { $buffer = array(); while (!feof($this->handler)) { $buffer[] = fgets($this->handler); } return join($buffer); } /** * データソースを閉じる */ function close() { if (!is_null($this->handler)) { fclose($this->handler); } } }
さて、もう一方の「何をするのか」側も見てみましょう。
Listingクラスは「何をするのか」側の最上位に位置するクラスで、利用者に提供するAPIを定義しています。このAPIは、ImplementorクラスであるDataSourceインターフェースと同じAPIとしています。また、内部にDataSource型のオブジェクトを保持するようになっており、open、read、closeの各メソッドは具体的な処理をこのオブジェクトに委譲しています。
この部分が「何をするのか」と「どうやって実現するのか」を結ぶ「橋」となっています。お分かりでしょうか?
また、このクラスに実装側の具体的なクラス名が出てきていないことを確認してください。つまり、具体的な実装を意識することなく、利用者側に機能のAPIを提供することが可能になっていることが分かります。
Listingクラス(Listing.class.php)
<?php require_once 'DataSource.class.php'; class Listing { private $data_source; /** * コンストラクタ * @param $source_name ファイル名 */ function __construct($data_source) { $this->data_source = $data_source; } /** * データソースを開く */ function open() { $this->data_source->open(); } /** * データソースからデータを取得する * @return array データの配列 */ function read() { return $this->data_source->read(); } /** * データソースを閉じる */ function close() { $this->data_source->close(); } }
次は、Listingクラスの機能を拡張したクラスです。ExtendedListingクラスはListingクラスを継承し、さらに新しい機能のためのメソッドreadWithEncodeが追加されています。
このクラスにも実装側の具体的なクラス名は出てきていませんね。具体的な実装と関係なく、機能が拡張できています。
ExtendedListingクラス(ExtendedListing.class.php)
<?php require_once 'Listing.class.php'; /** * Listingクラスで提供されている機能を拡張する * RefinedAbstractionに相当する */ class ExtendedListing extends Listing { /** * コンストラクタ * @param $source_name ファイル名 */ function __construct($data_source) { parent::__construct($data_source); } /** * データを読み込む際、データ中の特殊文字を変換する * @return 変換されたデータ */ function readWithEncode() { return htmlspecialchars($this->read(), ENT_QUOTES, mb_internal_encoding()); } }
クライアント側のコードも見てみましょう。ここでは、ListingクラスとExtendedListingクラスの両方をインスタンス化しています。コンストラクタの引数に、データの読み込み処理を具体的に実装したクラスFileDataSourceを指定しています。
クライアント側コード(bridge_client.php)
<?php require_once 'Listing.class.php'; require_once 'ExtendedListing.class.php'; require_once 'FileDataSource.class.php'; /** * Listingクラス、ExtendedListingクラスをインスタンス化する。 * 具体的な処理クラスとして、FileDataSourceクラスを使う。 * データファイルは、data.txt */ $list1 = new Listing(new FileDataSource('data.txt')); $list2 = new ExtendedListing(new FileDataSource('data.txt')); try { $list1->open(); $list2->open(); } catch (Exception $e) { die($e->getMessage()); } /** * 取得したデータの表示(readメソッド) */ $data = $list1->read(); echo $data; /** * 取得したデータの表示(readWithEncodeメソッド) */ $data = $list2->readWithEncode(); echo $data; $list1->close(); $list2->close();
今回サンプルとして用意したデータファイルは次のようなものです。
Bridgeパターンのオブジェクト指向的要素
Bridgeパターンは「ポリモーフィズム」と「委譲」を非常に活用しているパターンです。
機能を提供するクラス側では、まずクライアント側に提供する「機能のAPI」をクラスで定義し、そのクラスを継承することで機能の拡張を実現します。一方、実装を提供するクラス側でも同様に、機能側のクラスに提供する「実装のAPI」をインターフェースもしくはクラスで定義し、それらを実装もしくは継承することで異なる実装を実現しています。ここまでの内容を見てみると、機能を提供するクラス群と実装を提供するクラス群はそれぞれTemplate Methodパターンになる場合があります。お分かりでしょうか?
では、ここからが本番です。クライアントに提供する具体的な処理の実装を、実装を提供するクラス群に委譲している、つまり、実装を提供するクラス群の最上層で定義されている「実装のAPI」に基づいたクラスに任せています。ここがBridgeパターンの神髄ともいうべきところで、「Bridge」と名付けられた所以です。この委譲を使うことにより、実装側のクラスにある具体的な処理内容を意識することがなくなります。この「意識することがなくなる」ため、処理を切り替える場合も機能側に属するクラスを一切変更する必要がなくなります。また、あとで新しい実装を追加したりする事が可能になります。
また、Bridgeパターンでは、クライアント・機能側のクラスとの間にある「機能のAPI」と、機能側のクラス・実装側のクラスとの間にある「実装のAPI」で、ポリモーフィズムを二度使っている、という見方もできますね。
関連するパターン
- Abstract Factoryパターン
ConcreteImplementorを適切に構築するために使われる場合があります。
- Adapterパターン
Adapterパターンの構造と比較するとよく分かりますが、BridgeパターンはAdapterパターンと非常によく似ています。また、本質的にも変わりありません。
ただし、Adapterパターンのように「既存クラスを再利用するために繋ぎ合わせる」といった後天的な理由ではなく、「設計の段階で実装と機能を分離し、それぞれを繋ぎ合わせる」といった先天的な理由で導入されます。その場合、実装の変更が考えられる処理については、実装のクラス階層に処理を委譲するようにします。
まとめ
ここでは「機能」のクラス階層と「実装」のクラス階層を橋のように結ぶBridgeパターンについて見てきました。
PHPによるデザインパターン入門 - Chain of Responsibility〜処理のたらい回し
このエントリは、Do You PHP?(www.doyouphp.jp)で公開していたコンテンツを移行/加筆/修正したものです。公開の経緯はこちらをどうぞ。目次はこちらです。サンプルコードを手直ししたものをgithubに上げてありますのでそちらもどうぞ。
GoF本における分類
振る舞い+オブジェクト
はじめに
ここではChain of Responsibilityパターンについて説明します。
「Chain of Responsibility」とは長い名前ですね。直訳すると、「責任の鎖」となるでしょうか。
それぞれのクラスは「責任」を持っています。その責任を明確にするよう設計をおこなうことが、オブジェクト指向設計では大きなポイントとなります。
Chain of Responsibilityパターンは、自分の責任で対処する必要かどうかを判断し、自分で対処できない場合は他に任せてしまうパターンです。まるで、責任のたらい回しみたいですね。
では、早速見ていきましょう。
たとえば
条件によっておこなう処理を分岐させることはよくあることですね。たいていの場合、if文やswitch文を使って「この条件の場合はこう処理する」というコードを記述することになります。
ここで、入力された文字列を検証する場合を考えてみましょう。
入力値の検証には、文字列の長さのチェックやあるフォーマットに合っているかどうかのチェックがありますね。
一般的な検証処理の流れとしては、入力された文字列が所定のパターンにマッチするかどうかを判定し、マッチしない場合は適切なメッセージを表示する、というものになるかと思います。
この処理をif文を使って実装する場合、マッチングをおこなうパターンの数だけif文が繋がることになるでしょう。しかし、パターンの数が多い場合やマッチングの条件が複雑な場合、コードの見通しが非常に悪くなってしまいがちです。また、コードの再利用がしにくい状態になります。
if文やswitch文を使った条件分岐は、「どの場合にどう処理をすべきか」が全て1カ所にまとめられることになります。つまり、条件とそれに対応する処理の組を知っておく必要がある、ということです。このため、条件が複雑になったり分岐の数が多くなればなるほど、「知っておかなければならないこと」が増えてしまいます。
この場合、分岐の条件とそれに対応する処理の組ごとに分解できると、組ごとにその条件や処理内容に集中することができます。その結果、コードの見通しも良くなり、保守性や再利用性を高めることができそうです。
しかし、条件と処理の組に分解した場合、それらをどうやって組み立てて利用するかが問題になってしまいますね。分解してしまった分、扱いが大変になってしまうと分解した意味がありません。
こうした問題を解決するためのパターンとして、Chain of Responsibilityパターンがあります。
Chain of Responsibilityパターンとは?
Chain of Responsibilityパターンはオブジェクトの振る舞いに注目したパターンで、「処理を依頼する側」と「実際に処理をおこなう側」を分離することを目的としています。
GoF本では、Chain of Responsibilityパターンの目的は次のように定義されています。
1つ以上のオブジェクトに要求を処理する機会を与えることにより、要求を送信するオブジェクトと受信するオブジェクトの結合を避ける。受信する複数のオブジェクトをチェーン状につなぎ、あるオブジェクトがその要求を処理するまで、そのチェーンに沿って要求を渡していく。
まず、Chain of Responsibilityパターンの特徴である処理をおこなう「オブジェクトのチェーン」について説明しましょう。
このオブジェクトは、処理をおこなうための共通のAPIを持ち、それぞれ異なる処理を実装しています。この処理はif文などを使った場合に記述する「この条件の場合におこなう処理」になります。
また、内部に別の処理をおこなうオブジェクトを保持していて、自分が処理できないと判断した場合、そのオブジェクトに処理をお願いします。何だかバケツリレーに似ていますね。「要求」が入ったバケツを受け取り、自分自身が処理できない場合、次の人に渡して処理をお願いする、といった感じです。
一方、クライアント側は、処理オブジェクトのチェーンに要求を送信するだけです。あとは、その要求が処理オブジェクトのチェーンを伝わっていき、適切に処理可能なオブジェクトが処理を行います。これが、「実際に処理を実行するオブジェクトを動的に決定する」ということです。
では、どの処理オブジェクトも処理できないことはないのでしょうか?
残念ながら、処理できない場合も当然あります。これはチェーンが正しく作成されていない場合も同様で、チェーンの終端で要求が消滅してしまう、といったことも起こり得ます。つまり、処理オブジェクトのチェーンで必ず処理されるわけではない事に、注意が必要です。
Chain of Responsibilityパターンの構造
Chain of Responsibilityパターンのクラス図と構成要素は、次のとおりです。
- Handlerクラス
処理オブジェクトの親クラスに相当し、要求を処理するためのAPIを定義します。これは、サブクラスで具体的な処理が実装されます。
また、内部にHandler型のオブジェクトを保持します。自分が処理できなかった場合、このHandler型のオブジェクトに処理をお願いすることになります。
- ConcreteHandlerクラス
Handlerクラスのサブクラスです。処理オブジェクトの実クラスに相当します。このクラスは、Handlerクラスで定義されたAPIを実装します。なお、自分が担当する処理だけが実装されます。
- Clientクラス
チェーンを構成しているConcreteHandlerクラスに処理を送信します。
Chain of Responsibilityパターンのメリット
Chain of Responsibilityパターンのメリットとしては、以下のものが挙げられます。
- 要求の送信側と受信側の結びつきをゆるくする
Chain of Responsibilityパターンでは、要求の送信側(Clientクラス)で「どのオブジェクトに処理を行わせるか」ということを意識する必要がありません。「要求が適切に処理される」という事だけを知っていれば良いことになります。
結果として、Chain of Responsibilityパターンは、オブジェクトどうしの結びつきを緩めることができます。
- 新しい処理クラスを簡単に追加できる
要求の送信側と受信側の結びつきがゆるくなるため、新しい処理クラス(ConcreteHandlerクラス)を追加するのが非常に簡単です。
- 動的に処理チェーンを変更できる
処理オブジェクトのチェーンは、オブジェクトを繋げたものです。つまり、継承関係のようにプログラミング時に関係が決まるような静的な関連はありません。また、すべての処理オブジェクトは、具体的にはHandler型のオブジェクトです。このため、実行時にオブジェクトを抜き差しすることで、チェーンを動的に変更できます。
たとえば、ユーザ操作によって処理を変更する場合でも、処理オブジェクトを組み替えたり、追加したりできます。
Chain of Responsibilityパターンの適用例
Chain of Responsibilityパターンの適用例を見てみましょう。
ここでは、先に出てきた入力された文字列の検証にChain of Responsibilityパターンを適用した例になります。
まずは「条件とそれに対応する処理の組」に関連するクラスから見ていきます。
ValidationHandlerクラスはHandlerクラスに相当するクラスです。ここでは、抽象クラスとして定義しています。
また、実際の検証処理をおこなうexecValidationメソッドとエラーメッセージを取り出すgetErrorMessageメソッドは抽象メソッドになっています。この2つのメソッドが「条件判断」と「対応する処理」をおこなうメソッドになっており、このValidationHandlerクラスを継承したクラスで具体的な実装をおこなうことになります。
ValidationHandlerクラス(ValidationHandler.class.php)
<?php /** * Handlerクラスに相当する */ abstract class ValidationHandler { private $next_handler; public function __construct() { $this->next_handler = null; } public function setHandler(ValidationHandler $handler) { $this->next_handler = $handler; return $this; } public function getNextHandler() { return $this->next_handler; } /** * チェーンの実行 */ public function validate($input) { $result = $this->execValidation($input); if (!$result) { return $this->getErrorMessage(); } elseif (!is_null($this->getNextHandler())) { return $this->getNextHandler()->validate($input); } else { return true; } } /** * 自クラスが担当する処理を実行 */ protected abstract function execValidation($input); /** * 処理失敗時のメッセージを取得する */ protected abstract function getErrorMessage(); }
このクラスで注目するのはvalidateメソッドです。このメソッドが、処理オブジェクトの鎖にクライアントからの要求を流す役割を果たします。
具体的には、execValidationメソッドを呼び出して実際に処理をおこない、処理できたかどうかを判断します。成功した場合は、内部に保持したValidationHandlerオブジェクトを取り出し、次の検証処理をおこないます。失敗した場合は、エラーメッセージをクライアントに返します。
最終的に全てのValidationHandlerオブジェクトで検証をおこない、全ての処理に成功した場合はtrueを返します。
次にValidationHandlerクラスを継承したクラスたちを見ていきましょう。ここでは検証のパターンとして4つほど用意しています。
まず、AlphabetValidationHandlerクラスとNumberValidationHandlerクラスです。このクラスたちは、名前の通り入力された文字列がアルファベット、もしくは数字だけで構成されているかどうかを検証します。指定された文字以外で構成されている場合、検証失敗となります。処理の詳細は、それぞれのクラスのexecValidationメソッドとgetErrorMessageメソッドを確認してください。
AlphabetValidationHandlerクラス(AlphabetValidationHandler.class.php)
<?php require_once 'ValidationHandler.class.php'; /** * ConcreteHandlerクラスに相当する */ class AlphabetValidationHandler extends ValidationHandler { /** * 自クラスが担当する処理を実行 */ protected function execValidation($input) { return preg_match('/^[a-z]*$/i', $input); } /** * 処理失敗時のメッセージを取得する */ protected function getErrorMessage() { return '半角英字で入力してください'; } }
NumberValidationHandlerクラス(NumberValidationHandler.class.php)
<?php require_once 'ValidationHandler.class.php'; /** * ConcreteHandlerクラスに相当する */ class NumberValidationHandler extends ValidationHandler { /** * 自クラスが担当する処理を実行 */ protected function execValidation($input) { return (preg_match('/^[0-9]*$/', $input) > 0); } /** * 処理失敗時のメッセージを取得する */ protected function getErrorMessage() { return '半角数字で入力してください'; } }
NotNullValidationHandlerクラスは、入力された文字列が空文字でないかどうかを検証します。空文字の場合、検証失敗となります。
NotNullValidationHandlerクラス(NotNullValidationHandler.class.php)
<?php require_once 'ValidationHandler.class.php'; /** * ConcreteHandlerクラスに相当する */ class NotNullValidationHandler extends ValidationHandler { /** * 自クラスが担当する処理を実行 */ protected function execValidation($input) { return (is_string($input) && $input !== ''); } /** * 処理失敗時のメッセージを取得する */ protected function getErrorMessage() { return '入力されていません'; } }
ValidationHandlerクラスのサブクラスの最後はMaxLengthValidationHandlerクラスです。このクラスは、入力された文字列の長さが指定された長さ以下かどうかを検証します。この長さの指定は、コンストラクタでおこなっています。
MaxLengthValidationHandlerクラス(MaxLengthValidationHandler.class.php)
<?php require_once 'ValidationHandler.class.php'; /** * ConcreteHandlerクラスに相当する */ class MaxLengthValidationHandler extends ValidationHandler { private $max_length; public function __construct($max_length = 10) { parent::__construct(); if (preg_match('/^[0-9]{,2}$/', $max_length)) { throw new RuntimeException('max length is invalid (0-99) !'); } $this->max_length = (int)$max_length; } /** * 自クラスが担当する処理を実行 */ protected function execValidation($input) { return (strlen($input) <= $this->max_length); } /** * 処理失敗時のメッセージを取得する */ protected function getErrorMessage() { return $this->max_length . 'バイト以内で入力してください'; } }
そして、検証のクラス群を利用するクライアント側のコードです。動作を簡単に確認できるよう、入力用のHTMLフォームも表示します。
また、検証を実行するコードがValidationHandler型オブジェクトのvalidateメソッドを呼び出すだけになっていることを確認してください。if文で実装する場合と比べて、非常に簡単なコードになっていますね。
クライアント側コード(chain_of_responsibility_client.php)
<?php require_once 'MaxLengthValidationHandler.class.php'; require_once 'NotNullValidationHandler.class.php'; if (isset($_POST['validate_type']) && isset($_POST['input'])) { $validate_type = $_POST['validate_type']; $input = $_POST['input']; /** * チェーンの作成 * validate_typeの値によってチェーンを動的に変更 */ $not_null_handler = new NotNullValidationHandler(); $length_handler = new MaxLengthValidationHandler(8); $option_handler = null; switch ($validate_type) { case 1: include_once 'AlphabetValidationHandler.class.php'; $option_handler = new AlphabetValidationHandler(); break; case 2: include_once 'NumberValidationHandler.class.php'; $option_handler = new NumberValidationHandler(); break; } if (!is_null($option_handler)) { $length_handler->setHandler($option_handler); } $handler = $not_null_handler->setHandler($length_handler); /** * 処理実行と結果メッセージの表示 */ $result = $handler->validate($_POST['input']); if ($result === false) { echo '検証できませんでした'; } elseif (is_string($result) && $result !== '') { echo '<p style="color: #dd0000;">' . $result . '</p>'; } else { echo '<p style="color: #008800;">OK</p>'; } } ?> <form action="" method="post"> <div> 値:<input type="text" name="input"> </div> <div> 検証内容:<select name="validate_type"> <option value="0">任意</option> <option value="1">半角英字で入力されているか</option> <option value="2">半角数字で入力されているか</option> </select> </div> <div> <input type="submit"> </div> </form>
入力フォームのプルダウンで検証する内容を選択できるようになっていますが、これによってAlphabetValidationHandlerオブジェクトもしくはNumberValidationHandlerオブジェクトが生成され、検証オブジェクトの鎖に追加されます。
お分かりのように、検証のための処理を動的に追加しています。処理チェーンを動的に変更することができるのは、Chain of Responsibilityパターンの大きな特徴です。また、新しい検証クラスを作成し追加する場合も容易に対応できることが分かると思います。
最後に、Chain of Responsibilityパターンを適用したサンプルアプリケーションのクラス図を示します。
適用例の実行結果
Chain of Responsibilityパターンを適用したサンプルの実行結果ですが、文字列の検証に成功した場合は次のようになります。
一方、文字列の検証に失敗した場合は次のとおりです。
Chain of Responsibilityパターンのオブジェクト指向的要素
Chain of Responsibilityパターンは「ポリモーフィズム」を活用したパターンです。
Chain of Responsibilityパターンの特徴は、処理オブジェクトのチェーンです。つまり、Handler型のオブジェクトのチェーンです。このチェーンは、Handlerクラスの内部に保持されたHandler型のオブジェクトです。実際には、HandlerクラスのサブクラスであるConcreteHandlerクラスのインスタンスですが、Handlerクラス自身からはこのオブジェクトが具体的にどのクラスなのかは意識していません。ただ、Handler型のオブジェクトであるというだけです。
つまり、内部に保持されたオブジェクトは、具体的にどのようなクラスであれ、Handler型のオブジェクト、言い換えると、Handlerクラスのサブクラスのインスタンスであれば、問題なく動作するということになります。
この結果、チェーンの組み替えや、新しいConcreteHandlerクラスを追加したりできるのです。
関連するパターン
- Compositeパターン
CompositeパターンはChain of Responsibilityパターンと併用される場合があります。
まとめ
ここでは、オブジェクトを鎖のように繋いで問題を対処するChain of Responsibilityパターンを見てきました。
PHPによるデザインパターン入門 - Command〜要求をクラスで表す
このエントリは、Do You PHP?(www.doyouphp.jp)で公開していたコンテンツを移行/加筆/修正したものです。公開の経緯はこちらをどうぞ。目次はこちらです。サンプルコードを手直ししたものをgithubに上げてありますのでそちらもどうぞ。
GoF本における分類
振る舞い+オブジェクト
はじめに
ここではCommandパターンについて説明します。
commandという単語は「命令」という意味ですね。「コマンド」と書くと、DOSプロンプトやUNIX、Linuxのコンソールで入力するコマンドを連想される方も多いかと思います。
Commandパターンはその名の通り「命令」そのものをクラスとして表すパターンです。しかし、「命令」をクラスにするとどの様なことになるのでしょうか?
たとえば
ほとんどのアプリケーションでは、利用者はアプリケーションに何らかの要求を出し、アプリケーションは要求を受け取って処理を実行しています。先に出てきたDOSやLinuxなどのコマンドもその1つですし、WindowsやX WindowといったGUIでの操作も含まれます。
たとえば、ファイルを圧縮する場合を考えてみましょう。Windowsの場合、圧縮ソフトにファイルをドラッグアンドドロップするといった操作になりますし、Linuxなどのコンソールから実行する場合、圧縮コマンドにファイル名を指定して実行するでしょう。
この時、利用者はソフトウェアやコマンドを通じて「圧縮する」という要求を出し、対象のファイルがその要求を受け取っている、と考えることができます。また、ファイルに別の操作をおこなう、つまり別な要求を出す場合は、「処理をおこなうソフトやコマンド」を差し替えたものと考えられるでしょう。
オブジェクト指向プログラミングにおいて、「要求を送る」ということはオブジェクトのメソッドを呼び出すということになります。しかし、要求が複雑になったり要求の種類が多くなると、その実装にも限界が出てきますし保守性も悪くなってしまいます。
そこで、「要求を送る」「要求を受け取る」という考えに基づいて、「要求」自身をクラスとしてまとめてしまうとどうでしょうか?
そうすると、具体的な要求を1つのオブジェクトとして扱うことができ、複雑な要求の場合でも容易に扱えるようになります。また、送る要求オブジェクトを変えることで、異なる要求を実現こともできそうです。
Commandパターンは、このような特徴を持っているパターンです。
Commandパターンとは?
Commandパターンはオブジェクトの振る舞いに注目したパターンで、「要求」そのものをクラスとして表し、「要求を送る側」と「要求を受け取る側」を分離することを目的としています。
GoF本では、Commandパターンは以下のように定義されています。
要求をオブジェクトとしてカプセル化することによって、様々な要求または要求からなるキューやログによりクライアントをパラメータ化する。そして、取り消し可能な操作をサポートする。
Commandパターンでは、異なる種類の要求に対する処理を同じAPIを持つクラスとして実装します。その結果、処理クラスのインスタンスを切り替えるだけで、様々な要求に対する処理を実行できるようなります。また、Commandパターンを適用すると、新しい要求に対する処理クラスを実装するだけで、既存のクラスを修正することなく対応可能になります。
Commandパターンの構造
Commandパターンのクラス図と構成要素は、次のようになります。
- Commandクラス
命令を実行するためのAPIを定義します
- ConcreteCommandクラス
Commandクラスのサブクラスで、Commandクラスで定義されたAPIを実装します。
- Invokerクラス
命令実行の要求を出すクラスです。
- Receiverクラス
命令をどの様に実行するかを知っている唯一のクラスです。任意のクラスがReceiverクラスになることができます。
Commandパターンのメリット
Commandパターンのメリットとしては、次のものが挙げられます。
- 既存のコードを修正することなく機能拡張できる
Commandパターンを適用すると、要求の受付と要求に対応する処理を切り離して実装できます。その結果、新しい要求が追加された場合でも既存のクラスを修正する必要がなく、追加された要求を処理するためのクラスを実装するだけで済みます。
- クラスの再利用性を向上させる
命令そのものが独立したクラスとして実装されますので、他のアプリケーションでの再利用がしやすくなります。
- 処理のキューイング
要求と実際の実行を別のタイミングで実施することができるようになります。
- UndoやRedoのサポート
Commandクラスに実行したコマンド結果を保持しておくことで、Undo機能やRedo機能を実現することができます。
Commandパターンの適用例
早速、Commandパターンを適用してみましょう。
ここでは、ファイルの作成・圧縮・コピーをおこなうアプリケーションを用意しました。Commandパターンを適用し、ファイルに対する操作をコマンドとして定義しています。
まずは、Fileクラスから見ていきます。FileクラスはReceiverクラスに相当するクラスです。作成・圧縮・コピーそれぞれの処理に対するメソッドがありますが、今回はメッセージを表示するだけの実装としています。
Fileクラス(File.class.php)
<?php /** * Receiverクラスに相当する */ class File { private $name; public function __construct($name) { $this->name = $name; } public function getName() { return $this->name; } public function decompress() { echo $this->name . 'を展開しました<br>'; } public function compress() { echo $this->name . 'を圧縮しました<br>'; } public function create() { echo $this->name . 'を作成しました<br>'; } }
続いて、要求を処理する側のクラス群を見ていきましょう。
Commandインターフェースは、すべてのコマンドに共通のAPIであるexecuteメソッドを宣言しています。
Commandインターフェース(Command.class.php)
<?php /** * Commandクラスに相当する */ interface Command { public function execute(); }
このCommandインターフェースを実装したクラスが、TouchCommandクラス、CompressCommandクラス、CopyCommandクラスです。ConcreteCommandクラスに相当し、Commandインターフェースで宣言されたexecuteメソッドを実装しています。
これらのクラスは、コンストラクタでFileオブジェクトを受け取り、executeメソッドでそれぞれの要求に対する処理をおこないますが、具体的な処理は受け取ったFileオブジェクトに任せています。
TouchCommandクラス(TouchCommand.class.php)
<?php require_once 'Command.class.php'; require_once 'File.class.php'; /** * ConcreteCommandクラスに相当する */ class TouchCommand implements Command { private $file; public function __construct(File $file) { $this->file = $file; } public function execute() { $this->file->create(); } }
CompressCommandクラス(CompressCommand.class.php)
<?php require_once 'Command.class.php'; require_once 'File.class.php'; /** * ConcreteCommandクラスに相当する */ class CompressCommand implements Command { private $file; public function __construct(File $file) { $this->file = $file; } public function execute() { $this->file->compress(); } }
CopyCommandクラス(CopyCommand.class.php)
<?php require_once 'Command.class.php'; require_once 'File.class.php'; /** * ConcreteCommandクラスに相当する */ class CopyCommand implements Command { private $file; public function __construct(File $file) { $this->file = $file; } public function execute() { $file = new File('copy_of_' . $this->file->getName()); $file->create(); } }
QueueクラスはCommandオブジェクトを保持するInvokerクラスに相当するクラスで、実際にCommandオブジェクトを実行します。runメソッドを呼び出すと、内部に保持したCommandオブジェクトを順に実行します。
Queueクラス(Queue.class.php)
<?php require_once 'Command.class.php'; /** * Invokerクラスに相当する */ class Queue { private $commands; private $current_index; public function __construct() { $this->commands = array(); $this->current_index = 0; } public function addCommand(Command $command) { $this->commands[] = $command; } public function run() { while (!is_null($command = $this->next())) { $command->execute(); } } private function next() { if (count($this->commands) === 0 || count($this->commands) <= $this->current_index) { return null; } else { return $this->commands[$this->current_index++]; } } }
次は、これまで説明してきたクラス群を利用するクライアント側のコードです。
まず、Queueオブジェクトを生成した後、addCommandメソッドを使ってファイルに対する要求を表すCommandオブジェクトを追加しています。そして、最後に追加したCommandオブジェクトを実行しています。
クライアント側コード(command_client.php)
<?php require_once 'Queue.class.php'; require_once 'TouchCommand.class.php'; require_once 'CompressCommand.class.php'; require_once 'CopyCommand.class.php'; require_once 'File.class.php'; $queue = new Queue(); $file = new File("sample.txt"); $queue->addCommand(new TouchCommand($file)); $queue->addCommand(new CompressCommand($file)); $queue->addCommand(new CopyCommand($file)); $queue->run();
ここで、それぞれの要求が実行される様子をシーケンス図を使って確認しておきましょう。
Invokerクラスに相当するQueueオブジェクトが各ConcreteCommandオブジェクトを実行し、要求の受け取り側のFileオブジェクトにアクセスしている様子が分かりますね。
最後に、このサンプルアプリケーションのクラス図となります。
Commandパターンのオブジェクト指向的要素
Commandパターンは、「ポリモーフィズム」を利用したパターンになります。
具体的な「要求」は、CommandクラスのサブクラスであるConcreteCommandクラスで表されます。また、ConcreteCommandクラスではCommandクラスで定義されたAPIを具体的に実装しています。
また、Invokerクラスは内部にCommand型のオブジェクトを保持し、それらを実行します。この時、これらのオブジェクトはあくまでCommand型として扱われています。つまり、具体的なConcreteCommandクラスに依存していないのです。
多くのデザインパターンでもポリモーフィズムを使って具体的なクラスを差し替え可能になっています(「交換可能性」といいます)。Commandパターンでは、変化する可能性がある「要求」にポリモーフィズムを使うことで、異なる要求を容易に差し替えられるようにしています。
関連するパターン
- Compositeパターン
複数のコマンドをまとめたマクロコマンドを実現するために利用されます。
- Mementoパターン
コマンドの実行履歴を管理するために、Mementoパターンが利用されることがあります。
- Prototypeパターン
Commandクラスを複製したい場合にPrototypeパターンが利用されることがあります。
まとめ
ここでは「要求」そのものをクラスとして表し、「要求を送る側」と「要求を受け取る側」を分離するCommandパターンについて見てきました。
PHPによるデザインパターン入門 - Composite〜木構造を表す
このエントリは、Do You PHP?(www.doyouphp.jp)で公開していたコンテンツを移行/加筆/修正したものです。公開の経緯はこちらをどうぞ。目次はこちらです。サンプルコードを手直ししたものをgithubに上げてありますのでそちらもどうぞ。
GoF本における分類
構造+オブジェクト
はじめに
ここではCompositeパターンについて見ていきましょう。
「composite」とは「合成物」「混合物」という意味を持ちます。ということは、Compositeパターンは、何かを混ぜるためのパターンなのでしょうか?
Compositeパターンは、単一のオブジェクトとその集合のどちらも同じように扱えるようにするためのパターンです。つまり、「単一のオブジェクト」と「オブジェクトの集合」を混ぜて、アクセス方法を同じにしてしまうパターンです。
分かるような分からないような、不思議なパターンですね。では、早速見ていきましょう。
たとえば
ファイルシステムのディレクトリツリーを考えてみましょう。
Windowsであれば、エクスプローラでツリー状に連なったフォルダを表示できますね。このディレクトリツリーにはフォルダやファイルが含まれていますが、フォルダやファイルに対する新規作成や削除、コピーといった操作は共通です。エクスプローラを使っているときに、「これはフォルダだから、こうやって削除しよう」とか「これはファイルだからこうやってコピーしよう」というように意識しないで操作しているはずです。
また、フォルダはその下にフォルダを含む場合がありますね。場合によっては、フォルダの階層が何階層にもなることもあるでしょう。また、ファイルはフォルダに含まれている、とも言えるでしょう。しかし、これら場合も特に意識しないでコピーや削除といった操作をおこなうことができます。
これをオブジェクト指向的に考えてみると、「フォルダ」や「ファイル」はそれぞれクラスと考えることができます。具体的なフォルダやファイルは、それぞれのクラスのインスタンスになるでしょう。
そして、「フォルダ」や「ファイル」に対する操作は、それぞれのメソッドとして定義できそうです。たとえば、フォルダクラスの削除メソッドを呼び出すとフォルダが削除される、といった具合です。また、ファイルクラスの削除メソッドを呼び出した場合はファイルが削除されなければなりませんね。ついでに、フォルダかファイルかを意識しないで操作できれば、利用する側は非常に便利になりそうです。
あとは、どうやってツリー状に組み上げれば良いかという問題が残っています。何となく予想がつきましたか?そう、フォルダオブジェクトの内部に別のオブジェクトを持たせてやることで再帰的なツリーが表現できそうですね。
ここまでいろいろと考えてきましたが、何となくでもイメージできたでしょうか?実は、これがCompositeパターンなのです。
Compositeパターンとは?
Compositeパターンはオブジェクトの構造に注目したパターンで、単体のオブジェクトとオブジェクトの集合を同一視することを目的としています。
GoF本では、Compositeパターンの目的は次のように定義されています。
部分-全体階層を表現するために、オブジェクトを木構造に組み立てる。Compositeパターンにより、クライアントは、個々のオブジェクトとオブジェクトを合成したものを一様に扱うことができるようになる。
Compositeパターンでは、オブジェクトを木構造に組み立てるのが特徴です。この木構造は、ファイルシステム上のディレクトリツリーをイメージするとよく分かります。逆さにすると、枝葉が延びるような形になっていますね。
Compositeパターンでは、親子関係を持つオブジェクトを再帰的に保持することで木構造を組み立てます。また、任意の枝の部分や末端の葉の部分に対して、共通の手順でアクセスできるような仕組みを提供しています。つまり、単一のオブジェクトにも、複数のオブジェクトから形成されたオブジェクトにも、同じ手順でアクセスできるAPIを提供します。
Compositeパターンの構成要素
Compositeパターンの構成要素は、次のとおりです。
- Componentクラス
Clientクラスに対して、共通にアクセスさせるためのAPIを提供します。このAPIには、子に相当するオブジェクトにアクセスしたり、追加・削除するためのAPIも含まれます。
- Leafクラス
Componentクラスのサブクラスの1つです。このクラスは、木構造の末端に位置する葉に相当するクラスです。このクラスは、子に相当するオブジェクトを持ちません。
- Compositeクラス
Leafクラスと同様、Componentクラスのサブクラスの1つです。木構造の中で、任意枝に相当するクラスです。このクラスは、子に相当するオブジェクトを持ちます。また、Componentクラスで定義された子オブジェクトへのアクセスAPIや追加・削除APIなども実装します。
- Clientクラス
Compositeパターンのメリット
Compositeパターンのメリットとしては、以下のものが挙げられます。
- クライアント側の操作が簡単になる
クライアントは、単一のオブジェクト、もしくは複数のオブジェクトから形成されたオブジェクトに同じAPIでアクセスできます。また、木構造の枝の部分だろうが、葉の部分だろうが、同じAPIでアクセスできます。
通常ですと、今対象としているオブジェクトが枝に相当するのか葉に相当するのかを意識する必要があります。Compositeパターンを適用することで、アクセスする手順が統一されます。
- 新しい枝を簡単に追加できる
Compositeパターンでは、木構造の枝葉を同一視する事ができるため、新しい枝(Compositeクラス)を追加するのが非常に簡単です。また、他のComponentクラスやLeafクラスを修正する必要はありません。
Compositeパターンの適用例
Compositeパターンの適用例を見てみましょう。
ここでは、組織とそれに所属する社員のデータを表示するサンプルです。Compositeパターンを使って組織と社員を同一視している事を意識しながら見てください。
まずは、Componentクラスに相当するOrganizationEntryクラスです。ここでは抽象クラスとして定義しています。OrganizationEntryクラスには組織と社員に共通なAPIであるgetCodeメソッドとgetNameメソッドが定義されています。
また、抽象メソッドとしてaddメソッドが定義されています。このメソッドは再帰的な構造を作るために必要です。引数はOrganizationEntry型のオブジェクトで、この型がポイントになります。これについては、次のGroupクラスで説明していますので確認してくださいね。
もうひとつ、データを出力するdumpメソッドが実装されています。これはデフォルトの実装として用意してあります。
OrganizationEntryクラス(OrganizationEntry.class.php)
<?php /** * Componentクラスに相当する */ abstract class OrganizationEntry { private $code; private $name; public function __construct($code, $name) { $this->code = $code; $this->name = $name; } public function getCode() { return $this->code; } public function getName() { return $this->name; } /** * 子要素を追加する * ここでは抽象メソッドとして用意 */ public abstract function add(OrganizationEntry $entry); /** * 組織ツリーを表示する * サンプルでは、デフォルトの実装を用意 */ public function dump() { echo $this->code . ":" . $this->name . "<br>\n"; } }
続いて、Componentクラスを継承したクラスです。組織を表すGroupクラスはCompositeクラスとして、社員を表すEmployeeクラスはLeafクラスとなります。両クラスともOrganizationEntryクラスを継承し、OrganizationEntryクラスのaddメソッドの具体的な実装をおこなっています。
ここで、両者に実装の違いがあります。
組織を表すGroupクラスでは、それに属する組織や社員を追加できるよう、内部の配列にarray_push関数を使って保持するようになっています。この時、追加する内容は、OrganizationEntry型のオブジェクトです。つまり、OrganizationEntryクラスを継承しているGroupクラス、EmployeeクラスはいずれもOrganizationEntry型と言えますので、特に意識することなく、いずれのオブジェクトも引数として渡せるとことになります。
また、逆説的に内部に保持されるオブジェクトは、必ずOrganizationEntry型になることが保証されます。ここでdumpメソッドのforeach文を見てください。内部に保持した配列から1つずつオブジェクトを取り出して、そのdumpメソッドを呼び出していますね?配列の中身がどういった型のオブジェクトか分からない場合、実際にdumpメソッドがあるかどうかをチェックする必要がありますが、ここでは一切チェックをおこなっていません。これは先の説明のとおり、配列の中身が全てOrganizationEntry型のオブジェクトであることが保証されている、つまり必ずdumpメソッドが存在しているからこそ可能になっています。
Groupクラス(Group.class.php)
<?php require_once 'OrganizationEntry.class.php'; /** * Compositeクラスに相当する */ class Group extends OrganizationEntry { private $entries; public function __construct($code, $name) { parent::__construct($code, $name); $this->entries = array(); } /** * 子要素を追加する */ public function add(OrganizationEntry $entry) { array_push($this->entries, $entry); } /** * 組織ツリーを表示する * 自分自身と保持している子要素を表示 */ public function dump() { parent::dump(); foreach ($this->entries as $entry) { $entry->dump(); } } }
一方、社員を表すEmployeeクラスを見てみましょう。「社員」というものは「社員」自身に属する組織や社員を持つことはできません。ですので、ここではaddメソッドを呼び出した場合に例外を投げるよう実装しています。
Employeeクラス(Employee.class.php)
<?php require_once 'OrganizationEntry.class.php'; /** * Leafクラスに相当する */ class Employee extends OrganizationEntry { public function __construct($code, $name) { parent::__construct($code, $name); } /** * 子要素を追加する * Leafクラスは子要素を持たないので、例外を発生させている */ public function add(OrganizationEntry $entry) { throw new Exception('method not allowed'); } }
最後に、説明してきたクラス群を利用するクライアント側のコードです。
まず、組織の木構造を作っていますが、addメソッドの引数に注目してください。GroupオブジェクトだろうがEmployeeオブジェクトだろうが、構わず引数に指定されているのが分かると思います。
クライアント側コード(composite_client.php)
<?php require_once 'Group.class.php'; require_once 'Employee.class.php'; /** * 木構造を作成 */ $root_entry = new Group("001", "本社"); $root_entry->add(new Employee("00101", "CEO")); $root_entry->add(new Employee("00102", "CTO")); $group1 = new Group("010", "○○支店"); $group1->add(new Employee("01001", "支店長")); $group1->add(new Employee("01002", "佐々木")); $group1->add(new Employee("01003", "鈴木")); $group1->add(new Employee("01003", "吉田")); $group2 = new Group("110", "△△営業所"); $group2->add(new Employee("11001", "川村")); $group1->add($group2); $root_entry->add($group1); $group3 = new Group("020", "××支店"); $group3->add(new Employee("02001", "萩原")); $group3->add(new Employee("02002", "田島")); $group3->add(new Employee("02002", "白井")); $root_entry->add($group3); /** * 木構造をダンプ */ $root_entry->dump();
最後にサンプルコードのクラス図を示しておきます。
Compositeパターンのオブジェクト指向的要素
Compositeパターンは「ポリモーフィズム」を非常に活用したパターンです。
Componentクラスでは、Clientクラスに対して共通にアクセスさせるためのAPIを提供しています。また、Compositeクラスはごにょごにょと処理を実行し、CompositeクラスやLeafクラスのインスタンスを、Clientクラスに適切に返します。これらのインスタンスは、いずれもComposite型であるところがポイントです。Clientクラスは、返されたインスタンスが、Compositeクラスのインスタンスなのか、Leafクラスのインスタンスなのか分かりません。しかし、Component型であることは分かっています。Clientクラスでは、Componentクラスで提供されたAPIだけを使ってプログラミングすることで、Componentクラスの向こうにあるCompositeクラスやLeafクラスを意識することなく、同一視することができます。
また、ComponentクラスのサブクラスであるCompositeクラスでは、内部にComponent型のオブジェクトを保持しています。このオブジェクトが、自分自身の子に相当するオブジェクトになります。ここでも、このオブジェクトは「Component型である」というだけで、具体的にCompositeクラスのインスタンスなのか、Leafクラスのインスタンスなのかは分かりません。
何となく気づきましたか?そうです。Compositeクラスで実装する「子に対するアクセスメソッド」は、Componentクラスが提供しているAPIだけを使うことで、ここでも枝葉の同一視をしています。ClientクラスとComponentクラスの関係と同じですね。
関連するパターン
- Chain of Responsibilityパターン
Chain of Responsibilityパターンもオブジェクトどうしのつながりを持っているパターンです。
- Commandパターン
複数のコマンドを組み合わせ、大きなコマンドを作る場合にCompositeパターンが使われます。
- Decoratorパターン
Compositeパターンと良く併用されるパターンです。
まとめ
ここでは「単一のオブジェクト」と「オブジェクトの集合」を同一視するCompositeパターンについて見てきました。
PHPによるデザインパターン入門 - Builder〜生成の手順と手段を分離する
このエントリは、Do You PHP?(www.doyouphp.jp)で公開していたコンテンツを移行/加筆/修正したものです。公開の経緯はこちらをどうぞ。目次はこちらです。サンプルコードを手直ししたものをgithubに上げてありますのでそちらもどうぞ。
GoF本における分類
生成+オブジェクト
はじめに
ここではBuilderパターンについて見ていきましょう。
「builder」とは、「建築者」や「建設業者」の意味を持つ単語ですが、たとえば、家を建てることを考えてみましょう。最終的に建てられる家は、「どの様な順序で、どこに何を配置していくか」という「手順」と、「柱や壁、屋根に何を使うか」という「材料」によって大きく違ってきます。
Builderパターンは、この「手順」と「材料」を分けておき、同じ手順で異なるオブジェクトを生成させるパターンです。
たとえば
ある条件によって作成するオブジェクトを変更するといった場合を考えてみましょう。
普通に考えると、if文やswitch文で処理を分岐し、生成するオブジェクトを切り替えるだけで済んでしまいそうですね。
しかし、オブジェクトを生成するのに複雑な手順が必要だった場合はどうでしょうか?大量のif文やswitch文で処理を分岐してやる必要がありそうですね。
こういった場合、「何を作るのか」に依存しないように具体的な生成手順をまとめておけると、簡単に色々なオブジェクトを生成できそうです。
もうひとつ、あるフォーマットに従ったデータを読み込んで、そのデータを格納するクラスを考えてみましょう。この場合、データの解析処理はどこに記述すると良いでしょうか?
たとえば、このクラスの中でデータを処理させるとしましょう。この場合、データを渡すだけでクラスのインスタンスが生成でき、またデータの処理もクラスの内部に閉じこめることができます。
しかし、データの処理が非常に複雑な場合はどうでしょうか?このクラスのコードの大部分は、その処理に関するコードになってしまうでしょう。このクラスの本来の目的は、データを格納することのはずですが、データの解析処理も含めると非常に分かりにくいコードになってしまいます。
データを保持するクラスにはできるだけその目的に専念させ、データの処理部分は他のクラスに任せたいものです。そうすればクラスの「責任」がはっきりし、クラスの構造がシンプルになりますので、コードも分かりやすいものになるはずです。また、クラスの再利用性も高まると予想されます。
このように、オブジェクトを「何を生成するか」と「どのように生成するか」を分離するパターンがBuilderパターンです。
Builderパターンとは?
Builderパターンは、オブジェクトの生成に注目したパターンで、オブジェクトの「生成手順」と「生成手段」を分離することを目的としています。
GoF本では、Builderパターンの目的は次のように定義されています。
複合オブジェクトについて、その作成過程を表現形式に依存しないものにすることにより、同じ作成過程で異なる表現形式のオブジェクトを生成できるようにする
Builderパターンでは、まずクライアントがどのようなオブジェクトを生成するかを選択します。つまり「材料」を選択するということです。この材料は、最終的に生成されるオブジェクトの生成処理を知っています。この材料を「建築者」に渡すことで、実際のオブジェクトの生成をお願いします。
一方の「建築者」ですが、渡された材料からどの様なものができるのか知りません。ただ、自らが知っている手順に沿って、オブジェクトの生成をおこなうだけです。
1つのクラスに生成手順と生成手段をすべてまとめた場合、クラスが複雑になりすぎる傾向があります。ここでBuilderパターンを適用して生成手順を分離することで、構造がシンプルになり、再利用性も高まります。
Builderパターンの構造
Builderパターンのクラス図と構成要素は、次のとおりです。
- Builderクラス
オブジェクトの「生成手段」を提供するクラス群で最上位に位置するクラスです。オブジェクトを生成するためのAPIを定義します。
- ConcreteBuilderクラス
Builderクラスで提供されるAPIを実装するサブクラスです。また、生成したオブジェクトを取得するためのメソッドを提供します。
- Directorクラス
「建築者」に相当するクラスで、Builderクラスで定義されたAPIを使ってオブジェクトを生成します。
- Productクラス
最終的に生成されるオブジェクトのクラスです。
Builderパターンのメリット
Builderパターンのメリットとしては、以下のものが挙げられます。
- Productオブジェクトの生成過程や生成手段を隠すことができる
Builderクラスでは、Directorクラスにオブジェクトを生成するためのAPIを提供しています。Directorクラスでは、このAPIを使ってのみProductオブジェクトを生成します。つまり、DirectorクラスはProductオブジェクトの生成過程やProductオブジェクトの生成手段を知りません。このため、新しいProductオブジェクトを作る必要がある場合、新しいConcreteBuilderクラスを追加するだけで済みます。
- オブジェクトの生成過程や生成手段のコードを局所化できる
Builderパターンは、オブジェクトの生成過程と生成手段を分離するパターンです。Directorクラスにはオブジェクト生成過程のコードだけが、ConcreteBuilderクラスにはオブジェクトの生成手段のコードだけが記述されることになります。つまり、生成過程と生成手段それぞれに関するプログラムコードを凝縮できるということです。この結果、生成過程、生成手段を独立して修正・拡張することが可能になります。
Builderパターンの適用例
Builderパターンの適用例を見てみましょう。
これは、ニュース一覧をインターネット経由で取得し、一覧表示するアプリケーションです。
まずは、1つの記事を格納するNewsクラスから始めましょう。Newsクラスは、記事のタイトル、URL、記事の対象日付を保持するだけのクラスで、これら3要素をコンストラクタで受け取ります。特に問題はありませんね。
Newsクラス(News.class.php)
<?php class News { private $title; private $url; private $target_date; public function __construct($title, $url, $target_date) { $this->title = $title; $this->url = $url; $this->target_date = $target_date; } public function getTitle() { return $this->title; } public function getUrl() { return $this->url; } public function getDate() { return $this->target_date; } }
続けて、外部のサイトからニュース記事を取得し、Newsオブジェクトの配列を作成するクラスたちです。
まずは、最終的なNewsオブジェクトの配列を生成するNewsDirectorクラスです。Directorクラスに相当します。コンストラクタの引数に、NewsBuilder型のオブジェクトが指定できるようになっています。また、実際のNewsオブジェクトの生成は、内部に保持したNewsBuilderオブジェクトを使って生成しています。
NewsDirectorクラス(NewsDirector.class.php)
<?php require_once 'NewsBuilder.class.php'; /** * Directorクラスに相当する */ class NewsDirector { private $builder; private $url; public function __construct(NewsBuilder $builder, $url) { $this->builder = $builder; $this->url = $url; } public function getNews() { $news_list = $this->builder->parse($this->url); return $news_list; } }
次に、先ほど出てきたNewsBuilderクラスについて説明しましょう。
これはインターフェースとして宣言しており、parseメソッドを定義しています。先のNewsDirectorクラスは、このparseメソッドを使ってNewsオブジェクトの配列を取得しています。
NewsBuilderクラス(NewsBuilder.class.php)
<?php /** * Builderクラスに相当する */ interface NewsBuilder { public function parse($data); }
先ほどのNewsBuilderクラスを実装するクラスが、RssNewsBuilderクラスです。名前からも分かるように、ニュース記事としてRSSからデータを取得します。
RSS(RDF Site Summary)は、ウェブログや新聞社、その他企業でも更新情報の配信などに利用されている文章フォーマットです。
今回は、PHP5から導入されたSimpleXML拡張機能を利用しています。
RssNewsBuilderクラス(RssNewsBuilder.class.php)
<?php require_once 'News.class.php'; require_once 'NewsBuilder.class.php'; /** * ConcreteBuilderクラスに相当する */ class RssNewsBuilder implements NewsBuilder { public function parse($url) { $data = simplexml_load_file($url); if ($data === false) { throw new Exception('read data [' . htmlspecialchars($url, ENT_QUOTES, mb_internal_encoding()) . '] failed !'); } $list = array(); foreach ($data->item as $item) { $dc = $item->children('http://purl.org/dc/elements/1.1/'); $list[] = new News($item->title, $item->link, $dc->date); } return $list; } }
最後にクライアント側のコードです。NewsDirectorクラスのコンストラクタに、RssNewsBuilderオブジェクトとURLを渡していますね。たとえば、他のフォーマットで記述された記事データからNewsオブジェクトの配列を生成する場合、このRssNewsBuilderオブジェクトを変更するだけで対応可能になります。これは、「何を生成するか」と「どのように生成するか」を切り分けた結果、可能になるものです。
クライアント側コード(builder_client.php)
<?php require_once 'NewsDirector.class.php'; require_once 'RssNewsBuilder.class.php'; $builder = new RssNewsBuilder(); $url = 'http://www.php.net/news.rss'; $director = new NewsDirector($builder, $url); foreach ($director->getNews() as $article) { printf('<li>[%s] <a href="%s">%s</a></li>', $article->getDate(), $article->getUrl(), htmlspecialchars($article->getTitle(), ENT_QUOTES, mb_internal_encoding()) ); }
最後にサンプルコードのクラス図を示しておきます。
Builderパターンのオブジェクト指向的要素
Builderパターンは「ポリモーフィズム」と「委譲」を活用しているパターンです。
「建築者」であるDirectorクラスには、「材料」であるBuilder型のオブジェクト、具体的にはConcreteBuilderクラスのインスタンスが渡されます。ここで、渡されたBuilder型のオブジェクトが具体的にどのクラスのインスタンスなのかを知りません。しかし、Builderクラスで提供されているメソッドは知っていますし、それらを呼び出すことで目的とするオブジェクトが生成されることも知っています。ただ、具体的にどの様なオブジェクトが生成されるかは知りません。
ここまで見てきたように、DirectorクラスはBuilderクラスのAPIしか知りません。それらAPIの具体的な処理内容は、BuilderクラスのサブクラスであるConcreteBuilderクラスで実装されています。つまり、ConcreteBuilderクラスがどのようなものであれ、BuilderクラスのAPIが実装されているものであれば、Directorクラスは正しく機能するということになります。
この「具体的な実装が何かを知らない」ことが、オブジェクト指向プログラミングでは重要になってきます。また「知らないからこそ入れ替えが可能」になるのです。
また、DirectorクラスとBuilderクラスの間には、強い結びつきはありません。Builder型のオブジェクトをDirectorクラスの内部に保持することで、ゆるく結びつけられています。Directorクラスの処理内容は、内部に保持したBuilder型のオブジェクトのメソッドを呼び出し、具体的な処理を任せています。この関係を委譲と呼びます。この緩い結びつけのため、Builder型のオブジェクトをプログラムの実行中に変更したりできるのです。
関連するパターン
- Abstract Factoryパターン
Abstract Factoryパターンも複雑なオブジェクトを生成するパターンです。Builderパターンは複雑なオブジェクトを段階的に生成していく手順に注目したパターンですが、Abstract Factoryパターンは、それぞれの部品の集まりに注目したパターンです。
- Compositeパターン
Builderパターンによって生成されるオブジェクトは、Compositeパターンになる場合があります。
まとめ
ここでは、「何を生成するか」と「どのように生成するか」を切り分けるBuilderパターンを見てきました。