コーディング · デザインパターン
Observer パターンとは?例でわかりやすく解説
結論
Observer は振る舞いに関するデザインパターンで、Subject と呼ばれるオブジェクトが、Observer と呼ばれる依存先のリストを保持し、状態が変わると自動的に通知します。Observer は実行時に購読と購読解除ができ、Subject はその具象クラスを知る必要がありません。ブラウザのイベントリスナー、Node.js の EventEmitter、RxJS のようなリアクティブライブラリは、すべてこの考え方に基づいています。
目的と解決する問題
目的:オブジェクト間に一対多の依存関係を定義し、あるオブジェクトの状態が変わったときに、その依存先すべてに自動的に通知して更新することです。Observer は Gang of Four(GoF)の書籍にある振る舞いパターンの 1 つで、publish-subscribe やリスナーパターンとも呼ばれます。
Web ショップのショッピングカートを考えてみましょう。商品が追加されると、ヘッダーのカートアイコンは新しい個数を表示し、サイドバーは新しい合計を表示し、アナリティクスのモジュールはそのイベントを記録しなければなりません。カートがこれらをそれぞれ直接呼び出すと、カートのモジュールはヘッダー、サイドバー、アナリティクスに依存します。4 つ目の反応を加えるたびに、またカートを編集することになります。
Observer を使うと、カートは購読者のリストを持ち、変更があったときにそれぞれを呼び出すだけです。ヘッダー、サイドバー、アナリティクスは自分で購読します。カートはそれらをインポートせず、新しい機能はカートのコードに触れずにカートの変更へ反応できます。
構造と登場する役割
| 役割 | 責務 |
|---|---|
| Subject(発行者) | Observer のリストを保持し、購読と購読解除のメソッドを提供する。何か変化があったときに通知する |
| Observer(購読者) | update メソッドを持つインターフェース。多くの場合は単なるコールバック関数 |
| Concrete subject | 関心のある状態を保持し、変化したときに notify を呼ぶ |
| Concrete observer | 通知に反応する。たとえば UI の一部を再描画する |
通知には 2 つのスタイルがあります。push モデルでは、Subject が変更後のデータを通知と一緒に送ります。pull モデルでは「何かが変わった」とだけ伝え、Observer が必要なデータを Subject に問い合わせます。
TypeScript での実装
小さなジェネリックの Subject クラスが購読を管理します。Observer はただの関数で、TypeScript では最も一般的な形です。subscribe は購読解除の関数を返すので、呼び出し側はどのリスナーを登録したかを忘れずに済みます。
type Listener<T> = (value: T) => void;
class Subject<T> {
private listeners = new Set<Listener<T>>();
subscribe(listener: Listener<T>): () => void {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
protected notify(value: T) {
for (const listener of [...this.listeners]) listener(value);
}
}
type CartEvent = { count: number; total: number };
class Cart extends Subject<CartEvent> {
private items: { name: string; price: number }[] = [];
add(name: string, price: number) {
this.items.push({ name, price });
const total = this.items.reduce((sum, item) => sum + item.price, 0);
this.notify({ count: this.items.length, total });
}
}
const cart = new Cart();
cart.subscribe(({ count }) => console.log("header: " + count + " items"));
const stopAnalytics = cart.subscribe(({ total }) => console.log("analytics: total " + total));
cart.add("Book", 12);
stopAnalytics(); // unsubscribe
cart.add("Pen", 3);
// header: 1 items
// analytics: total 12
// header: 2 itemsnotify は、セットのコピー([...this.listeners])を反復します。こうすれば、通知の最中に自分自身を購読解除するリスナーがいても、ループを乱しません。
組み込みの EventTarget を使う
ブラウザ、Node.js、Deno は EventTarget を提供しているので、自前の Subject クラスは不要なことがよくあります。
const thermostat = new EventTarget();
thermostat.addEventListener("change", (e) => {
console.log("new temperature:", (e as CustomEvent<number>).detail);
});
thermostat.dispatchEvent(new CustomEvent("change", { detail: 21.5 }));
// new temperature: 21.5Node.js には、node:events モジュールの EventEmitter クラスもあり、on、off、emit メソッドを持ちます。
使うべき場面と避けるべき場面
- あるオブジェクトの変化がほかのオブジェクトを更新しなければならず、その数や種類が事前にわからないときに使います。
- コアのモジュールを、それに反応する UI、ログ、アナリティクスのコードから独立させておきたいときに使います。
- ユーザー入力、メッセージ、センサーの測定値のように、時間とともに起きるイベントに使います。
- 受信者が決まった 1 つだけなら使いません。直接のメソッド呼び出しやコールバック引数のほうがわかりやすいです。
- Observer が別の通知を引き起こす連鎖には注意しましょう。更新のループを招き、なぜ何かが変わったのかを見えにくくします。
- Subject が明記していない限り、Observer が呼ばれる順序に頼らないでください。
実際の利用例
- DOM:
element.addEventListener("click", handler)は、要素に Observer を購読させます。 - Node.js:
EventEmitterは、ストリーム、HTTP サーバー、その他多くのコアオブジェクトの基盤です。 MutationObserver、IntersectionObserver、ResizeObserverなどのブラウザの Observer API は、ページの変化をコールバックに通知します。- RxJS は、値のストリームを Observable としてモデル化し、Observer がそれを購読します。
- Java:
java.util.Observableは Java 9 で非推奨になりました。代わりにjava.util.concurrent.Flowインターフェース(パブリッシャーとサブスクライバー)やイベントリスナーのインターフェースが使われます。
関連するパターン
| パターン | Observer パターンとの違い |
|---|---|
| Mediator パターン | Mediator は参加するオブジェクトを知っていて、それらを調整します。Subject は単に一斉通知するだけで、Observer が何をするかは知りません。 |
| ブローカー付きの Publish-Subscribe | パブリッシャーとサブスクライバーは互いをまったく知らず、メッセージブローカーやイベントバスを介して、多くの場合プロセス間で通信します。 |
| Command パターン | 通知は Command オブジェクトを運ぶことがあります。Command は 1 つのリクエストに関するもので、Observer は一対多の通知に関するものです。 |
| Iterator パターン | Iterator は pull 型です(利用側が次の値を要求する)。Observer は push 型です(生産側が値の発生時に送る)。 |
よくある質問
Observer と publish-subscribe の違いは何ですか?
古典的な Observer パターンでは、Observer が Subject に直接登録します。publish-subscribe では、ブローカーやイベントチャネルが間に入るので、パブリッシャーとサブスクライバーは互いを知りません。この 2 つの名前は、同じ考え方に対して厳密に区別せず使われることも多いです。
Observer パターンでメモリリークを避けるにはどうすればよいですか?
Observer が不要になったら購読解除します。subscribe が購読解除の関数を返すようにする、あるいは addEventListener で AbortSignal を使うと、クリーンアップが簡単になります。
Observer は同期的に呼ばれますか?
上の例や EventTarget、EventEmitter では、リスナーは追加された順に同期的に実行されます。ほかのライブラリでは通知が非同期で届くこともあるので、ドキュメントを確認してください。
Observer はリアクティブプログラミングと同じですか?
リアクティブプログラミングは Observer の上に成り立っています。RxJS のようなライブラリは、通知のストリームをフィルタリング・結合・変換するオペレーターに加えて、エラーと完了に関するルールを追加します。