1.1概述
在许多设计中,经常涉及多个对象都对一个特殊对象中的数据变化感兴趣,而且这多个对象都希望跟踪那个特殊对象中的数据变化,在这样的情况下就可以使用观察者模式。
例如,某些寻找工作的人对“求职中心”的职业需求信息的变化非常关心,很想追踪“求职中心”中职业需求信息的变化。一位想知道“求职中心”职业需求信息变化的人需要成为“求职中心”的求职者,即让求职中心把自己登记到求职中心的“求职者”列表中,当一个人成为求职中心的求职者后,求职中心就会及时通知他最新的职业需求信息。
观察者模式是关于多个对象想知道一个对象中数据变化情况的一种成熟的模式。观察者模式中有一个称作“主题”的对象和若干个称作“观察者”的对象,“主题”和“观察者”间是一种一对多的依赖关系,当“主题”的状态发生变化时,所有“观察者”都得到通知。前面所述的“求职中心”相当于观察者模式的一个具体“主题”;每个“求职者”相当于观察者模式中的一个具体“观察者”。
1.2模式的结构
观察者模式的结构中包含四种角色:
(1)主题(Subject):主题是一个接口,该接口规定了具体主题需要实现的方法,比如,添加、删除观察者以及通知观察者更新数据的方法。
(2)观察者(Observer):观察者是一个接口,该接口规定了具体观察者用来更新数据的方法。
(3)具体主题(ConcreteSubject):具体主题是实现主题接口类的一个实例,该实例包含有可以经常发生变化的数据。具体主题需使用一个集合,比如ArrayList,存放观察者的引用,以便数据变化时通知具体观察者。
(4)具体观察者(ConcreteObserver):具体观察者是实现观察者接口类的一个实例。具体观察者包含有可以存放具体主题引用的主题接口变量,以便具体观察者让具体主题将自己的引用添加到具体主题的集合中,使自己成为它的观察者,或让这个具体主题将自己从具体主题的集合中删除,使自己不再是它的观察者。
观察者模式结构的类图如下所示:
1.3观察者模式的优点
(1)具体主题和具体观察者是松耦合关系。由于主题接口仅仅依赖于观察者接口,因此具体主题只是知道它的观察者是实现观察者接口的某个类的实例,但不需要知道具体是哪个类。同样,由于观察者仅仅依赖于主题接口,因此具体观察者只是知道它依赖的主题是实现主题接口的某个类的实例,但不需要知道具体是哪个类。
(2)观察者模式满足“开-闭原则”。主题接口仅仅依赖于观察者接口,这样,就可以让创建具体主题的类也仅仅是依赖于观察者接口,因此,如果增加新的实现观察者接口的类,不必修改创建具体主题的类的代码。。同样,创建具体观察者的类仅仅依赖于主题接口,如果增加新的实现主题接口的类,也不必修改创建具体观察者类的代码。
1.4适合使用观察者模式的情景
(1)当一个对象的数据更新时需要通知其他对象,但这个对象又不希望和被通知的那些对象形成紧耦合。
(2)当一个对象的数据更新时,这个对象需要让其他对象也各自更新自己的数据,但这个对象不知道具体有多少对象需要更新数据。
1.5观察者模式的应用
下面通过一个简单的问题来描述观察者模式中所涉及的各个角色,这个简单的问题是:有一个大学毕业生和一个归国留学者都希望能及时知道“求职中心”最炫的职业需求信息。
首先看一下本实例构建框架具体类和1.2模式的结构中类图的对应关系,如下图3所示:
图3 具体编写类及接口与类图对应关系
(1)主题(Subject)
本问题中,主题接口Subject规定了具体主题需要实现的添加、删除观察者以及通知观察者更新数据的方法。其代码如下:
package com.liuzhen.two_observer; public interface Subject { public void addObserver(Observer o); public void deleteObserver(Observer o); public void notifyObservers(); }
(2)观察者(Observer)
观察者是一个接口,该接口规定了具体观察者用来更新数据的方法。对于本问题,观察者规定的方法是:hearTelephone()(相当于观察者模式类图中的update()方法),即要求具体观察者都通过实现hearTelephone()方法(模拟接听电话)来更新数据。其代码如下:
package com.liuzhen.two_observer; public interface Observer { public void hearTelephone(String heardMess); }
(3)具体主题(ConcreteSubject)
具体主题维护着一个String字符串,用来表示“求职中心”的职业需求信息,当该String字符串发生变化时,具体主题遍历存放观察者引用集合。具体代码如下:
package com.liuzhen.two_observer; import java.util.ArrayList; public class SeekJobCenter implements Subject{ String mess; boolean changed; ArrayList<Observer> personList; //存放观察者引用的数组线性表 SeekJobCenter(){ personList = new ArrayList<Observer>(); mess = ""; changed = false; } public void addObserver(Observer o){ if(!(personList.contains(o))){ personList.add(o); //把观察者的引用添加到数组线性表 } } public void deleteObserver(Observer o){ if(personList.contains(o)){ personList.remove(o); //把观察者的引用移除数组线性表 } } public void notifyObservers(){ if(changed){ //通知所有的观察者 for(int i = 0;i < personList.size();i++){ Observer observer = personList.get(i); observer.hearTelephone(mess); //让所有的观察者接听电话 } } } public void getNewMess(String str){ //判断信息是否是新发布的 if(str.equals(mess)){ changed = false; } else{ mess = str; changed = true; } } }
(4)具体观察者(ConcreteObserver)
本问题中,实现观察者接口Observer的类有两个:一个UniversityStudent类,另一个是HaiGui。UniversityStudent类的实例调用hearTelephone(String heardMess)方法时,会将参数引用的字符串保存到一个文件中。HaiGui类的实例调用hearTelephone(String heardMess)方法时,如果参数引用的字符串中包含有“程序员”或软件,就将信息保存到一个文件中。
UniversityStudent类代码如下:
package com.liuzhen.two_observer; import java.io.*; public class UniversityStudent implements Observer { Subject subject; File myFile; UniversityStudent(Subject subject,String fileName){ this.subject = subject; subject.addObserver(this); //使当前实例成为subject所使用的具体主题的观察者 myFile = new File(fileName); } public void hearTelephone(String heardMess) { try{ RandomAccessFile out1 = new RandomAccessFile(myFile,"rw"); out1.seek(out1.length()); byte[] b = heardMess.getBytes(); out1.write(b); //更新文件中的内容 System.out.print("我是一个大学生,"); System.out.println("我向文件"+myFile.getName()+"写入如下内容:"); System.out.println(heardMess); } catch(IOException exp){ System.out.println(exp.toString()); } } }
HaiGui类代码如下:
package com.liuzhen.two_observer; import java.io.*; public class HaiGui implements Observer { Subject subject; File myFile; HaiGui(Subject subject , String fileName){ this.subject = subject; subject.addObserver(this); //使当前实例成为subject所引用的具体主题的观察者 myFile = new File(fileName); } public void hearTelephone(String heardMess) { try{ boolean boo = heardMess.contains("java程序员")||heardMess.contains("软件"); if(boo){ RandomAccessFile out = new RandomAccessFile(myFile,"rw"); out.seek(out.length()); byte[] b = heardMess.getBytes(); out.write(b); System.out.print("我是一个海归"); System.out.println("我向文件"+myFile.getName()+"写入如下内容:"); System.out.println(heardMess); } else{ System.out.println("我是海归,这次的信息中没有我需要的信息"); } } catch(IOException exp){ System.out.println(exp.toString()); } } }
(5)具体调用实现
通过TwoApllication类来具体实现上述相关类和接口,来实现观察者模式的运用,其代码如下:
package com.liuzhen.two_observer; public class TwoApplication { public static void main(String[] args) { SeekJobCenter center = new SeekJobCenter(); //具体主题center UniversityStudent zhanglin = new UniversityStudent(center,"A.txt"); //具体观察者zhanglin HaiGui wanghao = new HaiGui(center,"B.txt"); //具体观察者wanghao center.getNewMess("腾辉公司需要10个Java程序员。"); //具体主题给出新信息 center.notifyObservers(); //具体主题通知信息 center.getNewMess("海景公司需要8个动画设计师。"); center.notifyObservers(); center.getNewMess("仁海公司需要9个电工。"); center.notifyObservers(); center.getNewMess("仁海公司需要9个电工。"); //信息不是新的 center.notifyObservers(); //观察者不会执行更新操作 } }
运行结果:
我是一个大学生,我向文件A.txt写入如下内容:
腾辉公司需要10个Java程序员。
我是海归,这次的信息中没有我需要的信息
我是一个大学生,我向文件A.txt写入如下内容:
海景公司需要8个动画设计师。
我是海归,这次的信息中没有我需要的信息
我是一个大学生,我向文件A.txt写入如下内容:
仁海公司需要9个电工。
我是海归,这次的信息中没有我需要的信息
参考资料:
1.Java设计模式/耿祥义,张跃平著.——北京:清华大学出版社,2009.5