Born Too Late

Yuya's old tech blog.

PHP のマジックメソッドによる動的 Observer Pattern

2010-03-06 23:40:44

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 メソッドを使えば、簡単に同じようなことが実現できると思います。