回调机制和钩子模式

2021/04/05 Java高级知识 共 9339 字,约 27 分钟
闷骚的程序员

1. 回调机制

1.1 理解回调机制

理解回调的思路如下:

  1. 方法自己调用自己:递归。
  2. 对象自己引用自己:链表。
  3. A对象调用B对象的testB方法,B反过来再调用A的testA方法:回调。
  4. A类中注入了B对象,通过B对象去实现某些功能:委托(依赖/代理/注入)。

1.2 什么是回调?

A对象调用B对象的b方法,B对象反过来再调用a对象的a方法,此时,就称a方法为相对于b对象的回调方法。
回调的重点在于:回。即反过来调用。

1.3 回调机制的核心

必须由消费方A负责触发对B的调用,并将消费方A的实例对象想办法搞到服务方B中去,只有服务方B有了A的引用后,才能完成回调逻辑(因为B在执行完自己的某个方法后需要回调A的callback方法)。

1.4 一个简单的回调

ClassA:

package zeh.test.demo.com.callback.basecallback;
public class ClassA {
    public void testClassA() {
        ClassB classB = new ClassB(this);
        classB.testClassB();
    }
    public void testClassA_back() {
        System.out.println("执行A类中的 testClassA_back 方法");
    }
}

ClassB:

package zeh.test.demo.com.callback.basecallback;
public class ClassB {
    private ClassA classA;
    public ClassB(ClassA classA) {
        this.classA = classA;
    }
    public void testClassB() {
        System.out.println("执行B类中的 testClassB 方法");
        classA.testClassA_back();
    }
}

TestMain:

package zeh.test.demo.com.callback.basecallback;
public class TestMain {
    public static void main(String[] args) {
        ClassA classA = new ClassA();
        classA.testClassA();
    }
}

测试结果:

执行B类中的 testClassB 方法
执行A类中的 testClassA_back 方法

2. 回调机制探索

2.1 一个最基本的回调架子

从上面案例可以看出,回调的核心是,A调用了B,B又反过来调用了A。
因此,一个最基础的回调架子应该至少包括两部分:

  1. A方:即消费方(调用方),A方中定义业务方法和回调方法,A方得持有B的引用才能调用B的方法。
  2. B方:即服务方,B方法定义业务方法,并定义成员或者局部变量以便持有A方的引用。
  3. 控制程序,负责将A对象注入给B。

2.2 最基本的回调架子存在严重问题

分析上述案例,classA调用classB的testClassB()方法,classB又返过来调用classA的testClassA_back()方法,从回调上看逻辑很明确。
但是,如果这时候classA有一个兄弟,classFuck,这个兄弟呢和classA有完全不同的回调逻辑,这个时候如果再使用上面的架子的话,那估计得重新写一套代码了,因为无法复用上面的classB的逻辑。
然而,幸好我们知道java的多态和委托机制,很显然我们可以为原来的testClassA_back()方法定义一个统一的接口,这样不论classA有多少兄弟,只需要按需实现这个接口的方法就行啦。
这个接口,我们称之为回调接口。

2.3 使用多态和接口对回调逻辑进行解耦

ICallBack:

package zeh.test.demo.com.callback.supercallback;
// 回调接口
public interface ICallBack {
    void callback();
}

ClassA:

package zeh.test.demo.com.callback.supercallback;
public class ClassA implements ICallBack {
    @Override
    public void callback() {
        System.out.println("执行A类中的 callback 方法");
    }
    public void testClassA() {
        ClassB classB = new ClassB(this);
        classB.testClassB();
    }
}

ClassFuck:

package zeh.test.demo.com.callback.supercallback;
public class ClassFuck implements ICallBack{
    @Override
    public void callback() {
        System.out.println("执行Fuck类中的 callback 方法");
    }
    public void testClassFuck() {
        ClassB classB = new ClassB(this);
        classB.testClassB();
    }
}

ClassB:

package zeh.test.demo.com.callback.supercallback;
public class ClassB {
    private ICallBack callback;
    public ClassB(ICallBack callback) {
        this.callback = callback;
    }
    public void testClassB() {
        System.out.println("执行B类中的 testClassB 方法");
        callback.callback();
    }
}

TestMain:

