观察者模式(
Observer Pattern
)是对象之间一对多的依赖关系,当一个对象改变时,其他依赖它的对象都会收到通知并自动更新。
怎么来理解这句话呢?用微信朋友圈来举个例子,假如你就是被依赖的对象,你的好友都依赖你,这样的关系就形成了一对多,当你发朋友圈的时候你的好友都能收到通知,并且自动更新。
观察者模式是一个比较简单的模式,它的核心很简单,只有两个角色,一个是被依赖的对象,在朋友圈例子里面就是你自己,这个叫主题或者被观察者
,另一个对象是你的朋友叫观察者或者订阅者
,为了统一认知,后面都叫主题(被观察者
)和订阅者(观察者
)。
类图
类图不是目的,只是方便理解
[图片上传失败...(image-538efc-1527174205892)]
从类图上可以看到,主题不关心其他订阅者的实现,只关心Observer
接口,所有实现了Observer
接口并订阅了主题的对象都能在主题发生变化的时候得到通知。
实例
现在来实现一个汽车仪表盘,汽车在行驶过程中转速和速度都会一直处于变化中,我们现在通过观察者模式把转速和速度显示到仪表盘上。
首先,模拟一辆汽车从0-100加速的过程,这个过程中拿到汽车把实时数据:
/**
* 汽车回调数据,这里会根据汽车的速度变化,持续的传递转速和速度
*/
fun carInfo(power:Float, speed: Float)
{
}
/**
* 汽车引擎,模拟汽车从0-100加速
*/
for (speed in 0..1000)
{
Thread.sleep(10)
carInfo(speed/200f + Random().nextInt(2), speed.toFloat()/10)
}
// 部分汽车数据打印
power: 0.0 speed: 0.0
power: 0.005 speed: 0.1
power: 0.01 speed: 0.2
power: 0.015 speed: 0.3
power: 0.02 speed: 0.4
power: 1.025 speed: 0.5
power: 1.03 speed: 0.6
power: 1.035 speed: 0.7
power: 0.04 speed: 0.8
power: 0.045 speed: 0.9
power: 0.05 speed: 1.0
power: 1.055 speed: 1.1
既然要显示到仪表盘,现在还差一个仪表盘Display
用于显示速度和转速:
/**
* 仪表盘
* Created by Carlton on 2016/11/9.
*/
class Display
{
var power:Float = 0f
var speed:Float = 0f
fun display()
{
println("汽车当前的 转速:$power 速度:$speed")
}
}
现在我们把速度变化数据通过仪表盘Display
展示出来:
val display = Display()
/**
* 汽车回调数据,这里会根据汽车的速度变化,持续的传递转速和速度
*/
fun carInfo(power:Float, speed: Float)
{
display.power = power
display.speed = speed
display.display()
}
// 仪表盘数据
……
汽车当前的 转速:3.985 速度:79.7
汽车当前的 转速:4.99 速度:79.8
汽车当前的 转速:3.995 速度:79.9
汽车当前的 转速:4.0 速度:80.0
汽车当前的 转速:5.005 速度:80.1
汽车当前的 转速:5.01 速度:80.2
汽车当前的 转速:4.015 速度:80.3
汽车当前的 转速:4.02 速度:80.4
汽车当前的 转速:5.025 速度:80.5
汽车当前的 转速:5.03 速度:80.6
……
现在我们就实现了一个简易的汽车仪表盘展示数据,这样写有什么问题呢?如果我们给汽车扩展一个后视镜显示速度,中控台展示速度,我们又需要来修改carInfo()
去设置和显示后视镜、中控台,显然不符合设计原则。
现在我们知道整个系统中有两个角色:汽车变化的数据、仪表盘。按照观察者模式,把汽车变化的数据定义成主题(被观察者
),仪表盘定义订阅者(观察者
),然后用观察者模式重构整个系统。
首先,实现观察者接口:
/**
* 主题,有的地方叫观察者Observable
* @param T 更新的数据回调
* Created by Carlton on 2016/11/9.
*/
interface Subject
{
/**
* 注册成为观察者
*/
fun registeObserver(observer: Observer)
/**
* 删除观察者
*/
fun removeObserver(observer: Observer)
/**
* 通知观察者数据已经发生了变化
*/
fun notifyObservers(value: T)
}
/**
* 观察者
* @param T 观察者回调的数据类型
* Created by Carlton on 2016/11/9.
*/
interface Observer
{
/**
* 数据更新
*/
fun update(value: T)
}
接下来把数据封装成一个主题(被观察者
)CarSubject
:
/**
* 具体的主题,被观察者
* Created by Carlton on 2016/11/9.
*/
class CarSubject : Subject>
{
/**
* 观察者
*/
val observers = ArrayList>>()
override fun registerObserver(observer: Observer>)
{
observers.add(observer)
}
override fun removeObserver(observer: Observer>)
{
if(observers.contains(observer))
{
observers.remove(observer)
}
}
override fun notifyObservers(value: Array)
{
for (observer in observers)
{
observer.update(value)
}
}
}
现在改造一下我们的系统,把数据用主题绑定起来:
val carSubject = CarSubject()
/**
* 汽车回调数据,这里会根据汽车的速度变化,持续的传递转速和速度
*/
fun carInfo(power:Float, speed: Float)
{
carSubject.notifyObservers(arrayOf(power, speed))
}
到这里我们实现了一个可扩展的观察者模式系统,观察者模式中的接口部分一般都是固定的,包括java里面都有支持观察者模式,后面会说道,所以如果我们要实现一个观察者模式,接口部分我们只需要实现一次,或者直接使用java api提供的接口,主要需要实现主题或者观察者接口。
在上面的例子中,我们自己提供了观察者接口Subject
、Observer
,接着我们实现了一个主题CarSubject
用于封装汽车变化的数据,提供给其他对这个数据感兴趣的观察者们。那么,现在把仪表盘做为观察者,去订阅主题,修改一下之前的Display
:
/**
* 仪表盘,传递的数据是一个数组,0下标存的是转速,1下标存的是速度
* Created by Carlton on 2016/11/9.
*/
class Display : Observer>
{
override fun update(value: Array)
{
power = value[0]
speed = value[1]
display()
}
var power:Float = 0f
var speed:Float = 0f
fun display()
{
println("汽车当前的 转速:$power 速度:$speed")
}
}
运行系统:
val carSubject = CarSubject()
// 添加仪表盘观察者
carSubject.registerObserver(Display())
// 仪表盘显示
……
汽车当前的 转速:4.525 速度:90.5
汽车当前的 转速:4.53 速度:90.6
汽车当前的 转速:4.535 速度:90.7
汽车当前的 转速:5.54 速度:90.8
汽车当前的 转速:5.545 速度:90.9
汽车当前的 转速:4.55 速度:91.0
汽车当前的 转速:4.555 速度:91.1
汽车当前的 转速:4.56 速度:91.2
汽车当前的 转速:4.565 速度:91.3
……
如果现在仪表盘不需要监听主题的数据了,可以调用
carSubject.removeObserver()
移除对象,这样主题数据发生变化后,就不会通知到这个观察者对象。
接下来,添加中控台和后视镜的数据显示,把中控台和后视镜当成观察者去订阅CarSubject
。
这里解答一个疑惑,为什么观察者去订阅主题,反而要把订阅和移除订阅的方法放到主题里面而不是观察者里面,这样也很好理解啊?主要原因是现实世界和程序世界还是有区别,如果我们把这两个方法按照现实的理解放到观察者里面,代码会变得比较复杂,没有现在这种实现方式简单明确,有兴趣的可以自己去按照现实的理解方式实现一个。设计模式只是一种编程思想,不是编程的形式,理解到思想就行了。
新添加两个观察者,中控台(CenterConsoleDisplay
)、后视镜(RearviewBack
):
/**
* 中控台
* Created by Carlton on 2016/11/9.
*/
class CenterConsoleDisplay : Observer>
{
override fun update(value: Array)
{
println("中控台显示的速度:${value[1]}")
}
}
/**
* 后视镜
* Created by Carlton on 2016/11/9.
*/
class RearviewBack : Observer>
{
override fun update(value: Array)
{
println("后视镜显示: 速度 - ${value[1]} 转速 - ${value[0]}")
}
}
订阅这两个新的,启动:
val carSubject = CarSubject()
// 添加仪表盘观察者
carSubject.registerObserver(Display())
// 添加中控台
carSubject.registerObserver(CenterConsoleDisplay())
// 添加后视镜
carSubject.registerObserver(RearviewBack())
// 各个地方的数据展示
……
中控台显示的速度:98.9
后视镜显示: 速度 - 98.9 转速 - 4.945
汽车当前的 转速:4.95 速度:99.0
中控台显示的速度:99.0
后视镜显示: 速度 - 99.0 转速 - 4.95
汽车当前的 转速:4.955 速度:99.1
中控台显示的速度:99.1
后视镜显示: 速度 - 99.1 转速 - 4.955
汽车当前的 转速:5.96 速度:99.2
中控台显示的速度:99.2
后视镜显示: 速度 - 99.2 转速 - 5.96
……
数据的推和拉
观察者模式中获取数据的方式有两种,一种是推给观察者,一种是观察者根据需要自己拉,有什么区别呢?如果是推的方式不管观察者对这部分信息是否感兴趣都会推给观察者,有冗余数据比如我们的中控台(CenterConsoleDisplay
)只对速度感兴趣。如果是用拉的方式呢?这样就能根据观察者自己的需要获取想要的数据,Java里面两种方式都支持,下面改造一下通过拉的方式实现数据传递,这样的话CarSubject
也就是主题需要暴露一些获取数据的方法:
/**
* 观察者
* @param T 观察者回调的数据类型
* Created by Carlton on 2016/11/9.
*/
interface Observer
{
/**
* 数据更新
*/
fun update(value: T)
// ------------------ 变化的部分 --------------------------
/**
* 重载一个方法,让观察者可以用拉的方式获取数据
*/
fun update(subject: Subject)
// ------------------ 变化的部分 --------------------------
}
/**
* 主题,有的地方叫观察者Observable
* @param T 更新的数据回调
* Created by Carlton on 2016/11/9.
*/
interface Subject
{
/**
* 注册成为观察者
*/
fun registerObserver(observer: Observer)
/**
* 删除观察者
*/
fun removeObserver(observer: Observer)
/**
* 通知观察者数据已经发生了变化
*/
fun notifyObservers(value: T)
// ------------------ 变化的部分 --------------------------
/**
* 通知观察者数据已经发生了变化,可以拉数据了
*/
fun notifyObservers()
// ------------------ 变化的部分 --------------------------
}
/**
* 具体的主题,被观察者
* Created by Carlton on 2016/11/9.
*/
class CarSubject : Subject>
{
// ------------------ 变化的部分 --------------------------
var speed:Float = 0f
var power:Float = 0f
override fun notifyObservers()
{
for (observer in observers)
{
observer.update(this)
}
}
// ------------------ 变化的部分 --------------------------
/**
* 观察者
*/
val observers = ArrayList>>()
override fun registerObserver(observer: Observer>)
{
observers.add(observer)
}
override fun removeObserver(observer: Observer>)
{
if(observers.contains(observer))
{
observers.remove(observer)
}
}
override fun notifyObservers(value: Array)
{
for (observer in observers)
{
observer.update(value)
}
}
}
/**
* 中控台
* Created by Carlton on 2016/11/9.
*/
class CenterConsoleDisplay : Observer>
{
// ------------------ 变化的部分 --------------------------
override fun update(subject: Subject>)
{
val carSubject:CarSubject = subject as CarSubject
println("中控台显示的速度:${carSubject.speed}")
}
// ------------------ 变化的部分 --------------------------
override fun update(value: Array)
{
println("中控台显示的速度:${value[1]}")
}
}
/**
* 汽车回调数据,这里会根据汽车的速度变化,持续的传递转速和速度
*/
fun carInfo(power:Float, speed: Float)
{
// ------------------ 变化的部分 --------------------------
carSubject.power = power
carSubject.speed = speed
carSubject.notifyObservers()
// ------------------ 变化的部分 --------------------------
carSubject.notifyObservers(arrayOf(power, speed))
}
更灵活的设计
不知道大家发现一个问题没有,打印出来的速度和转速都是有小数,正常情况下速度表变化都是整数每次变化为1,为了说明简单的用速度和转速大于2的时候才通知观察者来代替这个需求。如果我们要处理这个问题可以在观察者对象中拿到数据后处理,不过观察者模式有一个比较优雅的处理方式setChanged()
,有什么用呢?用来标记数据是否发生了不变化如果没有发生变化则不通知观察者,这样我们就能控制何时通知观察者,现在改造一下CarSubject
,并在Subject中新增接口setChanged()
:
/**
* 具体的主题,被观察者
* Created by Carlton on 2016/11/9.
*/
class CarSubject : Subject>
{
var speed:Float = 0f
var power:Float = 0f
override fun notifyObservers()
{
if (!isChanged)
{
return
}
for (observer in observers)
{
observer.update(this)
}
isChanged = false
}
/**
* 数据是否发生变化
*/
var isChanged: Boolean = false
override fun setChanged()
{
isChanged = true
}
/**
* 观察者
*/
val observers = ArrayList>>()
override fun registerObserver(observer: Observer>)
{
observers.add(observer)
}
override fun removeObserver(observer: Observer>)
{
if(observers.contains(observer))
{
observers.remove(observer)
}
}
override fun notifyObservers(value: Array)
{
if (!isChanged)
{
return
}
for (observer in observers)
{
observer.update(value)
}
isChanged = false
}
}
现在在通知观察者之前我们必须设置数据更新标志调用setChanged()
,这里我们让转速和速度都大于2的时候才通知观察者:
/**
* 汽车回调数据,这里会根据汽车的速度变化,持续的传递转速和速度
*/
fun carInfo(power:Float, speed: Float)
{
carSubject.power = power
carSubject.speed = speed
// 这里我们可以让速度和转速大于2的时候才通知观察者
if (carSubject.power > 2 && carSubject.speed > 2)
{
carSubject.setChanged()
}
carSubject.notifyObservers()
carSubject.notifyObservers(arrayOf(power, speed))
}
Java里面的观察者
Java里面提供了一个被观察者类:java.util.Observable
这不是一个接口,里面有具体的实现,跟我们的CarSubject
一样,已经做好了添加观察者、重置标志符等功能,需要的时候直接继承。还有一个java.util.Observer
观察者接口,跟我们上面的是一样的。Java把java.util.Observable
定义成一个类,主要是流程化了主题功能,不像用接口的时候需要自己实现添加观察者等功能,这样也有一个问题就是扩展性变差了,因为接口总是比类要灵活。如果用Java自带的观察者API来实现我们的系统,只需要用CarSubject
来继承java.util.Observable
就可以了,CarSubject
中就不需要再去实现这些方法了,因为父类已经实现好了:
/**
* 注册成为观察者
*/
fun registerObserver(observer: Observer)
/**
* 删除观察者
*/
fun removeObserver(observer: Observer)
/**
* 通知观察者数据已经发生了变化
*/
fun notifyObservers(value: T)
/**
* 通知观察者数据已经发生了变化,可以拉数据了
*/
fun notifyObservers()
/**
* 标记数据变化
*/
fun setChanged()
MVC说几句
MVC有很多实现方式,但是用观察者模式实现是我觉得最好用的方式,我们把V想成订阅者,把M实现成主题,这样的话,当我们的Model
中有数据变化的时候就可以直接通知到View
,让View
用数据来更新界面,这样写出来的架构更容易理解和解耦。
总结
观察者模式主要有两个角色,一个是观察者,一个是被观察者,所有观察者都能够收到被观察者数据变化的通知,如果某个观察者不在关心主题的数据了,也可以从被观察者的列表中删除这个观察者,这样它就不会收到通知了。如果使用Java,没有特别的需求情况下,不建议自己实现观察者接口,而是直接使用Java API。观察者有很多可以应用的地方,非常有用的一种编程思路,比如RxJava里面等等很多框架都有观察者模式。观察者模式在项目中是经常使用的一种模式,当明白它的核心思想后,在项目中能帮助我们实现更好维护的代码。
查看更多
不登高山,不知天之高也;不临深溪,不知地之厚也
感谢指点、交流、喜欢