概要

あるオブジェクトの状態が変化したとき、それに依存する複数のオブジェクトに自動的に通知する。Observer パターンは、GUI のイベントリスナー、メッセージキューのサブスクライバー、ドメインイベントの伝播など、1対多の通知が必要な場面で広く使われます。java.util.Observer は Java 9 で非推奨となりましたが、パターンの考え方は java.beans.PropertyChangeListener や各種フレームワークのイベント機構に引き継がれています。この記事では、イベント通知システムを題材に Publisher と Listener の構造を実装し、record によるイベントデータの定義、Java 21 の sealed interface でイベント種別を型安全に分類する方法を確認します。

使いどころ

ユーザー登録イベントを複数のリスナー(ログ記録・ウェルカムメール送信・監査ログ)に同時通知する

在庫数の変更を画面表示コンポーネントとアラート通知コンポーネントに自動的に反映する

設定ファイルの変更を検知し、関連する全モジュールにリロードイベントを通知する

コード例

ObserverPatternDemo.java
import java.util.ArrayList;
import java.util.List;

public class ObserverPatternDemo {

    record Event(String type, Object data) {}

    interface EventListener {
        void onEvent(Event event);
    }

    static class EventPublisher {
        private final List<EventListener> listeners
                = new ArrayList<>();

        public void subscribe(EventListener listener) {
            listeners.add(listener);
        }
        public void unsubscribe(EventListener listener) {
            listeners.remove(listener);
        }
        public void publish(Event event) {
            System.out.println("[Publisher] " + event);
            for (var listener : listeners) {
                listener.onEvent(event);
            }
        }
    }

    static class LogListener implements EventListener {
        @Override public void onEvent(Event event) {
            System.out.println("[Log] " + event.type()
                             + ": " + event.data());
        }
    }

    static class EmailListener implements EventListener {
        @Override public void onEvent(Event event) {
            if ("USER_REGISTERED".equals(event.type())) {
                System.out.println("[Email] ウェルカムメール: "
                                 + event.data());
            }
        }
    }

    static class AuditListener implements EventListener {
        private int count = 0;
        @Override public void onEvent(Event event) {
            count++;
            System.out.println("[Audit] #" + count
                             + ": " + event);
        }
        public int getCount() { return count; }
    }

    public static void main(String[] args) {
        var publisher = new EventPublisher();
        var log = new LogListener();
        var email = new EmailListener();
        var audit = new AuditListener();

        publisher.subscribe(log);
        publisher.subscribe(email);
        publisher.subscribe(audit);

        publisher.publish(new Event("USER_REGISTERED",
                                    "user_001, 山田太郎"));
        publisher.publish(new Event("ORDER_PLACED",
                                    "ord_100, 5000円"));

        publisher.unsubscribe(email);
        publisher.publish(new Event("USER_REGISTERED",
                                    "user_002, 鈴木花子"));

        System.out.println("監査イベント数: "
                         + audit.getCount());
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record でイベントデータ(Event)を不変オブジェクトとして定義でき、アクセサメソッドで型安全にフィールド値を取得できる。

Java 17
// Java 17: record でイベントデータを型安全に
record Event(String type, Object data) {}

interface EventListener {
    void onEvent(Event event);
}
public void publish(Event event) {
    for (var listener : listeners) {
        listener.onEvent(event);
    }
}

Library Comparison

標準 API(自前 interface + List)リスナーの数が少なく、同期通知で十分な場面。外部依存なしで完結する。非同期通知やイベントフィルタリングが必要になると自前コードが増える。
java.beans.PropertyChangeListenerJavaBeans の仕様に準拠したプロパティ変更通知を行う場合。標準 API で利用可能。プロパティ名が文字列ベースのため、タイプミスに弱い。
Spring ApplicationEventSpring アプリケーション内でコンポーネント間のイベント駆動設計を行う場合。Spring 依存になるため、ライブラリ単体では使えない。

注意点

リスナーの登録を解除しないとメモリリークの原因になる。特に匿名クラスやラムダ式で登録した場合、解除用の参照を保持しておくこと

通知の順序はリスト登録順に依存するが、順序に依存するロジックを書くとリスナー追加時に予期しない挙動が起きる。通知順序に依存しない設計にする

リスナー内で例外が発生すると後続のリスナーに通知が届かない。try-catch で個別に保護するか、例外を収集して最後にまとめるかは設計で決める

java.util.Observer / Observable は Java 9 以降で非推奨。新規コードでは自前の interface か java.beans.PropertyChangeListener を使う

FAQ

java.util.Observer はなぜ非推奨になったのですか。

Observable がクラスで interface ではないため多重継承できず、通知メソッドの引数が Object で型安全性が低かったためです。PropertyChangeListener の利用が推奨されています。

非同期で通知したい場合はどうしますか。

ExecutorService にリスナーの呼び出しを submit する方法が一般的です。ただし、通知順序の保証や例外処理の設計が必要になります。

Observer と Pub/Sub の違いは何ですか。

Observer は Publisher が直接リスナーを保持します。Pub/Sub はメッセージブローカーが仲介し、Publisher と Subscriber が互いを知りません。疎結合の度合いが異なります。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます