java-recipes

ホーム デザインパターン › DP-19

DP-19: Observer パターン

あるオブジェクトの状態変化(イベント)を、登録された複数のオブザーバーに自動的に通知するパターンです。 イベント発行者(Publisher)とリスナー(Observer)が疎結合になるため、 新しいリスナーを追加しても発行者側を変更する必要がありません。

Observer パターンとは

「ユーザー登録が完了したとき、ウェルカムメールを送り、ログを記録し、ポイントを付与する」 といった処理を実装する場合、これら全てをひとつのメソッドに書いてしまうと、 後から「クーポンも発行する」を追加するたびにそのメソッドを修正しなければなりません。 Observer パターンでは、「ユーザー登録が完了した」というイベントを発行する側(Publisher)と 「そのイベントに反応する」側(Observer)を分離します。 発行者はリスナーの詳細を知らず、リスナーは発行者の詳細を知らない状態で連携できます。

Push型 vs Pull型

方式仕組みメリットデメリット
Push型Publisher がイベントデータをオブザーバーに送るオブザーバーが追加のデータ取得をしなくて良い不要なデータまで送られることがある
Pull型Publisher は変化があったことのみ通知し、オブザーバーが必要なデータを取得するオブザーバーが必要な情報だけ取得できるオブザーバーが Publisher への参照を保持する必要がある

Java 標準ライブラリの実例: java.beans.PropertyChangeListener

Java 9 以降、java.util.Observerjava.util.Observable は非推奨(deprecated)となりました。 代わりに java.beans.PropertyChangeListener の使用が推奨されています。

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

class UserModel {
    private final PropertyChangeSupport support = new PropertyChangeSupport(this);
    private String name;

    public void addPropertyChangeListener(PropertyChangeListener listener) {
        support.addPropertyChangeListener(listener);
    }

    public void setName(String newName) {
        String oldName = this.name;
        this.name = newName;
        // プロパティ変更を全リスナーに通知する
        support.firePropertyChange("name", oldName, newName);
    }
}

// 使用例
UserModel user = new UserModel();
user.addPropertyChangeListener(evt ->
    System.out.println("名前が変わりました: " + evt.getOldValue() + " → " + evt.getNewValue())
);
user.setName("山田太郎"); // → 「名前が変わりました: null → 山田太郎」

サンプルコード

ObserverPatternSample.java
import java.util.ArrayList;
import java.util.List;

public class ObserverPatternSample {

    // ---- オブザーバーのインターフェース ----
    interface EventListener {
        /** イベントが発生したときに呼び出されるメソッド */
        void onEvent(String eventType, Object data);
    }

    // ---- イベント発行者(Publisher / Subject)----
    static class EventPublisher {
        private final List<EventListener> listeners = new ArrayList<EventListener>();

        /** オブザーバーを登録する */
        public void subscribe(EventListener listener) {
            listeners.add(listener);
            System.out.println("[Publisher] リスナーを登録: " + listener.getClass().getSimpleName());
        }

        /** オブザーバーの登録を解除する */
        public void unsubscribe(EventListener listener) {
            listeners.remove(listener);
            System.out.println("[Publisher] リスナーを解除: " + listener.getClass().getSimpleName());
        }

        /**
         * 登録済みの全オブザーバーにイベントを通知する(Push型)。
         * Push型では Publisher がデータを全て送る。
         * Pull型では変化のみ通知し、オブザーバーが必要に応じてデータを取得する。
         */
        public void publish(String eventType, Object data) {
            System.out.println("[Publisher] イベント発行: type=" + eventType + ", data=" + data);
            for (EventListener listener : listeners) {
                listener.onEvent(eventType, data);
            }
        }
    }

    // ---- オブザーバー1: ログ出力リスナー ----
    static class LogListener implements EventListener {
        @Override
        public void onEvent(String eventType, Object data) {
            System.out.println("[LogListener] ログ記録: [" + eventType + "] " + data);
        }
    }

    // ---- オブザーバー2: メール通知リスナー ----
    static class EmailNotificationListener implements EventListener {
        @Override
        public void onEvent(String eventType, Object data) {
            // 「USER_REGISTERED」イベントのときだけメールを送信する
            if ("USER_REGISTERED".equals(eventType)) {
                System.out.println("[EmailListener] ウェルカムメールを送信: " + data);
            } else {
                System.out.println("[EmailListener] このイベントはメール対象外: " + eventType);
            }
        }
    }

