Golang设计模式-行为型-观察者模式
引例
某城市气象站需要你开发一个应用,该应用需要完成如下功能:
- 保存气象站提供的温度、湿度、PM2.5等气象数据,并提供更新接口,以便气象站在气象数据有更新时调用;
- 提供三种气象看板:
当前天气看板
负责显示实时温度和湿度;统计看板
负责显示最近10天的平均温度和湿度,空气质量看板
负责显示PM2.5的值; - 在气象数据有变化时及时通知到三种看板,以便看板能够及时更新。
最直观的实现
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
}
需求变更
- 气象站认为空气湿度也是体现空气质量的指标之一,因此
空气质量看板
需要加入空气湿度的数据; - 市民反应
统计看板
提供的数据参考意义不大,于是气象站决定删除这个看板。
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模块的代码。
存在的问题
- WeatherData直接调用具体气象看板的更新函数,违反了针对接口编程,不针对具体实现编程的设计原则;
- 对于每个气象看板的需求变更,我们都必须修改WeatherData模块的代码;
- 无法在运行时动态地增删气象看板;
- 系统中最容易改变的部分(气象看板)与WeatherData耦合,无法单独演化。
重构版本
重构要点
- 使用观察者模式重构系统;
- 主题(气象数据WeatherData)与观察者(气象看板)之间实现了松耦合,它们可以相互通信,但不太清楚彼此的细节;
- WeatherData针对抽象的看板编程,而非针对具体实现编程,看板的需求变化不再影响WeatherData模块的稳定性;
- 气象看板可以通过注册/去注册接口在运行时动态绑定/解绑定主题;
- 系统中最容易改变的部分(气象看板)被封装以来以便单独演化。
代码实现
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(¤tWeather)
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接口没有入参,这样观察者就不用被动接收自己不关心的数据了。
观察者模式
定义
观察者模式定义了对象之间的一对多依赖,这样一来,当一个对象状态改变时,它的所有依赖者都会收到通知并自动更新。
类图
相关设计原则
设计原则 | 观察者模式的实现 |
---|---|
针对接口编程,不针对实现编程 | 主题与观察者都使用接口,观察者利用主题的接口向主题注册,而主题则利用观察者接口通知观察者,这样可以让两者运行正常,又同时具有松耦合的特点。 |
多用组合,少用继承 | 观察者模式利用组合将许多观察者组合进主题中,对象之间的这种关系不是通过继承产生的,而是在运行时利用组合的方式产生的。 |
找出程序中会变化的方面,然后将其和固定不变的方面相分离 | 在观察者模式中,会改变的是主题的状态,以及观察者的数目和类型,利用该模式,你可以改变依赖于主题状态的对象,却不必改变主题,这就叫提前规划。 |
观察者模式的缺点
每种设计模式都有自己的适用场景,也有自己存在的不足,观察者模式适用于一个主题多个观察者的场景,但在某些场景下也有自己的缺点和不足:
- 观察者数量众多时,主题通知所有观察者耗时较长,程序性能会变差;
- 没有机制让观察者知道所观察主题的状态是如何改变的;
- 主题和观察者之间存在互相调用,可能存在循环依赖的问题,导致系统崩溃。