只一篇就够了·设计模式(3) - 观察者模式

观察者模式(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<T>
{
    /**
     * 注册成为观察者
     */
    fun registeObserver(observer: Observer<T>)

    /**
     * 删除观察者
     */
    fun removeObserver(observer: Observer<T>)

    /**
     * 通知观察者数据已经发生了变化
     */
    fun notifyObservers(value: T)
}

/**
 * 观察者
 * @param T 观察者回调的数据类型
 * Created by Carlton on 2016/11/9.
 */
interface Observer<in T>
{
    /**
     * 数据更新
     */
    fun update(value: T)
}

接下来把数据封装成一个主题(被观察者CarSubject:

/**
 * 具体的主题,被观察者
 * Created by Carlton on 2016/11/9.
 */
class CarSubject : Subject<Array<Float>>
{
    /**
     * 观察者
     */
    val observers = ArrayList<Observer<Array<Float>>>()
    override fun registerObserver(observer: Observer<Array<Float>>)
    {
        observers.add(observer)
    }

    override fun removeObserver(observer: Observer<Array<Float>>)
    {
        if(observers.contains(observer))
        {
            observers.remove(observer)
        }
    }

    override fun notifyObservers(value: Array<Float>)
    {
        for (observer in observers)
        {
            observer.update(value)
        }
    }
}

现在改造一下我们的系统,把数据用主题绑定起来:

val carSubject = CarSubject()

/**
 * 汽车回调数据,这里会根据汽车的速度变化,持续的传递转速和速度
 */
fun carInfo(power:Float, speed: Float)
{
    carSubject.notifyObservers(arrayOf(power, speed))
}

到这里我们实现了一个可扩展的观察者模式系统,观察者模式中的接口部分一般都是固定的,包括java里面都有支持观察者模式,后面会说道,所以如果我们要实现一个观察者模式,接口部分我们只需要实现一次,或者直接使用java api提供的接口,主要需要实现主题或者观察者接口。

在上面的例子中,我们自己提供了观察者接口SubjectObserver,接着我们实现了一个主题CarSubject用于封装汽车变化的数据,提供给其他对这个数据感兴趣的观察者们。那么,现在把仪表盘做为观察者,去订阅主题,修改一下之前的Display:

/**
 * 仪表盘,传递的数据是一个数组,0下标存的是转速,1下标存的是速度
 * Created by Carlton on 2016/11/9.
 */
class Display : Observer<Array<Float>>
{
    override fun update(value: Array<Float>)
    {
        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<Array<Float>>
{
    override fun update(value: Array<Float>)
    {
        println("中控台显示的速度:${value[1]}")
    }
}

/**
 * 后视镜
 * Created by Carlton on 2016/11/9.
 */
class RearviewBack : Observer<Array<Float>>
{
    override fun update(value: Array<Float>)
    {
        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<T>
{
    /**
     * 数据更新
     */
    fun update(value: T)
    // ------------------   变化的部分   --------------------------
    /**
     * 重载一个方法,让观察者可以用拉的方式获取数据
     */
    fun update(subject: Subject<T>)
    // ------------------   变化的部分   --------------------------
}

/**
 * 主题,有的地方叫观察者Observable
 * @param T 更新的数据回调
 * Created by Carlton on 2016/11/9.
 */
interface Subject<T>
{
    /**
     * 注册成为观察者
     */
    fun registerObserver(observer: Observer<T>)

    /**
     * 删除观察者
     */
    fun removeObserver(observer: Observer<T>)

    /**
     * 通知观察者数据已经发生了变化
     */
    fun notifyObservers(value: T)
    // ------------------   变化的部分   --------------------------
    /**
     * 通知观察者数据已经发生了变化,可以拉数据了
     */
    fun notifyObservers()
    // ------------------   变化的部分   --------------------------
}

/**
 * 具体的主题,被观察者
 * Created by Carlton on 2016/11/9.
 */
class CarSubject : Subject<Array<Float>>
{
    // ------------------   变化的部分   --------------------------
    var speed:Float = 0f
    var power:Float = 0f
    override fun notifyObservers()
    {
        for (observer in observers)
        {
            observer.update(this)
        }
    }
    // ------------------   变化的部分   --------------------------

    /**
     * 观察者
     */
    val observers = ArrayList<Observer<Array<Float>>>()
    override fun registerObserver(observer: Observer<Array<Float>>)
    {
        observers.add(observer)
    }

    override fun removeObserver(observer: Observer<Array<Float>>)
    {
        if(observers.contains(observer))
        {
            observers.remove(observer)
        }
    }

    override fun notifyObservers(value: Array<Float>)
    {
        for (observer in observers)
        {
            observer.update(value)
        }
    }
}

/**
 * 中控台
 * Created by Carlton on 2016/11/9.
 */
class CenterConsoleDisplay : Observer<Array<Float>>
{
    // ------------------   变化的部分   --------------------------
    override fun update(subject: Subject<Array<Float>>)
    {
        val carSubject:CarSubject = subject as CarSubject
        println("中控台显示的速度:${carSubject.speed}")
    }
    // ------------------   变化的部分   --------------------------

    override fun update(value: Array<Float>)
    {
        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<Array<Float>>
{
    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<Observer<Array<Float>>>()
    override fun registerObserver(observer: Observer<Array<Float>>)
    {
        observers.add(observer)
    }

    override fun removeObserver(observer: Observer<Array<Float>>)
    {
        if(observers.contains(observer))
        {
            observers.remove(observer)
        }
    }

    override fun notifyObservers(value: Array<Float>)
    {
        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<T>)

/**
 * 删除观察者
 */
fun removeObserver(observer: Observer<T>)

/**
 * 通知观察者数据已经发生了变化
 */
fun notifyObservers(value: T)

/**
 * 通知观察者数据已经发生了变化,可以拉数据了
 */
fun notifyObservers()

/**
 * 标记数据变化
 */
fun setChanged()

MVC说几句

MVC有很多实现方式,但是用观察者模式实现是我觉得最好用的方式,我们把V想成订阅者,把M实现成主题,这样的话,当我们的Model中有数据变化的时候就可以直接通知到View,让View用数据来更新界面,这样写出来的架构更容易理解和解耦。

总结

观察者模式主要有两个角色,一个是观察者,一个是被观察者,所有观察者都能够收到被观察者数据变化的通知,如果某个观察者不在关心主题的数据了,也可以从被观察者的列表中删除这个观察者,这样它就不会收到通知了。如果使用Java,没有特别的需求情况下,不建议自己实现观察者接口,而是直接使用Java API。观察者有很多可以应用的地方,非常有用的一种编程思路,比如RxJava里面等等很多框架都有观察者模式。观察者模式在项目中是经常使用的一种模式,当明白它的核心思想后,在项目中能帮助我们实现更好维护的代码。

😊查看更多😊

不登高山,不知天之高也;不临深溪,不知地之厚也
感谢指点、交流、喜欢

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容