【PHPで学ぶデザインパターン入門】第6回 Observerパターン

【PHPで学ぶデザインパターン入門】第6回 Observerパターン

王

こんにちは、王です。
【PHPで学ぶデザインパターン】第6回はObserverパターンのご紹介です。

有名なのでご存知の方も多いと思いますが、「IFTTT」というサービスがあります。

このサービスはIf This Then Thatの頭文字を取ったもので、例えば「私が帰宅したら、彼女に、SMSで知らせるようにする」みたいなことができます。このように、あるイベントが発生したときに特定の行動をするものです。

「Observerパターン」とは

「私が帰宅したら、彼女に、SMSで知らせるようにする」の例で言うと、イベントは「私が帰宅したら」、行動は「彼女にSMSで知らせるようにする」、そしてSMS通知を受け取るのは「彼女」。さらに、通知を受け取る人は彼女に限定する必要はなく、たくさんの人に送ることもできますよね。

これが立派な「Observerパターン」です。
「Observerパターン」は以下の2つの要素から構成されます。

  • Subject(観測したい事象):「私が帰宅すること」
  • Observer(観測者・観測したい事象が発生したときに通知を受け取る者):自分

Event-drivenプログラミングで多用されるパターンです。
JavaScriptを知っている方なら、もしかしたら知らずのうち使っている方も多いかと思います。最近Reactive Programmingが注目を集めているので、Observerパターン関連の語彙を増やしておくと何かと便利です。

目標

結論からいうと、下記のように使います。

// `Subject`を生成
$sms = new SMS();

// SNS(Subject)から通知を受け取るオブジェクト(Observer)を生成
$girlFriend = new GirlFriend( $sms );
$mum        = new Mum( $sms );

上記の結果が、以下の通りです。

今家帰ったよ!晩御飯何にする? (06時27分28秒)
遊びにくる??(06時27分28秒)


今家帰ったよ!晩御飯何にする? (07時27分28秒)
遊びにくる??(07時27分28秒)

実装コード

// 通知を発行するオブジェクト
abstract class Subject {
	protected $observers = array();

	public function registerObserver( $observer ) {
		$this->observers[] = $observer;
	}

	public function removeObserver( $observer ) {
		$oid = $observer->id;

		$this->observers = array_filter( $this->observers, function ( $observer ) use ( $oid ) {
			return $observer->id != $oid;
		} );

	}

	abstract public function notifyObservers();
}

// `Subject`の内部で暗黙的に`notify`メソッドを呼ぶことで、`Subject`からの通知を受け取るオブジェクト
abstract class Observer {
	public $id, $subject;

	abstract public function notify( $time );

	function __construct( Subject $subject ) {
		$this->id      = uniqid();
		$this->subject = $subject;
		$this->subject->registerObserver( $this );
	}
}

// 帰宅時に登録している全Observerに通知を発行するオブジェクト
class SMS extends Subject {
	private $time;

	public function notifyObservers() {
		/** @type Observer $observer */
		foreach ( $this->observers as $observer ) {
			$observer->notify( $this->time );
		}
	}

	public function IAmHome( $time = null ) {
		$this->time = $time ? date( 'h時i分s秒', $time ) : date( 'h時i分s秒' );
		$this->notifyObservers();
	}
}

// 通知を受け取る人々
class GirlFriend extends Observer {
	public function notify( $time ) {
		echo "今家帰ったよ!晩御飯何にする? ($time)<br>";
	}
}

class Mum extends Observer {
	public function notify( $time ) {
		echo "遊びにくる??($time)<br>";
	}
}

//メイン処理

$sms = new SMS();
// SNSから通知を受け取るようにする
$girlFriend = new GirlFriend( $sms );
$mum        = new Mum( $sms );

$sms->IAmHome();
echo "<hr>";
$sms->IAmHome( time() + 3600 ); // 一時間後

クラス図

Observer_pattern

構成要素の再確認

Subject
Observerのコレクションを保持し、コレクションからObserverを削除・追加する機能を持っている。状態変化時にObserverの更新インターフェイス(`notify`)に対応したメソッドをコールする。
Observer
Subjectからの通知(および、そのときの固有のデータ)を受け取る。そのためには通知を受け取るインターフェイス(`notify`)を実装する必要がある。

