オブザーバーパターン

オブジェクトの状態変化を複数の依存オブジェクトに自動通知するデザインパターン

設計パターンイベント駆動

オブザーバーパターンとは

オブザーバーパターンは、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 にアロー関数を直接書くと、その関数への参照がどこにも残らないため、後から同じ式をもう一度書いても別のオブジェクトになり解除できない。解除する予定のリスナーは変数に取り、onoff へ同じ参照を渡す。

実装例

身近な実装を並べると、同じ構造が層をまたいで繰り返されていることが分かる。

実装説明
DOM EventListenerelement.addEventListener('click', handler)
Node.js EventEmitteremitter.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 を知っているブローカーが仲介
スケールプロセス内プロセス間、サービス間
EventEmitterSNS, EventBridge

仲介者の有無は、障害の切り分け方まで変える。オブザーバーなら通知が届かないときに Subject の登録リストを見れば足りるが、Pub/Sub では発行側・ブローカーのルール・購読側のどこで落ちたかを別々に確かめる必要がある。

使いどころの判断は単純である。通知の相手が同じプロセス内にいて、その場で同期的に反応してほしいならオブザーバーで足りる。相手が別のサービスだったり、届かなかったときの再送や保存が要るならブローカーを挟む。

この記事は役に立ちましたか?

関連用語

関連する記事