《编程导论(Java)·9.3.1回调·3》回调的实现

版权声明:本文为博主原创文章,未经博主同意不得转载。 https://blog.csdn.net/yqj2065/article/details/31441221

接《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. 回调的实现

当下层模块状态发生某些变化时——通常由操作系统或JVM捕捉这样的状态变化并调用回调函数。程序猿最关心的是上层模块怎样提供回调的方法体。最理想的方式是在注冊时直接给出代码。如伪代码:

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."。

(上层不要轮询我,)我通知你。


★下层模块的观点:某些事件发生时,能够找到上层模块提供的处理代码 (回调函数)。

这就使得编写上层模块的程序猿有了新的体验——填空式编程。这就有了库与框架的差别——上层模块的程序猿直接调用的。属于库函数。要求上层模块的程序猿提供的回调函数的,属于框架。

假设我们通吃上层和下层。设计框架时使用回调机制;假设让应用程序猿填空。能够告诉他们一个术语——控制反转






猜你喜欢

转载自www.cnblogs.com/mqxnongmin/p/10612550.html