概述
有时被称作发布/订阅模式,观察者模式定义了一种一对多的依赖关系,让多个观察者对象同时监听某一个主题对象。这个主题对象在状态发生变化时,会通知所有观察者对象,使它们能够自动更新自己。
问题的提出
在生活当中,经常会遇到多种数据关注一个对象数据变化的情况。举个例子:生活中有温度记录仪,当温度发生变化时,需要完成一下功能:记录温度日志,显示温度变化曲线,当温度越界是触发扬声器发出声音等等。。。伪代码大致如下:
while(温度变化){ 记录温度日志; 显示温度变化曲线; 当温度越界是触发扬声器发出声音...... }
这种方法将所有的功能集中到一块,当需求分析发生变化时,例如增加新的检测功能 或者舍弃某一检测功能,程序都要修改,这是我们不希望的结果,观察者设计模式是解决这类问题的有效方法。
解决的问题
将一个系统分割成一个一些类相互协作的类有一个不好的副作用,那就是需要维护相关对象间的一致性。我们不希望为了维持一致性而使各类紧密耦合,这样会给维护、扩展和重用都带来不便。观察者就是解决这类的耦合关系的。
观察者模式
观察者模式适合解决多种对象跟踪一个对象数据变化的程序结构问题, 有一个称作“主题”的对象和若干个称作“观察者的对象”。
延续上面的例子:主题对象—–温度,3个观察者——–温度日志、温度曲线、温度告警。
因此设计观察者模式的只有两种角色:主题和观察者
从观察者模式中我们可以从以下递推中得出一些重要的结论
- 主题要知道有哪些观察者对其进行监测,因此主题类中一定有一个集合类成员变量,包含了观察这的对象集合。
- 既然包含了观察者对象的集合,那么观察者一定是多态的,有共同的父类接口
- 主题完成的主要功能是:可以添加观察者,可以撤销观察者,可以向观察者发消息,引起观察者相应。 这三个功能是固定的,因此主题类可以从固定的接口中派生。
因此,编写观察者模式,要完成以下功能的开发:
1. 主题ISubject接口定义
2. 主题类编写
3. 观察者接口IObserver接口定义
4. 观察者类实现
主题接口
public interface ISubject { /** * 注册观察者 * @param subject */ void register(IObserver obs); /** * 撤销观察者 * @param subject */ void unRegister(IObserver obs); /** * 通知观察者 * @param subject */ void notifyObservers();
观察者接口
public interface IObserver { /** * 响应操作 * @param data */ void refresh(String data); }
主题实现类
public class Subject implements ISubject { // 观察者维护变量 private Vector<IObserver> vec = new Vector<IObserver>(); // 主题中心数据 private String data ; public String getData() { return data; } /** * 主题注册(添加) * @param data */ public void setData(String data) { this.data = data; } @Override public void register(IObserver obs) { vec.add(obs); } @Override public void unRegister(IObserver obs) { if(vec.contains(obs)){ vec.remove(obs); } } @Override public void notifyObservers() { for(int i=0 ;i<vec.size(); i++){ IObserver obs = vec.get(i); obs.refresh(data); } } }
主题实现类Subject是观察者设计模式中最重要的一个类,包含了观察者对象的维护变量vec 以及主题中心数据data变量 与具体观察者对象的关联方法(通过notifyObservers()).
也就是说,从此类出发,可以更加深刻的理解为什么ISubject为什么定义了3个方法,IObserver接口为什么定义了1个方法。
观察者实现类
public class Observer implements IObserver { @Override public void refresh(String data) { System.out.println("I have received the data :" + data); } }
public class Observer2 implements IObserver { @Override public void refresh(String data) { System.out.println("Observer2 have received the data :" + data); } }
测试
public class Test { public static void main(String[] args) { // 主题实现类 Subject subject = new Subject(); // 观察者接口实例化 IObserver obs = new Observer(); IObserver obs2 = new Observer2(); // 注册观察者 subject.register(obs); subject.register(obs2); // 设置数据 subject.setData("OOOOOOOOOOOOOOOOOOOOOO"); // 通知观察者更新数据 subject.notifyObservers(); } }
输出:
Observer1 have received the data :OOOOOOOOOOOOOOOOOOOOOO
Observer2 have received the data :OOOOOOOOOOOOOOOOOOOOOO
该段代码的含义是:当主题中心数据变化(通过setData())后,主题类subject要调用notifyObservers()方法,通知所有的观察者接收数据并进行数据相应。
深入理解观察者模式
- 深入理解ISubject 和IObserver接口
- 推数据和拉数据
- 增加抽象类Abstract Subject
- 避免添加统一类型的观察者对象
- 反射技术的应用
1. 深入理解ISubject 和IObserver接口
上文中的Subject类中的中心数据data是String类型的,这也就决定了IObserver接口中定义的refresh()方法参数类型必须是String类型的。
若data改为其他类型,着IObserver接口等相关代码都需要修改。
事实上,我们只要把ISubject、IObserver接口改为泛型接口就可以了。
主题泛型接口ISubject
public interface ISubject<T> { public void register(IObserver<T> observer); public void unRegister(IObserver<T> observer); public void notifyObservers(); }
观察者泛型接口
public interface IObserver<T>{ public void refresh(T data) }
当把ISubject、IObserver接口修改为泛型接口后,要求参数T必须是类类型,不能是基本数据类型,比如不能是int ,但可以是Integer类型。
2. 推数据和拉数据
推数据的方式是指:具体主题将变化后的数据全部交给具体观察者,即将变化后的数据直接传递给具体观察者用于更新数据。
从上面观察者的接口定义中就可以很明显的看出
public interface IObserver{ public void refresh(String data); }
可以看出,主题对象直接将数据传递给观察者对象,这是“推”数据方式最大的特点。
与之相比,“拉”数据方式的特点:观察者对象可间接获取变化后的主题数据,观察者自己把数据拉过来。
将程序修改为拉数据的方式:
主题接口定义(同推数据的方式):
public interface ISubject { public void register(IObserver observer); public void unRegister(IObserver observer); public void notifyObservers(); }
观察者接口定义
public interface IObserver { // 参数为 ISubject接口类 public void refresh(ISubject subject); }
主题实现类 (区别在于notifyObservers方法):
public class Subject implements ISubject { // 主题实现类中维护的观察者接口对象 private Vector<IObserver> vec = new Vector<>(); // 主题数据 private String data ; /** * 获取主题 拉的方式 * @return */ public String getData() { return data; } /** * 主题数据添加 适用于推的方式 * @param data */ public void setData(String data) { this.data = data; } @Override public void register(IObserver observer) { vec.add(observer); } @Override public void unRegister(IObserver observer) { if(vec.contains(observer)){ vec.remove(observer); } } @Override public void notifyObservers() { for(int i=0 ;i < vec.size();i++){ IObserver observer = vec.get(i); // 代替原来的 refresh(data) observer.refresh(this); } } }
观察者实现类
public class Observer implements IObserver { @Override public void refresh(ISubject iSubject) { // 需要转化成 具体的实现类 Subject s = (Subject) iSubject; String data = s.getData(); System.out.println(data); } }
测试
public class Test { public static void main(String[] args) { Subject subject = new Subject(); IObserver observer = new Observer(); subject.setData("999999999999"); subject.register(observer); subject.notifyObservers(); } }
拉数据的方式:主要将观察者接口IObserver中的refresh(String data)修改为refresh(ISubject subject)。
可推测出:具体观察者子类对象一定能获取主题Subject对象,当然也可以间接的访问主题对象中的变量了。
从此观点出发就可以很好地理解 notifyObservers(ISubject) 和 refresh(ISubject) 方法代码的修改情况了。
3.增加抽象类Abstract Subject
假设有很多个主题类,按照以上的写法,每个主题类都要重写register()、unRegister()、notifyObservers()方法。
又假设这三个方法的代码恰巧是相同的(这种可能性是很大的,因为他们都是通用的方法),那么每个主题类的代码就显得重复了,用中间成类来解决代码重复问题是一个较好的办法。
代码如下:
主题接口:
public interface ISubject { public void register(IObserver obs); public void unRegister(IObserver obs); public void notifyObservers(); }
观察者接口:
public interface IObserver { /** * 采用拉数据的方式 * @param obj */ public void refresh(ISubject obj); }
增加的抽象层类AbsSubject
/** * 抽象類 * * @author Mr.Yang * */ public abstract class SubjectAbs implements ISubject { // 维护观察者接口集合的成员变量 private Vector<IObserver> vec = new Vector<IObserver>(); @Override public void register(IObserver obs) { vec.add(obs); } @Override public void unRegister(IObserver obs) { if (vec.contains(obs)) { vec.remove(obs); } } @Override public void notifyObservers() { for (int i = 0; i < vec.size(); i++) { IObserver obs = vec.get(i); // 采用拉数据的方式,让观察者主动从主题实例这里获取数据 obs.refresh(this); } } }
派生主题类Subject
/** * 派生主题类 Subject * @author Mr.Yang * */ public class Subject extends SubjectAbs { // 继承了抽象类 SubjectAbs // 子类独有的 private String data ; public String getData() { return data; } public void setData(String data) { this.data = data; } }
一个具体的观察者类
public class Observer implements IObserver { @Override public void refresh(ISubject obj) { // 强制转换类型 Subject subject = (Subject)obj; // 从主题实现类中通过拉数据的方式 获取 主题 String data = subject.getData(); System.out.println("拉数据的方式获取:" + data); } }
测试:
public class Test { public static void main(String[] args) { Subject subject = new Subject(); IObserver obs = new Observer(); subject.setData("XXXXXXXXXXXX"); subject.register(obs); subject.notifyObservers(); } }
有了中间抽象层abstract class SubjectAbs,灵活了具体主题类的开发:
- 使用class XXXSubject extends SubjectAbs{….}表名直接继承父类的register(),unRegister(),notifyObserver()[当然了也可以重写任意方法,非必选]。
- 还可以使用 class XXXSubject implements ISubject{…….},表名3个方法必须重写。
虽然SubjectAbs类中并没有抽象方法,但定义成了抽象类。这是因为从语义角度上来讲,该类并不是一个完整的主题类,它缺少主题数据,因此把它定义为抽象类。