CodeGym /課程 /JAVA 25 SELF /Observer 模式(觀察者)

Observer 模式(觀察者)

JAVA 25 SELF
等級 50 , 課堂 3
開放

1. 認識「觀察者」模式

「觀察者」(Observer)是最知名且基礎的設計模式之一。它描述了這樣的情境:一個物件(被觀察者,或 subject)在狀態變更時,通知已訂閱的其他物件(觀察者,observers)。

更直白地說:我們有一個 Telegram 頻道(被觀察的物件),以及「訂閱者」(觀察者)。每當有新貼文,頻道就會通知所有訂閱者,而他們再決定要怎麼做——閱讀、忽略或取消訂閱。

在程式設計中,此模式讓有興趣的物件能自動收到事件或狀態變更的通知,而不需要彼此直接耦合。 這對於構建彈性高、可擴展且易於維護的系統非常重要。

在哪裡會用到「觀察者」模式?

  • 在圖形介面(Swing、AWT、JavaFX)— 事件監聽器。
  • 在反應式(Reactive)類庫(RxJava、Project Reactor)。
  • 在商業邏輯:對模型狀態變更做出反應。
  • 在遊戲引擎(碰撞、勝利、失敗等事件)。
  • 凡是需要把「發生了什麼」與「該怎麼處理」解耦的地方。

與 Java 的事件與監聽器之關係

事實上,整個 Java 的事件模型都建構在「觀察者」之上。當你撰寫 button.addActionListener(listener);,你就在實作這個模式:

  • 被觀察者 — 按鈕(或其他元件)。
  • 觀察者 — 你的監聽器,實作 actionPerformed() 方法。
  • 事件 — 使用者點擊、滑鼠移入等。
  • 通知 — 元件呼叫 actionPerformed()。

這就是經典的 Observer 實作!

2. 「觀察者」模式的經典實作

我們用自己的類別(不依賴 Swing 與 AWT)來實作一遍,看看並沒有任何魔法。

模式的核心元素

  • Observable (Subject) — 被觀察的物件。保存觀察者清單,並在變更時通知他們。
  • Observer — 觀察者介面,通常具有 update() 方法。

範例:溫度計與空調

觀察者介面

public interface TemperatureObserver {
    void temperatureChanged(int newTemperature);
}

類別「溫度計」(被觀察者)

import java.util.*;

public class Thermometer {
    private int temperature;
    private final List<TemperatureObserver> observers = new ArrayList<>();

    public void addObserver(TemperatureObserver observer) {
        observers.add(observer);
    }

    public void removeObserver(TemperatureObserver observer) {
        observers.remove(observer);
    }

    public void setTemperature(int newTemperature) {
        if (this.temperature != newTemperature) {
            this.temperature = newTemperature;
            notifyObservers();
        }
    }

    private void notifyObservers() {
        for (TemperatureObserver observer : observers) {
            observer.temperatureChanged(temperature);
        }
    }
}

觀察者範例 —「空調」

public class AirConditioner implements TemperatureObserver {
    @Override
    public void temperatureChanged(int newTemperature) {
        if (newTemperature > 25) {
            System.out.println("空調已開啟!很熱:" + newTemperature + "°C");
        } else {
            System.out.println("空調已關閉。溫度:" + newTemperature + "°C");
        }
    }
}

使用方式

public class Main {
    public static void main(String[] args) {
        Thermometer thermometer = new Thermometer();
        AirConditioner conditioner = new AirConditioner();

        thermometer.addObserver(conditioner);

        thermometer.setTemperature(22); // 空調已關閉。溫度:22°C
        thermometer.setTemperature(28); // 空調已開啟!很熱:28°C
    }
}

就這麼簡單! 你可以再加上一百個觀察者——每當溫度改變時,他們都會收到通知。

模式圖示

flowchart LR
    T["溫度計 (Observable)"] -- 通知 --> AC["空調 (Observer)"]
    T -- 通知 --> L["記錄器 (Observer)"]
    T -- 通知 --> Alarm["警報器 (Observer)"]

現代細節:過時的 Observable 與新做法

在 Java 標準庫中曾有 java.util.Observablejava.util.Observer,但自 Java 9 起它們被標註為已過時(deprecated)。原因在於彈性不足(例如 Observable 是類別、不是介面,因此更難從其他類別繼承)。

現代做法是設計自己的監聽介面與訂閱/退訂邏輯(如上例)。這樣更有彈性、更安全,也更符合實務需求。

3. 範例:帶有訂閱者的迷你應用

來做一個「點擊計數器」,可訂閱其值的變更。

監聽器介面

public interface CounterListener {
    void counterChanged(int newValue);
}

計數器類別

import java.util.*;

public class Counter {
    private int value = 0;
    private final List<CounterListener> listeners = new ArrayList<>();

    public void addCounterListener(CounterListener l) {
        listeners.add(l);
    }

    public void removeCounterListener(CounterListener l) {
        listeners.remove(l);
    }

    public void increment() {
        value++;
        notifyListeners();
    }

    private void notifyListeners() {
        for (CounterListener l : listeners) {
            l.counterChanged(value);
        }
    }

    public int getValue() {
        return value;
    }
}

監聽器:輸出訊息

public class ConsoleCounterListener implements CounterListener {
    @Override
    public void counterChanged(int newValue) {
        System.out.println("計數器已變更:" + newValue);
    }
}

使用方式

public class Main {
    public static void main(String[] args) {
        Counter counter = new Counter();
        counter.addCounterListener(new ConsoleCounterListener());

        counter.increment(); // 計數器已變更:1
        counter.increment(); // 計數器已變更:2
    }
}

4. 有用的細節

現代替代與擴充

在實際專案中,常用匿名類別或 Lambda 來訂閱: counter.addCounterListener(newValue -> System.out.println("新值: " + newValue));

(要這樣做,介面必須是函式式介面——只含一個抽象方法。)

此外,反應式類庫(RxJava、Project Reactor)也很受歡迎,其中以事件流、過濾、非同步等能力來實作「觀察者」。要理解其本質,上文的經典架構已足夠。

「觀察者」模式在生活中的應用

  • 資料模型。 模型(任務清單、商品、使用者)的變更會通知視圖進行更新。
  • 日誌。 記錄器作為訂閱者,對全系統的事件做出反應。
  • 通知。 狀態變更時——傳送 email、推播通知、Telegram 訊息。
  • 遊戲。 血量變化、敵人出現、關卡完成。
  • 多執行緒。 一個執行緒發佈事件,其他執行緒回應。

5. 實作「觀察者」模式的常見錯誤

錯誤 1:忘了移除監聽器。 如果監聽器不再需要,但未被移除,它仍會接收通知。在長時間執行的應用中,這可能導致記憶體洩漏。

錯誤 2:在處理器中執行耗時或阻塞的操作。 如果處理器執行繁重工作(IO、資料庫),應用程式可能會「卡住」,尤其當通知來自 UI 執行緒時。請將繁重任務移到背景執行緒。

錯誤 3:監聽器中未處理的例外。 某個監聽器拋出的例外可能會中斷對其他監聽器的通知。將對監聽器的呼叫包在 try-catch 中,並記錄錯誤。

錯誤 4:同一個監聽器被註冊多次。 如果同一個監聽器被多次加入,它將會收到重複的事件。請留意註冊流程,並避免重複加入。

錯誤 5:被觀察者與觀察者之間耦合過緊。 如果被觀察者知道觀察者的具體實作,就違反了鬆耦合。只使用介面(例如 TemperatureObserverCounterListener)。

留言
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION