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.Observable 與 java.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:被觀察者與觀察者之間耦合過緊。 如果被觀察者知道觀察者的具體實作,就違反了鬆耦合。只使用介面(例如 TemperatureObserver、CounterListener)。
GO TO FULL VERSION