    // ---- オブザーバー3: 監査ログリスナー ----
    static class AuditListener implements EventListener {
        private int eventCount = 0;

        @Override
        public void onEvent(String eventType, Object data) {
            eventCount++;
            System.out.println("[AuditListener] 監査ログ #" + eventCount
                    + ": type=" + eventType + ", data=" + data);
        }

        public int getEventCount() {
            return eventCount;
        }
    }

    public static void main(String[] args) {
        System.out.println("=== Observer パターン: イベント通知システム ===");

        EventPublisher publisher = new EventPublisher();

        // オブザーバーを登録する
        System.out.println("\n--- リスナーの登録 ---");
        LogListener logListener = new LogListener();
        EmailNotificationListener emailListener = new EmailNotificationListener();
        AuditListener auditListener = new AuditListener();

        publisher.subscribe(logListener);
        publisher.subscribe(emailListener);
        publisher.subscribe(auditListener);

        // ユーザー登録イベントを発行する(3つのリスナー全員が通知を受ける)
        System.out.println("\n--- ユーザー登録イベントを発行 ---");
        publisher.publish("USER_REGISTERED", "userId=user_001, name=山田太郎");

        // 注文イベントを発行する
        System.out.println("\n--- 注文イベントを発行 ---");
        publisher.publish("ORDER_PLACED", "orderId=ord_100, amount=5000");

        // オブザーバーの登録解除
        System.out.println("\n--- EmailListener を解除して再度イベント発行 ---");
        publisher.unsubscribe(emailListener);
        publisher.publish("USER_REGISTERED", "userId=user_002, name=鈴木花子");

        System.out.println("\n--- 監査ログの集計 ---");
        System.out.println("受け取ったイベント数: " + auditListener.getEventCount());
    }
}

Java 8 版では EventListener インターフェースと EventPublisher クラスで Observer パターンを実装しています。subscribe/unsubscribe でリスナーを動的に管理でき、publish を呼ぶと登録済みの全リスナーに通知が届きます。

よくあるミス・注意点

⚠️ unsubscribe(登録解除)を忘れてメモリリークが発生する

Observer パターン最大の落とし穴です。 Publisher がオブザーバーへの参照を保持し続けるため、unsubscribe() を呼ばずに オブザーバーオブジェクトを「捨てた」つもりになっても、 Publisher からの参照が残るためガベージコレクションで回収されません。 画面遷移やオブジェクトのライフサイクル終了時には必ず登録解除しましょう。

⚠️ イベントの連鎖(Cascading)で無限ループになる

オブザーバーが通知を受けた際に、同じ Publisher に対して新たなイベントを発行すると、 そのイベントがまた同じオブザーバーに届き、無限ループになる場合があります。 イベントハンドラ内でイベントを発行する場合は、ループの終了条件を必ず設けましょう。

⚠️ java.util.Observer / java.util.Observable を使う(Java 9+ で非推奨)

古い書籍やコードには java.util.Observerjava.util.Observable を使った例が載っていますが、 Java 9 で非推奨(deprecated)になりました。 新しいコードでは java.beans.PropertyChangeListener か、 このサンプルのように独自のインターフェースを定義することをお勧めします。

テストする観点

  • publish したイベントが、登録済みの全オブザーバーに届くこと(3つ登録したら3つ全員に通知されること)
  • unsubscribe 後は、解除したオブザーバーにイベントが届かないこと
  • オブザーバーが0件の状態で publish しても例外が発生しないこと(境界値)
  • 同じオブザーバーを複数回 subscribe すると複数回通知されること(または1回のみにすること)を確認する
  • Java 17 版: Event record の type() と data() が正しい値を返すこと
  • Java 21 版: LogListener の switch 式が各 AppEvent サブタイプに対して正しいメッセージを生成すること
  • Java 21 版: UserRegistered・OrderPlaced・PaymentFailed それぞれを publish したとき、EmailNotificationListener が正しく反応すること

GitHub でソースコードを見る →