PHP のマジックメソッドによる動的 Observer Pattern
2010-03-07 15:30 追記
Observer Pattern について調べ直してみたところ、一般的な Obsesrver Pattern と、以下のコードは似て非なるものであることに気づきました。
このエントリは GoF パターンの教材にはなり得ないので、その点に注意してお読みください。
入浴中にふと思いつき、書いてみたコードを紹介。
Observer Pattern については詳しく紹介しないので、興味のある方は Wikipedia の Observer パターンの頁をご覧ください。かくいう私自身も、いわゆる GoF パターンとしての Observer Pattern を暗記しているわけではないのですが、Observer Pattern 的にはなっていると思います。
Notifier.php
監視者にメッセージを送る通知者クラス。
class Notifier { private $_observers = array(); public function addObserver($observer) { $this->_observers[] = $observer; return $this; } public function __call($methodName, $args) { foreach ($this->_observers as $observer) { call_user_func_array(array($observer, $methodName), $args); } return $this; } }
そして、次に監視者クラスを 2 つほど定義。
まずは文字列をテキストファイルに書き込んでいくロガー。
Logger.php
class Logger { private $_fp; public function __construct($fileName) { $this->_fp = fopen($fileName, 'a'); } public function __destruct() { fclose($this->_fp); } public function putString($str) { $this->_log($str); } public function putArray($arr) { foreach ($arr as $str) { $this->_log($str); } } private function _log($str) { fputs($this->_fp, date('Y-m-d H:i:s') . "\t" . $str . "\n"); } }
次は、顔文字の吹き出しとして、文字列を出力するもの。
Speaker.php
class Speaker { private $_face; public function __construct($face) { $this->_face = $face; } public function putString($str) { $this->_speak($str); } public function putArray($arr) { foreach ($arr as $str) { $this->_speak($str); } } public function _speak($str) { echo $this->_face . ' < ' . $str . "\n"; } }
そして、実際に実行するのは以下のコード。
main.php
require_once './Notifier.php'; require_once './Logger.php'; require_once './Speaker.php'; // 通知者オブジェクトの生成 $notifier = new Notifier; // 通知者に、監視者オブジェクトを登録する $notifier->addObserver(new Logger('./log.txt')); $notifier->addObserver(new Speaker('( `・ω・´)')); $str = "Pentagram / Relentless (1985)"; $arr = array( "01. Death Row", "02. All Your Sins", "03. Sign of the Wolf (Pentagram)", "04. The Ghoul", "05. Relentless", "06. Run My Course", "07. Sinister", "08. The Deist", "09. You're Lost I'm Free", "10. Dying World", "11. 20 Buck Spin" ); // いろいろなメソッドを実行 $notifier->putString($str); $notifier->putArray($arr);
これを実行すると、こうなります。
$ php main.php ( `・ω・´) < Pentagram / Relentless (1985) ( `・ω・´) < 01. Death Row ( `・ω・´) < 02. All Your Sins ( `・ω・´) < 03. Sign of the Wolf (Pentagram) ( `・ω・´) < 04. The Ghoul ( `・ω・´) < 05. Relentless ( `・ω・´) < 06. Run My Course ( `・ω・´) < 07. Sinister ( `・ω・´) < 08. The Deist ( `・ω・´) < 09. You're Lost I'm Free ( `・ω・´) < 10. Dying World ( `・ω・´) < 11. 20 Buck Spin
そして、同時に、以下のようなログファイルまで出力されます。
2010-03-06 22:42:09 Pentagram / Relentless (1985) 2010-03-06 22:42:09 01. Death Row 2010-03-06 22:42:09 02. All Your Sins 2010-03-06 22:42:09 03. Sign of the Wolf (Pentagram) 2010-03-06 22:42:09 04. The Ghoul 2010-03-06 22:42:09 05. Relentless 2010-03-06 22:42:09 06. Run My Course 2010-03-06 22:42:09 07. Sinister 2010-03-06 22:42:09 08. The Deist 2010-03-06 22:42:09 09. You're Lost I'm Free 2010-03-06 22:42:09 10. Dying World 2010-03-06 22:42:09 11. 20 Buck Spin
これがどう嬉しいのか。Observer Pattern の例としてよくあるのは、通知者オブジェクトの notify メソッドが呼び出されると、監視者オブジェクトたちの update メソッドを実行していく、というパターン。対する、この Notifier オブジェクトは、notify 的メソッドをいくつでも追加できるという特性を持ちます。いわば、動的 Observer Pattern。
putString メソッドも、putArray メソッドも、Notifier は知りませんが、自分が知らないメソッドを受けとると、監視者オブジェクトたちに「putString してください」「putArray してください」というメッセージを、引数を伴って通知して回るのです。
これを実現するのが、以前のメタプログラミングに関する記事で紹介した、__call というマジックメソッド。以前の繰り返しになりますが説明すると、実行しようとしたメソッドがなかったときの転送先のメソッドです。引数には、実行しようとしたメソッド名と、そのメソッドに渡された引数を受けます。
ところで Ruby には Observable という、Observer Pattern を実現するためのモジュールがあります。非常に動的な特性を持つ Ruby のことですから、上記の Notifier と同じような仕様になっているのではと思い、調べてみたところ、notify されたら update していくだけの普通のものでした。つまり、静的 Observer Pattern。
これは我ながらおもしろいアイディアではないかなと思っているのですが、やはり、PHP よりはむしろ Ruby のほうがしっくりパターンかもしれません。method_missing メソッドを使えば、簡単に同じようなことが実現できると思います。