Docs 一覧
Java Design Patterns #java #design-patterns #observer #oop #event-driven

Javaデザインパターン:Observerパターン完全解説

GoFの行動パターンの一つであるObserverパターンを、イベント駆動設計の観点からJavaコードで詳しく解説します。

Observerパターンとは

Observerパターンは、あるオブジェクト(Subject)の状態が変わったとき、 そのオブジェクトに依存する複数のオブジェクト(Observer)に自動的に通知するパターンです。 GoFの行動パターン(Behavioral Pattern)に分類されます。

「オブジェクト間の1対多の依存関係を定義する。あるオブジェクトの状態が変化すると、 依存するすべてのオブジェクトに自動的に通知され、更新される。」— GoF

問題:密結合な通知処理

Observerパターンを使わない場合、通知処理がこのように密結合になります:

// アンチパターン: 通知先を直接呼び出している
public class StockPrice {
    private double price;
    private EmailAlert emailAlert = new EmailAlert();
    private MobileApp mobileApp = new MobileApp();
    private Dashboard dashboard = new Dashboard();

    public void setPrice(double newPrice) {
        this.price = newPrice;
        // 通知先が増えるたびにここを変更しなければならない
        emailAlert.notify(newPrice);
        mobileApp.notify(newPrice);
        dashboard.notify(newPrice);
    }
}

通知先が1つ増えるたびに StockPrice を変更する必要があります。 開放閉鎖原則(OCP)に違反し、テストも困難です。

解決:Observerパターンの実装

Step 1: Observer インターフェースを定義

// Observer インターフェース
public interface StockObserver {
    void update(String stockSymbol, double oldPrice, double newPrice);
}

Step 2: Subject インターフェースを定義

// Subject インターフェース
public interface StockSubject {
    void addObserver(StockObserver observer);
    void removeObserver(StockObserver observer);
    void notifyObservers();
}

Step 3: Concrete Subject を実装

// Concrete Subject: 株価
public class StockMarket implements StockSubject {
    private final List<StockObserver> observers = new ArrayList<>();
    private final Map<String, Double> stockPrices = new HashMap<>();

    @Override
    public void addObserver(StockObserver observer) {
        observers.add(observer);
    }

    @Override
    public void removeObserver(StockObserver observer) {
        observers.remove(observer);
    }

    @Override
    public void notifyObservers() {
        // 実装は updatePrice() 内で行う
    }

    public void updatePrice(String symbol, double newPrice) {
        double oldPrice = stockPrices.getOrDefault(symbol, 0.0);
        stockPrices.put(symbol, newPrice);

        // すべての Observer に通知
        for (StockObserver observer : observers) {
            observer.update(symbol, oldPrice, newPrice);
        }
    }

    public double getPrice(String symbol) {
        return stockPrices.getOrDefault(symbol, 0.0);
    }
}

Step 4: Concrete Observer を実装

// Concrete Observer 1: メールアラート
public class EmailAlertObserver implements StockObserver {
    private final String email;
    private final double threshold; // 変動率の閾値 (%)

    public EmailAlertObserver(String email, double threshold) {
        this.email = email;
        this.threshold = threshold;
    }

    @Override
    public void update(String symbol, double oldPrice, double newPrice) {
        if (oldPrice == 0) return;
        double changeRate = Math.abs((newPrice - oldPrice) / oldPrice * 100);

        if (changeRate >= threshold) {
            System.out.printf("[EMAIL → %s] %s が %.1f%% 変動: %.2f → %.2f%n",
                email, symbol, changeRate, oldPrice, newPrice);
        }
    }
}
// Concrete Observer 2: モバイルプッシュ通知
public class MobilePushObserver implements StockObserver {
    private final String deviceId;

    public MobilePushObserver(String deviceId) {
        this.deviceId = deviceId;
    }

    @Override
    public void update(String symbol, double oldPrice, double newPrice) {
        String direction = newPrice > oldPrice ? "" : "";
        System.out.printf("[PUSH → %s] %s %s %.2f円%n",
            deviceId, symbol, direction, newPrice);
    }
}
// Concrete Observer 3: ダッシュボード(リアルタイム表示)
public class DashboardObserver implements StockObserver {
    private final Map<String, Double> displayedPrices = new HashMap<>();

    @Override
    public void update(String symbol, double oldPrice, double newPrice) {
        displayedPrices.put(symbol, newPrice);
        System.out.printf("[DASHBOARD] %s: %.2f円 (前回: %.2f円)%n",
            symbol, newPrice, oldPrice);
    }

