1.1 回调机制(Call back)

【注:本文是OOD第一章的内容;同时也是对《编程导论》的完善。】

在软件设计中,分层设计(Layered designs)是软件开发时常用的策略,例如管理信息系统常用的逻辑三层结构——表现层、业务逻辑层(business logic layer)和数据访问层(data access layer)。分层架构中,除(不依赖其他模块的)公共模块如信息系统中数据模块/model模块,可被各层调用外,一个层的模块只依赖于同一层或下层的模块。

单向依赖原则:依赖应该是单向的,避免循环或相互依赖。

为了讨论方便,可以简单地将代码分为两层。应用程序称为上层模块,而被应用程序依赖的库,称为下层模块。比如说,Java程序开发可以粗略地分为两层:应用程序和JDK(和第三方包),其中应用程序属于上层模块,而JDK属于下层模块。分层架构遵循一条军规:

★上层模块依赖于下层模块。下层模块不可能依赖于上层模块。

这是显而易见的。因为在设计下层模块/库时,不可能知道应用程序中使用什么类名、函数名等等,也就是说,下层模块对上层模块一无所知。(对于分层,本书点到为止。软件组织的探讨,更加抽象)。之所以提及和强调分层,因为回调机制内含分层的想法,即使在应用程序中,可以使用回调机制但是没有分层,事实上,隐含地分了层——程序员要做到单向依赖。

那么,什么是回调机制呢?

一门编程语言,使得下层模块/库可以调用上层模块定义的代码的机制,即为回调(call back,一个动词词组)机制。而上层模块所定义的被调用的代码,则称为回调函数 (简称回调、callback,一个名词)。

既然下层模块不可能依赖于上层模块,下层模块对上层模块一无所知,那么下层模块又怎样调用上层模块的函数呢?这就是回调机制魅力所在。

下层模块调用上层模块的函数,有两种使用场景:

  • 框架中。此时下层模块的框架,需要上层的应用程序提供具体的策略。本节介绍这方面的内容,通过一个例子的多种语言的实现,说明各种编程语言的核心技术。
  • 通知机制中。此时,下层模块调用上层模块的函数,目的是将下层知道的数据,通过实参传递给应用程序处理。[1.2好莱坞法则]介绍这方面的内容。

【在具有高阶函数的语言中,回调(函数)基本上是一个不需要存在的概念。比如说你天天吃肉(app设计时经常使用高阶函数),你会为过年吃肉(在设计框架时使用高阶函数)搞出一个名词加餐(回调函数)吗?】

1.1.1 什么是框架

按照wiki的说法,非常复杂,作为OOD学习的出发点之一,这里需要给框架一个十分简化的定义。下层(通常说底层)模块,常常被称为库/ Library ( 简写 Lib ),库中包括工具箱与框架。因此,框架是与工具箱相比较而存在的概念

工具箱是高级的子程序库。最简单的工具箱,常常被称为库函数,如java.lang .Math,它封装如函数abs、sin、max、random等,形成一个数学工具集;再如C语言的函数库;通常意义的工具箱,相对框架而言,它表现为一个完整的解决方案。如javaI/O流、Java集合类库、Spring依赖注入容器等。

框架/framework是在下层模块中定义的一个骨架式方案,处理特定应用中面临的共同的细节。

★框架是骨架式方案,需要上层模块(为该骨架式方案)提供代码支持。

在事件驱动程序/GUI编程、Java远程方法调用(Remote Method Invocation, RMI),applet等框架中,框架封装了控制逻辑。但是,框架仅仅是骨架,应用程序员按照自己面临的需求而使用框架时,要给出自己的功能实现代码——按照框架填入自己的东西。形象地说,应用程序员使用框架时要填空式编程。例如GUI编程,程序员按照需求使用各种窗口、菜单、按钮和各种控件得到自己需要的界面布局,界面布局的编程工作,其实使用Math工具没有差别。真正的差别在于,框架的设计者不可能预知应用程序员在点击一个按钮时,应用程序应该如何响应,而应用程序员则要给出界面的响应代码/功能实现代码,此时,应用程序员必须按照框架的约束来填入自己的东西。

