观察者模式
让你的对象 知悉现况
定义
定义了对象之间的一对多依赖,这 样一来,当一个对象改变状态时,它的所有依赖者都 会收到通知并自动更新
通俗的理解
打个比方 , 你和甲,乙都在找一份java 的工作 , 然后找到了一个猎头
那么猎头就是 主题
你和 甲, 乙 都是观察者
你们找到猎头的过程就是注册观察者/订阅的动作
突然你觉得,你想自己去找了,不再委托他了,这就是移除观察者的过程
猎头得到岗位通知,会给你们每个人都通知职位更新的信息 , 这就是发布
问题
现在要你设计如上所述的一个程序, 你会怎么设计?
还记得前面所说的策略模式吗?
还记得面向接口编程吗?
首先我们得有观察者,观察者肯定有很多,而且各种不一样,根据策略模式应该怎么设计呢
interface Observer{
//当主题发生变化时,这个方法被调用
update();
}
然后我们得有主题,同理
interface Subject{
//注册观察者
registerObserver(Observer o);
//移除观察者
removeObserver(Observer o);
//通知所有观察者
notifyObservers();
}
接下来我们有一个猎头,他应该对应一个具体的主题,并且有一个观察者注册列表,然后就是观察者关心的那些数据了
/**
* @author apdoer
* @version 1.0
* @date 2019/10/10 11:40
*/
public class HeadHunting implements Subject {
private List observers;
private BigDecimal salary;
private String workspace;
private Long workTime;
public HeadHunting() {
observers = new ArrayList();
}
@Override
public void registerObserver(Observer observer) {
observers.add(observer);
}
@Override
public void removeObserver(Observer observer) {
if (observers.contains(observer)){
observers.remove(observer);
}
}
@Override
public void notifyObservers() {
for (int i = 0; i < observers.size(); i++) {
Observer observer = (Observer) observers.get(i);
observer.update(salary,workspace,workTime);
}
}
/**
* 职位信息更新,通知观察者
*/
public void jobInfoChanged(){
notifyObservers();
}
public void setJobInfo(BigDecimal salary,String workspace,Long workTime){
this.salary = salary;
this.workspace = workspace;
this.workTime = workTime;
jobInfoChanged();
}
// other methods
}
最后,具体的观察者肯定不能少了,需要关心的数据,还有具体的主题,当然这里我们肯定不引用猎头Headhunting
,而是主题Subject
/**
* 观察者,求职者
* @author apdoer
* @version 1.0
* @date 2019/10/9 16:55
*/
public class JobSearcher implements Observer, Information {
private BigDecimal salary;
private String wordspace;
private Long workTime;
private Subject headHunting;
/**
*
* @param headHunting
*/
public JobSearcher(Subject headHunting ) {
this.headHunting = headHunting;
//注册观察者
headHunting.registerObserver(this);
}
@Override
public void showInfo() {
System.out.println("JobSearcher{" +
"salary=" + salary +
", wordspace='" + wordspace + '\'' +
", workTime=" + workTime +
", jobData=" + headHunting +
'}');
}
@Override
public void update(BigDecimal salary, String workspace, Long workTime) {
this.salary = salary;
this.wordspace = workspace;
this.workTime = workTime;
showInfo();
}
}
看起来是可以了
我们这样设计的目的是什么呢?
松耦合
观察者模式提供了一种对象设计,让主题和观察者之间松耦合
- 关于观察者的一切,主题只知道观察者实现了某个接口(也就是Observer接口)。主 题不需要知道观察者的具体类是谁、做了些什么或其他任何细节
- 任何时候我们都可以增加新的观察者。因为主题唯一依赖的东西是一个实现 Observer接口的对象列表,所以我们可以随时增加观察者。事实上,在运行时我们可 以用新的观察者取代现有的观察者,主题不会受到任何影响。同样的,也可以在任何 时候删除某些观察者。
- 有新类型的观察者出现时,主题的代码不需要修改。假如我们有个新的具体类需要当 观察者,我们不需要为了兼容新类型而修改主题的代码,所有要做的就是在新的类里 实现此观察者接口,然后注册为观察者即可。主题不在乎别的,它只会发送通知给所 有实现了观察者接口的对象
- 改变主题或观察者其中一方,并不会影响另一方。因为两者是松耦合的,所以只要他 们之间的接口仍被遵守,我们就可以自由地改变他们
设计原则
为了交互对象之间的松耦合设计而努力
前面的思路中有的只是伪代码,我们现在来完善他们
观察者接口
/**
* 观察者
* @author apdoer
* @version 1.0
* @date 2019/10/9 16:16
*/
public interface Observer {
/**
* 当观察的主题发生改变时,通知观察者
* @param salary 薪水
* @param workspace 工作地
* @param workTime 工作时间
*/
void update(BigDecimal salary,String workspace,Long workTime);
}
每个观察者可能并不需要相同的接收方式 / 信息,所有策略模式又可以用起来了
/**
*
* @author apdoer
* @version 1.0
* @date 2019/10/9 16:29
*/
public interface Information {
/**
* 当观察者需要显示信息时,调用此方法
*/
void showInfo();
}
你看,有一个求职者并不关心其他的,薪水就够了,所以她只希望猎头给她发送的短信里有薪水
/**
* 观察者,求职者 第二个
* @author apdoer
* @version 1.0
* @date 2019/10/9 16:55
*/
public class JobSearcher1 implements Observer, Information {
private BigDecimal salary;
private String wordspace;
private Long workTime;
private Subject headHunting;
/**
*
* @param headHunting
*/
public JobSearcher1(Subject headHunting ) {
this.headHunting = headHunting;
//注册观察者
headHunting.registerObserver(this);
}
@Override
public void showInfo() {
System.out.println("JobSearcher{" +
"salary=" + salary +
", jobData=" + headHunting +
'}');
}
@Override
public void update(BigDecimal salary, String workspace, Long workTime) {
this.salary = salary;
this.wordspace = workspace;
this.workTime = workTime;
showInfo();
}
}
最后,我们来测试一下
/**
* 提供职位
* @author apdoer
* @version 1.0
* @date 2019/10/9 17:08
*/
public class JobProvider {
public static void main(String[] args) {
HeadHunting hd = new HeadHunting();
Observer searcher = new JobSearcher(hd);
Observer searcher1 = new JobSearcher1(hd);
//通过这个方法来模拟提供给猎头的职位数据更新
hd.setJobInfo(new BigDecimal(20000),"深圳",24*7*3600*1000L);
hd.setJobInfo(new BigDecimal(11000),"杭州",10*5*3600*1000L);
hd.setJobInfo(new BigDecimal(12000),"长沙",7*5*3600*1000L);
}
}
可以看到,观察者都通知到了,当然你也可以把数据格式化好看一点,把猎头取个名字,当然了,那就不是这篇文章的重心了
现在是不是对观察者模式有点认识了?
那么我们这样的设计是ok 的吗
其实.jdk有自己的观察者设计
有哪些问题呢?
jdk 观察者模式的实现
Observable
类,跟我们的Subject
接口作用是类似的,这里列出方法,具体实现,大家可以自行查看
public class Observable {
private boolean changed = false;
private Vector<Observer> obs;
//注册观察者
public synchronized void addObserver(Observer o) {}
//移除观察者
public synchronized void deleteObserver(Observer o) {}
//通知观察者主题发生了变化
public void notifyObservers() {}
//通知观察者,但是可以自定义内容
public void notifyObservers(Object arg) {}
//清空观察者列表
public synchronized void deleteObservers() {}
//标记订阅的主题发生了变化
protected synchronized void setChanged() {}
//清除标记
protected synchronized void clearChanged() {}
//返回订阅主题是否发生变化
public synchronized boolean hasChanged() {}
//返回观察者的数量
public synchronized int countObservers() {}
}
Observer
接口,跟我们的Observer
没什么太大区别
public interface Observer {
//订阅的主题发生变化,这个方法会被调用,这里可以通过指定参数
void update(Observable o, Object arg);
}
jdk 观察者模式是怎么运作的呢
- 创建观察者,实现
Observer
接口,跟我们自己的差不多,有注册和移除等方法 - 创建主题,这里就只能继承
Observerable
类了, - 主题变化怎么通知给观察者呢 ?
- 先调用
setChanged()
方法标记主题已经发生了变化 - 然后调用
notifyObservers()
或者notifyObservers(Object args)
其中一个,后者支持将特定数据推送给观察者,即我们没有实现的推的模式
- 先调用
这里为什么需要先标记再通知呢?
其实这里的处理是很有必要的,在实际使用中,可能出现这样的情况
消息的来源可能变化更新非常快,而观察者们不希望所有的信息一改变就马上被通知
比如出了一个薪水8000的职位,观察者并不关心,
这一步处理提供了观察者模式更多的弹性
利用jdk的观察者模式重构刚才的设计
再来一个猎头,因为Observerable
类中都定义好了,我们这里的结构很简单
/**
* @author apdoer
* @version 1.0
* @date 2019/10/10 14:16
*/
public class JdkHeadHunting extends Observable {
private BigDecimal salary;
private String workspace;
private Long workTime;
public JdkHeadHunting() {
// 这里不同在初始化了,父类中有
}
public void jobInfoChanged(){
setChanged();
//拉的模式获取更新
notifyObservers();
}
public void setJobInfo(BigDecimal salary,String workspace,Long workTime){
this.salary = salary;
this.workspace = workspace;
this.workTime = workTime;
jobInfoChanged();
}
public BigDecimal getSalary() {
return salary;
}
public String getWorkspace() {
return workspace;
}
public Long getWorkTime() {
return workTime;
}
}
- 来个找工作的,这里就不演示不同显示需求的求职者了,跟上面同理
/**
* @author apdoer
* @version 1.0
* @date 2019/10/10 14:23
*/
public class JdkJobSearcher implements Observer, Information {
private BigDecimal salary;
private String wordspace;
private Long workTime;
private Observable headHunting;
public JdkJobSearcher(Observable headHunting) {
this.headHunting = headHunting;
headHunting.addObserver(this);
}
@Override
public void update(Observable o, Object arg) {
if (o instanceof JdkHeadHunting) {
JdkHeadHunting jdkHeadHunting = (JdkHeadHunting)o;
this.salary = jdkHeadHunting.getSalary();
this.wordspace = jdkHeadHunting.getWorkspace();
this.workTime = jdkHeadHunting.getWorkTime();
showInfo();
}
}
@Override
public void showInfo() {
System.out.println("JdkJobSearcher{" +
"salary=" + salary +
", wordspace='" + wordspace + '\'' +
", workTime=" + workTime +
", headHunting=" + headHunting +
'}');
}
}
测试一下
/**
* @author apdoer
* @version 1.0
* @date 2019/10/10 14:29
*/
public class JdkTest {
public static void main(String[] args) {
JdkHeadHunting hd = new JdkHeadHunting();
Observer searcher = new JdkJobSearcher(hd);
hd.setJobInfo(new BigDecimal(20000),"深圳",24*7*3600*1000L);
hd.setJobInfo(new BigDecimal(11000),"武汉",10*5*3600*1000L);
hd.setJobInfo(new BigDecimal(12000),"重庆",7*5*3600*1000L);
}
}
通知到了
有没有什么问题呢
我们多提供几个订阅者
修改测试类
先看我们自己实现的
public class JobProvider {
public static void main(String[] args) {
HeadHunting hd = new HeadHunting();
Observer searcher = new JobSearcher(hd);
Observer searcher1 = new JobSearcher1(hd);
Observer searcher2 = new JobSearcher2(hd);
hd.setJobInfo(new BigDecimal(20000),"深圳",24*7*3600*1000L);
hd.setJobInfo(new BigDecimal(11000),"杭州",10*5*3600*1000L);
hd.setJobInfo(new BigDecimal(12000),"长沙",7*5*3600*1000L);
}
}
再看看jdk实现的
/**
* @author apdoer
* @version 1.0
* @date 2019/10/10 14:29
*/
public class JdkTest {
public static void main(String[] args) {
JdkHeadHunting hd = new JdkHeadHunting();
Observer searcher = new JdkJobSearcher(hd);
Observer searcher1 = new JdkJobSearcher1(hd);
Observer searcher2 = new JdkJobSearcher2(hd);
hd.setJobInfo(new BigDecimal(20000),"深圳",24*7*3600*1000L);
hd.setJobInfo(new BigDecimal(11000),"武汉",10*5*3600*1000L);
hd.setJobInfo(new BigDecimal(12000),"重庆",7*5*3600*1000L);
}
}
看到区别没有,
对了
顺序不一样
会有什么缺点呢
java.uitl.Observable
实现了它的notifyObservers()
方法,这导致了通知观察者的次 序不同于我们先前的次序。谁也没有错,只是双方选择不同的方式实现罢了 一旦观察者/可观察者的实现有所改变,通知次序就会改变,很可能就会产生错 误的结果java.util.Observable
是一个“类”而不是一个"接 口",它甚至没有实现一个接口。不幸的是,java.util.Observable的实现 有许多问题,限制了它的使用和复用,你必须设计一个类继承它。如果某类想同时 具有Observable类和另一个超类的行为,就会陷入两难,毕竟Java不支持多重继承。 这限制了Observable的复用潜力setChanged()
方法被保护起来了(被定义成 protected),这意味着:除非你继承自Observable,否则你无法 创建Observable实例并组合到你自己的对象中来。这个设计违反了第二个设计原 则:“多用组合,少用继承”
至于怎么选择,看你自己的需求,反正现在的你自己弄一套观察者模式出来,也不是难事,哈哈
总结
- 多用组合,少用继承
- 针对接口编程,不针对实现 编程
- 为交互对象之间的松耦合设 计而努力
关于设计模式,相关代码已经上传到github 设计模式
欢迎大家加入qq群 859759121
,大量免费vip学习资源,一起成长,一起进步