往期回顾:

设计气象观测站

以书中的气象监测应用为例:现在有一个气象中心可以监测温度、湿度、气压三种数据,我们需要通过 WeatherData 对象来获取这些数据,然后将这些数据显示在特定的装置上。

WeatherData 拥有以下方法:

  • getTemperature():获取温度数据
  • getHumidity(): 获取湿度数据
  • getPressure():获取气压数据
  • measurementsChanged():一旦气象站更新数据,这个方法会被调用

这样一看似乎十分简单:我们只要在 measurementsChanged() 中通过一系列 getter 获取到气象台提供的温度、湿度与气压数据,然后再调用显示装置的更新数据方法即可。

但是,如果我们后续需要增加或减少显示装置应该怎么办呢?每次都要修改 measurementsChanged() 显然不是个好办法。

出版者与订阅者

想想在现实生活中我们是怎么享受报纸订阅服务的?

  • 报社负责出版报纸,可以接受人们的订阅或取消订阅
  • 如果我们向报社订阅了报纸,一旦有新报纸出版,报社就会送来新的报纸
  • 如果我们不想看报纸了,就取消订阅,报社就不会再送新报纸上门

气象站与显示装置之间其实也是这样的关系,气象站为「出版者」,显示装置为「订阅者」:需要获得气象站数据的显示装置可以向气象站申请「订阅」,这样一旦有气象数据更新,气象站就会通知申请订阅的显示装置;如果显示装置不再需要该气象站提供数据,则可以「取消订阅」,不再接受气象站的通知

上述「出版者」称为「主题」(Subject),「订阅者」称为「观察者」(Observer),两者构成了观察者模式的主要部分。

定义观察者模式

观察者模式定义如下:

观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象改变状态时,它的所有依赖者都会收到通知并且自动更新。

观察者模式类图

类图中包含两个接口定义:

  • 主题接口 Subject
    • registerObserver():添加订阅者
    • removeObserver():移除订阅者
    • notifyObserver():通知订阅者
  • 观察者接口 Observer
    • update():在主题 notifyObserver() 中被调用,用于更新观察者的数据

实现气象站

设计类图

根据上述观察者模式定义,我们先为气象站设计「主题」与「观察者」两个接口,除此之外也可以添加一个显示装置接口,专门负责显示装置的具体显示格式。

接口定义好后,就可以让具体的类来实现这些接口了:

  • WeatherData 作为具体主题,实现 Subject 主题接口
  • 各个显示装置作为具体观察者,实现 Observer 观察者接口

气象站类图

具体实现

PHP

<?php

/**
* 主题接口
*/
interface Subject
{
public function registerObserver($observer);

public function removeObserver($observer);

public function notifyObservers();
}

/**
* 观察者接口
*/
interface Observer
{
public function update($temp, $humidity, $pressure);
}

/**
* 显示装置显示接口
*/
interface DisplayElement
{
public function display();
}

/**
* WeatherData 实现主题接口
*/
class WeatherData implements Subject
{
// 观察者数组
private $observers;

// 温度
private $temperature;

// 湿度
private $humidity;

// 气压
private $pressure;

public function __construct()
{
$this->observers = [];
}

/**
* 加入新的观察者
*/
public function registerObserver($observer)
{
$this->observers[] = $observer;
}

/**
* 移除观察者
*/
public function removeObserver($observer)
{
$index = array_search($observer, $this->observers);
unset($this->observers[$index]);
}

/**
* 通知观察者
*/
public function notifyObservers()
{
// 遍历数组通知观察者
foreach ($this->observers as $observer) {
$observer->update($this->temperature, $this->humidity, $this->pressure);
}
}

public function measurementsChanged()
{
// 通知订阅者
$this->notifyObservers();
}

/**
* 气象站有新数据将调用该函数
*/
public function setMeasurements($temperature, $humidity, $pressure)
{
$this->temperature = $temperature;
$this->humidity = $humidity;
$this->pressure = $pressure;
$this->measurementsChanged();
}
}

// 创建显示装置

/**
* 显示装置 1: 只显示温度和湿度
*/
class FirstDisplay implements Observer, DisplayElement
{
// 温度
private $temperature;

// 湿度
private $humidity;

/**@var WeatherData $weatherData */
private $weatherData;

public function __construct($weatherData)
{
// 创建一个 WeatherData 实例
$this->weatherData = $weatherData;
$this->weatherData->registerObserver($this);
}

public function update($temperature, $humidity, $pressure)
{
$this->temperature = $temperature;
$this->humidity = $humidity;
$this->display();
}

public function display()
{
echo "当前温度:{$this->temperature},当前湿度:{$this->humidity}\n";
}
}

/**
* 显示装置 2:只显示气压
*/
class SecondDisplay implements Observer, DisplayElement {

// 气压
private $pressure;

/**@var WeatherData $weatherData */
private $weatherData;

public function __construct($weatherData)
{
// 创建一个 WeatherData 实例
$this->weatherData = $weatherData;
$this->weatherData->registerObserver($this);
}

public function update($temperature, $humidity, $pressure)
{
$this->pressure = $pressure;
$this->display();
}

public function display()
{
echo "当前气压:{$this->pressure}\n";
}
}

/**
* 显示装置 3(略)
* class ThirdDisplay
*/