疎結合(loose coupling)について

このObserverパターンを採用することで、大きなメリットが1つあります。それは、クラス間の結合が緩くなる(loose coupling)ことです。

疎結合とは、細分化された個々のコンポーネント同士の結びつきが比較的緩やかで、独立性が強い状態のことである。

疎結合では、個々のコンポーネント同士は相互に連携しているが、相互に依存している余地が少ない。そのためコンポーネント間の連携をあまり顧慮せず、それぞれのコンポーネントを交換したり改良したりするような柔軟な対処を行うことができる。

疎結合に対して、このコンポーネントが密接に連携している状態は密結合と呼ばれている。密結合状態のシステムは、動作は高速であるが、一方のコンポーネントが異常をきたしてしまうと他方のコンポーネントもその影響を受けてしまう。

疎結合はマルチプロセッサシステムのようなハードウェア的なものから、アプリケーションソフトのようなソフトウェア的なものまで、幅広く見られる状態である。

引用元:疎結合とは (loose coupling) そけつごう: – IT用語辞典バイナリ

オブジェクト指向設計において、クラスやオブジェクトに責務を割り当てるパターンや原則である「GRASP」のうちの1つです。SubjectとObserverは「疎結合」状態にあります。

なぜなら、

  • 互いに連携し合っている、つまり「結合」している
  • お互いのことをよく知らない、つまり依存性が低い→「疎結合」

この「依存性」の低さがポイントですね。

Subjectが知っていることといえば、

Observerオブジェクトにはnotifyというメソッドが必ず実装されている。
それ以外のことに関しては何も知る必要はない。

つまり、notifyというメソッドさえ実装していれば、クラスの種類は問わないということです。

Observerはどうでしょう?

Subjectには必ずregisterObserver()とremoveObserver()がある。
しかも実行してもしなくても何の副作用はない。

極論をすれば、Observerが存在しなくても、Subjectは正常に動く。Observerを増やしたいとき、Subjectに編集を加えることもありません。ObserverあるいはSubjectに編集を加えたとしても、お互いに影響することもありません。

それくらい「結合が緩い」です。かと言って、お互いは確かに連携している関係にある。それが「疎結合」です。

疎結合状態のシステムは柔軟性が高く、変更にも強いアーキテクチャを作ることに貢献します。他のパターンもそうですが、Observerパターンは「疎結合」をよく体現した模範的なパターンと言えます。

Publish/Subscribe パターン

Publish/Subscribeパターンは、Observerパターンが派生したものとみなされています。イベント駆動型のJavaScriptではよく利用されています。例えばブラウザの場合、dom.addEventListener() のようなAPIが「Publish/Subscribe パターン」にあたります。

とりあえず、

  • Publisher イコール Subject
  • Subscriber イコール Observer

と考えてください。

イメージとしては、Publisher(発行者)が発行するイベントをSubscriber(購読者)が購読して、イベントが発行されたら、登録されたコールバック(Subscriber)関数が実行されるのです。

上記の例(メイン処理の部分)を「Publish/Subscribe パターン」を利用して書くと下記になります。

// subscriber1
$message_to_girlfriend = function ( $time ) {
    echo "今家帰ったよ!晩御飯何にする? ($time)<br>";
};

// subscriber2
$message_to_mum = function ( $time ) {
    echo "遊びにくる??($time)<br>";
};

// subscriber3
$message_to_friend = function ( $time ) {
    echo "今暇??($time)<br>";
};

$pubSub = new PubSub();

$disposable = $pubSub->subscribe( 'sms', $message_to_girlfriend );
$pubSub->subscribe( 'sms', $message_to_mum );
$pubSub->subscribe( 'line', $message_to_friend );

$pubSub->publish( 'sms', time() );
echo '<hr>';
$pubSub->publish( 'line', time() + 3600 );
echo '<hr>';
$disposable->dispose(); // `$message_to_girlfriend` を通知リストから削除する
$pubSub->publish( 'sms', time() );

出力