所以,框架这种骨架式方案,显然地,框架需要上层模块提供支持代码,因此需要回调机制。【回调机制是编写框架/类库时需要的基本技术(回调机制/call back是框架设计的基本手段)。】

1.1.2 回调机制的实现

现在举一个例子,使读者对框架、回调机制等概念有一些感性认识。然后再介绍该例子背后蕴涵的道理。

【实验1:回调机制的实现】假定下层模块/框架中需要设计一个op函数,对两个参数值进行“某种”操作后返回一个值。

框架设计者不可能预判应用程序员可能进行何种操作,可能是相加,比较、求幂等等。那么如何调用应用程序员的代码,或者说下层要如何调用(call)上层的代码呢?各种编程语言采用了不同的方式来实现回调机制。在实现回调机制(或者说定义“某种”操作,参考《第2章行为参数化》)时,各门编程语言提供的实现,均涉及该语言的精华概念,而这些精华概念解决同一个问题。

 

1.C语言的回调机制

C函数指针中,给出了一些例子。C语言中经典的回调机制例子,是C语言的标准库qsort函数,其函数原型为extern void __cdecl qsort(void *, size_t, size_t, __cmpfunc *);

对于op函数,简化起见,对两个double进行某种操作,需要第三个参数来刻画进行“何种”具体的操作。C语言采用函数指针来实现回调机制。应用程序/上层模块包括函数plus和main。

void op(double a, double b, double(* how_op)(double, double)){// pt2Func
	return 2* how_op (a, b);    // call using function pointer
}

那么,app中的代码,先定义回调函数如double Plus    (double a,double b) { return a+b; }

 

int main(void){
	op(2, 5,  &Plus);// Pass a Function Pointer as an Argument 
	return 0;
}

以上层模块的main为出发点。main调用了下层的函数op,并在调用时将一个函数plus的指针作为实参传递给how_op形参;而op执行过程中将“回过头来调用”上层定义的函数plus。某种程度上,在C语言中,回调机制/Call back这个术语非常形象和直观。

引人注目的op函数中的第三个参数,即指针int (*How_op)( int, int),它指定了函数op能够使用的函数的种类,表示为(int, int)→int。可以将各种函数名赋值给一个函数指针,但是这些函数除函数名外,必须具有相同的形参列表和返回值类型,即函数指针的种类。

2. Scheme语言的回调机制

函数式编程语言如Scheme中,将函数作为一个数据类型。

★以函数作为参数或返回值的函数,被称为高阶函数(Higher-Order function)

Scheme语言定义函数op,和C语言相似,也需要第三个参数来刻画进行“何种”具体的操作,而op就是一个以函数how-op为形参的高阶函数。

(define (op a b how-op)
   ( how-op a b) )

//上层模块如下
(define (plus a b )
    (+ a b))	

(op 1 3 plus) ;;; Pass a function as an Argument。输出为4。或者 (op 1 3 +)
(op 2 3 (lambda(x y)(> x y))) ;;;输出为 #f。直接传递一个匿名函数(由lambda表达式定义)

3.Java语言的回调机制

具有动态绑定能力的面向对象的Java语言,不同于C和Scheme,不需要第三个参数来刻画进行“何种”具体的操作,因为抽象函数op本身就意味着对两个值进行“某种”操作。抽象函数op由二元操作/BinaryOP封装;它的实现类如AddOp,指定进行何种具体的操作——加法。

package chap1.callBack;
@FunctionalInterface public interface BinaryOP { //二元操作
      int op(int a, int b);
}

package chap1.callBack;
public class AddOp implements BinaryOP {
    @Override public int op(int m,int n){  
        return m+n;  
    }
}

