零基础设计模式——行为型模式 - 观察者模式
第四部分:行为型模式 - 观察者模式 (Observer Pattern)
接下来,我们学习非常重要且广泛应用的观察者模式,它也被称为发布-订阅 (Publish-Subscribe) 模式。
- 核心思想:定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
观察者模式 (Observer Pattern)
“定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。” (Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and updated automatically.)
想象一下你订阅了某个YouTube频道:
- YouTube频道 (Subject/Observable/Publisher):这是被观察的对象。当频道主上传新视频时,它的状态就改变了。
- 你和其他订阅者 (Observers/Subscribers):你们是观察者。你们都对这个频道的内容感兴趣。
- 订阅动作 (Register/Attach):你点击“订阅”按钮,就是将自己注册为该频道的一个观察者。
- 新视频通知 (Notify):当频道主发布新视频时,YouTube系统会自动向所有订阅者发送通知(比如邮件、App推送)。
- 查看新视频 (Update):你收到通知后,可以去查看新视频,即根据通知更新自己的状态(“已看”或了解新内容)。
在这个模型中,YouTube频道不需要知道具体是哪些用户订阅了它,它只需要维护一个订阅者列表,并在状态改变时通知列表中的所有订阅者。订阅者也不需要 sürekli (continuously) 去检查频道有没有新视频,它们只需要等待通知。
1. 目的 (Intent)
观察者模式的主要目的:
- 建立对象间的一对多依赖:一个主题对象 (Subject) 可以被多个观察者对象 (Observer) 依赖。
- 自动通知和更新:当主题对象的状态发生变化时,它会自动通知所有观察者,观察者可以据此更新自身状态。
- 解耦主题和观察者:主题对象只知道它有一系列观察者(通常通过一个抽象接口与之交互),但不需要知道观察者的具体类别。观察者也不知道其他观察者的存在。这使得主题和观察者可以独立地变化和复用。
2. 生活中的例子 (Real-world Analogy)
-
报纸/杂志订阅:
- 报社/出版社 (Subject):定期出版新的报纸/杂志。
- 订阅者 (Observers):订阅了报纸/杂志的人。
- 报社出版新的一期后,会将其发送给所有订阅者。
-
拍卖行竞拍:
- 拍卖师/拍卖品 (Subject):拍卖师报出新的价格(状态改变)。
- 竞拍者 (Observers):所有参与竞拍的人都关注当前最高价。当有新的出价时,所有竞拍者都会被告知。
-
天气预报站和用户:
- 天气预报站 (Subject):发布最新的天气信息。
- 关心天气的用户/App (Observers):订阅了天气更新。天气变化时,用户会收到通知。
-
GUI事件处理:
- 按钮、窗口等GUI组件 (Subject):当用户点击按钮或改变窗口大小时,组件状态改变。
- 事件监听器 (Observers):注册到组件上,对特定事件(如点击、大小改变)做出响应。
3. 结构 (Structure)
观察者模式通常包含以下角色:
-
Subject (主题/目标接口或抽象类):
- 知道它的所有观察者。可以有任意多个观察者观察同一个目标。
- 提供用于注册 (
attach()
或registerObserver()
) 和注销 (detach()
或removeObserver()
) 观察者对象的接口。 - 提供一个通知所有观察者的方法 (
notifyObservers()
)。
-
ConcreteSubject (具体主题/具体目标):
- 实现 Subject 接口。
- 存储具体的状态,当其状态改变时,会向所有已注册的观察者发出通知。
- 通常包含一个观察者列表。
-
Observer (观察者接口或抽象类):
- 定义一个更新接口 (
update()
),供主题在状态改变时调用。
- 定义一个更新接口 (
-
ConcreteObserver (具体观察者):
- 实现 Observer 接口。
- 维护一个指向具体主题对象的引用(可选,取决于“推”模型还是“拉”模型)。
- 在
update()
方法中实现对主题状态变化的响应逻辑。
推模型 (Push Model) vs. 拉模型 (Pull Model):
- 推模型:主题对象在通知观察者时,主动将改变的数据(或所有相关数据)作为参数传递给观察者的
update()
方法。观察者被动接收数据。- 优点:观察者不需要自己去查询状态,简单直接。
- 缺点:如果数据量大,或者并非所有观察者都需要所有数据,可能会传递不必要的信息。
- 拉模型:主题对象在通知观察者时,只告诉观察者“状态已改变”,而不传递具体数据。观察者在收到通知后,如果需要,再主动从主题对象那里拉取(
getState()
)所需的数据。- 优点:观察者可以按需获取数据,更灵活,避免了不必要的数据传输。
- 缺点:观察者需要知道主题对象并调用其方法获取状态,可能增加一点耦合(如果
update
方法不传递主题引用的话)。如果多个观察者在通知后都去拉数据,可能导致对主题的多次查询。
在实践中,update()
方法常常会把主题自身 (Subject
的引用) 作为参数传递给观察者,这样观察者就可以在需要时回调主题获取状态,结合了推(通知)和拉(获取数据)的特点。
4. 适用场景 (When to Use)
- 当一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这两者封装在独立的对象中以允许它们各自独立地改变和复用。
- 当对一个对象的改变需要同时改变其他对象,而不知道具体有多少对象有待改变时。
- 当一个对象必须通知其他对象,而它又不能假定其他对象是谁。换言之,你不想让这些对象紧密耦合。
- 在事件驱动的系统中,例如GUI编程、消息队列等。
- 当需要实现发布-订阅模型时。
5. 优缺点 (Pros and Cons)
优点:
- 松耦合:主题和观察者之间是松耦合的。主题只知道观察者实现了某个接口,观察者可以独立于主题和其他观察者而改变。
- 可扩展性好:可以轻松地增加新的观察者,而无需修改主题或其他观察者。
- 支持广播通信:主题的状态改变可以通知到所有已注册的观察者。
- 符合开闭原则:对扩展开放(可以增加新的观察者),对修改关闭(不需要修改现有主题或观察者来添加新观察者)。
缺点:
- 通知顺序不确定:如果观察者的通知顺序很重要,观察者模式本身不保证特定的通知顺序(除非在实现中特别处理)。
- 可能导致意外更新(级联更新):如果一个观察者的更新操作又触发了其他观察者(甚至是原始主题)的更新,可能会导致复杂的、难以追踪的级联更新,甚至循环依赖。
- 调试困难:由于是松耦合和动态通知,有时追踪一个状态改变如何影响所有观察者可能会比较复杂。
- “拉”模型可能导致效率问题:如果观察者在收到通知后频繁地从主题拉取数据,可能会影响性能。
6. 实现方式 (Implementations)
让我们以一个天气数据站 (WeatherStation) 作为主题,不同的显示设备 (DisplayDevice) 作为观察者为例。
观察者接口 (Observer)
// observer.go (Observer interface)
package observer// Observer 观察者接口
type Observer interface {Update(temperature float32, humidity float32, pressure float32) // 推模型// Update(subject Subject) // 拉模型或混合模型,Subject 是主题接口GetID() string // 用于演示移除特定观察者
}
// Observer.java (Observer interface)
package com.example.observer;public interface Observer {// Push model: subject pushes state to observervoid update(float temperature, float humidity, float pressure);// Pull model alternative (or combined):// void update(Subject subject); // Observer would then call subject.getState()
}
主题接口 (Subject)
// subject.go (Subject interface)
package subject // 或放在 observer 包中,或单独的 subject 包import "../observer"// Subject 主题接口
type Subject interface {RegisterObserver(o observer.Observer)RemoveObserver(o observer.Observer)NotifyObservers()
}
// Subject.java (Subject interface)
package com.example.subject;import com.example.observer.Observer;public interface Subject {void registerObserver(Observer o);void removeObserver(Observer o);void notifyObservers();
}
具体主题 (WeatherStation - ConcreteSubject)
// weather_station.go (ConcreteSubject)
package subjectimport ("../observer""fmt"
)// WeatherStation 具体主题
type WeatherStation struct {observers []observer.Observertemperature float32humidity float32pressure float32
}func NewWeatherStation() *WeatherStation {return &WeatherStation{observers: make([]observer.Observer, 0),}
}func (ws *WeatherStation) RegisterObserver(o observer.Observer) {fmt.Printf("WeatherStation: Registering observer %s\n", o.GetID())ws.observers = append(ws.observers, o)
}func (ws *WeatherStation) RemoveObserver(o observer.Observer) {found := falsefor i, obs := range ws.observers {if obs.GetID() == o.GetID() { // 假设通过 ID 比较ws.observers = append(ws.observers[:i], ws.observers[i+1:]...)fmt.Printf("WeatherStation: Removed observer %s\n", o.GetID())found = truebreak}}if !found {fmt.Printf("WeatherStation: Observer %s not found for removal\n", o.GetID())}
}func (ws *WeatherStation) NotifyObservers() {fmt.Println("WeatherStation: Notifying observers...")for _, obs := range ws.observers {obs.Update(ws.temperature, ws.humidity, ws.pressure)}
}// MeasurementsChanged 当天气数据变化时调用此方法
func (ws *WeatherStation) MeasurementsChanged() {fmt.Println("WeatherStation: Measurements changed.")ws.NotifyObservers()
}// SetMeasurements 设置新的天气数据,并通知观察者
func (ws *WeatherStation) SetMeasurements(temp, hum, pres float32) {fmt.Printf("WeatherStation: Setting new measurements (Temp: %.1f, Hum: %.1f, Pres: %.1f)\n", temp, hum, pres)ws.temperature = tempws.humidity = humws.pressure = presws.MeasurementsChanged()
}// Getters for pull model (not used in this push model example for Update)
func (ws *WeatherStation) GetTemperature() float32 { return ws.temperature }
func (ws *WeatherStation) GetHumidity() float32 { return ws.humidity }
func (ws *WeatherStation) GetPressure() float32 { return ws.pressure }
// WeatherStation.java (ConcreteSubject)
package com.example.subject;import com.example.observer.Observer;
import java.util.ArrayList;
import java.util.List;public class WeatherStation implements Subject {private List<Observer> observers;private float temperature;private float humidity;private float pressure;public WeatherStation() {this.observers = new ArrayList<>();}@Overridepublic void registerObserver(Observer o) {System.out.println("WeatherStation: Registering an observer.");observers.add(o);}@Overridepublic void removeObserver(Observer o) {int i = observers.indexOf(o);if (i >= 0) {observers.remove(i);System.out.println("WeatherStation: Removed an observer.");} else {System.out.println("WeatherStation: Observer not found for removal.");}}@Overridepublic void notifyObservers() {System.out.println("WeatherStation: Notifying observers...");for (Observer observer : observers) {observer.update(temperature, humidity, pressure);}}// This method is called when weather measurements changepublic void measurementsChanged() {System.out.println("WeatherStation: Measurements changed.");notifyObservers();}// Simulate new weather datapublic void setMeasurements(float temperature, float humidity, float pressure) {System.out.printf("WeatherStation: Setting new measurements (Temp: %.1f, Hum: %.1f, Pres: %.1f)%n",temperature, humidity, pressure);this.temperature = temperature;this.humidity = humidity;this.pressure = pressure;measurementsChanged();}// Getters for pull model (not directly used by Observer.update in this push model example)public float getTemperature() {return temperature;}public float getHumidity() {return humidity;}public float getPressure() {return pressure;}
}
具体观察者 (CurrentConditionsDisplay, StatisticsDisplay - ConcreteObserver)
// current_conditions_display.go (ConcreteObserver)
package observerimport "fmt"// CurrentConditionsDisplay 具体观察者,显示当前天气状况
type CurrentConditionsDisplay struct {id string // 用于标识temperature float32humidity float32// subject subject.Subject // 如果需要拉模型,则持有主题引用
}func NewCurrentConditionsDisplay(id string /*, sub subject.Subject*/) *CurrentConditionsDisplay {// display := &CurrentConditionsDisplay{id: id, subject: sub}display := &CurrentConditionsDisplay{id: id}// sub.RegisterObserver(display) // 观察者自我注册return display
}func (ccd *CurrentConditionsDisplay) Update(temp, hum, pres float32) {ccd.temperature = tempccd.humidity = humccd.display()
}func (ccd *CurrentConditionsDisplay) display() {fmt.Printf("Display-%s (Current Conditions): %.1fF degrees and %.1f%% humidity\n",ccd.id, ccd.temperature, ccd.humidity)
}func (ccd *CurrentConditionsDisplay) GetID() string {return ccd.id
}// statistics_display.go (Another ConcreteObserver)
package observerimport ("fmt""math"
)// StatisticsDisplay 具体观察者,显示天气统计数据
type StatisticsDisplay struct {id stringmaxTemp float32minTemp float32tempSum float32numReadings int
}func NewStatisticsDisplay(id string) *StatisticsDisplay {return &StatisticsDisplay{id: id,minTemp: math.MaxFloat32,}
}func (sd *StatisticsDisplay) Update(temp, hum, pres float32) {sd.tempSum += tempsd.numReadings++if temp > sd.maxTemp {sd.maxTemp = temp}if temp < sd.minTemp {sd.minTemp = temp}sd.display()
}func (sd *StatisticsDisplay) display() {avgTemp := sd.tempSum / float32(sd.numReadings)fmt.Printf("Display-%s (Avg/Max/Min temperature): %.1fF / %.1fF / %.1fF\n",sd.id, avgTemp, sd.maxTemp, sd.minTemp)
}func (sd *StatisticsDisplay) GetID() string {return sd.id
}
// CurrentConditionsDisplay.java (ConcreteObserver)
package com.example.observer;// import com.example.subject.Subject; // Needed if this observer registers itself or for pull modelpublic class CurrentConditionsDisplay implements Observer {private float temperature;private float humidity;// private Subject weatherStation; // For pull model or self-deregistrationprivate String id;public CurrentConditionsDisplay(String id /*, Subject weatherStation (optional for self-registration) */) {this.id = id;// this.weatherStation = weatherStation;// weatherStation.registerObserver(this); // Observer registers itself}@Overridepublic void update(float temperature, float humidity, float pressure) {this.temperature = temperature;this.humidity = humidity;display();}public void display() {System.out.printf("Display-%s (Current Conditions): %.1fF degrees and %.1f%% humidity%n",this.id, temperature, humidity);}
}// StatisticsDisplay.java (Another ConcreteObserver)
package com.example.observer;public class StatisticsDisplay implements Observer {private float maxTemp = -Float.MAX_VALUE;private float minTemp = Float.MAX_VALUE;private float tempSum = 0.0f;private int numReadings;private String id;public StatisticsDisplay(String id) {this.id = id;}@Overridepublic void update(float temperature, float humidity, float pressure) {tempSum += temperature;numReadings++;if (temperature > maxTemp) {maxTemp = temperature;}if (temperature < minTemp) {minTemp = temperature;}display();}public void display() {System.out.printf("Display-%s (Avg/Max/Min temperature): %.1fF / %.1fF / %.1fF%n",this.id, (tempSum / numReadings), maxTemp, minTemp);}
}
客户端使用
// main.go (示例用法)
/*
package mainimport ("./observer""./subject""fmt"
)func main() {weatherStation := subject.NewWeatherStation()currentDisplay1 := observer.NewCurrentConditionsDisplay("CCD1")statsDisplay1 := observer.NewStatisticsDisplay("StatsD1")// 注册观察者weatherStation.RegisterObserver(currentDisplay1)weatherStation.RegisterObserver(statsDisplay1)fmt.Println("--- First weather update ---")weatherStation.SetMeasurements(80, 65, 30.4)fmt.Println("\n--- Second weather update ---")weatherStation.SetMeasurements(82, 70, 29.2)// 创建并注册另一个 CurrentConditionsDisplaycurrentDisplay2 := observer.NewCurrentConditionsDisplay("CCD2")weatherStation.RegisterObserver(currentDisplay2)fmt.Println("\n--- Third weather update (with new observer CCD2) ---")weatherStation.SetMeasurements(78, 90, 29.2)// 移除一个观察者fmt.Println("\n--- Removing observer StatsD1 ---")weatherStation.RemoveObserver(statsDisplay1)fmt.Println("\n--- Fourth weather update (after removing StatsD1) ---")weatherStation.SetMeasurements(76, 85, 30.0)
}
*/
// Main.java (示例用法)
/*
package com.example;import com.example.observer.CurrentConditionsDisplay;
import com.example.observer.Observer;
import com.example.observer.StatisticsDisplay;
import com.example.subject.WeatherStation;public class Main {public static void main(String[] args) {WeatherStation weatherStation = new WeatherStation();Observer currentDisplay1 = new CurrentConditionsDisplay("CCD1");Observer statsDisplay1 = new StatisticsDisplay("StatsD1");// Register observersweatherStation.registerObserver(currentDisplay1);weatherStation.registerObserver(statsDisplay1);System.out.println("--- First weather update ---");weatherStation.setMeasurements(80, 65, 30.4f);System.out.println("\n--- Second weather update ---");weatherStation.setMeasurements(82, 70, 29.2f);// Create and register another displayObserver currentDisplay2 = new CurrentConditionsDisplay("CCD2");weatherStation.registerObserver(currentDisplay2);System.out.println("\n--- Third weather update (with new observer CCD2) ---");weatherStation.setMeasurements(78, 90, 29.2f);// Remove an observerSystem.out.println("\n--- Removing observer StatsD1 ---");weatherStation.removeObserver(statsDisplay1);System.out.println("\n--- Fourth weather update (after removing StatsD1) ---");weatherStation.setMeasurements(76, 85, 30.0f);}
}
*/
Java 内建支持:
Java 早期提供了 java.util.Observable
类和 java.util.Observer
接口。但 Observable
是一个类,这意味着你的主题类必须继承它,限制了其自身的继承能力。此外,Observable
的 setChanged()
方法是 protected
的,有时不够灵活。因此,现在更推荐自己实现观察者模式,或者使用更现代的库如 RxJava、JavaFX Properties/Bindings,或 java.beans.PropertyChangeListener
。
7. 总结
观察者模式(或发布-订阅模式)是构建可维护和可扩展的事件驱动系统的基石。它通过定义清晰的主题和观察者角色,以及它们之间的交互接口,实现了状态变更的自动通知和依赖对象间的松耦合。这使得系统中的各个部分可以独立演化,同时保持对相关变化的响应能力。从简单的GUI事件到复杂的消息队列系统,观察者模式无处不在。