Java基于回调的观察者模式详解

时间:2022-12-12 05:48:08

本文由“言念小文”原创,转载请说明文章出处

一、前言

什么是回调?回调如何使用?如何优雅的使用?
本文将首先详解回调的原理,然后介绍回调的基本使用方法,最后介绍基于回调的“观察者模式”实现,演示
如何优化回调使用方法。

二、什么是回调

案例1
现有一农场需要向气象局订阅天气预报信息。农场向气象局发出订阅请求,气象局接受农场的订阅请求后,
每天都会向农场推送后一天的天气信息。农场每天接受到天气预报信息,将做对应的生产安排,具体安排
如下:如果气温在0~10℃,播种小麦,如果气温在11~15℃播种大豆,如果气温在16~20℃播种棉花,否则
维护农场设备。

我们从“案例1”中可以提取回调的概念。
首先,农场向气象局订阅天气预报信息,气象局会在当天向农场发送次日的天气预报。这里有两个异步条件:
a.农场订阅天气预报后,气象局不可能立即回复此后每一天天气预报信息;
b.农场并不知道气象局会在前一天具体哪一个精确时间点将天气预报发送给自己。
因此“农场-气象局”之间信息传递是异步的。
其次,农场接收到天气预报信息后,才会进行“工作安排”,由于农场不知道天气预报信息返回的精确时间,因此进行
“工作安排”的时机实际是由气象局决定的。自然地,我们想到将“工作安排”用一个函数(func())来实现,并且该函数的
具体实现由农场(Farm类)来实施,而函数的调用位置及调用时机由气象局(MeteorologicalBureau类)来决定。这就是一个典型的回调场景,
而func()函数被称之为回调函数。下面我们给出回调的通俗描述:
程序中某一模块A(类/库/其他)中通过一段代码(类/函数)实现某一功能(模块A定义该功能实现的具体细节),但该段代码
执行并不取决于模块A,而是由模块B(类/库/其他)决定,这时通常预先将该段代码的入口地址作为参数传递
给模块B,由模块B在程序的运行期间根据具体情况来选择何时何地调用这段代码。这一过程便称作回调。
通过下图直观理解回调

Java基于回调的观察者模式详解

三、如何使用回调
我们通过实现“案例1”来演示如何使用回调

第一步,创建一个关于气象局的监听接口MeteorologicalBureauListener,该接口中声明气象局相关行为的函数,
这里声明了天气信息发布函数onRelease()。

public interface MeteorologicalBureauListener {

    /**
* 天气信息发布
* @param description 天气预报信息描述
* @param minTemperature 最低气温
* @param maxTemperature 最高气温
* @param minWindscale 最低风力
* @param maxWindscale 最高风力
*/
void onRelease(String description,
int minTemperature,
int maxTemperature,
int minWindscale,
int maxWindscale);
}

第二步,创建气象局类MeteorologicalBureau,该类负责接受天气预报信息的订阅和发布天气预报信息

/**
* 气象局(天气预报信息发布者)
* @author WenYong
*
*/
public class MeteorologicalBureau { private MeteorologicalBureauListener mListener; /**
* 注册对"气象局"类监听
* @param listener
*/
public void register(MeteorologicalBureauListener listener){
if(null == listener){
return;
}
mListener = listener;
} /**
* 取消注册对"气象局"类监听
* @param listener
*/
public void unregister(MeteorologicalBureauListener listener){
if(null == listener){
return;
}
if(mListener.equals(listener)){
mListener = null;
}
} /**
* 天气信息预测
*/
public void predict(){
new Thread(new Runnable() { public void run() {
try {
// 模拟耗时操作
Thread.sleep(9000);
} catch (InterruptedException e) {
e.printStackTrace();
}
mListener.onRelease("明日渝北区气温8~10℃,风力5~8级", 8, 10, 5, 8);
}
}).start(); } }

