最近写了一个“休息吧,程序员”的小工具,用到了观察者模式,感觉自己的理解还不够深入,借此机会稍微深入学习一下。
项目介绍:提醒程序员不要过度工作的一个脚本,暖心程序跟你道晚安,使用观察者模式实现,我把它叫做:休息吧,程序员!
po图
项目地址:https://github.com/cooljacket/relax_please
先po一下我实现出来的效果图(注意桌面右上角):
暖心跟你道晚安~
如老妈一般的监督你不要过劳!
项目设计
首先说下这个项目的组成:
1. 监听器:监听系统的键盘输入、系统时间
2. 发送提醒:一旦监听的事件发生(比如超过11点,该睡觉了),那么监听器就会触发这个“发送提醒器”,提醒器会以某种方式发送通知给使用者
这样看来,这个项目的对象关系是一个很经典的“观察者模式”的模型,下面先看一下我画的UML图:
看不懂UML图的朋友也没关系,稍微解释一下就知道了:
1. 监听器抽象接口Listener,它实现了所有监听器共有的功能,即绑定观察者,解绑观察者,(在所监听的事件发生的时候)通知所有绑定的观察者,以及监听事件。
2. 我需要监听两个事件,一个是使用电脑的情况(通过键盘输入来估计),一个是当前的工作时间(看看是否有熬夜),所以继承实现了两个具体的监听器,KeyBoardListener和SayGoodNightListener,它们都只需要实现listening函数即可,因为其它的都在抽象类Listener中实现好了!
3. 观察者抽象类Observer,这个其实只是个接口而已,不是一个类,因为update()虽然是共有的接口函数,但它的实体是因人而异的,没法在这个类里给出一个default的实现(对比一下Listener的notify方法就知道了)。
4. 目前的发通知给使用者,只实现了一种方式:发送泡泡弹窗到桌面右上角,所以只写了一个NotifySendObserver类。
值得注意的是:一个监听器可以有多个观察者,而一般一个观察者只能有一个监听器。比如监听是否熬夜这个事件,可以有多种通知方式,比如响闹铃,发送泡泡弹窗到桌面的右上角,记录熬夜情况到日志文件以备后续分析,等等。而一般一个观察者都是特化了的,不会用在多个监听器上面,比如你熬夜的时候响一段“睡吧睡吧我亲爱滴宝贝……”的摇篮曲,是针对特定的这个(熬夜)事件的!
什么是观察者模式?
引用一段话1来描述:
建立一种对象与对象之间的依赖关系,一个对象发生改变时将自动通知其他对象,其他对象将相应做出反应。在此,发生改变的对象称为观察目标,而被通知的对象称为观察者,一个观察目标可以对应多个观察者,而且这些观察者之间没有相互联系,可以根据需要增加和删除观察者,使得系统更易于扩展,这就是观察者模式的模式动机。
注意这里,观察者并不是真的去观察,而是被观察目标通知才会得知有事件发生的!
这是一个事件驱动模型,不直接想它的好处,先反过来想,如果不这样做,而是让NotifySendObserver主动去询问/检测事件有无发生,这样的话,得设置好去检测的时间间隔,除非不停去检测,否则肯定是有概率会错过一些事件的。忙死忙活,吃力不讨好^_^(手动微笑)。
而观察者模式正是把检测事件和响应事件这两个职责给分离了,各司其职。我这边自己监视使用者有没有熬夜,有的话,我就通过接口告诉你,你再发通知给使用者,叫ta赶紧睡觉。没有发通知给你,你也不用来问我,就是没事情发生,各自清闲~
再举个简单的例子,就是我们平时按手机app里的按钮,那个应用内部肯定不会时刻去检测这个按钮有没有被按下,而是按钮被按下的时候,系统会发送一个消息给应用去处理。
Why抽象接口?
有人可能会说,哎,你怎么要搞这么麻烦啊,无论是监听器,还是观察者,都要抽象出一个接口出来?
这里安利一个我自己到现在还没懂透的“面向对象五大原则SOLID”:
参考维基百科:https://zh.wikipedia.org/wiki/SOLID_(%E9%9D%A2%E5%90%91%E5%AF%B9%E8%B1%A1%E8%AE%BE%E8%AE%A1)
这里很明显运用的就是D,依赖反转原则。
问题还是回来了,用了有什么好处呢?不用,又有什么坏处呢?
这里推荐一篇关于依赖倒转原则的好文章,里边的“妈妈讲故事”的例子很鲜明地体现了这个原则的好处:高层模块可以自由选择底层模块,耦合性很低。
说人话,就是,在这个例子中,如果我不抽象一个Observer接口的话,那么Listener(高层模块)就直接依赖NotifySendObserver(底层模块)了,那如果我后续不想用这种通知方式,而是想改用音乐作为闹铃,是不是除了写多一个MusicObserver类,还要改动Listener类呢?
有人可能会说,这在python里都一样,因为每次运行都是重新编译,那试问一下,如果你是公开发布的一个东西,还能这么做吗?如果不是用python而是C++、Java之类的静态语言呢?
嗯,抽象接口的好处就是这样了。
而Listener作为一个抽象类,则不是同样的想法了。我想实现多个监听器类,它们有很鲜明的公共逻辑,比如上面的KeyBoardListener和SayGoodNightListener,那这个时候主要是继承重用的想法,而不是为了要依赖反转。