Golang设计模式-行为型-观察者模式

引例

某城市气象站需要你开发一个应用,该应用需要完成如下功能:

  1. 保存气象站提供的温度、湿度、PM2.5等气象数据,并提供更新接口,以便气象站在气象数据有更新时调用;
  2. 提供三种气象看板:当前天气看板负责显示实时温度和湿度;统计看板负责显示最近10天的平均温度和湿度,空气质量看板负责显示PM2.5的值;
  3. 在气象数据有变化时及时通知到三种看板,以便看板能够及时更新。

最直观的实现

type WeatherData struct {
	temperature float32
	humidity    float32
	pm2dot5     float32
}

func (data WeatherData) SetData(temperature float32, humidity float32, pm2dot5 float32) error {

	data.temperature = temperature
	data.humidity = humidity
	data.pm2dot5 = pm2dot5

	currentWeather.Update(temperature, humidity)
	statisticWeather.Update(temperature, humidity)
	airQuality.Update(pm2dot5)
	return nil
}

需求变更

  1. 气象站认为空气湿度也是体现空气质量的指标之一,因此空气质量看板需要加入空气湿度的数据;
  2. 市民反应统计看板提供的数据参考意义不大,于是气象站决定删除这个看板。
func (data WeatherData) SetData(temperature float32, humidity float32, pm2dot5 float32) error {

	data.temperature = temperature
	data.humidity = humidity
	data.pm2dot5 = pm2dot5

	currentWeather.Update(temperature, humidity)
	//statisticWeather.Update(temperature, humidity)
	airQuality.Update(humidity, pm2dot5)
	return nil
}

客户需求均与看板有关,与WeatherData模块都没有直接的关系,但我们的系统为了应对这些需求变更,不得不修改WeatherData模块的代码。

存在的问题

  1. WeatherData直接调用具体气象看板的更新函数,违反了针对接口编程,不针对具体实现编程的设计原则;
  2. 对于每个气象看板的需求变更,我们都必须修改WeatherData模块的代码;
  3. 无法在运行时动态地增删气象看板;
  4. 系统中最容易改变的部分(气象看板)与WeatherData耦合,无法单独演化。

重构版本

观察者模式-气象站

重构要点

  1. 使用观察者模式重构系统;
  2. 主题(气象数据WeatherData)与观察者(气象看板)之间实现了松耦合,它们可以相互通信,但不太清楚彼此的细节;
  3. WeatherData针对抽象的看板编程,而非针对具体实现编程,看板的需求变化不再影响WeatherData模块的稳定性;
  4. 气象看板可以通过注册/去注册接口在运行时动态绑定/解绑定主题;
  5. 系统中最容易改变的部分(气象看板)被封装以来以便单独演化。

代码实现

WeatherData模块管理气象数据,并提供动态注册/去注册接口:

type WeatherSubject interface {
	RegisterObserver(observer WeatherObserver) error
	DeregisterObserver(observer WeatherObserver) error

	GetTemperature() float32
	GetHumidity() float32
}

type WeatherData struct {
	temperature float32
	humidity    float32
	observers   []WeatherObserver
}

func (data *WeatherData) RegisterObserver(observer WeatherObserver) error {

	data.observers = append(data.observers, observer)
	return nil
}

func (data *WeatherData) DeregisterObserver(observer WeatherObserver) error {

	for idx, o := range data.observers {
		if o == observer {
			data.observers = append(data.observers[:idx], data.observers[idx+1:]...)
			return nil
		}
	}

	return fmt.Errorf("observer not found")
}

在气象数据发生改变时通知所有的气象看板更新数据:

func (data *WeatherData) SetData(temperature float32, humidity float32) error {

	if err := data.SetTemperature(temperature); err != nil {
		return err
	}

	if err := data.SetHumidity(humidity); err != nil {
		return err
	}

	return data.notifyObservers()
}

func (data *WeatherData) notifyObservers() error {

	for _, o := range data.observers {
		err := o.Update()
		if err != nil {
			return nil
		}
	}

	return nil
}

抽象一个气象看板接口,该接口定义了气象看板需要实现的数据更新接口:

type WeatherObserver interface {
	Update() error
}

当前天气看板实现了这个接口,接受到新的气象数据时实时显示出来:

type CurrentWeather struct {
	subject WeatherSubject
}

func (cw *CurrentWeather) Update() error {
	cw.display()
	return nil
}

func (cw *CurrentWeather) display() {
	fmt.Printf("\nCurrent temperature: %.1f C, ", cw.subject.GetTemperature())
	fmt.Printf("Current Humidity: %.1f%%\n", cw.subject.GetHumidity())
}

