hnwの日記

Symfony Event Dispatcherで遊んでみた

Symfony Event Dispatcher とは

Symfony Event DispatcherというのはPHPフレームワークであるsymfonyに含まれるライブラリで、GoF*1のObserverパターンの変種です。


このクラスは現在Symfony Event Dispatcherという名前で単体公開されていますので、symfony以外のプロジェクトでも気軽に利用できると思います。全部で300行程度の小さいライブラリで、テストコードが29件書かれています。


また、ガイドとAPIリファレンスが書かれています。レシピ集を見ればsymfonyでどう利用されているかを垣間見ることが出来ます。テストカバレッジ率100%なんてことも書いてありますね。


本稿では、PHP5に対応したObserverパターンの実装としてのSymfony Event Dispatcherについて紹介します。

Observerパターンのメリット

GoFパターンの中でも、Observerパターンはメリットがわかりにくいパターンなのではないでしょうか。僕だけかもしれませんが、拡張性を提供するという視点で言えば他のパターンでも十分な状況が多いので、何がどう素晴らしいのか最初はピンと来ませんでした。


具体例として、自作のクラスについて、ログを取る方法についての拡張性をユーザーに提供することを考えます。何もしないwriteLogメソッドを用意して、クラス内のあちこちでself::writeLogを呼び出すような実装にしたと仮定しましょう。デフォルトでは何もログが残りませんが、ログを取りたいプログラマは、継承したクラスにwriteLogメソッドを実装します。こうすればログの記録方法に関する拡張性を提供できます。

<?php
abstract class AbstractClass {
  protected abstract function writeLog($msg);
  public function someMethod() {
    ...
    $this->writeLog("some error");
    ...
  }
  public function anotherMethod() {
    ...
    $this->writeLog("another error");
    ...
  }
}
class MyClass extends AbstractClass{
  protected function writeLog($msg)
  {
    syslog(LOG_INFO, $msg);
  }
}
$myclass = new MyClass();
$myclass->someMethod();
$myclass->anotherMethod();


これはTemplate Methodパターンになっています。このパターンは継承を素直に利用しているので意図が分かりやすいですよね。後になって「ログの内容をメールで飛ばしたい」と言われた場合には、AbstractClassを継承した別のクラスを作り、そのwriteLogメソッドで$msgの内容をメールすれば良いわけです。


このように、拡張性をTemplate Methodパターンで提供すれば十分なこともあるでしょう。でも、「インスタンス生成後にログの記録方法を動的に変更したい」「2種類のログを同時に取りたい」などと言われてしまうと少し困ってしまいます。こうした場合にはObserverパターンで拡張性を提供した方が良いかもしれません。


上記の例では、AbstractClassクラスがtemplate method(上の例ではWritelogメソッド)を呼び出すことで拡張性を提供していました。これをObserverパターンで実装するとしたら、subject(観察されている人)がログの内容をobserver(観察する人)に通知するような実装にしておきます。複数のログを取りたい場合には、ログを記録するメソッドをobserverとして実装し、必要に応じてsubjectに登録します。こうすることで、2種類のobserverを登録すれば2種類のログが取れます。また、動的にobserverを登録・解除することで好きなタイミングでログの形式を切り替えることが可能です。


以下にSymfony Event Dispatcherでの実装例を示します。

<?php

require_once(dirname(__FILE__).'/lib/sfEventDispatcher.php');

class DispatcherHolder {
  private static $dispatcher = null;
  static function getDispatcher()
  {
    if (!self::$dispatcher) {
      self::$dispatcher=new sfEventDispatcher();
    }
    return self::$dispatcher;
  }
}
class SomeClass {
  protected $errorMessage = null;
  public function setErrorMessage($msg) {
    $this->errorMessage = $msg;
  }
  public function getErrorMessage() {
    return $this->errorMessage;
  }
  public function someMethod() {
    ...
    $this->setErrorMessage("some error");
    $ev = new sfEvent($this, "log.warn");
    DispatcherHolder::getDispatcher()->notify($ev);
    ...
  }
  public function anotherMethod() {
    ...
    $this->setErrorMessage("another error");
    $ev = new sfEvent($this, "log.warn");
    DispatcherHolder::getDispatcher()->notify($ev);
    ...
  }
}
class MyListener{
  public function logWithSyslog(sfEvent $ev)
  {
    syslog(LOG_ERR, $ev->getSubject()->getErrorMessage());
  }
  public function logWithMail(sfEvent $ev)
  {
    mail("log@example.com", "log from system", $ev->getSubject()->getErrorMessage());
  }
}

$myclass = new SomeClass();
$mylistener = new MyListener();
DispatcherHolder::getDispatcher()->connect('log.warn', array($mylistener, 'logWithSyslog'));
DispatcherHolder::getDispatcher()->connect('log.warn', array($mylistener, 'logWithMail'));

$myclass->someMethod();
$myclass->anotherMethod();


上のコードを説明すると、log.warnという名前のイベントに対してMyListener::logWithSyslog()と、MyListener::logWithMail()という2つのメソッドを登録しています。


