Observerパターンから学ぶSymfonyEventDispatcherの実装

SymfonyEventDispatcherは、デザインパターンの一種であるObserverパターンで実装されたライブラリです。
symfonyではこのライブラリを介してフレームワーク内の様々な処理を行っています。

Observerパターン

SymfonyEventDispatcherを理解する上で前提となるのが、このObserverパターンについての知識です。まずこちらの説明から。

Observerパターンとは?

オブジェクトの状態を他のオブジェクトから観察し、状態が変化した場合に観察者側にそれが通知される仕組みです。
このパターンは、主に以下のようなクラスから構成されます。

観察者(オブザーバ/Observer)
  • リスナー/Listener、ハンドラとも呼ばれる
  • Subjectの状態変化を観察し、変化が通知されると登録されている処理を行う。
観察対象(サブジェクト/Subject)
  • 自分を観察しているObserverのリストを保持している。
  • 自分の状態が変化すると、それをObserverに通知する

ここで注意しておきたいのが、観察者/観察対象とありますが実際に「観察者が観察対象を監視している」訳ではありません。状態変化があった際は「観察対象が観察者に通知」します。観察者は報告を受けて初めて状態変化に気づきます。

クラス図

  • Observerパターンをクラス図にすると以下のようになります。

Observer, Subjectにはそれぞれインターフェイスが定義されていて、具体的な実装はConcreteObserver, ConcreteSubjectにされています。(Concreteは"具体的な"って意味ですね)
こうすることにより、ConcreteObserverとConcreteSubjectの間に依存関係を作ることなく、処理を実装することができます。

サンプルコード

簡単なObserverパターンのサンプルを作ってみます。
セットされた時間帯にあった挨拶とランダムな顔文字を出力するプログラムです。(全然このパターンで書く必要ないのですがまぁ簡単な例ということで。。)

<?php
/**
 * Subjectインターフェイス
 */
interface Subject
{   
    public function attach(Observer $observer);
    public function detach(Observer $observer);
    public function notify();
}
/**
 * Observerインターフェイス
 */
interface Observer {
    public function update(Subject $subject);
}
/**
 * ConcreteSubjectクラス
 */
class ConcreteSubject implements Subject
{
    private $observers = array();   // 登録されたObserverを保持
    private $hour = 0;              // 時間帯
    
    public function __construct() {}
    
    // Observerを登録
    public function attach(Observer $observer)
    {
        $this->observers[get_class($observer)] = $observer; 
    }
    
    // Observerの登録を解除
    public function detach(Observer $observer)
    {
        unset($this->observers[get_class($observer)]);
    }
    
    // 通知を受け、登録されたObserverの処理を実行
    public function notify()
    {
        foreach ($this->observers as $observer) {
            $observer->update($this);
        }
    }
    
    public function setHour($hour)
    {
        $this->hour = $hour;
    }
    
    public function getHour()
    {
        return $this->hour;
    }
}
/**
 * ConcreteObserverクラス1
 */
class GreetingObserver implements Observer
{
    public function __construct() {}
    
    public function update(Subject $subject)
    {
        $hour = $subject->getHour();
        
        if ($hour >= 5 && $hour < 11) {
            echo "おはよう";
        } elseif ($hour >= 11 && $hour < 19) {
            echo "こんにちは";
        } else {
            echo "こんばんは";
        }
    }
}
/**
 * ConcreteObserverクラス2
 */
class FacemarkObserver implements Observer
{
    public $facemarks = array("\(^o^)/", "(・∀・)", "(´・ω・`)");
    
    public function __construct() {}
    
    public function update(Subject $subject)
    {
        $key = array_rand($this->facemarks, 1);
        echo $this->facemarks[$key],PHP_EOL;
    }
}

// クライアント側
$subject = new ConcreteSubject();
$grObserver = new GreetingObserver();
$fmObserver = new FacemarkObserver();
// Observerを登録
$subject->attach($grObserver);  // GreetingObserverを登録 ・・・1
$subject->attach($fmObserver);  // FacemarkObserverを登録 ・・・1

$subject->setHour(7); // 状態変化 ・・・2
$subject->notify();   // 通知   ・・・3

$subject->setHour(12);
$subject->notify();

$subject->detach($fmObserver);  // FacemarkObserverの登録を解除 ・・・4
$subject->setHour(23);
$subject->notify();
実行結果
おはよう\(^o^)/
こんにちは(・∀・)
こんばんは