统计看板同样实现了该接口,并记录和显示最近10次的气象数据的均值:

type StatisticWeather struct {
	subject      WeatherSubject
	temperatures []float32
	humidities   []float32
}

const (
	STATISTIC_PERIOD = 10
)

func (sw *StatisticWeather) Update() error {
	if len(sw.temperatures) >= STATISTIC_PERIOD {
		sw.temperatures = sw.temperatures[1:]
	}
	sw.temperatures = append(sw.temperatures, sw.subject.GetTemperature())

	if len(sw.humidities) >= STATISTIC_PERIOD {
		sw.humidities = sw.humidities[1:]
	}
	sw.humidities = append(sw.humidities, sw.subject.GetHumidity())

	sw.display()
	return nil
}

func (sw *StatisticWeather) display() {
	var sumTemperature float32
	var sumHumidity float32
	for _, temperature := range sw.temperatures {
		sumTemperature += temperature
	}

	for _, humidity := range sw.humidities {
		sumHumidity += humidity
	}

	fmt.Printf("\nAverage temperature: %.1f C, ", sumTemperature/float32(len(sw.temperatures)))
	fmt.Printf("Average Humidity: %.1f%%\n", sumHumidity/float32(len(sw.humidities)))
}

客户端(气象站)代码:

func main() {

	weatherData := new(WeatherData)

	currentWeather := CurrentWeather{weatherData}
	statisticWeather := StatisticWeather{weatherData, nil, nil}

	weatherData.RegisterObserver(&currentWeather)
	weatherData.RegisterObserver(&statisticWeather)

	weatherData.SetData(26, 60)
	weatherData.SetData(27, 70)
	weatherData.SetData(28, 80)

	weatherData.DeregisterObserver(&statisticWeather)

	weatherData.SetData(29, 90)
}

执行效果:

Current temperature: 26.0 C, Current Humidity: 60.0%

Average temperature: 26.0 C, Average Humidity: 60.0%

Current temperature: 27.0 C, Current Humidity: 70.0%

Average temperature: 26.5 C, Average Humidity: 65.0%

Current temperature: 28.0 C, Current Humidity: 80.0%

Average temperature: 27.0 C, Average Humidity: 70.0%

Current temperature: 29.0 C, Current Humidity: 90.0%

我们先将当前天气看板统计看板注册为气象数据的观察者,前三次气象站更新温度和湿度时,它们均在观察者之列,因此得到了通知。但是后来统计看板被删除,因此不再关注气象数据,我们再次更新温度和湿度时,只有当前天气看板得到了通知,这就是观察者在运行时的动态注册/去注册。

完整代码见:观察者模式go语言实现

主题数据的push与pull

观察者模式内,核心数据被保存在主题内,如本例的气象数据,各观察者获取主题数据有两种形式:

  • push:即主题主动将所有数据推送给观察者,而无论观察者是否关注所有的数据,如本例WeatherData将所有数据均推送给了看板,即使空气质量看板并不关心温度数据,也不得不实现Update接口被动接收所有数据;
  • pull:主题不再主动推送数据,而是由观察者在需要时自行调用主题提供的接口获取数据,这需要观察者维护一个主题对象的引用,如本例的各看板需要保存一个WeatherData对象的引用,且pull方式下,观察者的Update接口没有入参,这样观察者就不用被动接收自己不关心的数据了。

观察者模式

定义

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

类图

观察者模式-类图

相关设计原则

设计原则观察者模式的实现
针对接口编程,不针对实现编程主题与观察者都使用接口,观察者利用主题的接口向主题注册,而主题则利用观察者接口通知观察者,这样可以让两者运行正常,又同时具有松耦合的特点。
多用组合,少用继承观察者模式利用组合将许多观察者组合进主题中,对象之间的这种关系不是通过继承产生的,而是在运行时利用组合的方式产生的。
找出程序中会变化的方面,然后将其和固定不变的方面相分离在观察者模式中,会改变的是主题的状态,以及观察者的数目和类型,利用该模式,你可以改变依赖于主题状态的对象,却不必改变主题,这就叫提前规划。

观察者模式的缺点

每种设计模式都有自己的适用场景,也有自己存在的不足,观察者模式适用于一个主题多个观察者的场景,但在某些场景下也有自己的缺点和不足:

  1. 观察者数量众多时,主题通知所有观察者耗时较长,程序性能会变差;
  2. 没有机制让观察者知道所观察主题的状态是如何改变的;
  3. 主题和观察者之间存在互相调用,可能存在循环依赖的问题,导致系统崩溃。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值