アーキテクチャ

Observerパターンとは?Java・TypeScriptの実装例とPub/Subとの違い

Observerパターンとは?Java・TypeScriptの実装例とPub/Subとの違い

Observerパターン(オブザーバーパターン)は、監視される側の状態が変わったときに、登録済みの監視する側へまとめて通知を配る設計です。GoFの23パターンでは振る舞いに関するパターンに分類され、出版-購読型モデル(Publish-Subscribe)という別名も持ちます。考え方自体は単純ですが、実装で迷うのは「その言語の標準ライブラリのどれを使うか」です。とくにJavaは、多くの解説が挙げるjava.util.ObservableがJDK 9で非推奨になり、JDK 26でも非推奨のまま残っています。ここでは仕組みと登場人物を整理したうえで、実行して確認したコードと、購読解除漏れなど本番で踏む落とし穴まで扱います。

まとめ

  • サブジェクト(監視される側)がオブザーバー(監視する側)の一覧を持ち、状態変化時に登録順不定で通知を配る、というのがパターンの全体像です。
  • Javaでjava.util.Observableを継承する解説は古い情報です。JDK 26でも@Deprecated(since="9")のまま残っており、コンパイル時に警告が出ます。
  • 代替はJavadocが挙げる3つ、java.beans.PropertyChangeSupport、java.util.concurrentの並行データ構造、Flow APIです。
  • .NETではIObservable<T>を自作するより、標準イベントやSystem.Threading.Channelsが勧められています。
  • 本番で壊れる原因の大半は、購読解除漏れ・通知順序への依存・同期通知によるブロックに集中します。

以下、登場人物の役割分担から順に、言語ごとの実装と判断材料を見ていきます。

Observerパターンの仕組みと登場人物

サブジェクトとオブザーバーの役割分担

登場人物は2種類です。サブジェクト(Subject、Observableとも呼ばれます)は監視される側で、オブザーバーの登録・解除・通知という操作を持ちます。Javaの標準APIならaddPropertyChangeListener、removePropertyChangeListener、firePropertyChangeがそれぞれに対応します。オブザーバーは監視する側で、通知を受け取る更新用のメソッドを1つ持ちます。サブジェクトはオブザーバーを具体的な型ではなくインターフェース越しに保持するため、通知先が何個あってもサブジェクト側のコードは変わりません。これが「株価が変わったら画面とアラートと監査ログに知らせる」といった1対多の通知を、呼び出し側にif文を積まずに書ける理由です。

依存の向きは片方向です。サブジェクトはオブザーバーの実装を知らず、オブザーバーはサブジェクトのインターフェースだけを知ります。この構造は依存性逆転の原則と同じ考え方で、詳しくは依存性の注入(DI)と依存性逆転の原則(DIP)の違いで扱っています。

プッシュ型とプル型の通知方式

通知には、変化した値そのものを引数で渡すプッシュ型と、「変わった」という合図だけを送り、オブザーバーがサブジェクトへ問い合わせるプル型があります。プッシュ型は受け手が即座に処理できる代わりに、送る値を増やすたびにシグネチャが変わります。プル型は柔軟ですが、通知を受けた時点で既に次の変更が入っていると、取得した値が通知の原因と一致しません。JDKのJavadocがjava.util.Observableの非推奨理由に挙げる「状態変化と通知が1対1で対応しない」は、まさにこのプル型の弱点を指しています。

迷ったらプッシュ型に寄せ、引数が増えてきたら専用の値オブジェクトに束ねます。この切り分けなら、後から通知内容を足してもオブザーバー側の修正が要りません。

言語別の実装と標準機構の現在地

TypeScriptでの最小実装

まず自前実装の骨格です。ポイントは、subscribeが購読解除用の関数を返すこと、通知ループの前にオブザーバー一覧をコピーすること、1つのオブザーバーの例外が他へ波及しないよう握ることです。以下はNode.js 26.5.0で実行し、出力をコメントに記載しています。

type Unsubscribe = () => void;

interface StockObserver {
  update(symbol: string, price: number): void;
}

class StockTicker {
  #observers = new Set<StockObserver>();

