前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《Head First 设计模式》学习笔记 | 观察者模式

《Head First 设计模式》学习笔记 | 观察者模式

作者头像
江不知
发布2020-04-14 15:16:26
4730
发布2020-04-14 15:16:26
举报
文章被收录于专栏:编程拯救世界
往期回顾:

设计气象观测站

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

WeatherData 拥有以下方法:

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

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

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

出版者与订阅者

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

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

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

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

定义观察者模式

观察者模式定义如下:

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

观察者模式类图

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

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

实现气象站

设计类图

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

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

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

气象站类图

具体实现

PHP
代码语言:javascript
复制
<?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
代码语言:javascript
复制
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
代码语言:javascript
复制
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)
}

总结

  • 观察者模式定义了对象之间一对多的关系
  • 主题通过一个共同的接口来更新观察者
  • 主题和观察者之间用松耦合方式结合,主题不需要知道观察者的细节,具体观察者只需要实现观察者的接口
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-03-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 编程拯救世界 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 设计气象观测站
  • 出版者与订阅者
  • 定义观察者模式
  • 实现气象站
    • 设计类图
      • 具体实现
        • PHP
        • Python
        • Golang
    • 总结
    领券
    问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档