听说你还不会写观察者模式?
作者 | 静幽水
责编 | Elle
问题背景
话说程序员小强成功进入一家公司,并且老板也信守承诺给他分配了一个女朋友小美,老板这样做除了能让小强每天安心写代码之外,还有另外两个意图,第一就是小美是安插在小强身边的眼线,负责监督小强的工作,第二个也是最重要的目的是通过小美可以把公司重要的通知传递给小强。如下是过程示意图
以前我们是怎么用程序演示上面过程呢?(这里还没有使用观察者模式)为了简单,我不使用接口,直接使用老板,程序员,小美和客户端四个类。
老板类如下:
public class Boss{//用来定义今晚是否加班private boolean isnotifyProgrammer = false;public boolean getIsIsnotifyProgrammer() {return isnotifyProgrammer;}//通知所有程序员加班public void notifyProgrammer() {System.out.println("所有程序员今晚加班");this.isnotifyProgrammer = true;}
}
程序员类:
public class Programmer {private String name;//省去构造函数和set/get//接到加班通知,然后加班public void work_overtime() {System.out.println(name+"要加班");}
}
小美类,使用线程进行监控老板类,查看老板是否发出加班通知,如果发出就通知所有程序员加班
public class GrilFriend extends Thread{private Boss boss;private Programmer programmer;public GrilFriend(Boss _boss,Programmer _programmer){this.boss = _boss;this.programmer = _programmer;}@Overridepublic void run(){while (true){if(this.boss.getIsIsnotifyProgrammer()){this.programmer.work_overtime();}}}
}
继承Thread类,重写run()方法,然后start()就可以启动一个线程了。
客户端
package Observer;public class Client {public static void main(String[] args) throws InterruptedException {Boss boss = new Boss();Programmer programmer = new Programmer("小强");GrilFriend grilFriend = new GrilFriend(boss,programmer);grilFriend.start();boss.notifyProgrammer();Thread.sleep(1000);}
}
将老板和程序员小强的实例传进小美类中,然后开启线程进行监听老板是否发出通知,只要这时老板发出通知,程序员就可以接收到并执行加班的命令。输出如下:
小强要加班
小强要加班
.....
有何问题
上面这个程序,小美使用死循环来监听,导致CPU飙升,严重浪费了公司的资源,公司面临亏损,老板决定裁员来开源节流,就这样处在程序员和老板直接的小美就被裁掉了,没了小美之后,程序员就要上班时去老板办公室签到,下班时再去询问今晚是否加班。久而久之,程序员心生怨念,老板每天被问来问去也很烦,于是他们想到了一种新的解决方案。
解决方法
于是老板买来几个小喇叭,放在程序员的办公室里,而按钮放在老板的办公室里,如果有新的通知,就按喇叭,程序员们听到之后就前往老板的办公室领取具体的通知内容,这样就不需要程序员每天去老板办公室询问是否有新的通知了,而且还节约了成本,所谓一举两得。
怎么用代码实现上面的过程呢
首先定义一个老板的接口类,里面主要是公共方法如添加程序员对象和删除程序员对象以及通知所有程序员的方法。以后还有其他的小老板,都继承自这个接口:
import java.util.ArrayList;
import java.util.List;public class Boss{//定义一个列表,用来保存程序员对象private List<Programmer> programmers = new ArrayList<Programmer>();//添加一个程序员到列表中public void attach(Programmer programmer){programmers.add(programmer);}//从列表中删除某个程序员对象public void detach(Programmer programmer){programmers.remove(programmer);}//通知所有程序员加班public void notifyProgrammer(boolean isWorkOvertime) {//遍历程序员列表for(Programmer programmer:programmers)programmer.work_overtime(isWorkOvertime);}
}
当前老板的类,当前老板设置一个状态,就是今晚程序员是否需要加班,当这个状态发生改变时,就调用上面的notifyProgrammer()方法通知所有程序员:
//具体的老板-李老板
public class Li_Boss extends Boss {//是否加班的通知private boolean isWorkOvertime;public boolean getIsWorkOvertime(){return isWorkOvertime;}//当加班状态发生变化时,要通知程序员最新的加班状态public void setIsWorkOvertime(boolean isWorkOvertime){this.isWorkOvertime = isWorkOvertime;this.notifyProgrammer(isWorkOvertime);}
}
程序员的接口,只有一个方法,被通知的是否加班方法:
public interface IProgrammer {//被通知的方法public void work_overtime(boolean isWorkOvertime);
}
具体的程序员类,实现上面加班的方法,执行加班行为:
public class Programmer implements IProgrammer{private String name;public Programmer(String name) {this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;}//自己的加班状态private boolean WorkOvertimeState;//接到加班通知,然后更新自己的加班状态public void work_overtime(boolean isWorkOvertime) {WorkOvertimeState = isWorkOvertime;System.out.println(name+"是否要加班:"+WorkOvertimeState);}
}
客户端,使用上面的类,先创建一个老板对象和两个程序员对象,并把两个程序员对象添加到老板对象中的程序员列表中(相当于上班前来打卡,老板知道今天有哪些程序员来上班了),然后老板发布今晚加班的通知,所有程序员就会自动接收到通知,并执行加班的行为:
public class Client {public static void main(String[] args) throws InterruptedException {Li_Boss boss = new Li_Boss();Programmer programmer1 = new Programmer("小强");Programmer programmer2 = new Programmer("小华");//将上面两个程序员添加到老板类里的程序员列表中boss.attach(programmer1);boss.attach(programmer2);//老板发布通知,今晚加班boss.setIsWorkOvertime(true);}
}
打印结果如下:
小强是否要加班:true
小华是否要加班:true
模式讲解
以上的实现方式就是通过观察者模式实现的,观察者模式定义:
定义对象间的一种一对多的依赖关系。当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新。
上面老板和程序员的关系就是典型的 一对多关系,并且程序员是否加班的状态也是由老板决定的,程序员会根据老板的通知执行相对应的行为,而老板不需要知道具体有哪些程序员,只需要维护一个程序员的列表就可以了,通知时遍历这个列表。
老板相当于被观察者,也被称为目标:Subject,程序员是观察者,也就是Observer,一个目标可以有任意多个观察者对象,一旦目标的状态发生变化,所有注册的观察者都会得到通知,然后各个观察者会对通知做出相应的反应。
但是需要注意,观察者始终是处于一种被动的地位,也就是注册和删除都是由目标来决定的,观察者没有权限决定是否观察哪个目标。这叫做单向依赖,观察者依赖于目标,目标是不会依赖于观察者的。这一点也很好了解,就像是程序员没有权限决定听不听老板的通知一样,老板通知所有程序员加班,你不能决定你能不能接收到通知吧。
观察者也可以观察多个目标,但是应该为不同的观察者目标定义不同的回调函数用来区分,也就是每个通知对应一个响应函数。这一点也很容易理解,一家公司会有老板,还会有部门经理,主管等职位,程序员都要听他们的。
观察者模型结构示意图:
Subject目标对象,相当于上面的Boss类
Observer:观察者接口,提供目标通知时对应的更新方法,可以在这个方法中回调目标对象,以获取目标对象的数据,相当于上面的IProgrammer接口
ConcreteSubject:具体的目标实现类,相当于上面的Li_boss
ConcreteObserver:具体的观察者对象,用来接收通知并做出响应,相当于上面的Programmer类。
新的问题
公司这样执行了一段时间,新的问题又出现了,因为老板并不只是通知加班这么简单,还会通知其他的事情,可能每个程序员想得到的通知不一样,比如小强去出差,小华去休假。这个时候该怎么通知他们呢。
这就涉及观察者的两种模型,拉模型和推模型。上面的代码就是推模型实现的。
推模型:目标对象主动向观察者推送目标的详细信息,不管观察者是否需要,推送的信息通常是目标对象的全部或部分数据。如上面就是推送的是否加班的状态。
拉模型“目标对象在通知观察者的时候,只会传递少量信息,如果不知道观察者具体需要什么数据,就把目标对象自身传递给观察者,让观察者自己按需去取信息。
为了方便对比,接下来用拉模型实现通知加班的代码,首先看一下Boss类,只需要更改通知的方法:
import java.util.ArrayList;
import java.util.List;public class Boss{//定义一个列表,用来保存程序员对象private List<Programmer> programmers = new ArrayList<Programmer>();//添加一个程序员到列表中public void attach(Programmer programmer){programmers.add(programmer);}//从列表中删除某个程序员对象public void detach(Programmer programmer){programmers.remove(programmer);}//通知所有程序员加班public void notifyProgrammer() {//遍历程序员列表for(Programmer programmer:programmers)programmer.work_overtime(this);}
}
notifyProgrammer()方法去掉参数,调用 programmer.work_overtime()时将自身this传递进去。具体的老板类Li_Boss只需要实现这个方法就可以了。
IProgrammer接口中被通知的方法接收参数boss对象:
public interface IProgrammer {//被通知的方法public void work_overtime(Boss boss);
}
具体的programmer实现类,也只需要该接收通知的方法:
public class Programmer implements IProgrammer{......//自己的加班状态private boolean WorkOvertimeState;//接到加班通知,然后更新自己的加班状态public void work_overtime(Boss boss) {WorkOvertimeState = ((Li_Boss)boss).getIsWorkOvertime();System.out.println(name+"是否要加班:"+WorkOvertimeState);}
}
客户端不需要更改,输出结果和上面一样,没有问题。当有多个具体的观察者(程序员)类时,不同的类需要不同的信息,在目标类中定义多种状态,当具体的观察者接到通知后通过目标对象实例按需去状态就可以了。
推拉模型的比较:
推模型是假定目标对象知道观察者需要什么数据,相当于精准推送。拉模型目标对象不知道观察者需要什么数据,把自身对象给观察者,让观察者自己去取。
推模型使观察者模型难以复用,拉模型可以复用。
相关扩展
其实在java中已经有了观察者模式的实现,不需要自己从头写。在java.util包里面有一个类Observable,它实现了大部分我们需要的目标功能。接口Observer中定义了update方法。相比于上面自己的实现区别如下:
不需要定义观察者和目标的接口,JDK已经定义好了
在目标实现类中不需要维护观察者的注册信息,这个Observable类中帮忙实现好了。
触发方式需要先调用setChanged方法
具体的观察者实现中,update方法同时支持推模型和拉模型。
使用java中的Observable实现上面的程序:
目标对象 :
import java.util.Observable;//具体的老板-李老板
public class Li_Boss extends Observable {//是否加班的通知private String isWorkOvertime;public String getIsWorkOvertime(){return isWorkOvertime;}//当加班状态发生变化时,要通知程序员来领取新通知public void setIsWorkOvertime(String isWorkOvertime){this.isWorkOvertime = isWorkOvertime;//使用java中的Observable,这句话是必须的。this.setChanged();//拉模式this.notifyObservers();//推模式//this.notifyObservers(isWorkOvertime);}
}
观察者对象:
package Observer;import java.util.Observable;
import java.util.Observer;public class Programmer implements Observer {private String name;//构造函数和set方法省略//自己的加班状态private String WorkOvertimeState;//接到加班通知,然后更新自己的加班状态@Overridepublic void update(Observable observable, Object o) {//拉模式对应的更新方法WorkOvertimeState = ((Li_Boss)observable).getIsWorkOvertime();System.out.println(name+WorkOvertimeState);//推模式对应的更新方法
// WorkOvertimeState = o.toString() ;
// System.out.println(name+WorkOvertimeState);}
}
客户端:
public class Client {public static void main(String[] args){Li_Boss boss = new Li_Boss();Programmer programmer1 = new Programmer("小强");Programmer programmer2 = new Programmer("小华");//将上面两个程序员添加到老板类里的程序员列表中boss.addObserver(programmer1);boss.addObserver(programmer2);boss.setIsWorkOvertime("今晚加班");}
}
输出结果:
小华今晚加班
小强今晚加班
这样看观察者模式是不是非常简单了。
观察者模式的优点:
实现观察者和目标之间的抽象耦合:只是在抽象层面耦合了。
观察者模式实现了动态联动:一个操作会引起其他相关操作。
观察者支持广播通信:需要注意避免死循环。
观察者模式的缺点:
广播通信会引起不必要的操作。
何时使用观察者模式:
当一个抽象模型有两个方面,一个方面的操作依赖于另一个方面的状态变化时。
更改一个对象时,需要同时联动更改其他对象,但却不知道应该有多少对象需要被改变时。
当一个对象必须通知其他对象,但是又希望他们之间是松散耦合的。
模式变形
不久老板又陷入了苦恼,因为上面这种方式每次都会同时通知小华和小强,但是老板想只通知小强而不通知小华,想要区别对待观察者,例如当有用户反应公司的软件产品出现bug时,需要小华和小强去找bug并修复。但当出现不是特别紧急的bug时只需要小华一个人修改就可以了,如果出现了紧急的bug,需要马上修复的,就需要小华和小强同时去修复。该如何使用观察者实现上面的场景呢?
因为经过上面的学习,对于观察者已经了解了,代码不做太多的解释。
在这里可以把Bug当做目标,小强和小华都是观察者,首先定义观察者接口:
//观察者接口
public interface BugObserver {//传入被观察的目标对象public void update(BugSubject subject);//观察人员职务public void setName(String name);public String getName();
}
目标对象抽象接口:
import java.util.ArrayList;
import java.util.List;//目标对象
public abstract class BugSubject {//保存注册的观察者protected List<BugObserver> observers = new ArrayList<BugObserver>();public void attach(BugObserver observer){observers.add(observer);}public void detach(BugObserver observer){observers.remove(observer);}//通知具体的观察者程序员public abstract void notifyProgrammer();//获取当前bug级别public abstract int getBugLevel();}
具体的观察者实现
//具体的观察者实现
public class Programmer implements BugObserver{private String name;@Overridepublic void setName(String name) {this.name = name;}@Overridepublic String getName() {return this.name;}@Overridepublic void update(BugSubject subject) {System.out.println(name+"获取到通知,当前bug级别为"+subject.getBugLevel());}}
具体的目标实现:
//具体的Bug对象
public class Bug extends BugSubject {private int bugLevel = 0;public int getBugLevel(){return bugLevel;}public void setBugLevel(int bugLevel){this.bugLevel = bugLevel;this.notifyProgrammer();}public void notifyProgrammer(){for(BugObserver programmer:observers){if(this.bugLevel>=1){if("小华".equals(programmer.getName())){programmer.update(this);}}if(this.bugLevel>=2){if("小强".equals(programmer.getName())){programmer.update(this);}}}}
}
客户端测试一下:
public class Client {public static void main(String[] args){//创建目标对象Bug bug = new Bug();//创建观察者BugObserver bugObserver1 = new Programmer();bugObserver1.setName("小华");BugObserver bugObserver2 = new Programmer();bugObserver2.setName("小强");//注册观察者bug.attach(bugObserver1);bug.attach(bugObserver2);bug.setBugLevel(1);System.out.println("---------------------");bug.setBugLevel(2);}
}
输出结果如下:
小华获取到通知,当前bug级别为1
---------------------
小华获取到通知,当前bug级别为2
小强获取到通知,当前bug级别为2
可以看出,当Bug的级别不同的时候通知的人是不一样的,一级时只通知小华,二级的时候会通知小华和小强。
好了,这就是观察者模式的全部内容了。
声明:本文为作者投稿,版权归作者个人所有。
热 文 推 荐
☞雷军为什么要挖常程?
☞腾讯回应“暴力裁员”;小米否认常程与联想签有竞业禁止条款;NumPy 1.16.6 发布 | 极客头条
☞GitHub Action 有风险?!
☞达摩院十大科技趋势发布:2020 非同小可!
☞骗了马云 10 亿被骂 4 年后,院士王坚留下 4 条人生启示
☞阿里达摩院2020趋势第一弹:感知智能的“天花板”和认知智能的“野望”
☞你真的懂云计算吗?
☞万字长文回望2019:影响区块链行业发展的9大事件
你点的每个“在看”,我都认真当成了喜欢
听说你还不会写观察者模式?相关推荐
- 1.开篇(听说你还在艰难的啃react源码)
人人都能读懂的react源码解析(大厂高薪必备) 1.开篇(听说你还在艰难的啃react源码) 本教程目标是打造一门严谨(严格遵循react17核心思想).通俗易懂(提供大量流程图解,结合demo ...
- 4岁的儿子还不会写红黑树,我该怎么办?
程序员书库(ID:OpenSourceTop) 编译 书单来自:https://www.codemom.ai/best-coding-books-for-kids/ 身为一名程序员,很多人肯定会认为程 ...
- excel修改列名_听说你还在手动合并Excel,看看这个吧!?
Excel合并的应用场景 工作中,常常遇到将多个Excel进行合并的任务.例如,将各位参会人员的报名表合并成一张总的参会人员表,或是将不同客户的需求明细合并为一种总表. 常规的做法是新建一个空白的Ex ...
- 用计算机变的魔术,iPhone计算器不为人知的隐藏技能,听说竟然还能用它变魔术?...
原标题:iPhone计算器不为人知的隐藏技能,听说竟然还能用它变魔术? 相信大家平时想要算数的时候都会用自己的手机计算器吧,计算器虽然看起来很简单,但其实,有很多不为人熟知的地方,今天带大家探秘iPh ...
- 还不知道如何写文章上热榜吗?听1_bit大佬给你讲讲
还不知道如何写文章上热榜吗?听1_bit大佬给你分析分析 前言 标题:冲热榜文章如何起标题? 1. 不要设置门槛 2. 不要使用系列文章 3. 不使专业性过强的词汇 4. 不要过于长 5. 不要偏学术 ...
- 听说你还在为海量数据构建不同数据仓库?华为云学院 DataLake了解一下!
听说你还在为海量数据构建不同数据仓库?华为云学院 DataLake了解一下! By: FYS_CMSS 的CSDN 博客 "A data lake is a method of storin ...
- 听说你还在熬夜加班写 bug ?
程序员真的太太太太太难了 当你对程序进行了一个小修补 却导致了重大错误 测试的时候一切 ok 真正上线的时候 "嗯?这个环境怎么了!" 你问我程序员最讨厌的事是什么? ☟ ☟ ☟ ...
- 腾讯 Code Review 规范出炉!你还敢乱写代码??
点击上方"Java基基",选择"设为星标" 做积极的人,而不是积极废人! 源码精品专栏 原创 | Java 2020 超神之路,很肝~ 中文详细注释的开源项目 ...
- 听说,有的程序员写代码时,耳机里放的是相声
我是风筝,公众号「古时的风筝」,一个兼具深度与广度的程序员鼓励师,一个本打算写诗却写起了代码的田园码农! 文章会收录在 JavaNewBee 中,更有 Java 后端知识图谱,从小白到大牛要走的路都在 ...
最新文章
- lvm实现快速备份文件及数据库,lvm快照原理
- 关于oracle监听程序的相关问题及解决方法
- shell脚本编程for循环求阶乘_shell脚本编程(完结版).pdf
- 7-1 模拟EXCEL排序 (25 分)
- Ngui之UI框架的层级处理
- 初识Firebug 全文 — firebug的使用
- 让你成为高效的Web开发者的10个步骤
- @程序员,我最多再等你两天!
- Python-Matplotlib可视化(4)——添加注释让统计图通俗易懂
- 不会真有人觉得聊天机器人难吧——开篇
- Web开发(初级)- 常用css总结,方便查询
- 地脚螺钉直径系列_干数控这么久,现在才分清楚螺栓、螺钉、螺柱的区别
- Ansys 2022R2安装教程
- 泛微oa安装mysql,泛微OA系统安装升级重装手册(参考).doc
- SpringCloud常用注解
- 梅特勒托利多xk3124电子秤说明书_梅特勒托利多电子秤校正标定方法
- 机器人出卢安娜飓风_LOL:卢安娜的飓风已经过时?哪些英雄还会出这件装备?...
- 用于分类的神经网络算法,图像识别神经网络算法
- ARM服务器获取cpu信息,HPUX 查看系统信息(CPU,主机型号,物理内存等)
- 【图文教程】Shell基础知识
热门文章
- 使用火狐浏览器的原因是什么?使用英文版火狐的原因又是什么?
- 性能测试工具AlldayTest 的测试方法及说明
- 切换至 root 身份
- 【IDEA】idea中Git的使用小技巧
- 矩池云上安装CUDA头文件教程
- rust笔记2 OwnerShip基础概念
- python秒表代码_斌哥教你自制千分秒表(键盘控制)
- java分形树_跪求javaSE高手 graphics绘图问题 本来是递归算法实现的分形树 现要求通过改变递归深度来实现树的生长...
- Python学习:图形界面设计01
- Spring+MyBatis企业应用实战 - 笔记