PHPによるデザインパターン入門 - Mediator〜すべては相談役が知っている
このエントリは、Do You PHP?(www.doyouphp.jp)で公開していたコンテンツを移行/加筆/修正したものです。公開の経緯はこちらをどうぞ。目次はこちらです。サンプルコードを手直ししたものをgithubに上げてありますのでそちらもどうぞ。
GoF本における分類
振る舞い+オブジェクト
はじめに
ここではMediatorパターンについて説明します。
「mediator」とは「仲介者」「調停者」という意味です。
Mediatorパターンは、クラスどうしの複雑なやりとりを一極集中するためのパターンです。
では、早速見ていきましょう。
たとえば
突然ですが、みなさんは無線LANをご存じでしょうか?職場や学校、ご自宅でも使っている方は多いと思います。最近のノートパソコンは、まず間違いなく無線LANが使えるようになっていますね。
それでは、無線LANの接続形態にはどの様なものがあるでしょうか?それは、アドホックモードとインフラストラクチャモードです。前者はアクセスポイントを経由しないで、パソコンどうしでデータ通信をおこなうのに対し、後者はアクセスポイントを経由して他のパソコンとデータ通信をおこないます。
では、話を戻しましょう。オブジェクト指向プログラミングでは、お互いにやりとりをおこなうオブジェクトの集まりとしてプログラミングをおこないます。この「やりとり」を実現する最も簡単な方法は何でしょうか?そう、他のオブジェクトのメソッドを直接呼び出すことです。
しかし、オブジェクトどうしがお互いにやりとりをしてしまっては都合が悪い場合があります。特にオブジェクトの数が増えた場合、オブジェクトどうしのやりとりがあちらこちらで発生することになります。その結果、オブジェクトどうしの関係が分かりにくくなります。また、やりとりのための処理はそれぞれのオブジェクトに存在していますので、非常にメンテナンスしづらい状況に陥ってしまいます。
これはまさにアドホックモードと同じ状態です。
ここで、オブジェクトどうしのやりとりを仲介してくれる「仲介者」がいた場合はどうでしょうか?つまり、オブジェクトどうしのやりとりをインフラストラクチャモードにするということです。
この場合、他のオブジェクトとのやりとりは必ず仲介者を通じておこなうことになり、どのオブジェクトとやりとりをおこなうかは、仲介者が面倒を見るようにします。こうすると、オブジェクトは「どのオブジェクトのどのメソッドを呼び出す」といった難しいことを考える必要はなく、仲介者にお願いするだけで他のオブジェクトとやりとりができます。また、オブジェクトどうしの具体的なやりとりの手順が仲介者に集中しますので、管理も容易になります。
Mediatorパターンは、このような場面で適用されるパターンです。
Mediatorパターンとは?
Mediatorパターンはオブジェクトの振る舞いに注目したパターンで、やりとりをおこなうオブジェクトどうしの結びつきを緩めることを目的としています。
GoF本では、Mediatorパターンの目的は次のように定義されています。
オブジェクト群の相互作用をカプセル化するオブジェクトを定義する。Mediatorパターンは、オブジェクト同士がお互いを明示的に参照し合うことがないようにして、結合度を低めることを促進する。それにより、オブジェクトの相互作用を独立に変えることができるようになる。
Mediatorパターンは、関連しあうクラス群のやりとりを1つのオブジェクトに集約させ、クラス群のやりとりの仲介者とします。こうすると、クラスどうしのやりとりを一極集中させる、つまり仲介者とクラスの関係を一対多にします。それぞれのクラスは、仲介者のクラスだけを知っていれば、他のクラスを知らなくてもお互いやりとりをおこなうことができます。このため、知らなければならないクラスの数が減ることになり、結果としてクラスどうしの関連を弱くできます。
また、それぞれのクラスに「どのタイミングでどのクラスのどのメソッドを呼び出す」といった具体的なやりとりの内容を記述する必要がなくなりますので、修正をおこなう場合に大きなメリットとなります。
Mediatorパターンの構造
Mediatorパターンのクラス図と構成要素は、次のようになります。
- Mediatorクラス
仲介者のクラスです。Colleagueクラスからの通知を受け取るメソッドを定義します。
- ConcreteMediatorクラス
Mediatorクラスのサブクラスで、Mediatorクラスで定義されたメソッドを実装します。
- Colleagueクラス
Mediatorクラスに通知をおこなうクラスです。「colleague」とは「同僚」「仲間」という意味です。Mediatorクラスからの通知を受け取るメソッドを定義する場合もあります。
- ConcreteColleagueクラス
Colleagueクラスのサブクラスです。内部にMediatorオブジェクトを保持しており、このMediatorオブジェクトを通じて他のConcreteColleagueクラスとやりとりをおこないます。
Mediatorパターンのメリット
Mediatorパターンのメリットとしては、以下のものが挙げられます。
- クラスどうしのやりとりを一極集中する
Mediatorパターンを適用すると、クラスどうしのやりとりはMediatorオブジェクトを通じておこなわれるようになります。これにより、やりとりの具体的な方法をMediatorクラスに持たせることができます。
見方を変えると、それぞれのColleagueオブジェクトどうしがどういった関係なのかが集中する事になり、不明瞭になりがちなColleagueオブジェクトどうしの関係を明確にする助けにもなります。
- Colleagueオブジェクトの結びつきをゆるくする
Mediatorクラスを通じて他のColleagueオブジェクトにアクセスすることで、Colleagueオブジェクトどうしの結びつきがゆるくなり、MediatorクラスとColleagueクラスの関係は一対多になります。
Mediatorパターンの適用例
Mediatorパターンの適用例を見てみましょう。
ここでは、複数のユーザがチャットを使ってやりとりをしている模様を表すシンプルなアプリケーションを用意しました。
まずは、ユーザを表すクラスです。このクラスは、ColleagueクラスとConcreteColleagueクラスの両方に相当します。
Userクラス(User.class.php)
<?php require_once 'Chatroom.class.php'; class User { private $chatroom; private $name; public function __construct($name) { $this->name = $name; } public function getName() { return $this->name; } public function setChatroom(Chatroom $value) { $this->chatroom = $value; } public function getChatroom() { return $this->chatroom; } public function sendMessage($to, $message) { $this->chatroom->sendMessage($this->name, $to, $message); } public function receiveMessage($from, $message) { printf('<font color="008800">%sさんから%sさんへ</font>: %s<hr>', $from, $this->getName(), $message); } }
特徴的なのは、Userクラスの中にチャットする場を表すChatroomクラスのインスタンスを保持していることです。sendMessageメソッドで他のユーザにメッセージを送信しますが、この時直接そのユーザオブジェクトにアクセスするのではなく、このChatroomオブジェクトに送信しています。
次は、チャットする場であるChatroomクラスです。このクラスは、MediatorクラスとConcreteMediatorクラスに相当しています。
Chatroomクラスでは、ユーザーを表すUserオブジェクトを保持し、入室しているユーザを管理しています。
Chatroomクラス(Chatroom.class.php)
<?php require_once 'User.class.php'; class Chatroom { private $users = array(); public function login(User $user) { $user->setChatroom($this); if (!array_key_exists($user->getName(), $this->users)) { $this->users[$user->getName()] = $user; printf('<font color="#0000dd">%sさんが入室しました</font><hr>', $user->getName()); } } public function sendMessage($from, $to, $message) { if (array_key_exists($to, $this->users)) { $this->users[$to]->receiveMessage($from, $message); } else { printf('<font color="#dd0000">%sさんは入室していないようです</font><hr>', $to); } } }
また、ユーザどうしのメッセージの流れを管理しています。このアプリケーションでは、受け取ったメッセージを単純にユーザオブジェクトのreceiveMessageメソッドに渡しています。
次はクライアント側のコードです。
まず、Chatroomオブジェクトを生成し、それぞれのユーザを入室させています。そして、ユーザーどうしでメッセージのやりとりをおこなっています。
クライアント側コード(mediator_client.php)
<?php require_once 'Chatroom.class.php'; require_once 'User.class.php'; $chatroom = new Chatroom(); $sasaki = new User('佐々木'); $suzuki = new User('鈴木'); $yoshida = new User('吉田'); $kawamura = new User('川村'); $tajima = new User('田島'); $chatroom->login($sasaki); $chatroom->login($suzuki); $chatroom->login($yoshida); $chatroom->login($kawamura); $chatroom->login($tajima); $sasaki->sendMessage('鈴木', '来週の予定は?') ; $suzuki->sendMessage('川村', '秘密です') ; $yoshida->sendMessage('萩原', '元気ですか?') ; $tajima->sendMessage('佐々木', 'お邪魔してます') ; $kawamura->sendMessage('吉田', '私事で恐縮ですが…') ;
最後にサンプルコードのクラス図を示しておきます。
Mediatorパターンのオブジェクト指向的要素
Mediatorパターンはオブジェクトどうしのやりとりを「カプセル化」するパターンです。
冒頭にも出てきましたが、オブジェクト指向プログラミングでは、お互いにやりとりをおこなうオブジェクトの集まりとしてプログラミングをおこないます。このやりとりしあうオブジェクトの数が少ない場合、その複雑さは問題にならないかもしれません。しかし、オブジェクトの数が少し増えただけでも、お互いの関連の数はあっという間に増えてしまいます。
Mediatorパターンでは、これを「ColleagueオブジェクトはMediatorオブジェクトとやりとりをする」という非常にシンプルな処理に置き換えて、Colleagueオブジェクトどうしの具体的なやりとりやColleagueクラスどうしの関連を隠蔽しています。
関連するパターン
- Façadeパターン
Façadeパターンもクラスの集まりを対象にしたパターンです。
Mediatorパターンはクラスどうしのやりとり自体を抽出しますが、Façadeパターンはサブシステムを利用するためのAPIを抽出するパターンです。また、Mediatorパターンは構成しているオブジェクトと双方向のやりとりをおこなう場合がありますが、Façadeパターンではサブシステムに対して要求を送信する一方向のやりとりになります。
- Observerパターン
Mediatorパターンに似たパターンとしてObserverパターンがあります。MediatorパターンはColleagueオブジェクトどうしのやりとりの仲介者を用意します。一方のObserverパターンでは、Observerクラス自身が処理をおこないます。
また、Observerパターンを適用することでColleagueオブジェクトとMediatorオブジェクトを通信させることができます。
まとめ
ここでは複雑なクラス間のやりとりを一元管理するMediatorパターンについて見てきました。
PHPによるデザインパターン入門 - Interpreter〜言語の文法表現を通訳する
このエントリは、Do You PHP?(www.doyouphp.jp)で公開していたコンテンツを移行/加筆/修正したものです。公開の経緯はこちらをどうぞ。目次はこちらです。サンプルコードを手直ししたものをgithubに上げてありますのでそちらもどうぞ。
GoF本における分類
振る舞い+クラス
はじめに
ここではInterpreterパターンについて説明します。
「interpreter」とは「通訳者」「解釈者」という意味ですが、何を「通訳」「解釈」するのでしょうか?それは言語の文法表現です。
「言語」といっても「ある規則に従った文字列」と捉えると、HTMLやXMLなど文字通りの「言語」だけではなく、CSVなどのデータフォーマットも「言語」と言えるでしょう。
身近な例としてプログラミング言語を考えてみましょう。プログラミング言語には本書で取り上げているPHPを始め、PerlやRubyなどのコンパイルが不要な言語と、JavaやCなどのコンパイルが必要な言語があります。どのプログラミング言語を使っても最終的には記述したコードが実行されます。
ここで、コードが実行される手順を大まかに説明すると、次のようになります。
1番目の字句解析と2番目の構文解析がおこなわれた結果は木構造として表すことができ、構文木(Abstract Syntax Tree)と呼ばれます。たとえば、「$result = $a / 2 + 3 * $b / 2」の構文木は次のように表せます。
「$result = $a / 2 + 3 * $b / 2」の構文木
Interpreterパターンは、この得られた構文木を処理するための最適なパターンです。
たとえば
とある決まった問題がたびたび発生する場合、その問題を文章として表せられると便利なことがあります。これは「ミニ言語」と呼ばれる手法です。
ミニ言語は比較的古くから使われており、PHPのsprintf関数で指定する「%5d」のような表記や正規表現などがあります。
Interpreterパターンは、このようなミニ言語を実装する場合に適用されるパターンです。
たとえば、バッチ処理を考えてみましょう。バッチ処理では、とある決まった基本的な処理を順番に実行したり繰り返し実行して、大きな処理をおこないます。通常、この基本的な処理はOS固有のコマンドですが、「バッチ処理言語」として実装することもできます。この場合、バッチ処理言語固有の規則で処理を記述することになりますが、複雑な処理を1つの命令として記述できたり、具体的なOSや言語に依存しないようになります。
Interpreterパターンとは?
Interpreterパターンはクラスの振る舞いに注目したパターンで、文法を解析し、その結果を利用して処理をおこなうことを目的としています。
GoF本では、Interpreterパターンの目的は次のように定義されています。
言語に対して、文法表現と、それを使用して文を解釈するインタプリタを一緒に定義する。
Interpreterパターンでは文法で定義されている規則をクラスとして表し、それに対する振る舞いを併せて定義します。この規則とは、構文木における節や葉に相当します。そして、そのクラスのインスタンスを繋げることで構文木そのものを表現しつつ、構文木を処理します。
Interpreterパターンの構造
Interpreterパターンのクラス図と構成要素は、次のようになります。
- AbstractExpressionクラス
- TerminalExpressionクラス
AbstractExpressionクラスのサブクラスで、構文木の葉に相当する末端の規則を表します。また、AbstractExpressionクラスで定義されたメソッドを実装します。
- NonterminalExpressionクラス
AbstractExpressionクラスのサブクラスで、構文木の節に相当します。内部には、他の規則へのリンクを保持しています。また、AbstractExpressionクラスで定義されたメソッドを実装します。
- Contextクラス
構文木を処理するために必要な情報を保持するクラスです。
- Clientクラス
AbstractExpressionクラスを利用するクラスです。処理する構文木を作成したり、外部から与えられたりします。
Interpreterパターンのメリット
Interpreterパターンのメリットとしては、以下のものが挙げられます。
- 規則の追加や変更が簡単
Interpreterパターンの特徴の1つに、「1つの規則を1つのクラスで表す」というものが挙げられます。つまり、新しい規則を追加する場合はAbstractExpressionクラスのサブクラスを追加するだけで良くなります。
また、規則を修正する場合も、AbstractExpressionクラスのサブクラスを修正するだけです。
Interpreterパターンの適用例
Interpreterパターンの適用例として、簡単なミニ言語を作ってみましょう。
このミニ言語の文法は、次のように定義しました。
<Job> ::= begin <CommandList> <CommandList> ::= <Command>* end <Command> ::= diskspace | date | line
簡単に文法を説明しますと、この言語は「begin」という文字列で始まります。その間にコマンドの一覧を記述します。また、そのコマンド一覧は0個以上のコマンドで構成されており、「end」という文字列で終わります。コマンドは「diskspace」「date」「line」のいずれかになります。
この記述方法は、BNF(Backus Naur Form)と呼ばれる記法で、コンピュータ言語の文法を定義する際に使われます。また、RFC(Request for Comment)*1でもよく使われています。
さて、この文法をInterpreterパターンを使って表すとどうなるでしょうか?
何となくお気づきの方もいらっしゃるかもしれませんね。そう、BNFで記述された文法の一番左にある「
では、早速コードを見ていきましょう。
まずは、AbstractExpressionクラスに相当するCommandインターフェースからです。
Commandインターフェースでは、構文木に共通なAPIであるexecuteメソッドを定義しているだけです。
Commandクラス(Command.class.php)
<?php interface Command { public function execute(Context $context); }
次は、BNFにおける「
JobCommandクラス(JobCommand.class.php)
<?php class JobCommand implements Command { public function execute(Context $context) { if ($context->getCurrentCommand() !== 'begin') { throw new RuntimeException('illegal command ' . $context->getCurrentCommand()); } $command_list = new CommandListCommand(); $command_list->execute($context->next()); } }
executeメソッドに注目してください。このクラスではミニ言語の文法のうち、「
<Job> ::= begin <CommandList>
次は、「
CommandListCommandクラス(CommandListCommand.class.php)
<?php class CommandListCommand implements Command { public function execute(Context $context) { while (true) { $current_command = $context->getCurrentCommand(); if (is_null($current_command)) { throw new RuntimeException('"end" not found '); } elseif ($current_command === 'end') { break; } else { $command = new CommandCommand(); $command->execute($context); } $context->next(); } } }
コマンドの一覧からコマンドを取り出して1つずつ実行し、「end」が現れるとそこで終了します。このクラスも「
>||
|
続けてCommandCommandクラスです。このクラスはTerminalExpressionクラス、つまり構文木の「葉」にあたるクラスです
CommandCommandクラス(CommandCommand.class.php)
<?php class CommandCommand implements Command { public function execute(Context $context) { $current_command = $context->getCurrentCommand(); if ($current_command === 'diskspace') { $path = './'; $free_size = disk_free_space('./'); $max_size = disk_total_space('./'); $ratio = $free_size / $max_size * 100; echo sprintf('Disk Free : %5.1dMB (%3d%%)<br>', $free_size / 1024 / 1024, $ratio); } elseif ($current_command === 'date') { echo date('Y/m/d H:i:s') . '<br>'; } elseif ($current_command === 'line') { echo '--------------------<br>'; } else { throw new RuntimeException('invalid command [' . $current_command . ']'); } } }
このクラスも「
<Command> ::= diskspace | date | line
さて、ここまで見てきたコードに必要なクラスで、まだ説明していないクラスがあります。構文木の情報を保持するためのContextクラスです。
このクラスは、現在構文木のどこを解析しているか、つまり、現在解析の対象となっているコマンドや次に出てくるコマンドを管理しています。
Contextクラス(Context.class.php)
<?php class Context { private $commands; private $current_index = 0; private $max_index = 0; public function __construct($command) { $this->commands = split(' +', trim($command)); $this->max_index = count($this->commands); } public function next() { $this->current_index++; return $this; } public function getCurrentCommand() { if (!array_key_exists($this->current_index, $this->commands)) { return null; } return trim($this->commands[$this->current_index]); } }
最後に、入力フォームに入力されたコマンドの実行結果を表示するクライアント側のコードです。
interpreter_clientクラス(interpreter_client.php)
<?php require_once 'Context.class.php'; require_once 'Command.class.php'; require_once 'CommandCommand.class.php'; require_once 'CommandListCommand.class.php'; require_once 'JobCommand.class.php'; function execute($command) { $job = new JobCommand(); try { $job->execute(new Context($command)); } catch (Exception $e) { echo htmlspecialchars($e->getMessage(), ENT_QUOTES, mb_internal_encoding()); } echo '<hr>'; } $command = (isset($_POST['command'])? $_POST['command'] : ''); if ($command !== '') { execute($command); } ?> <form action="" method="post"> input command:<input type="text" name="command" size="80" value="begin date line diskspace end"> <input type="submit"> </form>
コマンドの初期値として「begin date line diskspace end」というコマンドを設定していますが、このコマンドの構文木は次のようになります。
Interpreterパターンのオブジェクト指向的要素
Interpreterパターンは「ポリモーフィズム」を非常に活用したパターンです。
ここまで見てきたように、AbstractExpressionクラスではそれぞれの規則に共通なAPIを提供しています。そして、AbstractExpressionクラスのサブクラスであるTerminalExpressionクラスやNonterminalExpressionクラスで、それぞれの規則の具体的な処理内容をこのこのAPIに実装しています。
また、NonterminalExpressionクラスは内部に他の規則、つまりTerminalExpressionオブジェクトもしくはNonterminalExpressionオブジェクトを保持しています。自身の処理によって、内部に保持したオブジェクトに次の処理を依頼します。
ここで、TerminalExpressionクラスもNonterminalExpressionクラスも同じAbstractExpression型のオブジェクトとして扱うことができます。ということは、内部に保持したオブジェクトに処理を依頼する際、そのオブジェクトがTerminalExpressionオブジェクトなのかNonterminalExpressionオブジェクトなのかを意識する必要がなくなります。
この「具体的な実装を意識する必要がない」ため、新しい規則を表すクラスを容易に追加できるのです。
関連するパターン
- Compositeパターン
気づいた方もいるかと思いますが、構文木を形成するAbstractExpressionクラスとそのサブクラスの構造はCompositeパターンと非常に似ています。
- Flyweightパターン
TerminalExpressionクラスにFlyweightパターンが適用される場合があります。
- Visitorパターン
構文木の各要素に対する振る舞いを1つのクラスにまとめたい場合、Visitorパターンが利用できます。
まとめ
ここでは構文木を構成・処理するInterpreterパターンについて見てきました。
こう見ると、Interpreterパターンは他のGoFパターンと比べても用途が非常に具体的なパターンと言えますね。
PHPによるデザインパターン入門 - Flyweight〜同じものは一度しか作らない
このエントリは、Do You PHP?(www.doyouphp.jp)で公開していたコンテンツを移行/加筆/修正したものです。公開の経緯はこちらをどうぞ。目次はこちらです。サンプルコードを手直ししたものをgithubに上げてありますのでそちらもどうぞ。
GoF本における分類
構造+クラス、オブジェクト
はじめに
ここではFlyweightパターンについて説明します。
flyweightと言えば、ボクシングの「フライ級」を連想する人も多いと思いますが、その通りです。Flyweightパターンは、フライ級のオブジェクトを効率よく共有するためのパターンなのです。オブジェクトを共有、つまり使いまわすことでメモリの節約を図ります。
たとえば
商品情報を格納するクラスを考えてみましょう。
商品情報には、商品番号や商品名などの「状況によって変わらない情報」と在庫数などの「状況によって変わる情報」があります。
ある商品を表すオブジェクトを生成するには、この商品クラスをインスタンス化しますが、「状況によって変わらない情報」を持つオブジェクトを毎回インスタンス化するのはもったいないですね。なぜなら、インスタンス化された商品オブジェクトは、それも全く同じ情報を持つものだからです。
一方で在庫数をこの商品クラスに持たせるのは得策ではありません。なぜなら、インスタンス化したタイミングで在庫数が変わっている可能性があるためです。さもないと、在庫が余りすぎたり足りなかったりしてしまうでしょう。
また、多くのシステムでは商品情報をデータベースに格納しています。つまり、商品クラスをインスタンス化する際にデータベースから商品情報を取得していることが多くなります。このため、毎回インスタンス化するコストが無視できない場合も出てきます。
こういった場合、どのような対処をおこなえば良いでしょうか?
一度インスタンス化した後にそのインスタンスを使いまわした方が良さそうです。そうすれば、インスタンス化のコストを大きく抑えることができます。また、インスタンス化に伴うメモリの消費量も抑えられると期待できます。
ここで説明するFlyweightパターンは、一度インスタンス化したオブジェクトを使い回し、生成されるオブジェクトの数やリソースの消費を抑えます。
Flyweightパターンとは?
Flyweightパターンは、オブジェクトの構造に注目したパターンで、多くのオブジェクトを効率よく扱うことを目的としています。
GoF本では、Flyweightパターンの目的は次のように定義されています。
多数の細かいオブジェクトを効率よくサポートするために共有を利用する。
Flyweightパターンでは、生成されるクラスとそのインスタンスを生成・管理するファクトリに分かれています。
クライアント側でそのクラスのインスタンスが必要になった場合、ファクトリに生成を依頼し、インスタンスを入手します。一方のファクトリ側は、生成したインスタンスを内部に保持します。再び生成の依頼を受けた場合、その保持したインスタンスを返します。つまり、一度生成したインスタンスをそのまま使いまわすわけです。
こうすることで、必要となったインスタンスだけ作成することになり、使用するメモリや生成時のコストを抑えることができます。
何か良いことずくめのようですが、Flyweightパターンを適用する場合に注意することがあります。それは、どのオブジェクトを共有して良いか、という点です。
「情報」には、環境や状況によって変化しないものと、そうでないものがあります。前者は「intrinsic」(「本質的な」の意)、後者は「extrinsic」(「非本質的な」の意)と呼ばれます。
環境や状況によって変化してしまう情報を、あちらこちらで共有してしまうと、他のすべての共有場所にその影響が出てしまうことになります。たとえば、誕生日は環境や状況によって変化しませんので、intrinsicな情報ということになります。しかし、年齢は時が経てば変わってしまいます。つまり、extrinsicな情報ということになります。
Flyweightパターンの構造
Flyweightパターンのクラス図と構成要素は、次のようになります。
- Flyweightクラス
共有するオブジェクトの共通APIを定義します。
- ConcreteFlyweightクラス
Flyweightクラスのサブクラスで、Flyweightクラスで定義されたAPIを実装するクラスです。このクラスのインスタンスが共有されますので、intrinsicな情報のみ保持するようにします。
- UnsharedConcreteFlyweightクラス
Flyweightクラスのサブクラスで、Flyweightクラスで定義されたAPIを実装するクラスです。このクラスのインスタンスは共有されません。従って、extrinsicな情報を保持しても構いません。
- FlyweightFactoryクラス
- Clientクラス
Flyweight型のオブジェクトへの参照を保持します。
Flyweightパターンのメリット
Flyweightパターンのメリットとしては、以下のものが挙げられます。
- 生成されるオブジェクトの数を抑える
Flyweightパターンでは、オブジェクトを共有するパターンです。一度生成したインスタンスを内部に保存しておき、再び生成する必要が生じた場合、その保持したインスタンスを返すようにしています。
このため、毎回newしてオブジェクトを生成するよりも、実際に生成されるオブジェクトの数を圧倒的に抑えることができます。
- リソースの消費を抑える
冒頭では、使用メモリの節約について説明しました。Flyweightパターンでは、メモリ以外のリソースも節約することができます。たとえば、インスタンスを生成する時間がそうです。インスタンスを生成することは、非常に時間がかかる処理の1つです。Flyweightパターンでは一度生成したオブジェクトを使いまわすことで、使用メモリを節約すると共に、生成にかかる時間も節約します。これによって、プログラムのパフォーマンスを上げることができます。
Flyweightパターンの適用例
では、Flyweightパターンの適用例を見てみましょう。
ここでは、ファイルに格納された商品情報から商品オブジェクトを生成し、一覧表示するアプリケーションです。また、生成した同じ商品オブジェクトを比較し、同一のオブジェクトかどうかを確認しています。
まずは、商品情報を格納するItemクラスから見てみます。
このクラスはコンストラクタに渡された商品情報を内部に保持し、その情報にアクセスするためのメソッドを用意しただけのシンプルなものです。このクラスには特に難しいところはないでしょう。
このオブジェクトにはintrinsicな情報、つまり、環境や状況によって変化しない情報だけが格納されます。
Itemクラス(Item.class.php)
<?php /** * FlyweightとConcreteFlyweightに相当する */ class Item { private $code; private $name; private $price; public function __construct($code, $name, $price) { $this->code = $code; $this->name = $name; $this->price = $price; } public function getCode() { return $this->code; } public function getName() { return $this->name; } public function getPrice() { return $this->price; } }
次は、商品オブジェクトを生成・管理するItemFactoryクラスです。このクラスは、Singletonパターンも適用されています。
ItemFactoryクラス(ItemFactory.class.php)
<?php require_once 'Item.class.php'; /** * FlyweightFactoryに相当する * また、Singletonパターンにもなっている * * なお、このサンプルではUnsharedConcreteFlyweightオブジェクトを * 返すメソッドは用意されていない */ class ItemFactory { private $pool; private static $instance = null; /** * コンストラクタ * このサンプルでは、インスタンス生成時に保持するオブジェクトを * すべて生成している */ private function __construct($filename) { $this->buildPool($filename); } /** * Factoryのインスタンスを返す */ public static function getInstance($filename) { if (is_null(self::$instance)) { self::$instance = new ItemFactory($filename); } return self::$instance; } /** * ConcreteFlyweightを返す */ public function getItem($code) { if (array_key_exists($code, $this->pool)) { return $this->pool[$code]; } else { return null; } } /** * データを読み込み、プールを初期化する */ private function buildPool($filename) { $this->pool = array(); $fp = fopen($filename, 'r'); while ($buffer = fgets($fp, 4096)) { list($item_code, $item_name, $price) = split("\t", $buffer); $this->pool[$item_code] = new Item($item_code, $item_name, $price); } fclose($fp); } /** * このインスタンスの複製を許可しないようにする * @throws RuntimeException */ public final function __clone() { throw new RuntimeException ('Clone is not allowed against ' . get_class($this)); } }
このクラスで注目するのは、buildPoolメソッドとgetItemメソッドです。
buildPoolメソッドはプライベートメソッドとなっており、コンストラクタから呼び出されています。このメソッドでは、ファイルに保存された商品情報を読み込んで商品インスタンスを生成し、そのインスタンスを内部に保持しています。
一方のgetItemメソッドでは、指定された商品番号のインスタンスをそのまま返しています。これにより、一度生成された商品インスタンスが使いまわされていることが分かると思います。
なお、商品データファイルは次のとおりで、タブ区切りテキストとなっています。
商品データファイル(data.txt)
ABC0001 限定Tシャツ 3800 ABC0002 ぬいぐるみ 1500 ABC0003 クッキーセット 800
そして、クライアント側のコードです。
まずはItemFactoryクラスのインスタンスを取得し、商品番号を指定して商品オブジェクトを取得しています。
また、商品番号「ABC0001」については、再度インスタンスを取得し、1度目に取得したインスタンスと2度目に取得したインスタンスを比較しています。
クライアント側コード(flyweight_client.class.php)
<?php require_once 'ItemFactory.class.php'; function dumpData($data) { echo '<dl>'; foreach ($data as $object) { echo '<dt>' . htmlspecialchars($object->getName(), ENT_QUOTES, mb_internal_encoding()) . '</dt>'; echo '<dd>商品番号:' . $object->getCode() . '</dd>'; echo '<dd>\\' . number_format($object->getPrice()) . '-</dd>'; } echo '</dl>'; } $factory = ItemFactory::getInstance('data.txt'); /** * データを取得する */ $items = array(); $items[] = $factory->getItem('ABC0001'); $items[] = $factory->getItem('ABC0002'); $items[] = $factory->getItem('ABC0003'); if ($items[0] === $factory->getItem('ABC0001')) { echo '同一のオブジェクトです'; } else { echo '同一のオブジェクトではありません'; } dumpData($items);
最後に、Flyweightパターンを適用したサンプルのクラス図を確認しておきましょう。
Flyweightパターンのオブジェクト指向的要素
「オブジェクト指向的な要素」という視点から見ると、Flyweightパターンはちょっと特殊な形をしています。あえて言えば、処理の「カプセル化」を利用しているパターンです。
Flyweightパターンでは、Flyweight型のオブジェクトを内部に保持して管理しています。同時に、他のクラスからFlyweight型のオブジェクトを取得する方法も提供しています。ただし、一度保持したオブジェクトが再度必要になったとき、新たに生成するのではなく、保持したオブジェクトを返します。この結果、オブジェクトを生成する処理をクライアント側から隠蔽することができます。
関連するパターン
- Compositeパターン
Compositeパターンは、Flyweightパターンと併用されることが多いパターンです。
- Stateパターン、Strategyパターン
StateパターンやStrategyパターンは、Flyweightパターンと併用すると効率的な実装ができる場面が多いパターンです。
まとめ
ここではたくさんの小さなオブジェクトを共有するFlyweightパターンについて見てきました。
PHPによるデザインパターン入門 - Decorator〜かぶせて機能UP
このエントリは、Do You PHP?(www.doyouphp.jp)で公開していたコンテンツを移行/加筆/修正したものです。公開の経緯はこちらをどうぞ。目次はこちらです。サンプルコードを手直ししたものをgithubに上げてありますのでそちらもどうぞ。
GoF本における分類
構造+オブジェクト
はじめに
ここではDecoratorパターンについて説明します。
Decoratorとは「装飾者」という意味があります。そもそも装飾するというのは、基本となる物がありそこに様々な効果をもたらす要素を加えていく行為です。クリスマスツリーに飾りをつけるのもそうですし、アイスクリームにトッピングをすることもそうです。
Decoratorパターンも同様なイメージとして捉えることができます。つまり、基本となるものに対して様々な機能をひとつひとつ加えていき、あたかも装飾するかのようなイメージです。
たとえば
世の中には様々なアプリケーションが存在しています。そのほとんどには画面があり、カスタマイズできるものが多くあります。
たとえば、Webブラウザを考えてみましょう。Webページを表示した時、一画面に収まりきらない場合には横スクロールバーや縦スクロールバーが表示されます。また、縦スクロールバーもしくは横スクロールバーだけ表示される時もありますし、両方表示される場合もあります。
このスクロールバーは、「Webページを表示する」というWebブラウザの基本的な機能に「画面をスクロールさせる」という機能を追加したものと捉えることができます。その他にも、様々な効果をもたらす機能がブラウザにありますが、いずれもWebブラウザの機能を拡張したものと言えるでしょう。
さて、それぞれのクラスには様々な機能があります。つまり、「責任」を持っているということです。この責任を追加したい場合、つまり機能を拡張したい場合、どの様にすれば良いでしょうか?
とっさに思いつくのは、継承を使ってサブクラス化することです。継承を使うことで、親クラスの責任を受け継ぎつつ、新しい責任を追加することができます。しかし、この「継承」が時として不便になってくる場合があります。
先ほどのスクロールバーの例で考えてみましょう。たとえば、「横スクロールバーは実装しないが、縦スクロールバーは実装したい」や「さらにその逆の実装パターンも存在する」といった要求が出てきた場合、それぞれの機能を実装したクラスが必要になります。
これは継承による機能拡張は「静的」な拡張だからです。言い換えると、クラスのコードを作成した時点でその責任が決定されるということです。コードを書いた時点で機能が決まってしまうため、当然ですが機能を組み合わせることが非常に難しくなります。
つまり、すべてのパターンを網羅しようとすると、作成すべきクラスの数が非常に多くなってしまいます。また、新しい機能を追加する場合も、すべてのパターンを網羅するために…これは非常に大変そうです。
また、「ユーザの操作によってスクロールバーを追加したり取り外したりしたい」といった要求もあるでしょう。継承を利用して機能を拡張した場合、このような要求に応えることも容易ではありません。
このような場面で、Decoratorパターンが有用になってきます。
Decoratorパターンとは?
Decoratorパターンはオブジェクトの構造に注目したパターンで、オブジェクトに対して機能を柔軟に追加したり取り外したりすることを目的としています。
GoF本では、Decoratorパターンの目的は次のように定義されています。
オブジェクトに責任を動的に追加する。Decorator パターンは、サブクラス化よりも柔軟な拡張方法を提供する。
クラスの機能、つまりクラスの責任を拡張するにはサブクラス化、つまり継承によって実現しますが、継承による拡張は、責任を「静的」に追加することを意味します。
一方、Decoratorパターンでは責任を「動的」に追加することができます。つまり、実行中に責任を追加したり外したりできるということです。これにより、オブジェクトの柔軟な機能拡張が可能です。
Decoratorパターンの構造
Decoratorパターンのクラス図は次のようになります。
- Componentクラス
拡張される機能を定義した抽象クラスです。
- ConcreteComponentクラス
Componentクラスで定義した機能を基本実装する、飾り付けされる具象クラスです。
- Decoratorクラス
Componentを継承し、さらにメンバ変数としてComponentを保持する抽象クラスです。
自身のOperationメソッドではComponentのOperationを呼び出すようにします。つまりOperationメソッドの具体的な実装をComponentへ委譲します。
- ConcreteDecoratorAクラス、ConcreteDecoratorBクラス
Componentに機能を追加するためにDecoratorクラスを継承し、自身のOperationメソッドで親クラスのOperationメソッドを利用しながら、機能の拡張(飾り付け)を行います。
Decoratorパターンのメリット
- 柔軟な拡張が可能
既存クラスのメソッドに機能を追加する場合、サブクラスを作成し、既存クラスの機能追加したいメソッドをオーバーライドする事が一般的かもしれません。つまり継承です。しかし、追加したい「機能」のパターンが複数ある場合、また、追加したい「機能」は他のパターンを踏まえた上で実装されることがある場合、さらには追加するパターンに「順序」が存在する場合、そのパターンの組み合わせを考慮したクラス設計が必要になります。機能が静的に追加されている状態であり、必要なパターンの組み合わせの数分、サブクラスの作成が必要となります。
こういった場合に、Decoratorパターンを適応することで、機能を動的に追加できるようになり、オブジェクトを利用する側でパターンの選択ができるようになります。Decoratorパターンは、継承ではなく委譲を利用することにより、柔軟な機能の拡張が可能となっています。
- 機能の実装を階層構造の上位で定義しなくて済む
Decoratorパターンは機能の枠を上層で定義(インターフェースなど)し、それに対しての実装をDecoratorで定義できるため、クラス階層の上層で機能を定義する必要がなくなります。こうすることで、再度編集する際に上層のクラスに手を入れる必要がなくなり、メンテナンス性の向上に繋がります。
Decoratorパターンの適用例
Decoratorパターンの適用例を見てみましょう。
ここでは、入力された文字列を加工し表示するアプリケーションにDecoratorパターンを適用してみます。文字列の加工には、以下のような種類を用意しました。
文字列の加工の種類
対応するクラス | 機能の概要 |
---|---|
UpperCaseText | 入力された文字の半角小文字を半角大文字に変換する |
DoubleByteText | 入力された文字の半角文字を全角文字に変換する |
まずは、Componentクラスに相当するTextインターフェースから見てみましょう。
このインターフェースは文字列の入力、出力のためのメソッドを定義したインターフェースです。
Textインターフェース(Text.class.php)
<?php /** * テキストを扱うインターフェースクラスです */ interface Text { public function getText(); public function setText($str); }
続けて、Textインターフェースを実装したPlainTextクラスです。ConcreteComponentクラスに相当します。
このクラスは、加工する前の文字列を管理するためのクラスです。
PlainTextクラス(PlainText.class.php)
<?php require_once('Text.class.php'); /** * 編集前のテキストを表すクラスです */ class PlainText implements Text { /** * インスタンスが保持する文字列です */ private $textString = null; /** * インスタンスが保持する文字列を返します */ public function getText() { return $this->textString; } /** * インスタンスに文字列をセットします */ public function setText($str) { $this->textString = $str; } }
次はDecoratorクラスに相当するTextDecoratorクラスです。
TextDecoratorクラスは、Textインターフェースを継承しさらに、Text型のインスタンスを変数として保持しています。そして、getTextメソッドやsetTextメソッドでは保持しているText型のインスタンスを実行しています。
TextDecoratorクラス(TextDecorator.class.php)
<?php require_once('Text.class.php'); /** * Textクラスを修飾するDecoratorです */ abstract class TextDecorator implements Text { /** * Text型の変数です */ private $text; /** * インスタンスを生成します */ public function __construct(Text $target) { $this->text = $target; } /** * インスタンスが保持する文字列を返します */ public function getText() { return $this->text->getText(); } /** * インスタンスに文字列をセットします */ public function setText($str) { $this->text->setText($str); } }
そして、次は実際の「decorate」をおこなうクラス群です。
ConcreteDecoratorクラスに相当するUpperCaseTextクラスとDoubleByteTextクラスは、TextDecoratorを継承するクラスです。
これらのクラスのgetTextメソッドでは、内部に保持したText型オブジェクトのgetTextメソッドを実行します。そして、その戻り値に自分自身が担当する処理を施し、結果を返します。
UpperCaseTextクラスでは半角小文字を半角大文字に変換、DoubleByteTextクラスでは半角文字を全角文字に変換する処理を、それぞれおこないます。
UpperCaseTextクラス(UpperCaseText.class.php)
<?php require_once('TextDecorator.class.php'); /** * TextDecoratorクラスの実装クラスです */ class UpperCaseText extends TextDecorator { /** * インスタンスを生成します */ public function __construct(Text $target) { parent::__construct($target); } /** * 半角小文字を半角大文字に変換して返します */ public function getText() { $str = parent::getText(); $str = mb_strtoupper($str); return $str; } }
DoubleByteTextクラス(DoubleByteText.class.php)
<?php require_once('TextDecorator.class.php'); /** * TextDecoratorクラスの実装クラスです */ class DoubleByteText extends TextDecorator { /** * インスタンスを生成します */ public function __construct(Text $target) { parent::__construct($target); } /** * テキストを全角文字に変換して返します * 半角英字、数字、スペース、カタカナを全角に、 * 濁点付きの文字を一文字に変換します */ public function getText() { $str = parent::getText(); $str = mb_convert_kana($str,"RANSKV"); return $str; } }
最後にクライアント側のコードです。簡単に動作を確認できるよう、入力用のHTMLフォームも表示します。
入力用のHTMLフォームにはテキストボックスと2つのチェックボックスがあります。テキストボックスには任意の文字列を入力します。チェックボックスは、入力した文字列に対する装飾が選べるようになっています。
クライアント側コード(decorator_client.php)
<?php require_once('UpperCaseText.class.php'); require_once('DoubleByteText.class.php'); require_once('PlainText.class.php'); $text = (isset($_POST['text'])? $_POST['text'] : ''); $decorate = (isset($_POST['decorate'])? $_POST['decorate'] : array()); if ($text !== '') { $text_object = new PlainText(); $text_object->setText($text); foreach ($decorate as $val) { switch ($val) { case 'double': $text_object = new DoubleByteText($text_object); break; case 'upper': $text_object = new UpperCaseText($text_object); break; default: throw new RuntimeException('invalid decorator'); } } echo htmlspecialchars($text_object->getText(), ENT_QUOTES, mb_internal_encoding()) . "<br>"; } ?> <hr> <form action="" method="post"> テキスト:<input type="text" name="text"><br> 装飾:<input type="checkbox" name="decorate[]" value="upper">大文字に変換 <input type="checkbox" name="decorate[]" value="double">2バイト文字に変換 <input type="submit"> </form>
それでは、このサンプルのクラス図を確認しておきましょう。
Decoratorパターンのオブジェクト指向的要素
Decoratorパターンは「ポリモーフィズム」を非常に活用したパターンです。
Decoratorパターンは、責任を「追加する側」のDecoratorクラスが「追加される側」のComponentクラスを包み込んで機能を拡張します。この時、「追加する側」は「追加される側」と同じAPIを持っています。なぜなら、両者ともこの共通APIを定義したComponentクラスを実装しているためです。つまり、Componentクラスの利用側から見ると、責任を「追加する側」と「追加される側」を同一視することができます。言い換えると、責任を「追加する側」のクラスがあろうとなかろうと、また「追加する側」のクラスが具体的にどのConcreteDecoratorクラスかを意識することなく、「追加される側」のクラスを利用できる、ということです。
また、Decoratorクラスは、Component型のオブジェクトを内部に保持しています。このオブジェクトは「Component型である」というだけで、具体的にどのクラスのインスタンスなのかは分かりません。逆に言うと、Component型のオブジェクトであれば、分け隔てなく扱うことができることを意味しています。ここでも具体的なクラスに依存するのではなく、そのインターフェースに依存する構造が利用されています。
関連するパターン
- Adapter パターン
AdapterパターンもDecoratorパターンと同様、オブジェクトを包み込むパターンです。
Decoratorパターンはオブジェクトを包み込むことで、オブジェクトの責任を変化させるパターンです。一方のAdapterパターンは、オブジェクトを包み込んでそのAPIを変化させるパターンです。
- Composite パターン
CompositeパターンはDecoratorパターンの構造と非常に良く似ており、オブジェクトの集約を目的としたパターンです。
- Strategy パターン
Strategyパターンもオブジェクトの責任を変えるためのパターンです。
Decoratorパターンはオブジェクトの責任を変えるために「外側」からアプローチしますが、Strategyパターンは「内側」をごっそり変えるアプローチを採ります。
Componentクラスが大きく、Decoratorパターンを適用した場合にコストがかかりすぎる場合に使用される場合があります。
まとめ
ここではオブジェクトの責任を外側から変えるDecoratorパターンについて見てきました。