時刻をプロパティに持つとあるクラス(Subject)が1つと、このSubjectの状態が変化した際に通知を受けるクラス(Observer)が2種類用意されています。
SubjectにはあらかじめこのObserverを2つ登録しておき(1)、時刻が変化(2)するとそれを通知(3)します。
Subjectでは通知を受けると、登録されたObserverの処理を行います。1つ目に登録されたGreetingObserverでは時間に合わせたテキストを出力し、2つ目のFacemarkObserverではランダムに顔文字を出力します。
FacemarkObserverの登録を解除(4)すると、次の通知からは顔文字の出力はされません。
このように、Observerを登録したり解除したりすることで、Subjectの中身を書き換えることなく、状態変化があった際の処理を変えることが出来ます。

SymfonyEventDispatcher

次に、このObserverパターンで実装されているSymfonyEventDispatcherについてです。
柔軟性を持たせるために、シンプルなObserverパターンとは少し違う形で作られています。

SymfonyEventDispatcherとは?

SymfonyEventDispatcherでは、複数のSubjectとObserverをsfEventDispatcherというクラスで管理するようになっています。また、状態の変化はイベント(sfEvent)を用いて通知されます。
先ほどのような通常のObserverパターンでは、SubjectのインスタンスがなければObserverを登録することが出来ませんが、symfonyではObserverはSubjectに対してではなくイベント名に対して登録されるため、SubjectがなくてもObserverを登録することが出来ます。
SymfonyEventDispatcherは以下のようなクラスから構成されます。

sfEvent

イベントに関する情報(観察対象(Subject)のインスタンスや、イベント名、イベントの状態など)を保持するクラスです。

sfEventDispatcher

イベント名とObserverの結びつけの保持や、Observerへの通知を行うクラスです。

サンプルコード

SymfonyEventDispatcherを使って、先ほどと同じプログラムを書いてみます。
(Listenerの部分は、Observerと読み替えてください)

<?php
class greetingListener
{
    public function __construct() {}
    	
	public function update(sfEvent $event)
	{
	    $hour = $event["hour"];
	    
	    if ($hour >= 5 && $hour < 11) {
	        echo "おはよう";
	    } elseif ($hour >= 11 && $hour < 19) {
	        echo "こんにちは";
	    } else {
	        echo "こんばんは";
	    }
	}
}
class FacemarkListener
{
    public $facemarks = array("\(^o^)/", "(・∀・)", "(´・ω・`)");
    
    public function __construct() {}
    
    public function update(sfEvent $event)
    {
        $key = array_rand($this->facemarks, 1);
        echo $this->facemarks[$key],PHP_EOL;
    }
}

$dispatcher = new sfEventDispatcher();

$grListener = new GreetingListener();
$fmListener = new FacemarkListener();
$dispatcher->connect("test.greeting", array($grListener, "update"));  // test.greetingというイベントに対してGreetingListenerのupdate処理を登録 ・・・1
$dispatcher->connect("test.greeting", array($fmListener, "update"));  // test.greetingというイベントに対してFacemarkListenerのupdate処理を登録 ・・・1

// 状態変化をイベントとしてdispatcherに通知 ・・・2、3
$dispatcher->notify(new sfEvent(null, "test.greeting", array("hour"=>7)));
$dispatcher->notify(new sfEvent(null, "test.greeting", array("hour"=>12)));
$dispatcher->disconnect("test.greeting", array($fmListener, "update"));  // test.greetingというイベントに対するFacemarkListenerの登録を解除 ・・・4
$dispatcher->notify(new sfEvent(null, "test.greeting", array("hour"=>23)));
実行結果
おはよう(´・ω・`)
こんにちは\(^o^)/
こんばんは

dispatcherにはあらかじめイベント名とListenerの処理を2つ登録しておき(1)、時刻が変化するとそれをイベントとしてdispatcherに通知します。(2、3)
dispatcherでは、通知を受けると、受け取ったイベント名に対して登録されているLitenerの処理を呼び出します。
FacemarkListenerの登録を解除(4)すると、次の通知からは顔文字の出力はされません。
間にdispatcherが挟まっていますが、行っている処理としては最初に出したサンプルと同じですよね。

このように、symfonyではdispatcherを用いることでフレームワーク内のどこからでも柔軟にイベントの管理を行うことが出来ます。
今回は通知するイベントの種類としてnotify()しか載せていませんが、他にもnotifyUntil()、filter()といったものもありますので、マニュアル等参考に使い分けてみてください。