package zeh.test.demo.com.callback.supercallback;
public class TestMain {
    public static void main(String[] args) {
        ClassA classA = new ClassA();
        classA.testClassA();
        ClassFuck classFuck = new ClassFuck();
        classFuck.testClassFuck();
    }
}

测试结果:

执行B类中的 testClassB 方法
执行A类中的 callback 方法
执行B类中的 testClassB 方法
执行Fuck类中的 callback 方法

2.4 解耦分析

  1. 将回调逻辑抽取出来,通过单独的接口进行封装,定义顶层的抽象回调方法。
  2. 所有的客户端分别去实现回调接口,不同的客户端实现不同的回调逻辑,服务方只需要通过多态的方式注入回调接口的对象即可。
    也就是说,为了适配不同的客户端不同的回调请求,我们通过接口将回调定义进行抽象。这样一来,便通过多态实现了回调的通用性。
    但是,仔细观察,发现每一个客户端代码都需要实现回调接口ICallBack。这样做有好处也有坏处:
    好处是客户端代码明确和回调逻辑进行绑定。
    坏处是解耦不彻底。
    因此,可以将回调逻辑和客户端代码进一步进行彻底解耦。

2.5 彻底解耦客户端和回调逻辑

先使用内部类分别实现回调逻辑
ICallBack:

package zeh.test.demo.com.callback.supercallback;
// 回调接口
public interface ICallBack {
    void callback();
}

ClassA:

package zeh.test.demo.com.callback.supercallback;
public class ClassA {
    public void testClassA() {
        ClassB classB = new ClassB(new CallBackImpl());
        classB.testClassB();
    }
    public class CallBackImpl implements ICallBack {
        @Override
        public void callback() {
            System.out.println("CallBackImpl in ClassA 中的callback方法 ");
        }
    }
}

ClassFuck:

package zeh.test.demo.com.callback.supercallback;
public class ClassFuck{
    public void testClassFuck() {
        ClassB classB = new ClassB(new CallBackImpl());
        classB.testClassB();
    }
    public class CallBackImpl implements ICallBack {
        @Override
        public void callback() {
            System.out.println("CallBackImpl in ClassFuck 中的callback方法 ");
        }
    }
}

ClassB:

package zeh.test.demo.com.callback.supercallback;
public class ClassB {
    private ICallBack callback;
    public ClassB(ICallBack callback) {
        this.callback = callback;
    }
    public void testClassB() {
        System.out.println("执行B类中的 testClassB 方法");
        callback.callback();
    }
}

TestMain:

package zeh.test.demo.com.callback.supercallback;
public class TestMain {
    public static void main(String[] args) {
        ClassA classA = new ClassA();
        classA.testClassA();
        ClassFuck classFuck = new ClassFuck();
        classFuck.testClassFuck();
    }
}

测试结果:

执行B类中的 testClassB 方法
CallBackImpl in ClassA 中的callback方法
执行B类中的 testClassB 方法
CallBackImpl in ClassFuck 中的callback方法

2.6 使用匿名内部类对上述代码进行改造

使用匿名内部类分别实现回调逻辑 ClassA:

package zeh.test.demo.com.callback.supercallback;
public class ClassA {
    public void testClassA() {
        ClassB classB = new ClassB(new ICallBack() {
            @Override
            public void callback() {
                System.out.println("CallBackImpl in ClassA 中的callback方法 ");
            }
        });
        classB.testClassB();
    }
}

ClassFuck:

package zeh.test.demo.com.callback.supercallback;
public class ClassFuck {
    public void testClassFuck() {
        ClassB classB = new ClassB(new ICallBack() {
            @Override
            public void callback() {
                System.out.println("CallBackImpl in ClassFuck 中的callback方法 ");
            }
        });
        classB.testClassB();
    }
}

分析:
到这一步,实际上才是我们最常见的回调机制的实现。
即通过指定某个回调接口的lambda表达式实现,传入该回调器,相当于客户端实现了回调接口。
当客户端调用服务端某个方法时,服务方回头又反过来调用客户端实现的回调逻辑。
回调的核心概念就是: 我调你完你调我。

