オブザーバーパターン
オブジェクトの状態変化を複数の依存オブジェクトに自動通知するデザインパターン
オブザーバーパターンとは
オブザーバーパターンは、GoF が 1994 年に「Design Patterns」で体系化した 23 パターンのうち、振る舞いに関するパターンの 1 つである。Subject (発行者) が自分の状態変化を、登録された Observer (購読者) へ自動で通知する。
このパターンが解くのは、変化を知りたい側が増えても変化を起こす側を書き換えずに済ませる、という問題である。Subject は Observer の具体的な型や処理内容を知らないため、通知先の追加・削除を Subject の変更なしに行える。プロセス内でオブジェクトが直接通知する形が本来のオブザーバーパターンで、DOM のイベントリスナーや Node.js の EventEmitter がその代表である。EventBridge や SNS のようにブローカーが仲介してプロセス境界を越えるものは、同じ発想を分散環境へ広げた Pub/Sub であり、後述のとおり区別して扱う。
基本構造
Subject (発行者) がリスナーのリストを保持し、状態変化時に全リスナーに通知する。リスナーは on で登録、off で解除でき、Subject は各リスナーの具体的な処理内容を知らない。この疎結合により、通知先を動的に追加・削除できる。
type Listener<T> = (data: T) => void;
class EventEmitter<T> {
private listeners: Listener<T>[] = [];
on(listener: Listener<T>) { this.listeners.push(listener); }
off(listener: Listener<T>) { this.listeners = this.listeners.filter(l => l !== listener); }
emit(data: T) { this.listeners.forEach(l => l(data)); }
}
const emitter = new EventEmitter<string>();
emitter.on((msg) => console.log(`A: ${msg}`));
emitter.on((msg) => console.log(`B: ${msg}`));
emitter.emit('hello'); // A: hello, B: hello
この実装で最初につまずくのは off である。on にアロー関数を直接書くと、その関数への参照がどこにも残らないため、後から同じ式をもう一度書いても別のオブジェクトになり解除できない。解除する予定のリスナーは変数に取り、on と off へ同じ参照を渡す。
実装例
身近な実装を並べると、同じ構造が層をまたいで繰り返されていることが分かる。
| 実装 | 説明 |
|---|---|
| DOM EventListener | element.addEventListener('click', handler) |
| Node.js EventEmitter | emitter.on('data', handler) |
| React useState | 状態変更 → 再レンダリング (購読の登録と解除をフレームワークが隠している) |
| EventBridge | イベント → ルール → ターゲット (ブローカーが仲介する Pub/Sub 型) |
React の再レンダリングとの関係
React の再レンダリングは、購読の登録と解除を開発者に書かせない形へ畳み込んだものである。
function Counter() {
const [count, setCount] = useState(0);
// setCount が呼ばれると、React が自動的に再レンダリング
// → 購読の登録と解除は React 側が管理する
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
通知先を登録しているのは開発者のコードではなく React である。そのため Subject に相当する存在が表に出ず、状態を持つ側と描画する側が同じ関数に同居しているように見える。逆に React の外にある状態 (ブラウザ API や外部のストア) を購読するときはこの隠蔽が効かないため、登録と解除を自分で書くことになる。
メモリリークの防止
登録と解除を自分で書く場合、解除の抜けはそのままメモリリークになる。
// ❌ リスナーの解除を忘れる → メモリリーク
useEffect(() => {
window.addEventListener('resize', handleResize);
}, []);
// ✅ クリーンアップで解除
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => window.removeEventListener('resize', handleResize);
}, []);
リークの正体は、解除されなかったリスナーが window から参照され続け、そのリスナーが閉じ込めている変数ごと回収されなくなることである。しかも症状はメモリ使用量より先に、画面を開き直すたびに登録が積み上がって同じ処理が回数分走る二重発火として現れることが多い。
オブザーバー vs Pub/Sub
この 2 つは混同されやすい。分かれ目は仲介者がいるかどうかである。
| 観点 | オブザーバー | Pub/Sub |
|---|---|---|
| 結合度 | Subject が Observer を知っている | ブローカーが仲介 |
| スケール | プロセス内 | プロセス間、サービス間 |
| 例 | EventEmitter | SNS, EventBridge |
仲介者の有無は、障害の切り分け方まで変える。オブザーバーなら通知が届かないときに Subject の登録リストを見れば足りるが、Pub/Sub では発行側・ブローカーのルール・購読側のどこで落ちたかを別々に確かめる必要がある。
使いどころの判断は単純である。通知の相手が同じプロセス内にいて、その場で同期的に反応してほしいならオブザーバーで足りる。相手が別のサービスだったり、届かなかったときの再送や保存が要るならブローカーを挟む。
この記事は役に立ちましたか?
関連用語
デザインパターン
ソフトウェア設計で繰り返し現れる問題に対する再利用可能な解決策のカタログ
EventBridge
AWS のサーバーレスイベントバスで、イベント駆動アーキテクチャの中核を担う
React
コンポーネントベースの UI ライブラリで、宣言的な記法と仮想 DOM の差分計算で UI の更新を扱う
仮想 DOM
実際の DOM の軽量なコピーをメモリ上に保持し、差分だけを効率的に更新する仕組み
副作用
関数が戻り値以外に外部の状態を変更する操作で、テストやデバッグを困難にする要因
Worker Threads
Node.js で CPU 集約的な処理をメインスレッドをブロックせずに別スレッドで実行する仕組み