这反映了面向对象语言的强大之处:定义函数op时,不需要第三个参数指定进行“何种”具体的操作。Java可以通过BinaryOP的子类型提供支持代码,更多的提供支持代码的方式,将在[1.1.4匿名类和λ表达式]中介绍。

从实现回调机制的角度看,各语言使用的技术、术语如下

  • C,函数指针
  • Scheme,高阶函数
  • Java,动态绑定/多态

    1.1.3 什么是回调(函数)?

 

 

 

应用程序App可以使用工具类AddOp,也可以根据自己的情况提供回调函数。app在实现回调时,可以设计

  • 如同AddOp一样的独立的实现类
  • 可以使用匿名类(anonymous classes)
  • 可以使用λ表达式[Java 8]。
package test;
import util.AddOp;
import util.DoubleOP;
import static util.Print.pln;//System.out.printin
public class App {
    public static void test() {
        //<span style="font-family:Arial, Helvetica, sans-serif;">框架和库函数的区别?</span>
        double d = new AddOp().op(1, 1.0);
        pln(d);
        //匿名类
        DoubleOP clazz = new DoubleOP() {
            @Override
            public double op(double m, double n) {
                return m + n;
            }
        };
        d = clazz.op(1, 2);        pln(d);
        //λ表达式
        clazz = (m, n) -> {
            return m + 2 * n;
        };
        d = clazz.op(1, 3);        pln(d);
        // λ表达式作为实参
        d = use((m, n) -> m - n, 1, 4);
        pln(d);
    }

    public static double use(DoubleOP clazz, double m, double n) {
        return clazz.op(m, n);
    }
}

在Java8之前,我们讨论回调函数时,通常介绍接口DoubleOP封装了回调接口op,op(double ,double );

在Java8之后,我们讨论回调函数时,更多地介绍use(DoubleOP clazz, double m, double n) ,函数接口(类似C函数指针)。

 

4.探讨

什么是回调(函数)?

这里的要点是,我们讨论或定义回调(函数)这个概念时,是否需要加入分层的限制。

按照一般的解释:“回调通常指可以被作为参数传递给其他代码的可执行代码块,或者一个可执行代码的引用”。这就类似Lisp中的高阶函数——回调函数被高阶函数作为参数。这样的“被作为参数的函数”,不管是在C或Lisp中,与一般的函数有什么区别呢?

好吧,Lisp中不谈回调函数,而C语言中也不纠结地把Plus 称为回调函数,你把op(double a, double b, double(*pt2Func)(double, double))称为回调函数都可以,反正你需要知道的只是函数指针),总之:C语言用函数指针实现回调机制(Call back),下层调用函数指针指向的函数。显然,回调(函数)就失去了讨论的价值。

应用程序可以调用回调函数吗?在C语言中,如果把Plus   称为回调函数,很难阻止程序员调用回调函数(Java中也可以调用回调函数。但是Java中回调函数与一般函数的区别比较明显)

因此,我在讨论回调(函数)这个概念时加入分层的限制。

因此,回调机制是编写框架/类库时需要的基本技术(回调机制/call back是框架设计的基本手段)。回调机制 =控制反转IoC。(IoC与依赖注入无关!!!)

更一般地,回调机制可以理解为:在设计框架时使用高阶函数。没有高阶函数,那么各种语言就需要给出自己的替代品。高阶函数是单纯的编程语言的概念,不涉及程序组织的分层概念;而回调,需要这个分层概念。回调使用的语言技术,和在非分层场合时的一般方法,没有什么区别。

因此:

★一个回调函数/回调方法(简称回调、callback)是上层模块实现的,将被下层模块(反过来)“执行”的方法。

从提供回调函数的方法体的角度看,λ表达式最为简洁。因此,Java 8你肯定要学习,大家都会马上使用它。

★回调函数的基本特征:上层模块中编写的,上层模块不会去调用的函数

 

 

 

 

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页