リスナー(=observer)に通知を行うには、sfEventDispatcher::notify()を利用します。第1引数で指定したイベントのイベント名に対応するリスナーメソッドが呼ばれます。


継承ベースでの例に比べ、Observerパターンを使うと柔軟な拡張性を持たせることができます。一方で、関係するクラス数もメソッド数も増えており、少々煩雑になっています。


この例からもわかるように、どのデザインパターンを採用すべきかには常にトレードオフがあります。複雑なものを採用した方が将来の変更に対応しやすくなるかもしれません。しかし、変更が無い場合にはコード量と複雑さを増やすだけになってしまいます。


僕の考えるObserverパターンの使いどころは、次のような場合です。

  • インスタンスの挙動を動的に変更したい場合
    • 今回のログの例なら、致命的エラーが起きた場合に限りログの方式を変更したいなど
  • インスタンスの挙動を変更する際、選択肢の中の複数を採用することがある場合
    • 今回のログの例なら、syslogとメールとを併用したいなど


GoF本に書いてあるObserverパターンの「目的」も紹介しておきます。

目的


あるオブジェクトが状態を変えたときに、それに依存するすべてのオブジェクトに自動的にそのことが知らされ、また、それらが更新されるように、オブジェクト間に一対多の依存関係を定義する。

Symfony Event Dispatcherの特徴

Symfony Event DispatcherはGoF本で紹介されているObserverパターンの変種だと紹介しましたが、目的によってはオーバースペックかもしれません。僕の考えるSymfony Event Dispatcherの特徴は次の点です。

  • subjectとobserverの間を取り持つクラスが存在する
  • observerの登録がメソッド単位になっている


それほど特別な特徴ではないと思いますが、これらについて順に見ていきましょう。

subjectとobserverの間を取り持つクラスが存在する

要するに、sfEventDispatcherが存在すること、イベント名が導入されていることで、機能と柔軟性が増しているということです。一方で、複雑性も増しています。


GoFのObserverパターンの最もシンプルな実装では、observerの登録はSubjectクラスのAttachメソッドの引数としてobserverのインスタンスを渡すことで行います。言い換えると、observerの登録を行うにはsubjectのインスタンスを把握しておく必要があります。


Symfony Event Dispatcherであれば、subjectとリスナー(=observer)との間をsfEventDispatcherクラスが取り持っていること、更にリスナーの登録をイベント名に対して行うため、リスナーはsubjectのインスタンスを知らなくても構いませんし、対応するsubjectが複数個あっても対応できます。それどころか、subjectのインスタンスが存在しなくても問題ありません。


また、イベント名ごとに適切なリスナーに通知してくれるので、複数種類のイベントが存在する場合には見通しがよくなります。


捕足しておくと、GoFでもObserverパターンにMediatorパターンを組み合わせてChangeManagerクラスを導入するという話題がありますので、GoFでも十分考慮されているバリエーションだとは思います。

observerの登録がメソッド単位になっている

GoF本にならえば、observerはabstractObserverを継承したクラスであり、そのnotifyメソッドを使って通知を受けるわけですが、これは静的型付け言語ならではの制約といえます。


そもそも通知の際にはobserverのnotifyメソッドしか使わないわけですから、observerのインスタンスを登録する代わりにメソッドを登録しても問題ないですよね。実際にSymfony Event Dispatcherはそうなっています。上の例で言えば、sfEventDispatcher::connectの第2引数でインスタンスとメソッド名の組を渡しています。


これなら1クラスに複数のobserverを作ることも可能ですから、ソースコードをシンプルに保てます。また、登場するクラスの増加を抑えられるので、性能面でもメリットになります。

他のライブラリとの違い

PEAR::Event_Dispatcherも同様のクラスなのですが、Symfony Event Dispatcherと極めて似ています。sfEventDispatcher=Event_Dispatcher、sfEvent=Event_Notificationという対応でしょう。


PEAR::Event_Dispatcherに対するSymfony Event Dispatcherのメリットとして、PHP5に対応していること、開発が継続していること、変数値のフィルタリングを前提とした通知(sfEventDispatcher::filter()メソッド)ができること、といった点が挙げられます。


一方、PEAR::Event_Dispatcherの方が高機能な部分もあります。ざっとマニュアルを見たところでは、次の機能をSymfony Event Dispatcherで実装するのは面倒そうです。

  • イベント名に関わらず、登録されている全observerに通知する
  • observer登録時に、登録以前に送られたイベントを通知する


これとは別に、SPLにもSplObserverとSplSubjectというインターフェースがあります。これはGoFの最もシンプルなObserverパターンを実現するものだと思いますが、シンプルすぎて使いどころが難しい気がします。

PHPのバージョンについて

Symfony Components - Standalone libraries for PHPにはPHP5.2.4以降が必須だと書いてありますが、Symfony Event DispatcherについてはPHP5.0.0以降の全バージョンで全部のテストが通りました。ご参考まで。

*1:オブジェクト指向における再利用のためのデザインパターン』のことをマニアの人が省略するときの言い方です