  // 購読と同時に「解除する関数」を返すのが定石
  subscribe(observer: StockObserver): Unsubscribe {
    this.#observers.add(observer);
    return () => { this.#observers.delete(observer); };
  }

  publish(symbol: string, price: number): void {
    // 通知中の購読解除に備えてコピーを回す
    for (const observer of [...this.#observers]) {
      try {
        observer.update(symbol, price);
      } catch (e) {
        console.error("observer failed:", (e as Error).message);
      }
    }
  }

  get observerCount(): number { return this.#observers.size; }
}

const ticker = new StockTicker();
const board: StockObserver = { update: (s, p) => console.log(`[board] ${s} ${p}`) };
const stopBoard = ticker.subscribe(board);

ticker.publish("ISO", 980);   // [board] ISO 980
stopBoard();
ticker.publish("ISO", 990);   // 出力なし(購読解除済み)
console.log(ticker.observerCount); // 0

通知ループでコピーを取るのは、オブザーバーがupdateの中で購読を足したり外したりするケースに備えるためです。JavaScriptのSetは走査中に削除しても安全ですが、走査中に追加した要素は同じパスで訪問されるため、今回の通知の対象ではない購読者まで呼ばれます。JavaのArrayListのように、走査中の変更が例外になる言語もあります。

Javaでjava.util.Observableを選ばない理由

Javaの解説記事ではjava.util.Observableを継承しjava.util.Observerを実装する例が今も多く見つかりますが、この2つは@Deprecated(since="9")で、JDK 26のJavadocにも同じ記述が残っています。Javadocが挙げる理由は、イベントモデルが限定的であること、通知順序が規定されていないこと、状態変化と通知が1対1で対応しないことの3点です。OpenJDK 26.0.2のjavacに-Xlint:deprecationを付けると、継承と実装の両方に「java.utilのObservableは推奨されません」という警告が出ます。

Javadocが示す代替は3つあります。リスナー単位で細かく購読したいならjava.beansパッケージ、スレッド間で順序を保証したいならjava.util.concurrentの並行データ構造、バックプレッシャを含むリアクティブな流れが必要ならFlow APIです。次は1つ目のPropertyChangeSupportを使った例で、こちらもOpenJDK 26.0.2で実行して出力を確認しています。

import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;

public class StockTicker {
    private final PropertyChangeSupport support = new PropertyChangeSupport(this);
    private int price;

    public void addListener(PropertyChangeListener listener) {
        support.addPropertyChangeListener("price", listener);
    }

    public void removeListener(PropertyChangeListener listener) {
        support.removePropertyChangeListener("price", listener);
    }

    public void setPrice(int newPrice) {
        int old = this.price;
        this.price = newPrice;
        support.firePropertyChange("price", old, newPrice);
    }

    public static void main(String[] args) {
        StockTicker ticker = new StockTicker();
        PropertyChangeListener board =
            e -> System.out.println("[board] " + e.getOldValue() + " -> " + e.getNewValue());
        ticker.addListener(board);
        ticker.setPrice(980);   // [board] 0 -> 980
        ticker.setPrice(1200);  // [board] 980 -> 1200
        ticker.setPrice(1200);  // 値が同じため通知されない
        ticker.removeListener(board);
        ticker.setPrice(990);   // 出力なし
    }
}

3回目のsetPrice(1200)が無出力なのは仕様です。firePropertyChangeは旧値と新値が等しい場合に通知を発火しません。「同じ値で上書きされたこと」自体をイベントにしたい設計では、この挙動が落とし穴になります。なおjava.beansはモジュールjava.desktopに属するため、jlinkで絞ったランタイムでは含め忘れに注意してください。

非同期にしたい、あるいは購読側の処理速度に合わせて流量を制御したい場合は、java.util.concurrent.SubmissionPublisherとFlow.Subscriberの組み合わせに寄せます。onSubscribeで受け取ったFlow.Subscriptionに対してrequest(n)を呼んだ分だけonNextが届くため、受け手が詰まったときに送り手を待たせられます。

C#でIObservableを直接実装する前の選択肢

.NETはSystem.IObservable<T>とSystem.IObserver<T>でこのパターンを表現します。どちらも.NET Framework 4.0から入っている標準インターフェースです。オブザーバー側はOnNext、OnError、OnCompletedの3つを実装し、プロバイダー側のSubscribeは購読解除用のIDisposableを返します。Microsoft Learnのオブザーバー設計パターンの解説には、オブザーバーが通知を受け取る順序は定義されていないと明記があります。

ただし同じページは、これらのインターフェースを自分で実装する前に別の選択肢を検討するよう促しています。1つのアプリ内の単純な通知なら標準のイベントのほうが慣用的で実装も簡単、コンシューマー側がペースを決める非同期シーケンスならIAsyncEnumerable<T>、バックプレッシャ付きの生産者・消費者ならSystem.Threading.Channels、イベントの合成やフィルタリングが複雑ならRx.NETを使う、という整理です。自作のIObservable<T>実装が正解になる場面は、実際にはかなり限られます。フレームワーク側での代表例が診断イベントを配るDiagnosticListenerです。

言語別の標準機構の対応表

言語・環境 標準の仕組み 購読解除の手段
Java PropertyChangeSupport / Flow API removePropertyChangeListener / Subscription.cancel
C#・.NET event / IObservable<T> -= 演算子 / IDisposable.Dispose
ブラウザ・Node.js EventTarget removeEventListener / AbortController
Kotlin Delegates.observable / StateFlow デリゲート破棄 / コルーチンスコープ終了
PHP・Laravel Eloquent Observer ObservedBy属性の削除
Swift Combine Publisher AnyCancellable の破棄

いずれも「登録したものを必ず外せる」ことが設計の中心にあります。Swiftの実装はCombine(Swift)とは?Publisher・Subscriberの仕組みと2026年の採用判断を解説で個別に扱っています。

Pub/Subとの違いと使い分け

Observerパターンは出版-購読型モデル(Publish-Subscribe)という別名でも呼ばれるため、メッセージングのPub/Subと同じものだと説明されることがあります。実務上は分けて考えたほうが安全です。Observerパターンではサブジェクトがオブザーバーの参照を直接持ち、通知は同じプロセス内のメソッド呼び出しとして届きます。一方のPub/Subは、送り手と受け手の間にブローカーやトピックを挟み、互いの存在を知りません。

この差は障害時の挙動に出ます。Observerパターンでは購読者が例外を投げれば呼び出し元まで伝播し、プロセスが落ちれば購読情報も消えます。ブローカー方式では購読情報がブローカー側に残り、受け手が落ちている間のメッセージを保持する設定も選べます。プロセスをまたぐ通知、再送、順序保証のいずれかが要件に入った時点で、Observerパターンの自作ではなくMQTTとは?Pub/Subの仕組み・QoS・MQTT 5.0の実装で扱うようなメッセージング基盤へ移すのが妥当です。イベント駆動アーキテクチャという言葉で語られるのは、通常こちらの層を指します。

Observerパターンで踏む3つの落とし穴

パターン自体は数十行で書けます。しかし本番で問題になる箇所は決まっていて、教科書的な解説ではまず触れられません。実装前に次を確認してください。

購読解除漏れによるメモリリーク

最も多い障害です。サブジェクトがオブザーバーへの強参照を保持し続けるため、オブザーバー側のオブジェクトが役目を終えても、サブジェクトが生きている限り回収されません。画面の生成と破棄を繰り返すUIや、リクエストごとにハンドラを登録するサーバーで、ヒープが単調増加する形で表面化します。

対策は、購読時に解除手段を必ず受け取り、破棄処理と同じ場所に置くことです。TypeScriptなら購読解除関数を返す、JavaならremovePropertyChangeListenerをclose()に書く、.NETならSubscribeが返すIDisposableを捨てない、という具合です。ブラウザとNode.jsではAbortControllerを使うと、複数の購読を1回のabort()でまとめて解除できます。

const target = new EventTarget();
const controller = new AbortController();

target.addEventListener("price", (e) => console.log("[board]", e.detail), {
  signal: controller.signal,
});

target.dispatchEvent(new CustomEvent("price", { detail: 980 }));  // [board] 980
controller.abort();
target.dispatchEvent(new CustomEvent("price", { detail: 1200 })); // 出力なし

「弱参照でリークを防ぐ」という案は一見きれいですが、オブザーバーがラムダ式やクロージャの場合、他から参照されないため即座に回収され、通知が届かなくなる不具合に化けます。解除タイミングを設計で決めるほうが確実です。

通知順序への依存と再入

JavaのJavadocも.NETのドキュメントも、通知順序は保証しないと明記しています。にもかかわらず「先にキャッシュを更新するオブザーバーが動く前提」でログ出力側を書く、といったコードは珍しくありません。登録順に依存する処理が2つ以上あるなら、それは1つのオブザーバーにまとめるべき処理です。

もう1つが再入です。オブザーバーの中でサブジェクトの状態を書き換えると、その場で次の通知が始まり、条件次第で終わらないループになります。通知処理の中で書き戻しが必要なら、値が実際に変わったときだけ通知する、あるいは変更をキューに積んで通知ループの外で処理する、のどちらかを選びます。前者はJavaのPropertyChangeSupportが既定で行っている振る舞いです。

同期通知による呼び出し元のブロック

素直に実装したObserverパターンの通知は同期呼び出しです。オブザーバーの1つがHTTPリクエストやDB書き込みを行えば、その待ち時間はサブジェクトの状態更新を呼んだ側の処理時間にそのまま乗ります。オブザーバーが10個あれば、遅い1個が全体の応答を決めます。

判断基準は単純です。オブザーバーの処理がI/Oを含むなら同期通知にしません。JavaならFlow APIか実行用のスレッドプールを挟み、.NETならSystem.Threading.Channelsで受け渡します。非同期にする場合は、通知の取りこぼしを許容するのか、キューを溢れさせずに送り手を待たせるのかを先に決めてください。この選択を後回しにすると、負荷が上がったときに原因の特定が難しい形で壊れます。

MediatorパターンとStrategyパターンとの使い分け

Observerパターンと混同されやすいのがMediatorパターンです。Observerパターンは1つのサブジェクトから多数へ一方向に通知が流れます。Mediatorパターンは、多数のオブジェクトが相互に影響し合う関係を仲介役1つに集約し、双方向のやり取りを整理します。通知の向きが一方向ならObserver、「AがBを変えるとBがCを変える」のような相互作用ならMediatorが合います。

Strategyパターンとは目的そのものが違います。Strategyは処理内容を差し替えるためのパターンで、通知の仕組みは持ちません。「通知を受けたオブザーバーが、状況に応じて処理を差し替える」形で併用はできますが、どちらかがどちらかの代わりになる関係ではありません。個別の解説はStrategyパターンとは?PHP・Java・TypeScriptの実装例と使いどころとGoFのデザインパターンとは?23種類の一覧と代表パターンをコードで解説にあります。

ブラウザのMutationObserverとの関係

ブラウザにはMutationObserver、IntersectionObserver、ResizeObserverなど、Observerを名乗るAPIが複数あります。これらはGoFのObserverパターンの実装例ではなく、「変化を監視してコールバックを呼ぶ」という同じ発想をブラウザ側が提供したものです。MutationObserverはコンストラクタにコールバックを渡し、observe()で監視対象と条件を指定し、disconnect()で停止します。監視条件はchildList(子ノードの増減)、attributes(属性変更)、subtree(子孫まで対象)などを組み合わせて指定します。

自作のオブザーバーと違い、コールバックには変更内容の配列がまとめて渡されます。1件ずつ即時に呼ばれる想定でDOM操作を書くと、同じ処理が重複して走る作りになりがちです。またdisconnect()を呼ばないままDOMを差し替えると、監視が生き続けて処理が積み上がります。前節のメモリリークと同じ性質の問題です。

よくある質問

オブザーバーとは何を指しますか

ソフトウェア設計の文脈では、監視対象の状態変化の通知を受け取る側のオブジェクトを指します。英語のobserverは観測者・監視者という意味で、会議に議決権なしで参加する立場を指す一般用語や、監視系ソフトウェアの製品名としても使われます。本記事で扱うのは設計パターンとしてのオブザーバーです。

java.util.Observableは今も使えますか

削除はされておらず、JDK 26でもクラス自体は残っているため動きます。ただしJDK 9から非推奨で、コンパイル時に警告が出ます。新規実装ではPropertyChangeSupport、java.util.concurrentの並行データ構造、Flow APIのいずれかを選んでください。

ObserverパターンとPub/Subは何が違いますか

Observerパターンはサブジェクトが購読者の参照を直接持ち、同一プロセス内で同期的に通知します。Pub/Subは間にブローカーやトピックを挟み、送り手と受け手が互いを知りません。プロセスをまたぐ通知や再送が必要ならPub/Sub側の仕組みを使います。

Kotlinではどう書きますか

プロパティ1つの変化を拾うだけなら、kotlin.propertiesのDelegates.observable(initialValue)が使えます。ハンドラは値が変わった後に呼ばれ、プロパティ・旧値・新値を受け取ります。複数の購読者へ配る、あるいは非同期に流す場合はStateFlowやSharedFlowを選びます。

LaravelのEloquent Observerも同じものですか

発想は同じで、対象がORMのモデルイベントに固定されている点が違います。php artisan make:observerでクラスを生成し、モデル側の#[ObservedBy]属性か、サービスプロバイダでのModel::observe()で登録します。作成・更新・削除などのイベントごとにメソッドを実装する形です。

関連記事

お気に入りに入れた記事の一覧

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.07.21 テックブログ Apache Tomcatの脆弱性一覧【2026年10月】最新版11.0.27・10.1.61・9.0.123とCVE-2026-59083ほかの影響確認・対応手順
  2. 2026.10.21 テックブログ NEC委託先の不正アクセスとは|顧客システム暗号化・情報漏えいの経緯と委託元が点検する正規アカウント
  3. 2026.10.21 テックブログ ふるさとチョイス不正アクセスで445万件流出:対象確認・パスワード変更と再開時期
  4. 2026.10.22 テックブログ Tomcatバージョン一覧とサポート期限【2026年10月】9.0系は2027年3月末で終了・9.1系と11.0系への移行判断
  5. 2024.07.05 コラム XAMPPの脆弱性とは?危険性とCVE-2024-4577への対策をわかりやすく解説【2026年10月】

RELATED POSTS 関連記事

目次