接《9.3.1Java回调 ·1》(概念)和《编程导论(Java)·9.3.1回调·2》什么是好莱坞法则
本文改写《回调·3》,因为Java8引入了Lambda Expressions
★一个回调函数/回调方法(简称回调、callback)是上层模块实现的,将被下层模块(反过来)“执行”的方法。
【回调,或隐式调用Implicit invocation(某些软件架构的作者使用的术语)】
例子:上层Client需要更新进度条——显示复制任务完成的进度时,下层模块Server如何将进度数据传递给上层的Client呢?
通常有两种解决方案: ①轮询;②回调或通知。【书上的图9-12,在一个包中定义了4个类型,按照分层的要求,应该把代码分别放在不同包中——这里修改了书上的相关内容(包括代码)。但是我就不方便截图了。】
在图9-11中,Client定义的方法callback(int),将被Server这个被调用者反过来调用。请注意图中的分层线,通常下层模块Server不知道上层定义的接口也不应该/能够直接调用上层接口。如何解决这个小问题呢?可以在公共模块/下层模块中设计一个抽象类或接口如IXxx,定义回调方法的规范,而Server调用公共模块IXxx的抽象方法callback(int)即可。换言之,Server不能够调用上层模块的方法,那么调用本层的IXxx的方法总是可以的。类型之间的关系如图9-12所示。
【不好画图啊,先这样将就一下】图9-12及下面的例程说明了Java中使用回调的基本结构。它由上层模块Client、TestCallback和下层被调用者Server和公共模块IClient组成。Client的callback(int) 方法被称为回调。而IClient定义的抽象方法callback(int) 被称为回调接口,大多数情况下,回调接口也简称回调。(书上原文:注意:通常公共模块、上层模块和下层模块均有自己的包,这里简单地将它们全部放在同一个包API.event中,仅仅为了方便地查看相关代码。在实践中,3个模块通常由不同的程序员编写。)
package API.event.lower;
/**
* 通常在公共模块中设计一个抽象类或接口如IClient,定义回调的契约/规范。
* 公共模块通常有自己的包。
* @author yqj2065
* @version 0.2
*/
public interface IClient{
/**
* 回调接口,参数为底层将上传的数据。
* 例如 copy的进度。
*/
public void callback(int i);
}
package API.event.lower;
/**
* 下层Server知道拷贝的进度。
* copy()在适当的时机调用回调来通知上层。
* @author yqj2065
*/
public class Server{
private IClient whoCallMe;//必须获得一个IClient 的引用,由构造器的参数提供
public Server(IClient listener) {//
whoCallMe = listener;
}
public void copy() {
for(int i=0;i<100;i++){
//在适当的时机调用回调
if (i%10 == 0) {
whoCallMe.callback(i/10);
}
}
System.out.println("copy() over");
}
}
上层模块代码如下:
package API.event;//意味上层模块所在的包
import API.event.lower.Server;//使用
import API.event.lower.IClient;//继承
/**
* 上层模块Client,是公共模块/下层模块IClient的实现类。
* @author yqj2065
* @version 0.1
*/
public class Client implements IClient{
/**
* 调用下层Server的copy()方法。
* 但是下层必须知道要通知谁(一个IClient的引用)
* 因而在Server(IClient)中传递this。
*/
public void call() {
new Server(this).copy();//传递this
}
/*
* 回调方法。下层模块执行时,传回一些数据。
*/
@Override public void callback(int i) {
System.out.println(i+"0%");
}
}//class Client
package API.event;
import API.event.lower.Server;//使用
public class TestCallback{
public static void test(){
new Client().call();
}
public static void foo(){
new Server(new Client()).copy();
}
}
这个例程说明:回调方法是某个上层模块实现的某个功能,但是所有的上层模块都不会直接调用它,设计它的目的就是为了下层模块的反向“调用”。
另外,回调(callback)是一个名词,而非动词(call back)。并非意味着“我调用你,你调用我”,例如任一上层模块如TestCallback可以调用下层Server的copy(),但是它不提供回调,而由Client提供。
2. 好莱坞法则
前面站在上层模块Client的角度考虑回调,现在站在下层模块Server的角度重新考虑回调。另外增添一点变化——多个Client的对象或多个IClient的其他实现类对下层Server2对象的某种状态变化感兴趣。
以现实生活中的场景为例:一些男女演员们(Client)都对某导演(Server2)是否拍新片子感兴趣。导演肯定和的士司机一样,不喜欢演员们天天打电话询问。于是导演提供了一个让感兴趣的演员们留下电话号码的接口register(IXxx listener),演员工会TestCallback2组织大家登记。一旦导演准备拍摄一部新片子(sthHappened())就通知所有已登记的演员。而对于那些打电话询问的演员,导演告诉他们一条好莱坞法则:"Don't call me; I'llcall you."。
类型之间的关系如图9-13所示(略)。对比图9-12及其例程,本例程中Server2并不关心Client是否调用自己——即call()是无关紧要的。这是极其重要的一个细节:Client与Server2之间没有依赖关系。
公共模块IClient、上层模块Client不需要做任何变动。其他代码如下
package API.event.lower;
import java.util.List;
import java.util.ArrayList;
public class Server2{//导演
private List<IClient> listeners = new ArrayList<IClient>();//电话簿
/**
* 监听器注册
*/
public void register(IClient listener) {
listeners.add(listener);
}
public void sthHappened(){//某种状态发生改变
for(IClient x: listeners) {
x.callback(12345);//
}
}
}
package API.event;
import API.event.lower.Server2;//使用
/**
* TestCallback2.java.
* 上层模块
* @author yqj2065
*/
class TestCallback2{
public static void test(){
Server2 s =new Server2();
s.register( new Client());
s.register( new Client());
s.sthHappened();//这里由上层模块触发事件的发生
}
}
TestCallback2.test()执行时,首先创建Server2对象s,并将两个Client对象进行注册,因而s对象的listeners保留两者的引用;当调用sthHappened()时——由上层模块触发事件的情形在网络程序中会出现,s先后调用两个IClient对象的callback(int)方法,IClient对象执行回调对sthHappened事件做出回应。请注意:GUI等程序中,一般不会出现上层模块触发事件发生,这里的作法仅仅是为了方便演示。
本例程的关键在于下层模块状态发生某些变化时——某些事件发生时,可以 找到上层模块相应的处理代码(回调函数)。而这一点正是Java委托事件模型的核心。3. 回调的实现
s.register(λi.(操作i)) //λ表达式
事实上,封装代码的callback(int)方法的方法名不需要存在(只需要参数和对参数的处理代码),更不用说封装callback(int)方法的类和对象。还记得冯•诺依曼的存储-程序概念吗?可执行代码也被储存在内存中。从提供回调的方法体角度,在编程领域,
★回调通常指可以被作为参数传递给其他代码的可执行代码块,或者一个可执行代码的引用。
如果能够将可执行代码封装成方法如foo(),而方法名foo又可以作为其他方法的参数,则可以register(foo)实现回调。在JavaScript, Perl和 PHP中可以如此实现。
如果能够操作可执行代码的内存地址即函数指针(function pointers) 则可以像C或C++那样实现回调。
如果习惯面向对象的传统方式,方法必须由类封装,那么Java封装抽象方法callback(int)的IClient和实现类Client提供了一个清晰但是笨重的实现方式。Java匿名类(参见9.4.5节)则对此稍加改进。
现在,Java8的λ表达式,终于完成了回调的原意——代码的参数化,即doSth( foo )按照统一的形式,对传入foo进行处理。(书上列举的C的函数指针(function pointers) 和C#1.0的委托(delegate)及其简化——C# 2.0的匿名方法或C# 3.0的λ表达式,可以删除)
package API.event;
import API.event.lower.Server2;//使用
import API.event.lower.IClient;//使用
/**
* TestCallback2.java.
* 上层模块
* @author yqj2065
*/
class TestCallback2{
public static void test(){
Server2 s =new Server2();
s.register( new Client());
s.register( new Client());
s.sthHappened();//这里由上层模块触发事件的发生
}
public static void test2() {
// λ表达式Vs. Java匿名类(参见9.4.5节)
Server2 s =new Server2();
IClient listener1=(i)->{System.out.println("+" + i + "0%");};
s.register(listener1);
s.register((i)->{System.out.println("++" + i + "0%");});
s.register(new IClient(){
@Override public void callback(int i){
System.out.println("==" + i + "0%");
}
});
s.sthHappened();
}
}
为了方便地使用Lambda表达式取代匿名类,Java8新引入了概念: 函数接口(functional interface),即仅仅显式声明了一个自己的抽象方法的接口(可以用@FunctionalInterface标注)。
IClient listener1=(i)->{System.out.println("+" + i + "0%");};
λ表达式的类型,叫做“目标类型(target type)”,必须是函数接口。上面的赋值语句,将λ表达式——事实上是函数接口的实现类的引用(也可以理解为像C或C++那样的函数指针)赋值给函数接口。
BTW:
回调机制——使用回调、好莱坞法则设计程序/框架的风格。或者说,下层模块lower.Server调用同层(或更下层的)lower.IClient的方法m(),而执行了上层模块(lower.IClient的子类型)Client的m()方法的编程方式。通常解释为动词(call back)。
★上层模块的观点:回调以通知取代轮询。
既然有“通知”,那么自然地包含了观察者模式。对应于上层模块的,是观察者observer/被通知者;对应于下层模块的,就是源source或目标/subject。
练习9-22:将Server2更名为Subject/目标或Source/事件源,Client更名为Observer/观察者或Listener,网络学习:观察者/Observer设计模式。它以好莱坞法则作为内在准则。
★下层模块的观点:好莱坞法则(Hollywood Principle):"Don't call me; I'll call you."。(上层不要轮询我,)我通知你。
★下层模块的观点:某些事件发生时,可以找到上层模块提供的处理代码 (回调函数)。
这就使得编写上层模块的程序员有了新的体验——填空式编程。这就有了库与框架的区别——上层模块的程序员直接调用的,属于库函数;要求上层模块的程序员提供的回调函数的,属于框架。如果我们通吃上层和下层,设计框架时使用回调机制;如果让应用程序员填空,可以告诉他们一个术语——控制反转