## 2.7 方法间的通过参数传递回调对象 我们一般注入回调对象时,比如客户端A调用B,B里面委托了A,当执行了B的指定方法后,线程又回头来执行A的某个逻辑,A对象注入到B中的,即A此时是作为B的一个成员变量存在的。
当然,这也符合我们常规的注入、委托、依赖等定义。因为大多数的委托机制,被委托的对象都是作为成员存在的。
然而,我们上面的所有案例,ClassB中的ClassA对象实际上都是通过方法参数注入进来的。
比如下面案例:
客户端程序:

package zeh.test.demo.com.call.back.method;
// 客户端 
// 提供回调逻辑
// 将客户端和控制程序分离
public class Client {
    public void callback() {
        System.out.println("客户端提供的回调方法");
    }
}

服务端程序:

package zeh.test.demo.com.call.back.method;
// 服务端程序 
// 暴露触发程序和回调注册接口
public class Server {
    public void handler(Client client) {
        System.out.println("服务端暴露的触发接口");
        //服务端业务逻辑执行完毕则执行客户端提供的回调逻辑
        client.callback();
    }
}

控制程序:

package zeh.test.demo.com.call.back.method;
public class Controller {
    public static void main(String[] args) {
        // 回调对象实际上直接作为方法参数传递进去,而不是作为一个成员注入进去
        new Server().handler(new Client());
    }
}

执行结果:

服务端暴露的触发接口
客户端提供的回调方法
Process finished with exit code 0

感想: 平时我们创建一个目标对象,执行目标对象的某个方法,如果在这个方法里面传入了另外一个对象,则这个对象很可能就是一个回调对象。
因为你传递进去实际上就是为了使用这个对象的某个功能,除非你传递进去并不进行任何调用。

3. 钩子模式

3.1 升级上述案例场景,所有客户端的回调逻辑完全相同

现在客户端ClassA和ClassFuck,需要的回调逻辑完全相同,此时,如果再分别由ClassA和ClassFuck实现回调接口、或者各自内部实现匿名内部类等方式,就显得冗余。
因此,既然回调逻辑完全相同,则可以将回调逻辑的实现单独提取为一个独立的实现类。
如下:
ICallBack:

package zeh.test.demo.com.callback.supercallback;
// 回调接口
public interface ICallBack {
    void callback();
}

CallbackImpl:

package zeh.test.demo.com.callback.supercallback;
// 专门搞一个类来实现接口 ICallBack
public class CallbackImpl implements ICallBack {
    @Override
    public void callback() {
        System.out.println("callback in CallbackImpl");
    }
}

ClassA:

package zeh.test.demo.com.callback.supercallback;
public class ClassA {

    // 这样一来,ClassA实际上和CallbackImpl完全脱离了关系。
    public void testClassA() {
        ClassB classB = new ClassB(new CallbackImpl());
        classB.testClassB();
    }
}

ClassFuck:

package zeh.test.demo.com.callback.supercallback;
public class ClassFuck {
    public void testClassFuck() {
        ClassB classB = new ClassB(new CallbackImpl());
        classB.testClassB();
    }
}

ClassB:

package zeh.test.demo.com.callback.supercallback;
public class ClassB {
    private ICallBack callback;
    public ClassB(ICallBack callback) {
        this.callback = callback;
    }
    public void testClassB() {
        System.out.println("执行B类中的 testClassB 方法");
        // 当callback对象不为null时执行它的方法
        if(callback != null){
            callback.callback();
        }
    }
}

TestMain:

package zeh.test.demo.com.callback.supercallback;
public class TestMain {
    public static void main(String[] args) {
        ClassA classA = new ClassA();
        classA.testClassA();
    }
}

测试结果:

执行B类中的 testClassB 方法
callback in CallbackImpl

结果分析:

  1. 单独拎出来一个实现类 CallbackImpl ,去实现回调接口 ICallBack 。
  2. 多个客户端比如ClassA,ClassFuck都调用ClassB的服务,只需要将回调器CallbackImpl对象注入到ClassB中即可。
  3. 当ClassB的方法执行时,ClassB会调用CallbackImpl对象的callback()方法。
    等等…ClassB现在调用CallbackImpl的callback方法?这个callback方法不是客户端ClassA和ClassFuck实现的啊,这就不是回调!
    这种实现和回调的区别在于:
    回调是客户端向服务端注入自己,并调用服务端的某个方法,服务端又回过来调用客户端的某个方法,重点在于
    而这种实现,客户端向服务端注入一个第三方实例,调用服务端的某个方法,服务端将请求转发到客户端注入的这个实例上去了。
    在java中,这种实现有个专门的模式,叫做钩子模式
    一般往往是服务方定义了某个接口,并且在服务方程序的某个流程节点埋好了这个接口对象。当用户实现了这个接口后,服务端就会在合适的时机来调用该实现。
    就像服务方在它的执行时机中挂了好多钩子,钩子的具体实现由用户自己指定,只要用户往上面挂东西了,时机一到服务端就会主动执行这个钩子实例。