// 测试调用
// 创建一个 WeatherData 对象
$weatherData = new WeatherData();
// 创建显示装置 1,传入 WeatherData 对象
$firstDisplay = new FirstDisplay($weatherData);
// 传入模拟气象数据
$weatherData->setMeasurements(80, 70, 30.4);
$weatherData->setMeasurements(70, 60, 29.2);
// 取消订阅
$weatherData->removeObserver($firstDisplay);
// 创建显示装置 2,传入 WeatherData 对象
$secondDisplay = new SecondDisplay($weatherData);
$weatherData->setMeasurements(90, 60, 29.2);

/* Output:
当前温度:80,当前湿度:70
当前温度:70,当前湿度:60
当前气压:29.2
*/

Python

class Subject:
def __init__(self):
"""
主题(出版者)
"""
self._observers = []

def register(self, observer):
"""
添加观察者
"""
if observer not in self._observers:
self._observers.append(observer)

def remove(self, observer):
"""
移除观察者
"""
try:
self._observers.remove(observer)
except ValueError:
pass

def notify(self):
"""
发送通知给所有观察者
"""
for observer in self._observers:
observer.update()


class WeatherData(Subject):
def __init__(self):
Subject.__init__(self)
self._temperature = 0 # 温度
self._humidity = 0 # 湿度
self._pressure = 0 # 气压

def set_measurements(self, temperature, humidity, pressure):
"""
气象数据发生变动时调用该函数
"""
self._temperature = temperature
self._humidity = humidity
self._pressure = pressure
self.notify()

@property
def temperature(self):
return self._temperature

@property
def humidity(self):
return self._humidity

@property
def pressure(self):
return self._pressure


class FirstDisplay:
def __init__(self, weatherData):
"""
显示装置 1:显示温度和湿度
"""
self._weather_data = weatherData
self._weather_data.register(self)
self._temperature = 0
self._humidity = 0

def update(self):
"""
更新数据
"""
self._temperature = self._weather_data.temperature
self._humidity = self._weather_data.humidity
self.display()

def display(self):
"""
显示数据
"""
print("当前温度:%s,当前湿度:%s" % (self._temperature, self._humidity))


class SecondDisplay:
def __init__(self, weatherData):
"""
显示装置 2:显示气压
"""
self._weather_data = weatherData
self._weather_data.register(self)
self._pressure = 0

def update(self):
"""
更新数据
"""
self._pressure = self._weather_data.pressure
self.display()

def display(self):
print("当前气压:%s" % self._pressure)


def main():
# 创建一个 WeatherData 对象
weather_data = WeatherData()
# 创建显示装置 1
first_display = FirstDisplay(weather_data)
# 传入模拟数据
weather_data.set_measurements(21, 50, 3)
weather_data.set_measurements(3, 70, 4)
# 移除
weather_data.remove(first_display)
# 添加装置 2
second_display = SecondDisplay(weather_data)
weather_data.set_measurements(21, 50, 30)


if __name__ == "__main__":
main()

"""
Output:
当前温度:21,当前湿度:50
当前温度:3,当前湿度:70
当前气压:30
"""

Golang

package main

import (
"fmt"
)

// Subject 主题
type Subject interface {
Register(observer Observer)
Remove(obeserver Observer)
Notify()
}

// WeatherData 具体主题
type WeatherData struct {
observers []Observer
temperature int
humidity int
pressure float32
}

// 注册
func (w *WeatherData) Register(observer Observer) {
w.observers = append(w.observers, observer)
}

// 取消订阅
func (w *WeatherData) Remove(observer Observer) {
// 双指针法:找到需要取消订阅的对象并覆盖
j := 0
for _, ob := range w.observers {
if ob != observer {
w.observers[j] = observer
j++
}
}
w.observers = w.observers[:j]
}

// 通知所有订阅者
func (w *WeatherData) Notify() {
for _, observer := range w.observers {
observer.update(w.temperature, w.humidity, w.pressure)
}
}

// 设置新的数据
func (w *WeatherData) SetMeasurements(temperature int, humidity int, pressure float32) {
w.temperature = temperature
w.humidity = humidity
w.pressure = pressure
w.Notify()
}

// Observer 观察者
type Observer interface {
update(temperature int, humidity int, pressure float32)
display()
}

// FirstDisplay 显示装置 1
type FirstDisplay struct {
temperature int
humidity int
pressure float32
}

func (display *FirstDisplay) update(temperature int, humidity int, pressure float32) {
display.temperature = temperature
display.humidity = humidity
display.pressure = pressure
display.display()
}

func (display *FirstDisplay) display() {
fmt.Printf("当前温度:%d, 当前湿度:%d\n", display.temperature, display.humidity)
}

// SecondDisplay 显示装置 2
type SecondDisplay struct {
temperature int
humidity int
pressure float32
}

func (display *SecondDisplay) update(temperature int, humidity int, pressure float32) {
display.temperature = temperature
display.humidity = humidity
display.pressure = pressure
display.display()
}

func (display *SecondDisplay) display() {
fmt.Printf("当前气压:%.2f\n", display.pressure)
}

func main() {
weatherData := WeatherData{}
// 创建显示装置 1
firstDisplay := &FirstDisplay{}
weatherData.Register(firstDisplay)
weatherData.SetMeasurements(23, 50, 23.1)
// weatherData.Remove(firstDisplay)
// 创建显示装置 2
secondDisplay := &SecondDisplay{}
weatherData.Register(secondDisplay)
weatherData.SetMeasurements(22, 70, 24.2)
}

总结

  • 观察者模式定义了对象之间一对多的关系
  • 主题通过一个共同的接口来更新观察者
  • 主题和观察者之间用松耦合方式结合,主题不需要知道观察者的细节,具体观察者只需要实现观察者的接口