引入
书接上回,我们来聊聊啥是观察者模式?
这是一个用于一对多的单向数据传输的一种成熟的模式,主要使用场景就是有多个对象需要知道一个对象中的数据变化。
适合用观察者模式的场景
- 当一个对象的数据更新时需要通知其他对象,但这个对象又不想和这些被通知的对象现成紧耦合(就是我的具体主题在维护时只需要更新我们观察者数组,不需要在内部加引用)
- 当一个对象的数据更新时,这个对象需要让其他对象也各自更新自己的数据,但这个对象不知道具体有多少对象需要更新数据,也不知道具体观察者是否需要这些变换后的数据(这个往往就采用拉数据的方式,我连具体数据都不给你了,而只提供获得这些数据的方法,给你发个消息告诉你我的数据更新了,你需要啥数据自己调用方法获取)
观察者模式的定义
定义对象间的一种一对多的依赖关系,当一个对象的状态发生变化时,所有依赖它的对象都得到通知并被自动更新
观察者模式的结构
结构包括四种角色:
- 主题:是一个接口,规定了具体主题需要实现的方法(如,添加、删除观察者以及通知观察者更新数据的法)
- 观察者:是一个接口,规定了具体观察者用来更新数据的方法
- 具体主题:是实现主题接口的一个实例,包含有可以经常发生变化的数据,里面会定义三个私有成员,分别用来存放消息、状态是否改变的标识值、存放观察者引用的数组线性表。
- 具体观察者:是实现观察者接口的一个实例,包含有可以存放具体主题引用的主题接口变量
在具体实现的时候通知方式会出现两种极端的方式:
- 推数据:具体主题将变化的数据全部交给具体观察者
- 拉数据:具体主题在数据变化后,向具体观察者发一个通知,具体的数据需要具体观察者自己通过方法得到
推数据的例子
从类图我们就能看出具体主题和具体观察者之间松耦合,增加新的观察者只用调用addOberver方法,而不需要修改原来的代码,满足了开闭原则;同样的,由于具体观察者仅依赖主题接口新增具体主题,也不必修改具体观察者的代码。
(这里的类图是Astah的格式,接口的实现应该用虚线空心三角形线符号表示,消息的传递)
考试建议写成下面这种格式:
主题接口类:
//主题
public interface Subject {
public void addObserver(Observer o);
public void deleteObserver(Observer o);
public void notifyObserver();
}
观察者接口类:
//观察者
public interface Observer {
public void hearTelephone(String heardMess);
}
具体主题
import java.util.ArrayList;
//具体主题
public class SeekJobCenter implements Subject{
String mess;//用来存放消息
boolean changed;//用来表示具体主题的状态是否改变
ArrayList<Observer> personList; //存放观察者引用的数组线性表
SeekJobCenter(){
personList = new ArrayList<Observer>();
mess = "";
changed = false;
}
//新增观察者
//这里就只是将观察者的引用存放到线性表中,可以有防止在这里直接引用具体的观察者,从而达到分离的目的
@Override
public void addObserver(Observer o) {
if(!(personList.contains(o)))//如果没记录下来就把观察者的引用添加到数组线性表中
personList.add(o);
}
@Override
public void deleteObserver(Observer o) {
if(personList.contains(o))
personList.remove(o);
}
//推数据的方式
@Override
public void notifyObserver() {
if(changed){
for (int i = 0; i < personList.size(); i++) {
Observer observer = personList.get(i);
observer.hearTelephone(mess); //调用观察者的获取数据的方法
}
changed = false;
}
}
public void giveNewMess(String str){
if(str.equals(mess)) //如果消息一致就不改变
changed = false;
else{
mess = str;
changed = true;
}
}
}
具体观察者
import java.io.*;
//具体观察者
public class UniversityStudent implements Observer{
Subject subject; //引用主题接口,所以具体观察者会有一条箭头指向主题接口对象
File myFile;
UniversityStudent(Subject subject , String fileName){
this.subject = subject;
subject.addObserver(this);
myFile = new File(fileName);
}
@Override
public void hearTelephone(String heardMess) {
try {
RandomAccessFile out = new RandomAccessFile(myFile ,"rw");
out.seek(out.length());
byte[] b = heardMess.getBytes();
out.write(b); // 更新文件中的内容
System.out.println("我是一个大学生,");
System.out.println("我向文件" +myFile.getName()+"写入如下内容");
System.out.println(heardMess);
} catch (IOException e) {
e.printStackTrace();
}
}
}
具体观察者2
这个和上一个观察者的主要区别就是在监听函数中对推过来的数据有筛查
import java.io.*;
public class HaiGui implements Observer{
Subject subject; //引用主题接口
File myFile;
HaiGui(Subject subject , String fileName){
this.subject = subject;
subject.addObserver(this);
myFile = new File(fileName);
}
@Override
public void hearTelephone(String heardMess) {
try {
boolean boo = heardMess.contains("albert")||heardMess.contains("albertOS");
if(boo){
RandomAccessFile out = new RandomAccessFile(myFile ,"rw");
out.seek(out.length());
byte[] b = heardMess.getBytes();
out.write(b); // 更新文件中的内容
System.out.println("我是一个海归,");
System.out.println("我向文件" +myFile.getName()+"写入如下内容");
System.out.println(heardMess);
}
else {
System.out.println("这次信息中没有我需要的信息");
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
简单调用一下:
public class Application {
public static void main(String[] args) {
SeekJobCenter center = new SeekJobCenter();//具体主题
UniversityStudent albert = new UniversityStudent(center , "A.txt");//具体观察者albert
HaiGui albertOS = new HaiGui(center , "B.txt");//具体观察者albertOS
center.giveNewMess("albert需要10000个粉丝");
center.notifyObserver();
center.giveNewMess("albert需要10000个粉丝");//如果信息重复
center.notifyObserver();//观察者不会执行发送更新操作,这个是在更新函数中增加了对消息的判断
}
}
拉数据的例子
主题和观察者是不变的,主要区别是在具体主题的实现逻辑上,我们可以将上面的例子改一下,添加一个新的信息,提供两种获取不同信息的方法。
具体主题:
import java.util.ArrayList;
public class ShopSubject implements Subject{
//这里有三个消息,我们不清楚观察者是否需要全部信息,所以每次更新的时候只发送一个通知过去
String goodsName;
double oldPrice,newPrice;
ArrayList<Observer> customerList;
ShopSubject(){
customerList = new ArrayList<>();
}
@Override
public void addObserver(Observer o) {
if(!(customerList.contains(o)))//如果没记录下来就把观察者的引用添加到数组线性表中
customerList.add(o);
}
@Override
public void deleteObserver(Observer o) {
if(customerList.contains(o))
customerList.remove(o);
}
@Override
public void notifyObserver() {
for (int i = 0; i < customerList.size(); i++) {
Observer observer = customerList.get(i);
observer.update();
}
}
public void setDiscountGoods(String name, double oldPrice,double newPrice){
this.goodsName = name;
this.oldPrice = oldPrice;
this.newPrice = newPrice;
notifyObserver();//更新数据之后,通知所有观察者
}
public String getGoodsName(){
return goodsName;
}
public double getOldPrice(){
return oldPrice;
}
public double getNewPrice(){
return newPrice;
}
}
具体的观察者收到通知后就可以自己调用get方法拿自己想要的数据。
public class Customer implements Observer{
Subject subject;
String goodsName , personName;
Customer(Subject subject,String personName){
this.subject = subject;
this.personName = personName;
subject.addObserver(this);
}
//这里是拉数据的例子,推数据方法暂不实现
@Override
public void hearTelephone(String heardMess) { }
@Override
public void update() {
if (subject instanceof ShopSubject){
goodsName = ((ShopSubject)subject).getGoodsName();//调用具体主题提供的方法
System.out.println("打折的商品是 "+goodsName);
}
}
}
总结
大家可以看出推数据和拉数据只是实现方法上的不同,甚至这个拉数据我就是推数据的例子上新增的一个具体主题,基本不需要修改原来的代码,就给具体观察者提供了拉数据和推数据两种方法,我新增主题或者新增观察者完全不需要修改原有代码。
观察者模式的优点
- 具体主题和具体观察者是松耦合的关系
- 观察者模式满足开闭原则
观察者模式的缺点
Java不支持多继承,所以当观察者很多的时候,所有的具体观察者都只有一个父类接口。
修改数据发送逻辑的时候也需要修改原有的代码,这个从我在推数据的基础上增加拉数据的方式也可以看出来