零基础设计模式——行为型模式 - 观察者模式

第四部分:行为型模式 - 观察者模式 (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)

观察者模式的主要目的:

  1. 建立对象间的一对多依赖:一个主题对象 (Subject) 可以被多个观察者对象 (Observer) 依赖。
  2. 自动通知和更新:当主题对象的状态发生变化时,它会自动通知所有观察者,观察者可以据此更新自身状态。
  3. 解耦主题和观察者:主题对象只知道它有一系列观察者(通常通过一个抽象接口与之交互),但不需要知道观察者的具体类别。观察者也不知道其他观察者的存在。这使得主题和观察者可以独立地变化和复用。

2. 生活中的例子 (Real-world Analogy)

  • 报纸/杂志订阅

    • 报社/出版社 (Subject):定期出版新的报纸/杂志。
    • 订阅者 (Observers):订阅了报纸/杂志的人。
    • 报社出版新的一期后,会将其发送给所有订阅者。
  • 拍卖行竞拍

    • 拍卖师/拍卖品 (Subject):拍卖师报出新的价格(状态改变)。
    • 竞拍者 (Observers):所有参与竞拍的人都关注当前最高价。当有新的出价时,所有竞拍者都会被告知。
  • 天气预报站和用户

    • 天气预报站 (Subject):发布最新的天气信息。
    • 关心天气的用户/App (Observers):订阅了天气更新。天气变化时,用户会收到通知。
  • GUI事件处理

    • 按钮、窗口等GUI组件 (Subject):当用户点击按钮或改变窗口大小时,组件状态改变。
    • 事件监听器 (Observers):注册到组件上,对特定事件(如点击、大小改变)做出响应。

3. 结构 (Structure)

观察者模式通常包含以下角色:

  1. Subject (主题/目标接口或抽象类)

    • 知道它的所有观察者。可以有任意多个观察者观察同一个目标。
    • 提供用于注册 (attach()registerObserver()) 和注销 (detach()removeObserver()) 观察者对象的接口。
    • 提供一个通知所有观察者的方法 (notifyObservers())。
  2. ConcreteSubject (具体主题/具体目标)

    • 实现 Subject 接口。
    • 存储具体的状态,当其状态改变时,会向所有已注册的观察者发出通知。
    • 通常包含一个观察者列表。
  3. Observer (观察者接口或抽象类)

    • 定义一个更新接口 (update()),供主题在状态改变时调用。
  4. ConcreteObserver (具体观察者)

    • 实现 Observer 接口。
    • 维护一个指向具体主题对象的引用(可选,取决于"推"模型还是"拉"模型)。
    • update() 方法中实现对主题状态变化的响应逻辑。

推模型 (Push Model) vs. 拉模型 (Pull Model)

  • 推模型 :主题对象在通知观察者时,主动将改变的数据(或所有相关数据)作为参数传递给观察者的 update() 方法。观察者被动接收数据。
    • 优点:观察者不需要自己去查询状态,简单直接。
    • 缺点:如果数据量大,或者并非所有观察者都需要所有数据,可能会传递不必要的信息。
  • 拉模型 :主题对象在通知观察者时,只告诉观察者"状态已改变",而不传递具体数据。观察者在收到通知后,如果需要,再主动从主题对象那里拉取(getState())所需的数据。
    • 优点:观察者可以按需获取数据,更灵活,避免了不必要的数据传输。
    • 缺点:观察者需要知道主题对象并调用其方法获取状态,可能增加一点耦合(如果 update 方法不传递主题引用的话)。如果多个观察者在通知后都去拉数据,可能导致对主题的多次查询。

在实践中,update() 方法常常会把主题自身 (Subject 的引用) 作为参数传递给观察者,这样观察者就可以在需要时回调主题获取状态,结合了推(通知)和拉(获取数据)的特点。

4. 适用场景 (When to Use)

  • 当一个抽象模型有两个方面,其中一个方面依赖于另一个方面。将这两者封装在独立的对象中以允许它们各自独立地改变和复用。
  • 当对一个对象的改变需要同时改变其他对象,而不知道具体有多少对象有待改变时。
  • 当一个对象必须通知其他对象,而它又不能假定其他对象是谁。换言之,你不想让这些对象紧密耦合。
  • 在事件驱动的系统中,例如GUI编程、消息队列等。
  • 当需要实现发布-订阅模型时。

5. 优缺点 (Pros and Cons)

优点:

  1. 松耦合:主题和观察者之间是松耦合的。主题只知道观察者实现了某个接口,观察者可以独立于主题和其他观察者而改变。
  2. 可扩展性好:可以轻松地增加新的观察者,而无需修改主题或其他观察者。
  3. 支持广播通信:主题的状态改变可以通知到所有已注册的观察者。
  4. 符合开闭原则:对扩展开放(可以增加新的观察者),对修改关闭(不需要修改现有主题或观察者来添加新观察者)。

缺点:

  1. 通知顺序不确定:如果观察者的通知顺序很重要,观察者模式本身不保证特定的通知顺序(除非在实现中特别处理)。
  2. 可能导致意外更新(级联更新):如果一个观察者的更新操作又触发了其他观察者(甚至是原始主题)的更新,可能会导致复杂的、难以追踪的级联更新,甚至循环依赖。
  3. 调试困难:由于是松耦合和动态通知,有时追踪一个状态改变如何影响所有观察者可能会比较复杂。
  4. "拉"模型可能导致效率问题:如果观察者在收到通知后频繁地从主题拉取数据,可能会影响性能。

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 是一个类,这意味着你的主题类必须继承它,限制了其自身的继承能力。此外,ObservablesetChanged() 方法是 protected 的,有时不够灵活。因此,现在更推荐自己实现观察者模式,或者使用更现代的库如 RxJava、JavaFX Properties/Bindings,或 java.beans.PropertyChangeListener

7. 总结

观察者模式(或发布-订阅模式)是构建可维护和可扩展的事件驱动系统的基石。它通过定义清晰的主题和观察者角色,以及它们之间的交互接口,实现了状态变更的自动通知和依赖对象间的松耦合。这使得系统中的各个部分可以独立演化,同时保持对相关变化的响应能力。从简单的GUI事件到复杂的消息队列系统,观察者模式无处不在。

相关推荐
掉鱼的猫37 分钟前
Solon AI + MCP实战:5行代码搞定天气查询,LLM从此告别数据孤岛
java·openai·mcp
带刺的坐椅1 小时前
Solon AI + MCP实战:5行代码搞定天气查询,LLM从此告别数据孤岛
java·mcp·solon-ai
androidwork2 小时前
嵌套滚动交互处理总结
android·java·kotlin
草履虫建模2 小时前
Tomcat 和 Spring MVC
java·spring boot·spring·spring cloud·tomcat·mvc·intellij-idea
枣伊吕波2 小时前
第十三节:第七部分:Stream流的中间方法、Stream流的终结方法
java·开发语言
程序员爱钓鱼2 小时前
Go同步原语与数据竞争:原子操作(atomic)
后端·面试·go
天天摸鱼的java工程师2 小时前
Kafka是如何保证消息队列中的消息不丢失、不重复?
java·后端·kafka
天天摸鱼的java工程师2 小时前
SpringBoot 自动配置原理?@EnableAutoConfiguration 是如何工作的?
java·后端
一点也不想取名2 小时前
解决 Java 与 JavaScript 之间特殊字符传递问题的终极方案
java·开发语言·javascript
27669582922 小时前
朴朴超市小程序 sign-v2 分析
java·python·小程序·逆向分析·朴朴超市·sign-v2·朴朴