前言:继续《设计模式就该这么学》系列文章,今天以当前比较火的微信订阅号给大家介绍应用得比较多的一种设计模式——观察者模式,之后再来介绍java拉模型方式的内置设计模式实现,最后附带一个项目实际观察者应用的例子!
《设计模式就该这么学》系列文章:
一. 什么是观察者模式
以《Head First 设计模式》这本书中的定义:
观察者模式:它定义了对象之间的一(Subject)对多(Observer)的依赖,这样一来,当一个对象(Subject)改变时,它的所有的依赖者都会收到通知并自动更新。
首先看下观察者模式的类图
- 主题(Subject)接口:对象使用此接口注册为观察者,或者把自己从观察者中移除。
- 观察者(Observer)接口:所有潜在的观察者都必须实现该接口,这个接口只有update一个方法,他就是在主题状态发生变化的时候被调用。
- 具体主题(ConcreteSubject)类:它是要实现Subject接口,除了注册(registerObserver)和撤销(removeObserver)外,它还有一个通知所有的观察者(notifyObservers)方法。
- 具体观察者(ConcreteObserver)类:它是要实现ObserverJ接口,并且要注册主题,以便接受更新。
二、以微信订阅号来深入介绍观察者模式
看了上面定义及类图好像不太容易理解,微信订阅号我相信大家都不陌生,接下来就微信订阅号的例子来介绍下观察者模式。首先看下面一张图:
如上图所示,微信订阅号就是我们的主题,用户就是观察者。他们在这个过程中扮演的角色及作用分别是:
- 订阅号就是主题,业务就是推送消息
- 观察者想要接受推送消息,只需要订阅该主题即可
- 当不再需要消息推送时,取消订阅号关注即可
- 只要订阅号还在,观察者可以一直去进行关注
接下来让我们通过一段示例,三位同事zhangsai、liyong、liujing订阅人民日报订阅号为例来介绍观察者模式(Obsever),代码如下:
//1、人民日报接口 public interface PeoplesDaily { //添加订阅者 void RegisterObserver(Observer observer); //取消订阅 void RemoveObserver(Observer observer); //发送人民日报 void notifyObservers(); } //2、订阅者接口 public interface Observer { //有新的人民日报了就会被执行通知 void update(); } //3、人民日报 public class PeopleNewsPaper implements PeoplesDaily { private List<Observer> subList = new List<Observer>(); public void RegisterObserver(Observer observer) { subList.Add(observer); } public void RemoveObserver(Observer observer) { if (subList.IndexOf(observer) >= 0) { subList.Remove(observer); } } //推送人民日报消息了~~ public void notifyObservers() { for (Observer sub : subList) { sub.update(); } } } //4、订阅者 public class subHuman implements Observer { //订阅者的名字 private string name; public subHuman(string f_name) { name = f_name; } //通知订阅者有新人民日报推送消息了 public void update() { system.out.println(p_name + "!! 有新的人民日报消息了,请查收!"); } } //5、测试开始订阅,和调用了 public static void Main(string[] args) { PeopleNewsPaper paper = new PeopleNewsPaper(); subHuman zhsangsai = new subHuman("张赛"); subHuman liyong = new subHuman("李勇"); subHuman liujin = new subHuman("刘晶"); //张赛订阅人民日报 paper.RegisterObserver(zhsangsai); //李勇订阅人民日报 paper.RegisterObserver(liyong); //刘晶订阅人民日报 paper.RegisterObserver(liujin); //有新人民日报推送消息了 paper.notifyObservers(); system.out.println("---------------发完人民日报了------------------"); //张赛不想订了,取消人民日报 paper.RemoveObserver(zhsangsai); //又有新人民日报了 就没有张赛的人民日报 了 paper.notifyObservers(); }
测试结果:
张赛!! 有新的人民日报消息了,请查收! 李勇!! 有新的人民日报消息了,请查收! 刘晶!! 有新的人民日报消息了,请查收! ---------------发完人民日报了------------------ 张赛!! 有新的人民日报消息了,请查收! 李勇!! 有新的人民日报消息了,请查收!
三、再来说设计模式的推拉模型
在观察者模式中,又分为推模型和拉模型两种方式。
- 推模型:主题对象向观察者推送主题的详细信息,不管观察者是否需要,每次有新的信息就会推送给它的所有的观察者。
- 拉模型:主题对象是根据观察者需要更具体的信息,由观察者主动到主题对象中获取,相当于是观察者从主题对象中拉数据。
而它们的区别在于:
“推”的好处包括:
1、高效。如果没有更新发生,不会有任何更新消息推送的动作,即每次消息推送都发生在确确实实的更新事件之后,所以这种推送是有意义的。
2、实时。事件发生后的第一时间即可触发通知操作。
“拉”的好处包括:
1、如果观察者众多,那么主题要维护订阅者的列表臃肿,把订阅关系解脱到Observer去完成,什么时候要自己去拉数据就好了。
2、Observer可以不理会它不关心的变更事件,只需要去获取自己感兴趣的事件即可。
根据上面的描述,发现前面的例子就是典型的推模型,下面我先来介绍下java内置的拉模型设计模式实现,再给出一个拉模型的实例。
在JAVA编程语言的java.util类库里面,提供了一个Observable类以及一个Observer接口,用来实现JAVA语言对观察者模式的支持。
Observer接口:这个接口代表了观察者对象,它只定义了一个方法,即update()方法,每个观察者都要实现这个接口。当主题对象的状态发生变化时,主题对象的notifyObservers()方法就会调用这一方法。
public interface Observer { void update(Observable o, Object arg); }
Observable类:这个类代表了主体对象,主题对象可以有多个观察者,主题对象发生变化时,会调用Observable的notifyObservers()方法,此方法调用所有的具体观察者的update()方法,从而使所有的观察者都被通知更新自己
package java.util; public class Observable { private boolean changed = false; private Vector obs; public Observable() { obs = new Vector<>(); } //添加一个观察者 public synchronized void addObserver(Observer o) { if (o == null) throw new NullPointerException(); if (!obs.contains(o)) { obs.addElement(o); } } //删除一个观察者 public synchronized void deleteObserver(Observer o) { obs.removeElement(o); } public void notifyObservers() { notifyObservers(null); } //通知所有的观察者 public void notifyObservers(Object arg) { Object[] arrLocal; synchronized (this) { if (!changed) return; arrLocal = obs.toArray(); clearChanged(); } //调用Observer类通知所有的观察者 for (int i = arrLocal.length-1; i>=0; i--) ((Observer)arrLocal[i]).update(this, arg); } public synchronized void deleteObservers() { obs.removeAllElements(); } protected synchronized void setChanged() { changed = true; } protected synchronized void clearChanged() { changed = false; } //省略...... }
接下来再介绍我用java这种内置的观察者设计模式在项目中的一个实际应用,详细请看我的这篇博文:观察者模式实际应用:监听线程,意外退出线程后自动重启
这里只介绍下思路:
项目场景:用户那边会不定期的上传文件到一个ftp目录,我需要实现新上传的文件做一个自动检测,每次只要有文件新增,自动解析新增文件内容入库,并且要保证该功能的稳定性!!
实现思路:
1、监听器初始化创建:首先在tomcat启动的时候,利用监听器初始化创建一个监控文件新增线程,如下:
@Component public class ThreadStartUpListenser implements ServletContextListener { //监控文件新增线程 private static WatchFilePathTask r = new WatchFilePathTask(); private Log log = LogFactory.getLog(ThreadStartUpListenser.class); @Override public void contextDestroyed(ServletContextEvent paramServletContextEvent) { // r.interrupt(); } @Override public void contextInitialized(ServletContextEvent paramServletContextEvent) { //将监控文件类添加为一个观察者,并启动一个线程 ObserverListener listen = new ObserverListener(); r.addObserver(listen); new Thread(r).start(); // r.start(); log.info("ImportUserFromFileTask is started!"); } }
2、主体对象:即下面的监控文件新增类WatchFilePathTask ,每次有新文件进来,自动解析该文件,挂掉之后,调用动doBusiness()里面的notifyObservers()方法,伪代码如下:
//继承java内置观察者模式实现的Observable 类 public class WatchFilePathTask extends Observable implements Runnable { private Log log = LogFactory.getLog(WatchFilePathTask.class); private static final String FILE_PATH = ConfigUtils.getInstance() .getValue("userfile_path"); private WatchService watchService; /** * 此方法一经调用,立马可以通知观察者,在本例中是监听线程 */ public void doBusiness() { if (true) { super.setChanged(); } notifyObservers(); } @Override public void run() { try { //这里省略监控新增文件的方法 }catch (Exception e) { e.printStackTrace(); doBusiness();// 在抛出异常时调用,通知观察者,让其重启线程 } } }
3、观察者对象:即上面出现的ObserverListener类,当主题对象的的notifyObservers()方法被调用的时候,就会调用该类的update()方法,伪代码如下:
//实现java内置观察者模式实现的Observer接口,并且注册主题WatchFilePathTask,以便线程挂掉的时候,再重启这个线程 public class ObserverListener implements Observer { private Log log = LogFactory.getLog(ObserverListener.class); /** * @param o * @param arg */ public void update(Observable o, Object arg) { log.info("WatchFilePathTask挂掉"); WatchFilePathTask run = new WatchFilePathTask(); run.addObserver(this); new Thread(run).start(); log.info("WatchFilePathTask重启"); } }
关于这个例子更多详细实现,请查看我的这篇文章:观察者模式实际应用:监听线程,意外退出线程后自动重启
学习本就是一个不断模仿、练习、再到最后面自己原创的过程。
虽然可能从来不能写出超越网上通类型同主题博文,但为什么还是要写?
于自己而言,博文主要是自己总结。假设自己有观众,毕竟讲是最好的学(见下图)。于读者而言,笔者能在这个过程get到知识点,那就是双赢了。
当然由于笔者能力有限,或许文中存在描述不正确,欢迎指正、补充!
感谢您的阅读。如果本文对您有用,那么请点赞鼓励。