3.2 钩子模式案例

定义钩子接口:

package zeh.test.demo.com.gouzi;
// 钩子接口
public interface MyGouZi {
    void invoke();
}

实现钩子接口:

package zeh.test.demo.com.gouzi;


public class MyGouZiImpl implements MyGouZi {
    @Override
    public void invoke() {
        System.out.println("执行钩子对象的invoke方法");
    }
}

定义客户端ClassA:

package zeh.test.demo.com.gouzi;
// 客户端程序
public class ClassA {
    public void methodA() {
        // 客户端向服务端注册钩子对象
        ClassB classB = new ClassB(new MyGouZiImpl());
        classB.methodB();
        classB.methodB2();
        classB.methodB3();
    }
}

定义服务端ClassB:

package zeh.test.demo.com.gouzi;
// 服务端程序
public class ClassB {
    private MyGouZi gouZi;
    public ClassB(MyGouZi gouZi) {
        this.gouZi = gouZi;
    }
    public void methodB() {
        System.out.println("执行classB的methodB方法");
        // 如果服务端预埋的钩子对象不为空的话,则执行钩子对象
        if (gouZi != null) {
            gouZi.invoke();
        }
    }
    public void methodB2() {
        System.out.println("执行classB的methodB2方法");
        if (gouZi != null) {
            gouZi.invoke();
        }
    }
    public void methodB3() {
        System.out.println("执行classB的methodB3方法");
        if (gouZi != null) {
            gouZi.invoke();
        }
    }
}

测试程序:

package zeh.test.demo.com.gouzi;
public class TestMain {
    public static void main(String[] args) {
        ClassA classA = new ClassA();
        classA.methodA();
    }
}

测试结果:

执行classB的methodB方法
执行钩子对象的invoke方法
执行classB的methodB2方法
执行钩子对象的invoke方法
执行classB的methodB3方法
执行钩子对象的invoke方法

分析: 钩子模式可以实现方法的统一转发。
比如上述案例,ClassA客户端访问了ClassB服务端的所有方法,只要访问服务端的任意一个方法都会将请求转发到钩子实例的invoke方法上面去。
动态代理底层就使用的这种方式。

4. 总结回调机制和钩子模式

总结回调机制:
回调机制演变到最后也离不开多态和委托等基础特性。
最简单的回调不需要抽象出回调接口。
然而随着回调的演变,往往需要服务方来定义一个回调接口提供给各个客户端,由客户端去实现回调接口并向服务端注入回调对象。
回调机制是委托机制的一种特殊场景,两者都能实现方法转发,但有区别:

  1. 委托机制只是将对a方法的访问转发到对应的b方法上。
  2. 回调机制将对a方法的访问最终转发到对调用方自己的某个方法上。

总结钩子模式:
钩子模式是回调机制的变种,回调强调将流程转发到客户端自己的回调方法上;钩子是将流程转发到用户实现好的钩子实例上。
钩子模式必须存在钩子接口,而且接口必须是服务端预留给客户去定制实现的。

很多书籍和博客不区分回调模式和钩子模式,认为两者是相同的,实际上回调和钩子很类似,就是最终转发的目标不同。回调最终又转发给了客户端子机,钩子转发给了用户实现好的钩子实例。

tips:
回调和钩子都使用的比较多。
尤其是钩子模式,想Servlet里面的Filter、Listener等接口都是web容器提前埋好的钩子接口,只要用户实现了这些钩子实例,就会在其对应的生命周期节点加载执行。
不论是回调还是钩子,都可以实现类似AOP那种事件通知机制,即“当….的时候就….”。

文档信息

Search

    Table of Contents