概述
在整理 Netty 博客中的同步异步模块时,突然遇到回调函数这个概念。起始我心想:回调函数不就是两个函数互相调用么,后来本着认真求是的态度,在知乎查阅了几篇关于回调函数的理解后,才意识到之前自己的理解相当浅显。因此才有了本篇博客来详细介绍回调函数相关知识。
回调函数
简单来说,一次回调的全过程中,被调用方 调用的 调用方 传来的函数就是 回调函数。
这样讲可能比较抽象,还比较绕。这里我通过简单示例加以说明:
我去书店买书,书店老板告诉我想买的书暂时没有。我留电话给书店老板,告诉他等有书的时候通知我。后来书店新购了我想买的书,书店老板打电话通知我,我再来买书。
上述例子实际就是一次回调全过程的抽象,从中可以引出以下几个概念:
- 登记回调函数:我留电话给书店老板
- 调用回调函数:书店老板打电话通知我
- 响应回调函数:接到书店老板电话后,我去买书
再看这个例子,完成回调的大前提是:书店必须包含提醒顾客的服务,假如书店老板本身比较懒,不愿意通知客户,那整个回调就不可能建立。引入代码也就是说,要想完成回调,首先需要额外开发,使调用方、被调用方支持回调这项服务。
其次,上述示例是通过打电话的形式通知,除此之外还有很多其它通知方式:QQ、微信,街上偶遇等等。假设书店老板比较豁达,他根据客户提出的方式来通知客户,此时具体哪种通知方式是由客户决定。引入代码也就是说,回调函数是由调用发起方决定的,被调用方根据发起方参数内容调用相应的函数。
再举例,假设我买书前喝了酒,在得知书店暂时没有这本书后,我只告诉老板等有了书通知我,却没有告诉他任何联系我的方式,此时书店老板新购新书后,肯定没办法通知我,也就没办法完成回调。代入代码也就是说:要想完成整个回调过程,调用方必须告诉被调用方要执行的函数,被调用方本身是不完整的。
总结一下,整个回调过程是通过三方完成的:调用方、被调用方、回调函数。再回头看概述中我之前的理解,两个函数互相调用只有两方关系,直接忽视了调用方或回调函数,因此这个观点肯定是错的。
最后我根据上述示例提出一个问题:假设书店老板只支持电话方式通知,那么整个过程还构成回调吗?
关于这个问题,我个人认为不构成回调。因为此时通知的方式已经由被调用方决定,调用方此时传过来的号码只能算作是传参数,不能算作回调函数。告诉老板打电话通知这种方式才能算作是回调函数。
回调的优势
使用回调最大的优势就是灵活,它将一部分逻辑从服务端提出到客户端,客户端可以根据不同的业务类型传递不同的方法给服务端,此时服务端可以在不额外开发的情况下支持各种场景。
举个简单的例子:超市商品频繁更换,每个商品优惠也大不相同,此时就可以通过回调的方式实现优惠逻辑,服务端只需提供最终接口即可。下面我通过伪代码实现这块逻辑:
服务端:
public class Service {
// 返回商品折扣后价格
public int price(Client client) {
return client.discount();
}
}
客户端:
public abstract class Client {
// 单价
int value;
// 数量
int num;
// 具体折扣方式
public int discount() {
// 默认无折扣
return num * value;
}
}
// 商品1:苹果
public class Apple extends Client {
@Override
public int discount() {
// 满40减5
int temp = num * value;
return temp >= 40 ? temp - 5 : temp;
}
}
后续商品的折扣无论怎么变化,只需要重写客户端中的 discount() 方法即可,服务端无须做任何改动,极大的提高了程序的灵活性。
看到这里可能有读者疑惑,为什么不直接在客户端计算,非要在服务端回调一次?
主要是因为上述代码中服务端过于简单,假如超市要添加记账,根据商品类型统计销量等功能时,就可以将这部分逻辑添加到 price() 方法中,方便统计,此时就可以体现出回调的优势。
回调的类型
根据服务端响应的通信方式,回调可以分为 同步回调