    public void printAll() {
        System.out.println("=== 現在の株価一覧 ===");
        displayedPrices.forEach((symbol, price) ->
            System.out.printf("  %s: %.2f円%n", symbol, price));
    }
}

Step 5: 実際の使用例

public class Main {
    public static void main(String[] args) {
        StockMarket market = new StockMarket();

        // Observer を登録
        EmailAlertObserver emailAlert =
            new EmailAlertObserver("zhongtao@example.com", 5.0); // 5%以上変動でメール
        MobilePushObserver mobilePush = new MobilePushObserver("iPhone-XYZ");
        DashboardObserver dashboard = new DashboardObserver();

        market.addObserver(emailAlert);
        market.addObserver(mobilePush);
        market.addObserver(dashboard);

        // 株価を更新 → 全 Observer に自動通知
        System.out.println("--- トヨタ株価更新 ---");
        market.updatePrice("TOYOTA", 2850.00);

        System.out.println("\n--- ソニー株価更新(大幅変動)---");
        market.updatePrice("SONY", 12500.00);
        market.updatePrice("SONY", 13200.00); // 5.6%上昇 → メールが飛ぶ

        System.out.println("\n--- Observer の動的な削除 ---");
        market.removeObserver(mobilePush); // モバイル通知を解除
        market.updatePrice("TOYOTA", 2900.00); // mobilePush には通知されない

        System.out.println();
        dashboard.printAll();
    }
}

出力結果:

--- トヨタ株価更新 ---
[PUSH → iPhone-XYZ] TOYOTA ↑ 2850.00円
[DASHBOARD] TOYOTA: 2850.00円 (前回: 0.00円)

--- ソニー株価更新(大幅変動)---
[PUSH → iPhone-XYZ] SONY ↑ 12500.00円
[DASHBOARD] SONY: 12500.00円 (前回: 0.00円)
[EMAIL → zhongtao@example.com] SONY が 5.6% 変動: 12500.00 → 13200.00
[PUSH → iPhone-XYZ] SONY ↑ 13200.00円
[DASHBOARD] SONY: 13200.00円 (前回: 12500.00円)

--- Observer の動的な削除 ---
[EMAIL → zhongtao@example.com] TOYOTA が ... (閾値未達のため省略)
[DASHBOARD] TOYOTA: 2900.00円 (前回: 2850.00円)

=== 現在の株価一覧 ===
  TOYOTA: 2900.00円
  SONY: 13200.00円

UML クラス図

┌───────────────┐         ┌──────────────────────┐
│  StockMarket  │────────▶│   StockObserver      │
│  (Subject)    │ notifies│   <>       │
├───────────────┤         ├──────────────────────┤
│ -observers    │         │ +update(symbol,      │
│ -stockPrices  │         │   oldPrice, newPrice)│
├───────────────┤         └──────────┬───────────┘
│ +addObserver()│                    │ implements
│ +removeObs.() │          ┌─────────┼──────────┐
│ +updatePrice()│          ▼         ▼          ▼
└───────────────┘  ┌───────────┐ ┌────────┐ ┌──────────┐
                   │  Email    │ │Mobile  │ │Dashboard │
                   │  Alert    │ │Push    │ │Observer  │
                   └───────────┘ └────────┘ └──────────┘

Java 標準ライブラリとの関係

Java 標準ライブラリにも Observer パターンの実装が組み込まれています。 ただし、java.util.Observerjava.util.Observable は Java 9 で非推奨(Deprecated)になりました。 現代の Java 開発では以下の代替を使用します。

用途 推奨クラス/フレームワーク
GUIイベント処理 java.beans.PropertyChangeListener
リアクティブストリーム java.util.concurrent.Flow(Java 9+)
Springのイベント ApplicationEvent + @EventListener
Android LiveData、StateFlow(Kotlin)

Strategy パターンとの違い

前回解説した Strategy パターンと比較すると、 目的が明確に異なります。

Strategy パターン Observer パターン
目的 アルゴリズムの交換 状態変化の通知
関係 1対1(Context と Strategy) 1対多(Subject と複数Observer)
タイミング 呼び出し時に選択 状態変化時に自動発火

まとめ

Observerパターンは、「状態変化を複数の場所に伝えたい」ときに最も力を発揮します。 Subject と Observer を疎結合に保つことで、新しい通知先を追加しても既存コードを一切変更せずに済みます。 現代のリアクティブプログラミング(RxJava、Kotlin Flow)も、この思想を発展させたものです。