PHPによるデザインパターン入門 - Factory Method〜生成処理と使用処理を分離する
このエントリは、Do You PHP?(www.doyouphp.jp)で公開していたコンテンツを移行/加筆/修正したものです。公開の経緯はこちらをどうぞ。目次はこちらです。サンプルコードを手直ししたものをgithubに上げてありますのでそちらもどうぞ。
GoF本における分類
生成+クラス
はじめに
ここでは、Factory Methodパターンについて説明します。
「factory」とはもちろん「工場」という意味です。パターン名に使われているほどですから、もちろんそれなりの理由があります。工場とは一般的に何かを製造する施設ですよね…ここまで言うと、もうお気づきかもしれません。
では、なぜ「工場」という名前が使われているのか、これから説明していきます。
たとえば
たとえば、作曲家と作品のデータがCSVファイルとして保存されており、これを表示する場合を考えてみましょう。
CSVファイルの1レコードには作曲家名とその作曲家の作品名が存在します。表示ルールとして、複数の作品を持つ作曲家の場合は初めに作曲家名を表示し、続けて作品名を表示するものとします。
Music.csv
ルートヴィヒ・ヴァン・ベートーヴェン,ピアノソナタ第23番ヘ短調「熱情」 ヴォルフガング・アマデウス・モーツァルト,魔笛 ヴォルフガング・アマデウス・モーツァルト,セレナーデト長調 K.525「小夜曲」 アントニン・ドヴォルザーク,交響曲第9番ホ短調「新世界より」
ここで、簡単に以下のようなコードを書いてみます。
client.php
<html lang="ja"> <head> <title>作曲家と作品たち</title> </head> <body> <?php $handle = fopen ("Music.csv","r"); $column = 0; $tmp = ""; while ($data = fgetcsv ($handle, 1000, ",")) { $num = count ($data); for ($c = 0; $c < $num; $c++) { if ($c == 0) { if ($column != 0 && $data[$c] != $tmp) { echo "</ul>"; } if ($data[$c] != $tmp) { echo "<b>" . $data[$c] . "</b>"; echo "<ul>"; $tmp = $data[$c]; } }else { echo "<li>"; echo $data[$c]; echo "</li>"; } } $column++; } echo "</ul>"; fclose ($handle); ?> </body> </html>
client.phpでは、CSVファイルを読み込みながらHTMLを出力しています。よくありがちなコードですね。特に問題はないでしょう。
しかし世の中そうは簡単にいかないものです。たとえば、次のような修正をおこなう必要が出てきたとします。
- CSV形式のデータを利用する他にXML形式のデータをサポートする
- 表示はCSVの場合と同じとする
- 読み込み、表示に利用するデータファイルは外部から渡される
- CSV、XMLデータのどちらを利用するかは、渡されたファイルの拡張子で判断する
単純に考えると、if文を使ってCSVファイルなのかXMLファイルを判定しながら、データの読み込みと表示をおこなえば、ひとまずは要求を満たすことができるでしょう。 しかし、さらなる別形式のデータをサポートする必要が出てきた場合、またデータの読み込み、表示コードを別に書き、条件分岐して…と、繰り返していくうちに条件分岐の羅列になってしまい、メンテナンス性が悪くなる事がわかります。
また、こうなってはデータの読み込み部分を再利用することは難しくなります。
ここで考え方を変えてみましょう。CSV形式やXML形式のデータを利用して達成したい事は、データを表示することです。表示するためにはデータを読み込む必要があります。データの形式がなんであれデータを読み込み、表示する。この2つが共通な機能として存在します。
共通の機能といっても、当然具体的な読み込み方法や表示方法は共通ではありません。このコードの違いを埋める仕組みができれば、より分かりやすくメンテナンスしやすいコードになりそうです。
このような状況で活躍するのが、Factory Methodパターンです。
Factory Methodパターンとは?
Factory Methodパターンはオブジェクトの生成の方法に注目したパターンで、オブジェクトを生成するためのAPIを定義し、クラスの継承を使って生成されるオブジェクトを切り替えることを目的としています。
GoF本では、Factory Methodパターンの目的は次のように定義されています。
オブジェクトを生成するときのインタフェースだけを規定して、実際にどのクラスをインスタンス化するかはサブクラスが決めるようにする。Factory Methodパターンは、インスタンス化をサブクラスに任せる。
Factory Methodパターンとは、その名の通り「工場」のような振る舞いをします。何を作る工場かというと、「クラスのインスタンス」という製品を製造する工場です。
さて、この「Factory」で生成される「オブジェクト」はどのように実装されているのかは分かりませんが、どのように実装されていても、同様の「機能」を提供します。この同様の「機能」を実現するために「インターフェース」を定義し、Factoryで生成されるクラスが実装を行っています。言い換えると、Factoryで生成されるインスタンスはどのクラスのものであれ、同様の「機能」を持っていると言えます。
Factory Methodパターンを利用することで、オブジェクトの利用側はどのインスタンスが生成されるのかを知る必要がなくなります。繰り返しになりますが、利用側が知る必要があるのは製品の「機能」であり、具体的にどのクラスのインスタンスであるかを知る必要はありません。
また、通常、オブジェクトはnew演算子を使って生成されますが、Factory Methodパターンではその代わりとなるインスタンス生成用のクラスが用意されます。このため、Virtual Constructor(仮想的なコンストラクタ)と呼ばれる事もあります。
Factory Methodパターンの構造
Factory Methodパターンのクラス図と構成要素は、次のようになります。
Factory Methodパターンの構成要素は、次のとおりです。
- Productクラス
オブジェクト生成メソッド(工場)で生成されるオブジェクト(製品)のAPIを定義するクラスです。オブジェクト生成メソッドは、「factory method」とも呼ばれます。
- ConcreteProductクラス
Productクラスのサブクラスで、Productクラスで定義されたAPIを実装したクラスです。
- Creatorクラス
オブジェクト生成メソッドを提供するクラスです。このメソッドは、Product型のオブジェクトを返します。また、あるConcreteProductオブジェクトを返すために、デフォルトの実装がなされる場合もあります。
- ConcreteCreatorクラス
Factory Methodパターンのメリット
Factory Methodパターンのメリットとしては、以下のものが挙げられます。
- オブジェクトの生成処理と使用処理を分離できる
複数のオブジェクトを扱う場合、if文やswitch文を使ってオブジェクトの生成コードを記述し、それらを利用するコードも同じコード内に記述してしまいがちです。こういった場合、オブジェクト生成のコードと利用側のコードを分けておくと、後々のメンテナンスが楽になります。
Factory Methodパターンは、「オブジェクト生成側」と「オブジェクトの利用側」を分離するパターンです。「オブジェクト生成側」はCreatorクラスとConcreteCreatorクラス、「オブジェクト利用側」はクライアント側のコードがそれぞれ担います。その間をやりとりするオブジェクトが、ProductクラスとConcreteProductクラスになります。これにより、生成側はProductクラスを返すコード、一方の利用側はProductクラスを利用するコードを記述するだけで良くなり、それぞれの処理内容に専念することができます。
- オブジェクトの利用側とオブジェクトのクラスの結びつきを低くする
Factory Methodパターンを使用することで、オブジェクトの利用側とオブジェクトの結びつきを低くする事ができます。これは、利用側でオブジェクトを直接生成しない、つまり、利用側のコードに「new クラス名」と直接書かなくてすむ、ということを意味します。この結果、利用側とオブジェクトの結びつきがゆるくなります。たとえば、生成するクラスの種類や生成手順が変更された場合でも、ファクトリ側を手直しするだけですみます。
Factory Methodパターンの適用例
ここでは冒頭に出てきたデータを表示する例にFactory Methodパターンを適用してみましょう。
まずは、CSVデータとXMLデータを扱うクラスの共通機能をインターフェースとして定義しましょう。ここではReaderとします。このクラスでは共通機能であるデータの読み込みと表示を行うメソッドを定義します。
Reader.class.php
<?php /** * 読み込み機能を表すインターフェースクラスです */ interface Reader { public function read(); public function display(); }
次に、CSVデータを扱うクラスCSVFileReaderを作成し、このクラスでReaderインターフェースを実装します。
CSVFileReader.class.php
<?php require_once("Reader.class.php"); /** * CSVファイルの読み込みを行なうクラスです */ class CSVFileReader implements Reader { /** * 内容を表示するファイル名 * * @access private */ private $filename; /** * データを扱うハンドラ名 * * @access private */ private $handler; /** * コンストラクタ * * @param string ファイル名 * @throws Exception */ public function __construct($filename) { if (!is_readable($filename)) { throw new Exception('file "' . $filename . '" is not readable !'); } $this->filename = $filename; } /** * 読み込みを行ないます */ public function read() { $this->handler = fopen ($this->filename, "r"); } /** * 表示を行ないます */ public function display() { $column = 0; $tmp = ""; while ($data = fgetcsv ($this->handler, 1000, ",")) { $num = count ($data); for ($c = 0; $c < $num; $c++) { if ($c == 0) { if ($column != 0 && $data[$c] != $tmp) { echo "</ul>"; } if ($data[$c] != $tmp) { echo "<b>" . $data[$c] . "</b>"; echo "<ul>"; $tmp = $data[$c]; } }else { echo "<li>"; echo $data[$c]; echo "</li>"; } } $column++; } echo "</ul>"; fclose ($this->handler); } }
同様に、XMLデータを扱うクラスXMLFileReaderも作成しましょう。
XMLFileReader.class.php
<?php require_once("Reader.class.php"); /** * XMLファイルの読み込みを行なうクラスです */ class XMLFileReader implements Reader { /** * 内容を表示するファイル名 * * @access private */ private $filename; /** * データを扱うハンドラ名 * * @access private */ private $handler; /** * コンストラクタ * * @param string ファイル名 * @throws Exception */ public function __construct($filename) { if (!is_readable($filename)) { throw new Exception('file "' . $filename . '" is not readable !'); } $this->filename = $filename; } /** * 読み込みを行ないます */ public function read() { $this->handler = simplexml_load_file($this->filename); } /** * 文字コードの変換を行います */ private function convert($str) { return mb_convert_encoding($str, mb_internal_encoding(), "auto"); } /** * 表示を行ないます */ public function display() { foreach ($this->handler->artist as $artist) { echo "<b>" . $this->convert($artist['name']) . "</b>"; echo "<ul>"; foreach ($artist->music as $music) { echo "<li>"; echo $this->convert($music['name']); echo "</li>"; } echo "</ul>"; } } }
次にFactoryクラスを作成しましょう。ここではReaderFactoryとします。このクラスはReaderクラスのインスタンスを生成するクラスになります。
ReaderFactoryクラスは、どの形式が指定された場合にどのReaderクラスのインスタンスを生成するかを判断します。
今回はファイルの拡張子で判断するのでしたね。ファイル名が「〜.csv」の場合はCSVデータであり、「〜.xml」の場合はXMLデータとします。
ReaderFactory.class.php
<?php require_once('Reader.class.php'); require_once('CSVFileReader.class.php'); require_once('XMLFileReader.class.php'); /** * Readerクラスのインスタンス生成を行なうクラスです */ class ReaderFactory { /** * Readerクラスのインスタンスを生成します */ public function create($filename) { $reader = $this->createReader($filename); return $reader; } /** * Readerクラスのサブクラスを条件判定し、生成します */ private function createReader($filename) { $poscsv = stripos($filename, '.csv'); $posxml = stripos($filename, '.xml'); if ($poscsv !== false) { $r = new CSVFileReader($filename); return $r; } elseif ($posxml !== false) { return new XMLFileReader($filename); } else { die('This filename is not supported : ' . $filename); } } }
それでは、最初にお見せしたclient.phpを変更しましょう。CSVファイルやXMLファイルの読み込み部分を、今回作成したクラスを利用するように変更します。
factory_client.php
<?php require_once('ReaderFactory.class.php'); ?> <html lang="ja"> <head> <title>作曲家と作品たち</title> </head> <body> <?php /** * 外部からの入力ファイルです */ $filename = 'Music.xml'; $factory = new ReaderFactory(); $data = $factory->create($filename); $data->read(); $data->display(); ?> </body> </html>
どうでしょうか?利用側のコードが非常にすっきりした事がわかると思います。
今後他に様々な形式のデータを利用する場合やDBからデータを取得したりインターネットを通じてデータをやり取りする場合、クライアント側のコードを変更する必要がなくなります。
なお、Factory Methodパターンを適用したサンプルのクラス図は次のようになります。
Factory Methodパターンのオブジェクト指向的要素
Factory Methodパターンは「継承」を利用しているパターンです。
ProductクラスとConcreteProductクラス、CreatorクラスとConcreteCreatorクラス、それぞれの間で継承関係があります。親クラスであるProductクラスとCreatorクラスは、親クラスどうしでどういった連携を行うかを決めます。具体的には、「オブジェクト生成メソッドからProduct型のオブジェクトを返す」といったものです。その具体的な実装は、それぞれのサブクラスであるConcreteProductクラスとConcreteCreatorクラスに任せています。この
処理の大枠を親クラスで規定し、具体的な処理内容をサブクラスに任せる
という部分は、まさにTemplate Methodパターンとなっています。
関連するパターン
- Template Methodパターン
Factory Methodパターンは、Template Methodパターンの代表的な適用例です。通常、親クラスで処理の大枠を定義したメソッド(template methodと言います)が、factory methodになります。
- Singletonパターン
Creatorクラスは、Singletonパターンとして作られることが多くあります。これは、プログラム内で同じ工場が複数必要になることがほとんどないためです。
- Abstract Factoryパターン
Abstract Factoryパターンもfactory methodを使って実装されることが多いパターンです。
まとめ
ここでは、オブジェクトの生成処理と使用処理を分離するFactory Methodパターンについて見てきました。
PHPによるデザインパターン入門 - Façade〜シンプルな唯一の窓口
このエントリは、Do You PHP?(www.doyouphp.jp)で公開していたコンテンツを移行/加筆/修正したものです。公開の経緯はこちらをどうぞ。目次はこちらです。サンプルコードを手直ししたものをgithubに上げてありますのでそちらもどうぞ。
GoF本における分類
構造+クラス、オブジェクト
はじめに
ここではFaçadeパターンについて説明します。
「façade」は「ファサード」と読みます。あまり聞き慣れない言葉ですが、フランス語で「正面窓口」という意味です。
Façadeパターンは、複雑な関連を持つクラス群を簡単に利用するための「窓口」を用意するパターンです。
たとえば
世の中には個人的にしか利用しない簡単なシステムから世界規模で大々的に利用されるシステムまで、大小さまざまなシステムがあります。
最初はごく簡単な機能だけしかなかった小さなシステムも、度重なる機能拡張や修正で徐々にそのシステムは大きくなっていきます。システムの規模が大きくなればなるほどクラスの数は増えていき、それに従ってクラスどうしの関係も複雑になっていきます。
クラスどうしの関係が複雑になってくると、それを利用する側に急激な負担が発生します。それはクラスの使い方です。ありがちな話として、「このクラスはあのクラスと一緒に使う必要がある」とか「このクラスを使う場合は、前もってこちらのクラスのメソッドを呼び出し、あらかじめ処理をしておかなければいけない」といったものです。つまり、あるクラスを利用するために複数の他のクラスを知っている必要がある、ということです。
こういった状況では、利用する側にミスが発生しやすくなり、思わぬ不具合やシステム障害の温床になってしまいます。また、システムのセキュリティ面では、使い方が複雑になればなるほどセキュリティの確保がしにくい状況になります。
もし、クラスどうしの複雑な関係を意識しなくても利用できるシンプルな「窓口」があればどうでしょうか?利用する側は非常に便利になり、ミスが少なくなりそうですね。またセキュリティの確保も、バラバラなクラスどうしのままよりは格段に改善されそうです。
このような場面でFaçadeパターンが活躍します。
Façadeパターンとは?
Façadeパターンはクラスやオブジェクトの構造に注目したパターンで、複雑に連携しあうクラスやオブジェクトを容易に扱うためのAPIを提供することを目的としています。
GoF本では、Façadeパターンの目的は次のように定義されています。
サブシステム内に存在する複数のインターフェースに1つの統一インターフェースを与える。Façadeパターンはサブシステムの利用を容易にするための高レベルインターフェースを定義する。
Façadeパターンは、複雑に関連しあうクラス群を隠蔽するようなクラスを用意し、そのクラスに統一されたAPIを実装します。利用側はそのAPIを通じてクラス群を利用します。
こう見てみると、みなさんもすでにどこかで使ったことがあるかもしれませんね。
一般的に、システムがある規模より大きくなると、より小さなサブシステム単位に分割されます。このサブシステムをシンプルに利用できるようにするために、Façadeパターンが利用されます。Façadeパターンを適用すると、あるAPIだけを呼び出すだけでサブシステムを利用できるようになります。また、サブシステムどうしの依存関係もシンプルになります。
Façadeパターンの構造
Façadeパターンのクラス図と構成要素は、次のようになります。
- Façadeクラス
サブシステムで提供される統一APIを持つクラスです。サブシステム内のクラス同士の関係を知っています。また、クライアントからの要求を、サブシステム内の適切なオブジェクトに委譲します。
- サブシステム内のクラス群
サブシステムを構成するクラス群です。Façadeクラスの存在は知りません。
- Clientクラス
サブシステムを利用するクラスです。Façadeクラスを通じてサブシステムにアクセスします。
Façadeパターンのメリット
Façadeパターンのメリットとしては、以下のものが挙げられます。
- サブシステムの構成要素を隠蔽する
Façadeパターンを適用すると、クライアントからはサブシステムの入り口しか見えなくなります。結果、クライアントが意識しなければならないクラスの数を抑えることができ、そのサブシステムを簡単に利用できます。
- サブシステムとクライアントの結びつきをゆるくする
Façadeクラスを通じてサブシステムにアクセスすることで、サブシステムとクライアントの結びつきがゆるくなります。つまり、クライアントとサブシステム内部との独立性がより高くなると言えます。独立性が高くなることで、クライアントのコードに影響を与えることなく、サブシステム内部を変更できます。
Façadeパターンの適用例
Façadeパターンの適用例を見てみましょう。
ここでは、注文処理をおこなうアプリケーションを取り上げます。この注文処理はサブシステムとして扱うことができるため、Façadeパターンを適用して注文処理を利用するためのシンプルなAPIを提供すると共に、クライアントとの結びつきをゆるくしています。
まずは、商品クラスと注文商品クラス、注文クラスの3つから見ていきましょう。この3クラスはとてもシンプルなクラスです。
商品を表すItemクラスは、商品ID、商品名、単価の各情報を保持するだけのシンプルなものです。コンストラクタで全ての情報を引数として受け取り、それぞれの情報にアクセスするためのメソッドを用意しています。
Itemクラス(Item.class.php)
<?php class Item { private $id; private $name; private $price; public function __construct($id, $name, $price) { $this->id = $id; $this->name = $name; $this->price = $price; } public function getId() { return $this->id; } public function getName() { return $this->name; } public function getPrice() { return $this->price; } }
注文する商品を表すOrderItemクラスは、内部に商品クラスのインスタンスとその注文数量を保持します。Itemクラスと同様、コンストラクタでItemオブジェクトと数量を受け取り、それらにアクセスするためのメソッドもあります。
OrderItemクラス(OrderItem.class.php)
<?php require_once 'Item.class.php'; class OrderItem { private $item; private $amount; public function __construct(Item $item, $amount) { $this->item = $item; $this->amount = $amount; } public function getItem() { return $this->item; } public function getAmount() { return $this->amount; } }
OrderクラスはOrderItemオブジェクトを格納するクラスです。OrderItemを追加するためのaddItemがあります。
Orderクラス(Order.class.php)
<?php require_once 'OrderItem.class.php'; class Order { private $items; public function __construct() { $this->items = array(); } public function addItem(OrderItem $order_item) { $this->items[$order_item->getItem()->getId()] = $order_item; } public function getItems() { return $this->items; } }
では、ここからは商品の注文処理をおこなうクラス群を見ていきましょう。
さて、商品の注文処理ではどの様な処理をおこなう必要があるでしょうか?
商品在庫の確認や引当処理、決済や確認メールの送信など様々な処理が考えられますが、ここでは簡易的に商品在庫の引当と注文情報の処理の2つをおこないます。この2つの処理は、実際にはデータベースなどにアクセスしてそれぞれの情報を更新したり追加したりしますが、このアプリケーションではメッセージを表示するだけとなっています。
これらの商品在庫の引当と注文情報の処理をおこなうクラスが、ItemDaoクラスとOrderDaoクラスです。これらのクラスには、データベースだけに関する処理をまとめるDAO(Data Access Object)パターンが適用されています。
ItemDaoクラスは、インスタンス化と同時に商品情報を初期化しています。また、Singletonパターンも適用されており、ItemDaoインスタンスを取得するためのgetInstanceメソッドが用意されています。また、商品IDからItemオブジェクトを取得するfindByIdメソッド、在庫の引当処理をおこなうsetAsideメソッドがあります。
ItemDaoクラス(ItemDao.class.php)
<?php require_once 'OrderItem.class.php'; class ItemDao { private static $instance; private $items; private function __construct() { $fp = fopen('item_data.txt', 'r'); /** * ヘッダ行を抜く */ $dummy = fgets($fp, 4096); $this->items = array(); while ($buffer = fgets($fp, 4096)) { $item_id = trim(substr($buffer, 0, 10)); $item_name = trim(substr($buffer, 10, 20)); $item_price = trim(substr($buffer, 30)); $item = new Item($item_id, $item_name, $item_price); $this->items[$item->getId()] = $item; } fclose($fp); } public static function getInstance() { if (!isset(self::$instance)) { self::$instance = new ItemDao(); } return self::$instance; } public function findById($item_id) { if (array_key_exists($item_id, $this->items)) { return $this->items[$item_id]; } else { return null; } } public function setAside(OrderItem $order_item) { echo $order_item->getItem()->getName() . 'の在庫引当をおこないました<br>'; } /** * このインスタンスの複製を許可しないようにする * @throws RuntimeException */ public final function __clone() { throw new RuntimeException ('Clone is not allowed against ' . get_class($this)); } }
setAsideメソッドでは、引当をおこなう商品名とメッセージが表示されるだけですが、本来はデータベースにアクセスして在庫情報を更新するコードが記述されます。
なお、商品情報は固定長データとしてファイルに登録されています。なお、データフォーマットの詳細は表を参照してください。
商品情報(item_data.txt)
商品ID 商品名 価格 1 限定Tシャツ 1500 2 ぬいぐるみ 2000 3 クッキーセット 800
商品情報のファイルフォーマット
項目 | 開始位置 | 終了位置 | 備考 |
---|---|---|---|
商品ID | 1 | 10 | |
商品名 | 11 | 30 | |
価格 | 31 | - |
次はOrderDaoクラスです。このクラスにはcreateOrderメソッドのみが宣言されています。このメソッドは、Orderオブジェクトを受け取り、注文情報を表示しています。本来はデータベースにアクセスし、注文情報を登録する処理が記述されることになります。
OrderDaoクラス(OrderDao.class.php)
<?php require_once 'Order.class.php'; class OrderDao { public static function createOrder(Order $order) { echo '以下の内容で注文データを作成しました'; echo '<table border="1">'; echo '<tr>'; echo '<th>商品番号</th>'; echo '<th>商品名</th>'; echo '<th>単価</th>'; echo '<th>数量</th>'; echo '<th>金額</th>'; echo '</tr>'; foreach ($order->getItems() as $order_item) { echo '<tr>'; echo '<td>' . $order_item->getItem()->getId() . '</td>'; echo '<td>' . $order_item->getItem()->getName() . '</td>'; echo '<td>' . $order_item->getItem()->getPrice() . '</td>'; echo '<td>' . $order_item->getAmount() . '</td>'; echo '<td>' . ($order_item->getItem()->getPrice() * $order_item->getAmount()) . '</td>'; echo '</tr>'; } echo '</table>'; } }
お待たせしました。それでは、Façadeクラスに相当するOrderManagerクラスを見てみましょう。
OrderManagerクラスはクライアント側から注文処理をおこなうためのAPIを定義し、具体的な注文処理を隠蔽する役割を担います。orderメソッドがそれに相当します。
OrderManagerクラス(OrderManager.class.php)
<?php require_once 'Order.class.php'; require_once 'ItemDao.class.php'; require_once 'OrderDao.class.php'; class OrderManager { public static function order(Order $order) { $item_dao = ItemDao::getInstance(); foreach ($order->getItems() as $order_item) { $item_dao->setAside($order_item); } OrderDao::createOrder($order); } }
Façadeパターンを適用しない場合、これらの処理を呼び出すコードをクライアント側に記述する必要があります。これでは、利用側は「注文処理」でおこなう処理や順序を全て知っている必要があり、クライアントとそれぞれの処理を担当するクラスの結びつきが強くなってしまいます。
Façadeパターンを適用して「注文する」というAPI(ここではorderメソッド)を用意することで、クライアント側はそのAPIを使うだけのシンプルなコードなり、またFaçadeクラスだけに依存するコードになります。
では、Façadeパターンを適用した場合のクライアント側のコードはどの様になるか、見てみましょう。
facade_clientクラス(facade_client.php)
<?php require_once 'Order.class.php'; require_once 'OrderItem.class.php'; require_once 'ItemDao.class.php'; require_once 'OrderManager.class.php'; $order = new Order(); $item_dao = ItemDao::getInstance(); $order->addItem(new OrderItem($item_dao->findById(1), 2)); $order->addItem(new OrderItem($item_dao->findById(2), 1)); $order->addItem(new OrderItem($item_dao->findById(3), 3)); /** * 注文処理はこの1行だけ */ OrderManager::order($order);
クライアント側ではOrderオブジェクトに商品を追加し、注文処理を実行しています。実際の注文処理が1行で実現されていることに注目してください。
Façadeパターンのオブジェクト指向的要素
Façadeパターンは非常に大きな「カプセル化」をおこなうパターンです。
クライアントの視点でサブシステムを見てみると、Façadeクラスで提供された統一APIのみが見えます。ここで、サブシステム自体を「非常に大きなクラス」と捉えると、提供された統一APIはメソッドに相当します。そのAPIの向こうには何やら複雑なものがあるのですが、クライアントからは見えませんし、意識する必要もありません。実際には、Façadeクラスの内部では背後にあるサブシステム内部のクラス群を使って、本来クライアントがおこなうべき複雑な処理をおこないます。
つまりFaçadeパターンは、クラスの集まりであるサブシステムに対してカプセル化をおこなっていると言えます。カプセル化とは、データとその操作をまとめて「オブジェクト」として定義し、オブジェクト内部の動作や構造を隠蔽することですが、Façadeパターンではサブシステムの内部状態や構造、またその複雑さを見事に隠蔽しています。
関連するパターン
- Abstract Factoryパターン
プラットフォームに依存するクラスを隠蔽するために、Façadeパターンの代わりに利用される場合があります。
- Mediatorパターン
Mediatorパターンもクラスの集まりを対象にしたパターンです。
Façadeパターンはサブシステムを利用するためのAPIを抽出しますが、Mediatorパターンはクラスどうしのやりとり自体を抽出するパターンです。
まとめ
ここでは複雑なクラス関係をシンプルにするための統一インターフェースを提供するFaçadeパターンについて見てきました。
PHPによるデザインパターン入門 - Iterator〜順々にアクセスする
このエントリは、Do You PHP?(www.doyouphp.jp)で公開していたコンテンツを移行/加筆/修正したものです。公開の経緯はこちらをどうぞ。目次はこちらです。サンプルコードを手直ししたものをgithubに上げてありますのでそちらもどうぞ。
GoF本における分類
振る舞い+オブジェクト
はじめに
ここではIteratorパターンについて説明します。
「iterate」は「繰り返す」「反復する」といった意味ですので、「iterator」は「反復するもの」となるでしょうか。
名前から想像できるように、Iteratorパターンはオブジェクトに対する反復操作をおこなうための統一APIを提供するパターンです。
たとえば
PHPで複数の値を含むリストを表現する場合、どの様にコードを記述していますか?おそらく、配列を使うことが多いのではないかと思います。
複数の値を含むリスト
<?php $arr = array('PHP', 'デザインパターン', 'iterator'); $arr[] = 'GoFパターン'; $arr['version'] = 'PHP5';
次に、このリストに含まれる値をすべて表示する場合はどうでしょうか?多くの場合、for文やforeach文を使うと思います。特にforeach文を使用すると、添字が文字列である連想配列も扱うことができますね。
リストに含まれる値をすべて表示するPHPスクリプト
<?php // for文を使用した場合 for ($i = 0; $i < count($arr); $i++) { echo $arr[$i] . '<br>'; } // foreach文を使用した場合 foreach ($arr as $value) { echo $value . '<br>'; }
では、ディレクトリツリーを表すような不規則な多次元配列の場合はどうでしょうか?ある程度のプログラミング経験があれば、再帰を使えば何とかなりそうだと気づくかもしれませんね。
このように、基本的にはfor文やforeach文を使用することで、リストの要素を取り出すことができます。
ここまではどの様なリストを処理するのかが、あらかじめ分かっている状況でしたね。しかし、どの様なリストを処理するのか分からない場合はどうでしょうか?つまり、リストの内部構造が分からない状況です。ここで「すべての場合を想定してループを用意して…」と考えてしまうかもしれませんが、ちょっと現実的ではありませんね。
また、要素の値を判断して取り出す順序を変えたい場合はどうでしょうか?たとえば、氏名と年齢、性別を持つユーザオブジェクトのリストを考えてみましょう。このリストから氏名順にユーザオブジェクトを取り出したい場合もあれば、年齢順の場合もあるでしょう。女性、男性のどちらかだけというのもあり得ますね。また、それらの逆順で取り出したい場合もあるかもしれません。
このような色々な要求に対応する場合、要素を取り出すスクリプトコードを修正することになってしまいがちです。つまり、コードの再利用はできないということです。
さて、こういった場合、どの様にすれば良いでしょうか?
最初のコード例では、for文もforeach文もリストから1つずつ要素を取り出していました。ここに注目してみましょう。
やりたいことは、リストから要素を取り出すことです。しかし、リストはどの様な構造なのか分からなかったり、取り出す方法が複数あるかもしれません。
そこで、「リストから1つずつ要素を取り出す」役割を担うものを用意するとどうでしょうか?利用側は「次の要素をちょうだい」とお願いするだけで済むようになりそうです。
一方の「リストから1つずつ要素を取り出す」役の方はどのようになりそうでしょうか?たとえば「次の要素をちょうだい」と依頼された場合は、リストの構造によって返す要素を決定し、その要素を返す処理をおこなえば良いことになります。つまり、利用者に、「どの様な構造を持つリストなのか」を意識させないようにできます。また、ある条件にマッチする値だけ取り出したい場合も、この「リストから1つずつ要素を取り出す」役にうまく納めることができそうです。
このように、リストの内部構造を隠したまま、それぞれの要素にアクセスさせるためのパターンがIteratorパターンです。
Iteratorパターンとは?
Iteratorパターンはオブジェクトの振る舞いに注目したパターンで、利用者にリストの内部構造を隠したまま、それぞれの要素にアクセスさせるためのAPIを提供することを目的としています。
GoF本では、Iteratorパターンの目的は次のように定義されています。
集約オブジェクトが基にある内部表現を公開せずに、その要素に順にアクセスする方法を提供する。
Iteratorパターンでは、リストのように複数のオブジェクトをまとめる集約オブジェクトを走査するためのAPIを提供します。これにより、利用側は集約オブジェクトの内部を意識することなく、要素にアクセスできます。その結果、異なる内部構造を持つリストの要素に同じAPIでアクセスできます。
また、走査処理の具体的な実装を変えることで、逆方向に走査させたり任意の要素に直接アクセスさせることもできます。
Iteratorパターンの構造
Iteratorパターンのクラス図と構成要素は、次のようになります。
- Iteratorクラス
要素にアクセスするためのAPIを提供します。
- ConcreteIteratorクラス
Iteratorクラスのサブクラスで、定義されたAPIを実装するクラスです。ここには、リストの内部構造に依存する走査処理が実装されます。
- Aggregateクラス
- ConcreteAggregateクラス
Aggregateクラスのサブクラスで、リスト固有のIteratorオブジェクトを返します。
Iteratorパターンのメリット
Iteratorパターンのメリットとしては、以下のものが挙げられます。
- リストの具体的な内部構造をクライアントから隠蔽する
リストを走査する処理はすべてConcreteIteratorクラス内に閉じこめられます。このため、クライアントはリストの内部構造を意識することなく走査することができます。
- リストに対する操作方法を複数用意できる
リストとそれを走査する役割のConcreteIteratorクラスが分かれていますので、異なる実装のConcreteIteratorクラスを用意することで、走査方法を容易に変更できます。
Iteratorパターンの適用例
Iteratorパターンの適用例を見てみましょう。
PHP5よりSPL(Standard PHP Library)拡張が追加されました。SPL拡張では標準的な関数やクラス、インターフェース、例外が定義されていますが、様々なイテレータも併せて用意されています。PHP5を使ってIteratorパターンを適用する場合、SPL拡張をそのまま、もしくは拡張して使用する場面が多くなるでしょう。
ここで挙げたサンプルアプリケーションは社員一覧を表示するものですが、次のSPL標準クラス・インターフェースを使用しています。
サンプルで使用するSPL標準クラス・インターフェース
名称 | 概要 |
---|---|
ArrayObjectクラス | 配列をオブジェクトとして扱うためのラッパークラス |
ArrayIteratorクラス | 配列用のiteratorクラス。ConcreteIteratorクラスに相当する。 |
FilterIteratorクラス | iterator用のフィルタクラス。また、抽象クラスとして定義されているため、利用するにはクラスの継承をおこないacceptメソッドを実装する必要がある。 |
IteratorAggregateインターフェース | Aggregateクラスに相当する。iteratorを生成するためのメソッドgetIteratorが宣言されている。 |
では、早速サンプルアプリケーションのコードを見ていきましょう。
まずは社員を表すEmployeeクラスです。このクラスは、コンストラクタに氏名・年齢・職務名を受け取り、内部に保持するだけのクラスです。特に難しいところはないと思います。
Employeeクラス(Employee.class.php)
<?php class Employee { private $name; private $age; private $job; public function __construct($name, $age, $job) { $this->name = $name; $this->age = $age; $this->job = $job; } public function getName() { return $this->name; } public function getAge() { return $this->age; } public function getJob() { return $this->job; } }
次は社員リストを表すEmployeesクラスです。SPL拡張のIteratorAggregateインターフェースを実装し、ConcreteAggregateクラスに相当します。
Employeesクラス(Employees.class.php)
<?php require_once 'Employee.class.php'; class Employees implements IteratorAggregate { private $employees; public function __construct() { $this->employees = new ArrayObject(); } public function add(Employee $employee) { $this->employees[] = $employee; } public function getIterator() { return $this->employees->getIterator(); } }
Employeesクラスは内部に複数のEmployeeオブジェクトを保持しますが、配列ではなくArrayObjectオブジェクトで管理します。また、getIteratorメソッドを実装し、ArrayObjectクラスのgetIteratorメソッドを呼び出しています。このメソッドからは、ArrayIteratorオブジェクトが返されます。利用側は、このArrayIteratorオブジェクトを使って社員リストにアクセスします。
続いて、SalesmanIteratorクラスを見てみましょう。このクラスはSPL拡張のFilterIteratorクラスを継承しています。
SalesmanIteratorクラス(SalesmanIterator.class.php)
<?php require_once 'Employee.class.php'; class SalesmanIterator extends FilterIterator { public function __construct($iterator) { parent::__construct($iterator); } public function accept() { $employee = $this->getInnerIterator()->current(); return ($employee->getJob() === 'SALESMAN'); } }
FilterIteratorクラスは「オブジェクトを包み込むクラス」で、包み込まれるオブジェクトに付加的な機能を提供します。これは、FilterIteratorクラスの抽象メソッドであるacceptメソッドを実装することで実現します。
SalesmanIteratorクラスでは、包み込むArrayIteratorオブジェクトに「動作を変更する」という機能を提供するために、acceptメソッドに取得する条件を実装しています。その結果、条件にマッチした値のみを取り出すことが可能です。今回は、職務名が「SALESMAN」であるオブジェクトだけを取り出すようになっています。
また、FilterIteratorクラスを使わなくとも、独自のIteratorを実装しても実現可能です。この場合も走査対象のEmployeesクラスやEmployeeクラスには影響を与えません。
最後に、説明してきたクラス群を利用するクライアント側のコードです。
まず、社員リストを作成し、イテレータを取り出しています。先ほど説明したとおり、イテレータはArrayIteratorオブジェクトになります。その後、イテレータのメソッドを利用して一覧を表示する場合と、foreach文を使って表示しています。
クライアント側コード(iterator_client.php)
<?php require_once 'Employee.class.php'; require_once 'Employees.class.php'; require_once 'SalesmanIterator.class.php'; function dumpWithForeach($iterator) { echo '<ul>'; foreach ($iterator as $employee) { printf('<li>%s (%d, %s)</li>', $employee->getName(), $employee->getAge(), $employee->getJob()); } echo '</ul>'; echo '<hr>'; } $employees = new Employees(); $employees->add(new Employee('SMITH', 32, 'CLERK')); $employees->add(new Employee('ALLEN', 26, 'SALESMAN')); $employees->add(new Employee('MARTIN', 50, 'SALESMAN')); $employees->add(new Employee('CLARK', 45, 'MANAGER')); $employees->add(new Employee('KING', 58, 'PRESIDENT')); $iterator = $employees->getIterator(); /** * iteratorのメソッドを利用する */ echo '<ul>'; while ($iterator->valid()) { $employee = $iterator->current(); printf('<li>%s (%d, %s)</li>', $employee->getName(), $employee->getAge(), $employee->getJob()); $iterator->next(); } echo '</ul>'; echo '<hr>'; /** * foreach文を利用する */ dumpWithForeach($iterator); /** * 異なるiteratorで要素を取得する */ dumpWithForeach(new SalesmanIterator($iterator));
ご覧の通り、イテレータのメソッドでもforeach文でも同様の結果を得られます。つまり、foreach文が暗黙的なイテレータとなり得ます。このため、Iteratorパターンを適用した場合、イテレータのメソッドではなくforeach文が使われることが多いでしょう。ただし、取り出す方法はConcreteIteratorクラスに依存しています。
そこで最後に、異なるイテレータを使って営業職の社員だけを一覧表示しています。このとき、Employeesオブジェクトや表示するためのコードを修正する必要がありませんし、新しいFilterIteratorクラスのサブクラスを作成するだけで新しい取得方法を追加できるということを確認してください。
最後にIteratorパターンを適用したアプリケーションのクラス図は次のようになっています。なお、色付きのインターフェースは、先の説明では出てきていないものになります。
Iteratorパターンのオブジェクト指向的要素
Iteratorパターンは「非カプセル化」をおこなうパターンです。
これまで見てきたように、Iteratorパターンでは集約オブジェクトを走査する「操作」を他のクラスに任せています。
本来、オブジェクト指向では、関連するデータと操作を1つの塊(オブジェクト)として扱うことでカプセル化を行いますので、集約オブジェクト自身に操作するためのコードを用意するのが本来の姿かもしれません。しかし、無理にカプセル化することでオブジェクトが肥大化したり、複雑になり過ぎる場合があります。たとえば、様々な走査をサポートしたい場合を考えてみてください。
また、集約オブジェクトを走査する場面はかなり多いものと思います。当然、多くのクラスに似たようなコードを持たせることになりますね。しかし、それぞれのクラスに走査のためのコードを記述するよりも、別のクラスに記述して再利用した方が有利になると考えられます。また、集約オブジェクトに影響を与えないで具体的な実装を変えることもできます。
Iteratorパターンでは、集約オブジェクトに対する操作を別クラスとして切り出して、オブジェクトが不要に肥大化したり複雑になることを避けています。確かに、集約オブジェクトを操作する場合は切り出されたクラスと連携する必要がありますが、切り出されたクラスを切り替えることで様々な操作をサポートできるようになっています。
PHPによるデザインパターン入門 - Abstract Factory〜関連する部品をまとめて作る工場
このエントリは、Do You PHP?(www.doyouphp.jp)で公開していたコンテンツを移行/加筆/修正したものです。公開の経緯はこちらをどうぞ。目次はこちらです。サンプルコードを手直ししたものをgithubに上げてありますのでそちらもどうぞ。
GoF本における分類
生成+オブジェクト
はじめに
ここではAbstract Factoryパターンについて説明します。
「abstract factory」を直訳すると「抽象的な工場」となりますね。抽象的な工場…これは一体何なのでしょうか?
GoFパターンの1つにFactory Methodパターンがあります。Factory Methodパターンは製品であるオブジェクトを作る「工場」を用意するパターンです。ここで見ていくAbstract Factoryパターンも同様にオブジェクトを生成するパターンの1つで、関連し合うオブジェクトの集まり、つまり部品の集まりを生成します。
ここで、「抽象的な」というところがポイントです。この工場は「抽象的」な工場で、生成される部品も「抽象的」な部品になります。
オブジェクト指向プログラミングでは、「物を抽象化する」ということが重要なポイントとなります。つまり、具体的な物を直接扱うのではなく、「具体的な物を抽象化した物」を扱うということです。
Abstract Factoryパターンは、具体的なクラスを明確にすることなく、関連し合うオブジェクトの集まりを生成するパターンです。
たとえば
データベースを使ったアプリケーションを構築する場合を考えてみましょう。
近年の開発・設計方法論では、データベースに関する処理内容とそれ以外のアプリケーション固有の処理(ビジネスロジック)とを分けて設計・実装することが主流になっています。なぜなら、接続やデータの取得、トランザクションなどデータベースだけに関する処理は、本来ビジネスロジックとは関係がないためです。
この手法はDAO(Data Access Object)パターンと呼ばれます。DAOパターンを適用する場合、通常エンティティ(データベースの表)の単位でクラスを作成します。
さて、データベースを使ったアプリケーションには、当然ですが何らかのデータベースが必要になります。しかし、プログラムを作成できても肝心のデータベースがないと動作しない、ひいてはテストできないといった状況になりがちです。しかし、場合によってはデータベースが用意できないまま先行して開発を行わなければならないこともあります。このような場合、データベースアクセスをすると見せかける擬似的なオブジェクト(モックオブジェクト)を使うと非常に有効です。このオブジェクトは「アクセスしているように見せかける」だけで、実際にはデータベースにアクセスしません。つまり、データベースがなくても、ビジネスロジック側を作成したりテストできます。
しかし、モックオブジェクトを使って開発した場合でも、最終的には実際にデータベースにアクセスするクラスに切り替える必要があります。
これはかなり大変な作業になります。しかし、ここでビジネスロジックを記述したコードを書き換えてしまっては再度テストをおこなわなければなりません。これでは何のためにモックオブジェクトを用意したのか分からなくなってしまいます。
データベースアクセスクラスの集まりを、整合性を保ったまま簡単に切り替える…このような場面でAbstract Factoryパターンが活躍します。
Abstract Factoryパターンとは?
Abstract Factoryパターンはオブジェクトの生成に注目したパターンで、関連するオブジェクトをその具体的なクラスを意識させないで生成することを目的としています。
GoF本では、Abstract Factoryパターンの目的は次のように定義されています。
互いに関連したり依存し合うオブジェクト群を、その具象クラスを明確にせずに生成するためのインターフェースを提供する。
Abstract Factoryパターンでは、部品の役割を持つクラスとその部品を作る工場の役割を持つクラスが存在します。
ただし、その工場には関連し合う部品を生成するためのメソッドがそれぞれ用意されます。また、関連し合う部品群の種類に応じて、その工場自身も「工場を生成するための工場」によって生成されます。
これにより、状況に応じて生成される具体的な部品群を切り替えることができます。
まとめると、Abstract Factoryパターンでは工場から生成される部品は抽象化されており、部品の具体的な内容や生成手順をクライアントが意識しなくて済むようになります。
Abstract Factoryパターンの構造
Abstract Factoryパターンのクラス図と構成要素は、次のようになります。
部品であるAbstractProductクラスを生成するためのAPIを定義するクラスです。生成するのがConcreteProductクラスではなくAbstractProductクラスとして定義されているところがポイントです。
- ConcreteFactoryクラス
AbstractFactoryクラスのサブクラスで、定義された生成メソッドを実装するクラスです。ここには、具体的な部品であるConcreteProductクラスを返すよう実装されます。
- AbstractProductクラス
部品ごとのAPIを定義するクラスです。
- ConcreteProductクラス
AbstractProductクラスのサブクラスで、ConcreteFactoryクラスから返される具体的な部品クラスです。
- Clientクラス
AbstractFactoryクラスとAbstractProductクラスのAPIのみを利用してプログラミングをおこないます。
Abstract Factoryパターンのメリット
Abstract Factoryパターンのメリットとしては、以下のものが挙げられます。
- 具体的なクラスをクライアントから隠蔽する
クライアントは具体的な工場や部品に直接アクセスするのではなく、抽象化された工場や部品のAPIのみを使って部品オブジェクトを生成したりアクセスしたりできます。このため、クライアントを変更することなく、具体的な工場や部品を変更することが可能です。
- 利用する部品群の整合性を保つ
関連し合うオブジェクトを扱う場合、その整合性を保つことが重要になります。Abstract Factoryパターンを適用することで、オブジェクト群の整合性を容易に保つことができます。
- 部品群の単位で切り替えができる
ConcreteFactoryクラスは関連する部品の集まりを生成します。つまり、ConcreteFactoryクラスを切り替えることで、関連する部品の集まりを容易に切り替えることができます。
Abstract Factoryパターンの適用例
Abstract Factoryパターンの適用例を見てみましょう。
ここでは、商品情報と注文情報を取得するアプリケーションを示しています。Abstract Factoryパターンを適用し、実データを扱う部品群とダミーデータを扱う部品群を一度に切り替えられるようにしています。また、先ほどのDAOパターンを適用して、データの取得部分を隠蔽しています。
まずは、AbstractFactoryクラスに相当するクラスから見ていきましょう。
DaoFactoryインターフェースは、2つの部品を生成するためのAPIが定義されています。createItemDaoメソッドとcreateOrderDaoメソッドです。特に難しいところはありませんね。
DaoFactoryインターフェース(DaoFactory.class.php)
<?php interface DaoFactory { public function createItemDao(); public function createOrderDao(); }
次は、DaoFactoryインターフェースを実装したクラスです。それぞれのcreateItemDaoメソッドとcreateOrderDaoメソッドに注目してください。DbFactoryクラスは実データ、MockFactoryクラスはダミーデータをそれぞれ扱うDAOオブジェクトを生成していることが分かります。
DbFactoryクラス(DbFactory.class.php)
<?php require_once 'DaoFactory.class.php'; require_once 'DbItemDao.class.php'; require_once 'DbOrderDao.class.php'; class DbFactory implements DaoFactory { public function createItemDao() { return new DbItemDao(); } public function createOrderDao() { return new DbOrderDao($this->createItemDao()); } }
MockFactoryクラス(MockFactory.class.php)
<?php require_once 'DaoFactory.class.php'; require_once 'MockItemDao.class.php'; require_once 'MockOrderDao.class.php'; class MockFactory implements DaoFactory { public function createItemDao() { return new MockItemDao(); } public function createOrderDao() { return new MockOrderDao(); } }
続いて、DAOに関連するクラス群を見ていきましょう。
ItemDaoインターフェースは商品情報、OrderDaoインターフェースは注文情報をそれぞれ取得するためのAPIを定義しています。これらのインターフェースは、AbstractProductクラスに相当します。
このサンプルアプリケーションでは、findByIdメソッドにIDを渡すことで情報を取り出せるようにしています。
ItemDaoクラス(ItemDao.class.php)
<?php interface ItemDao { public function findById($item_id); }
OrderDaoクラス(OrderDao.class.php)
<?php interface OrderDao { public function findById($order_id); }
次はConcreteProductクラスに相当するクラスです。
先のAbstractFactoryクラスとConcreteFactoryクラスの関係と同様に、実データを扱うクラスとダミーデータを扱うクラスに分かれています。
実データを扱うクラスはDbItemDaoクラスとDbOrderDaoクラスです。このサンプルアプリケーションでは、商品情報と注文情報をテキストファイルに保存してあり、DbItemDaoクラスとDbOrderDaoクラスがインスタンス化されるときに読み込まれ、商品・注文の各オブジェクトが生成されるようになっています。
データファイルと商品クラス、注文クラスについては、後ほど説明します。
DbItemDaoクラス(DbItemDao.class.php)
<?php require_once 'ItemDao.class.php'; require_once 'Item.class.php'; class DbItemDao implements ItemDao { private $items; public function __construct() { $fp = fopen('item_data.txt', 'r'); /** * ヘッダ行を抜く */ $dummy = fgets($fp, 4096); $this->items = array(); while ($buffer = fgets($fp, 4096)) { $item_id = trim(substr($buffer, 0, 10)); $item_name = trim(substr($buffer, 10)); $item = new Item($item_id, $item_name); $this->items[$item->getId()] = $item; } fclose($fp); } public function findById($item_id) { if (array_key_exists($item_id, $this->items)) { return $this->items[$item_id]; } else { return null; } } }
DbOrderDaoクラス(DbOrderDao.class.php)
<?php require_once 'OrderDao.class.php'; require_once 'Order.class.php'; class DbOrderDao implements OrderDao { private $orders; public function __construct(ItemDao $item_dao) { $fp = fopen('order_data.txt', 'r'); /** * ヘッダ行を抜く */ $dummy = fgets($fp, 4096); $this->orders = array(); while ($buffer = fgets($fp, 4096)) { $order_id = trim(substr($buffer, 0, 10)); $item_ids = trim(substr($buffer, 10)); $order = new Order($order_id); foreach (split(',', $item_ids) as $item_id) { $item = $item_dao->findById($item_id); if (!is_null($item)) { $order->addItem($item); } } $this->orders[$order->getId()] = $order; } fclose($fp); } public function findById($order_id) { if (array_key_exists($order_id, $this->orders)) { return $this->orders[$order_id]; } else { return null; } } }
一方のダミーデータを扱うクラスは、MockItemDaoクラスとMockOrderDaoクラスとなります。これらのクラスは、決まった商品データと注文データを返します。
MockItemDaoクラス(MockItemDao.class.php)
<?php require_once 'ItemDao.class.php'; require_once 'Item.class.php'; class MockItemDao implements ItemDao { public function findById($item_id) { $item = new Item('99', 'ダミー商品'); return $item; } }
MockOrderDaoクラス(MockOrderDao.class.php)
<?php require_once 'OrderDao.class.php'; require_once 'Order.class.php'; class MockOrderDao implements OrderDao { public function findById($order_id) { $order = new Order('999'); $order->addItem(new Item('99', 'ダミー商品')); $order->addItem(new Item('99', 'ダミー商品')); $order->addItem(new Item('98', 'テスト商品')); return $order; } }
次に、商品クラスと注文クラスを説明します。これらのクラスは、純粋に商品の情報や注文の情報を内部に保持するだけの役割を担っています。
商品を表すItemクラスは、コンストラクタに商品情報(商品IDと商品名)を受け取り、その値にアクセスするためのメソッドが用意されているだけの単純なクラスです。
Itemクラス(Item.class.php)
<?php class Item { private $id; private $name; public function __construct($id, $name) { $this->id = $id; $this->name = $name; } public function getId() { return $this->id; } public function getName() { return $this->name; } }
注文を表すOrderクラスは、コンストラクタで注文IDを受け取り、注文情報に含まれる商品情報はaddItemメソッドを通じて追加します。また、注文IDや含まれている商品情報へアクセスするためのメソッドも用意されています。
Orderクラス(Order.class.php)
<?php class Order { private $id; private $items; public function __construct($id) { $this->id = $id; $this->items = array(); } public function addItem(Item $item) { $id = $item->getId(); if (!array_key_exists($id, $this->items)) { $this->items[$id]['object'] = $item; $this->items[$id]['amount'] = 0; } $this->items[$id]['amount']++; } public function getItems() { return $this->items; } public function getId() { return $this->id; } }
ここで、実データについて見ておきましょう。
商品情報と注文情報は、固定長データとして用意しました。フォーマットはデータファイル内の先頭行にもありますが、詳細は表を参照してください。
商品情報(item_data.txt)
商品ID 商品名 1 限定Tシャツ 2 ぬいぐるみ 3 クッキーセット
商品情報のファイルフォーマット
項目 | 開始位置 | 終了位置 | 備考 |
---|---|---|---|
商品ID | 1 | 10 | |
商品名 | 11 | - |
注文情報(order_data.txt)
注文ID 商品ID 1 3 2 1,3 3 1,2,3 4 2 5 2,3
注文情報のファイルフォーマット
項目 | 開始位置 | 終了位置 | 備考 |
---|---|---|---|
注文ID | 1 | 10 | |
商品ID | 11 | - | 複数の場合、商品IDをカンマ区切りで繋げる |
ようやく最後のクライアント側コードの説明です。
注目していただきたいのが、このコードには具体的な部品クラス、つまりConcreteProductクラスに相当するクラスが一切出てきていないということです。
唯一登場しているのがConcreteFactoryクラスに相当するDbFactoryクラスとMockFactoryクラスです。つまり、ConcreteFactoryクラスを切り替えるだけで、部品群を綺麗に切り替えることができています。
クライアント側コード(abstract_factory_client.php)
<?php if (isset($_POST['factory'])) { $factory = $_POST['factory']; switch ($factory) { case 1: include_once 'DbFactory.class.php'; $factory = new DbFactory(); break; case 2: include_once 'MockFactory.class.php'; $factory = new MockFactory(); break; default: throw new RuntimeException('invalid factory'); } $item_id = 1; $item_dao = $factory->createItemDao(); $item = $item_dao->findById($item_id); echo 'ID=' . $item_id . 'の商品は「' . $item->getName() . '」です<br>'; $order_id = 3; $order_dao = $factory->createOrderDao(); $order = $order_dao->findById($order_id); echo 'ID=' . $order_id . 'の注文情報は次の通りです。'; echo '<ul>'; foreach ($order->getItems() as $item) { echo '<li>' . $item['object']->getName(); } echo '</ul>'; } ?> <hr> <form action="" method="post"> <div> DaoFactoryの種類: <input type="radio" name="factory" value="1">DbFactory <input type="radio" name="factory" value="2">MockFactory </div> <div> <input type="submit"> </div> </form>
最後にAbstract Factoryパターンを適用したアプリケーションのクラス図は次のようになっていますので確認してみてください。
適用例の実行結果
Abstract Factoryパターンを適用したサンプルの実行結果は、次のようになります。
Abstract Factoryパターンのオブジェクト指向的要素
Abstract Factoryパターンは「ポリモーフィズム」を活用したパターンです。
具体的な部品クラスであるConcreteProductクラスは、それぞれの部品ごとに共通のAPIを持っています。これは親クラスであるAbstractProductクラスで定義されています。つまり、クライアント側はAbstractProductクラスのAPIだけを使ってプログラミングすることで、具体的なConcreteProductクラスに依存しないコードになります。
このことは、部品工場であるAbstractProductクラスとConcreteFactoryクラスにも言えます。この2つのクラスには親子関係がありますが、親クラスであるAbstractFactoryクラスで提供されるAPIだけを使うことで、どのConcreteFactoryクラスなのかを意識する必要がなくなり、具体的なConcreteFactoryクラスに依存しないコードになります。
また、工場から生成される部品はAbstractProductクラスとして生成されますので、ここでも、どのConcreteProductクラスなのかを意識する必要がありません。
このため、どのConcreteFactoryクラスを使うかを切り替えるだけで、芋づるのように生成されるConcreteProductクラス群を切り替えることができます。
オブジェクト指向プログラミングでは、具体的なクラスではなくインターフェースや抽象化されたクラスのAPIに対してプログラミングすることが重要になります。
関連するパターン
- Factory Methodパターン、Prototypeパターン
AbstractFactoryクラスはFactory Methodパターンを使って実装される場合が多いですが、Prototypeパターンを適用して実装することも可能です。
- Singletonパターン
ConcreteFactoryクラスにSingletonパターンが適用される場合が多くあります。
まとめ
ここでは関連し合う部品群を生成するAbstract Factoryパターンについて見てきました。