第三步,创建农场类,该类订阅天气预报信息,并在接收到天气预报信息后做对应工作安排

/**
* 农场(天气预报信息订阅者)
* @author WenYong
*
*/
public class Farm { private MeteorologicalBureau mBureau;
private MeteorologicalBureauListener mBureauListener; public Farm(MeteorologicalBureau bureau){
mBureau = bureau;
} /**
* 订阅天气信息
*/
public void subscribe(){
System.out.println(TestUtils.getTimeStamp() + "," + "农场订阅天气预报信息");
mBureauListener = new MeteorologicalBureauListener() { public void onRelease(String description, int minTemperature,
int maxTemperature, int minWindscale, int maxWindscale) {
System.out.println(TestUtils.getTimeStamp() + ","
+ "农场接收到天气信息:" + description);
doAfterReceiveWheatherInfo(minTemperature, maxTemperature);
}
};
mBureau.register(mBureauListener);
} /**
* 取消订阅天气信息
*/
public void unsubscribe(){
mBureau.unregister(mBureauListener);
} /**
* 接收到气象局发布的天气信息后,农场做对应的工作安排
* @param minTemperature 明日最低气温
* @param maxTemperature 明日最高气温
*/
private void doAfterReceiveWheatherInfo(int minTemperature, int maxTemperature){
String timeStamp = TestUtils.getTimeStamp();
if(minTemperature >= 0 && minTemperature <= 10){
System.out.println(timeStamp + "," + "农场明日工作安排:播种小麦");
}else if(minTemperature >= 11 && minTemperature <= 15){
System.out.println(timeStamp + "," + "农场明日工作安排:播种大豆");
}else if(minTemperature >= 16 && minTemperature <= 20){
System.out.println(timeStamp + "," + "农场明日工作安排:播种棉花");
}else{
System.out.println(timeStamp + "," + "农场明日工作安排:维护设备");
}
} }

第四步,编写测试类,首先new一个农场对象并订阅天气预报信息,然后气象局调用predict()函数预测天气并发布预报

public class Test {

    public static void main(String[] args) {
MeteorologicalBureau bureau = new MeteorologicalBureau();
// 农场订阅天气信息
new Farm(bureau).subscribe();
// 气象局进行天气信息预测
bureau.predict();
}
}

第五步,运行,结果如下:
2019-02-06 21-54-59,农场订阅天气预报信息
2019-02-06 21-55-08,农场接收到天气信息:明日渝北区气温8~10℃,风力5~8级
2019-02-06 21-55-08,农场明日工作安排:播种小麦

从结果不难看出,农场向气象局订阅天气预报后,9s后气象局向农场发布了天气预报信息,然后农场根据天气预报信息
做出了对应工作安排。

原理分析:
好了,看到了结果之后,我们来分析如何实现回调的。第一步在气象局的监听接口MeteorologicalBureauListener中声明
了天气信息发布接口onRelease()。然后第二步在农场类Farm的天气订阅方法subscribe()中,以内部类对象的方式实现MeteorologicalBureauListener
接口并重写onRelease()方法,然后通过mBureau.register(mBureauListener)将内部类对象传递给气象局对象,这样实际就将Farm对象中实现的onRelease()方法
传递给了气象局对象。从而气象局对象就可以根据具体情况来调用该方法了。再看测试类Test中,首先农场订阅天气信息的过程,就将
Farm对象中定义的接口方法onRelease()传递给了气象局对象,然后气象局对象调用predict(),该方法先模拟耗时9s,然后便执行了onRelease()方法,这样
相当于便将天气信息发布给了Farm对象,由于农场对象事先已定义好接收到天气预报信息后的工作安排doAfterReceiveWheatherInfo(),故而当onRelease()被
气象局回调后,紧接着便执行了农场的工作安排。

