第四部分:行为型模式 - 观察者模式 (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)
go
// observer.go (Observer interface)
package observer
// Observer 观察者接口
type Observer interface {
Update(temperature float32, humidity float32, pressure float32) // 推模型
// Update(subject Subject) // 拉模型或混合模型,Subject 是主题接口
GetID() string // 用于演示移除特定观察者
}
java
// Observer.java (Observer interface)
package com.example.observer;
public interface Observer {
// Push model: subject pushes state to observer
void update(float temperature, float humidity, float pressure);
// Pull model alternative (or combined):
// void update(Subject subject); // Observer would then call subject.getState()
}
主题接口 (Subject)
go
// subject.go (Subject interface)
package subject // 或放在 observer 包中,或单独的 subject 包
import "../observer"
// Subject 主题接口
type Subject interface {
RegisterObserver(o observer.Observer)
RemoveObserver(o observer.Observer)
NotifyObservers()
}
java
// 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)
go
// weather_station.go (ConcreteSubject)
package subject
import (
"../observer"
"fmt"
)
// WeatherStation 具体主题
type WeatherStation struct {
observers []observer.Observer
temperature float32
humidity float32
pressure 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 := false
for 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 = true
break
}
}
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 = temp
ws.humidity = hum
ws.pressure = pres
ws.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 }
java
// 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<>();
}
@Override
public void registerObserver(Observer o) {
System.out.println("WeatherStation: Registering an observer.");
observers.add(o);
}
@Override
public 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.");
}
}
@Override
public void notifyObservers() {
System.out.println("WeatherStation: Notifying observers...");
for (Observer observer : observers) {
observer.update(temperature, humidity, pressure);
}
}
// This method is called when weather measurements change
public void measurementsChanged() {
System.out.println("WeatherStation: Measurements changed.");
notifyObservers();
}
// Simulate new weather data
public 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)
go
// current_conditions_display.go (ConcreteObserver)
package observer
import "fmt"
// CurrentConditionsDisplay 具体观察者,显示当前天气状况
type CurrentConditionsDisplay struct {
id string // 用于标识
temperature float32
humidity 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 = temp
ccd.humidity = hum
ccd.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 observer
import (
"fmt"
"math"
)
// StatisticsDisplay 具体观察者,显示天气统计数据
type StatisticsDisplay struct {
id string
maxTemp float32
minTemp float32
tempSum float32
numReadings int
}
func NewStatisticsDisplay(id string) *StatisticsDisplay {
return &StatisticsDisplay{
id: id,
minTemp: math.MaxFloat32,
}
}
func (sd *StatisticsDisplay) Update(temp, hum, pres float32) {
sd.tempSum += temp
sd.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
}
java
// CurrentConditionsDisplay.java (ConcreteObserver)
package com.example.observer;
// import com.example.subject.Subject; // Needed if this observer registers itself or for pull model
public class CurrentConditionsDisplay implements Observer {
private float temperature;
private float humidity;
// private Subject weatherStation; // For pull model or self-deregistration
private String id;
public CurrentConditionsDisplay(String id /*, Subject weatherStation (optional for self-registration) */) {
this.id = id;
// this.weatherStation = weatherStation;
// weatherStation.registerObserver(this); // Observer registers itself
}
@Override
public 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;
}
@Override
public 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);
}
}
客户端使用
go
// main.go (示例用法)
/*
package main
import (
"./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)
// 创建并注册另一个 CurrentConditionsDisplay
currentDisplay2 := 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)
}
*/
java
// 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 observers
weatherStation.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 display
Observer 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 observer
System.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事件到复杂的消息队列系统,观察者模式无处不在。