今家帰ったよ!晩御飯何にする? (1424847839)
遊びにくる??(1424847839)


今暇??(1424851439)


遊びにくる??(1424847839)

この例から見て、このパターンの一番大きな特徴はイベント名を動的に定義しているところです。
「Observerパターン」を使って同じことを達成するためには、Subjectを複数個作成しなければならないのですが、「Publish/Subscribe パターン」ではイベントがあらかじめ決められていないため、後から自由にいくらでも追加していくスタイルを取っています。
その他、PublishとSubscribeを完全に分離することで、疎結合化をさらに進めることができたことにも注目すべきです。

実装コード

// `dispose`というメソッドを使ってコールバックを通知リストから削除することができるクラス
class Disposable {
	/** @type  PubSub $pubSub */
	private $callback, $pubSub;

	function __construct( $pubSub, $callback ) {
		$this->pubSub   = $pubSub;
		$this->callback = $callback;
	}

	public function dispose() {
		$this->pubSub->unsubscribe( $this->callback );
	}
}

// Publish/Subscribe クラス
class PubSub {
	private $topics = array();

	function subscribe( $topic, $callback ) {

		if ( ! isset( $this->topics[ $topic ] ) ) {
			$this->topics[ $topic ] = array();
		}

		array_push( $this->topics[ $topic ], $callback );

		return new Disposable( $this, $callback );
	}

	function unsubscribe( $callback ) {

		foreach ( $this->topics as $i => $topic ) {
			foreach ( $topic as $j => $_callback ) {
				if ( $callback === $_callback ) {
					array_splice( $this->topics[ $i ], $j, 1 );

					return $callback;
				}
			}
		}

		return false;
	}

	function publish( $topic ) {

		if ( ! isset( $this->topics[ $topic ] ) ) {
			return false;
		}

		$args = func_get_args();
		array_shift( $args );
		foreach ( $this->topics[ $topic ] as $callback ) {
			call_user_func_array( $callback, $args );
		}

		return $this;
	}
}

// ------------------ メイン処理 ------------------

// subscriber1
$message_to_girlfriend = function ( $time ) {
	echo "今家帰ったよ!晩御飯何にする? ($time)<br>";
};

// subscriber2
$message_to_mum = function ( $time ) {
	echo "遊びにくる??($time)<br>";
};

// subscriber3
$message_to_friend = function ( $time ) {
	echo "今暇??($time)<br>";
};

$pubSub = new PubSub();

$disposable = $pubSub->subscribe( 'sms', $message_to_girlfriend );
$pubSub->subscribe( 'sms', $message_to_mum );
$pubSub->subscribe( 'line', $message_to_friend );

$pubSub->publish( 'sms', time() );
echo '<hr>';
$pubSub->publish( 'line', time() + 3600 );
echo '<hr>';
$disposable->dispose(); // `$message_to_girlfriend` を通知リストから削除する
$pubSub->publish( 'sms', time() );

まとめ

いかがでしたか?
私個人の感覚的には、かなり直感的かつ実用的なパターンだと思います。
特にWhen A Then Bのような実装が必要になったときに、一番最適なパターンではないでしょうか。さまざまな通知システムなどでご利用くださいませ!

 

【PHPで学ぶデザインパターン入門】

【PHPで学ぶデザインパターン入門】第1回 Strategyパターン

【PHPで学ぶデザインパターン入門】第2回 Decoratorパターン

【PHPで学ぶデザインパターン入門】第3回 Stateパターン

【PHPで学ぶデザインパターン入門】第4回 Iteratorパターン

【PHPで学ぶデザインパターン入門】第5回 Factoryパターン

LIGはWebサイト制作を支援しています。ご興味のある方は事業ぺージをぜひご覧ください。

Webサイト制作の実績・料金を見る

この記事のシェア数

LIGの王です。ウェブの全てを学ぶ為、中国は四川省より日本にやってきました。王という名に恥じぬよう、ウェブ業界のKINGとなるべく日々頑張っております。よろしくお願いいたします。

このメンバーの記事をもっと読む
デザイン力×グローバルな開発体制でDXをトータル支援
お問い合わせ 会社概要DL