四、回调进阶(基于回调的“观察者模式”实现)
在学会了回调的基本使用方法后,我们将案例1稍加修改,增加一个天气预报订阅者
案例2
现有一农场和一机场需要向气象局订阅天气预报信息。农场和机场向气象局发出订阅请求,气象局接受订阅请求后,
每天都会向农场和机场推送后一天的天气信息。农场每天接受到天气预报信息,将做对应的生产安排,具体安排
如下:如果气温在0~10℃,播种小麦,如果气温在11~15℃播种大豆,如果气温在16~20℃播种棉花,否则
维护农场设备;机场接收到天气预报信息,将采取对应的运营管理措施,具体如下:如果风力小于5级,不做预警正常起飞,
如果风力5~8级,预警起飞,如果风力大于8级,暂停起飞。

案例2中看一看出,气象局发布信息是一对多的关系,如下图:

Java基于回调的观察者模式详解
这便是我们开发中经常遇到的观察者模式(设计模式中的观察者模式在此不多做介绍),农场和机场作为“观察者”向气象局订阅天气预报信息,气象局作为信息发布者
每天以一对多的方式,向农场和机场“广播”信息。那么如何通过回调实现”一对多“的信息发布呢?

第一步,创建一个关于气象局的监听接口MeteorologicalBureauListener,该接口中声明气象局相关行为的函数,
这里声明了天气信息发布函数onRelease()。

public interface MeteorologicalBureauListener {

    /**
* 天气信息发布
* @param description 天气预报信息描述
* @param minTemperature 最低气温
* @param maxTemperature 最高气温
* @param minWindscale 最低风力
* @param maxWindscale 最高风力
*/
void onRelease(String description,
int minTemperature,
int maxTemperature,
int minWindscale,
int maxWindscale);
}

第二步,创建气象局类MeteorologicalBureau,该类负责接受天气预报信息的订阅和发布天气预报信息。需要注意
这里使用了一个并发队列来存储机场和农场传递过来的MeteorologicalBureauListener实现对象的引用。之所以使用ConcurrentLinkedQueue
是为了防止在后面遍历的时候出现多线程问题:遍历的同时被修改,从而导致软件闪退。

/**
* 气象局(天气预报信息发布者)
* @author WenYong
*
*/
public class MeteorologicalBureau { private ConcurrentLinkedQueue<MeteorologicalBureauListener> mListenerQueue; public MeteorologicalBureau(){
mListenerQueue = new ConcurrentLinkedQueue<>();
} /**
* 注册对"气象局"类监听
* @param listener
*/
public void register(MeteorologicalBureauListener listener){
if(null == listener){
return;
}
if(mListenerQueue.contains(listener)){
return;
}
mListenerQueue.add(listener);
} /**
* 取消注册对"气象局"类监听
* @param listener
*/
public void unregister(MeteorologicalBureauListener listener){
if(null == listener){
return;
}
if(!mListenerQueue.contains(listener)){
return;
}
mListenerQueue.remove(listener);
} /**
* 天气信息预测
*/
public void predict(){
new Thread(new Runnable() { public void run() {
try {
// 模拟耗时操作
Thread.sleep(9000);
} catch (InterruptedException e) {
e.printStackTrace();
}
release(mListenerQueue, "明日渝北区气温8~10℃,风力5~8级", 8, 10, 5, 8);
}
}).start(); } /**
* 天气预报信息发布
* @param queue 监听队列
* @param description 天气预报描述
* @param minTemperature 最低气温
* @param maxTemperature 最高气温
* @param minWindscale 最低风力
* @param maxWindscale 最高风力
*/
private void release(ConcurrentLinkedQueue<MeteorologicalBureauListener> queue,
String description,
int minTemperature,
int maxTemperature,
int minWindscale,
int maxWindscale){
if(null == queue || queue.isEmpty()){
return;
}
Iterator<MeteorologicalBureauListener> it = queue.iterator();
while(it.hasNext()){
it.next().onRelease(description, minTemperature,
maxTemperature, minWindscale, maxWindscale);
}
} }

第三步,创建农场类(农场类代码和案例1相同这里不再重复贴出)和机场类

/**
* 机场(天气预报信息订阅者)
* @author WenYong
*
*/
public class Airport { private MeteorologicalBureau mBureau;
private MeteorologicalBureauListener mBureauListener; public Airport(MeteorologicalBureau bureau){
mBureau = bureau;
} /**
* 订阅天气信息
*/
public void subscribe(){
System.out.println(TestUtils.getTimeStamp() + "," + "机场订阅天气预报信息");
mBureauListener = new MeteorologicalBureauListener() { public void onRelease(String description, int minTemperature,
int maxTemperature, int minWindscale, int maxWindscale) {
System.out.println(TestUtils.getTimeStamp() + ","
+ "机场接收到天气信息:" + description);
doAfterReceiveWheatherInfo(minWindscale, maxWindscale);
}
};
mBureau.register(mBureauListener);
} /**
* 取消订阅天气信息
*/
public void unsubscribe(){
mBureau.unregister(mBureauListener);
} /**
* 接收到气象局发布的天气信息后,机场做对应的运营管理措施
* @param minWindscale
* @param maxWindscale
*/
private void doAfterReceiveWheatherInfo(int minWindscale, int maxWindscale){
String timeStamp = TestUtils.getTimeStamp();
if(maxWindscale < 5){
System.out.println(timeStamp + "," + "机场明日运营管理措施:不做预警正常起飞");
}else if(minWindscale >= 5 && maxWindscale <= 8){
System.out.println(timeStamp + "," + "机场明日运营管理措施:预警起飞");
}else{
System.out.println(timeStamp + "," + "机场明日运营管理措施:暂停起飞");
}
} }

第四步,编写测试类,首先分别new一个农场对象和机场对象,并订阅天气预报信息,然后气象局调用predict()函数预测天气并发布预报

public class Test {

    public static void main(String[] args) {
MeteorologicalBureau bureau = new MeteorologicalBureau();
// 农场订阅天气信息
new Farm(bureau).subscribe();
// 机场订阅天气信息
new Airport(bureau).subscribe();
// 气象局进行天气信息预测
bureau.predict();
}
}

第五步,运行,结果如下:
2019-02-07 10-35-54,农场订阅天气预报信息
2019-02-07 10-35-54,机场订阅天气预报信息
2019-02-07 10-36-03,农场接收到天气信息:明日渝北区气温8~10℃,风力5~8级
2019-02-07 10-36-03,农场明日工作安排:播种小麦
2019-02-07 10-36-03,机场接收到天气信息:明日渝北区气温8~10℃,风力5~8级
2019-02-07 10-36-03,机场明日运营管理措施:预警起飞

从结果可以看出,农场和机场分别向气象局订阅了天气预报信息,9s模拟耗时后,气象局向它们发布了天气预报信息,二者并根据对应
天气信息作了对应工作安排和运营管理。

原理分析:
案例2中回调的实现原理与案例1中相同,在此不再赘述。不同点在于案例2如何实现“一对多”的回调。气象局类MeteorologicalBureau中使用
并发队列mListenerQueue来存储机场和农场传递过来的MeteorologicalBureauListener实现对象的引用,这样气象局就可以调用二者中实现的onRelease()方法。
MeteorologicalBureau中调用私有的release()来对mListenerQueue中对象实现遍历,从而遍历各订阅对象中onRelease()方法。

五、结语
回调是我们日常开发工作中使用最为基础最为频繁的技术手段,不论是同步调用还是异步调用场景(特别是异步调用使用尤其多)有大量应用。
如果您也是跟我当初一样是初入行的小白,希望本文对您有用,另外在java开发中经常用到判null处理,本文代码中经常使用在判null时,大量
使用return处理,个人觉得这是一个好习惯,多使用return以减少逻辑判断的嵌套,使代码更容易阅读。