转载

Java如何实现一个回调地狱(Callback Hell)?

对于回调地狱(Callback hell),想必大家都不陌生,尤其对于前端的朋友,当然前端的朋友通过各种办法去避免回调地狱,比如Promise。但是对于后端的朋友,尤其在RxJava、Reactor等反应式编程框架兴起之后,对于回调地狱只是听得多,但是见得的少。

为了更好了解回调地狱Callback hell问题在哪,我们首先需要学会怎么写出一个回调地狱。在之前,我们得知道什么是回调函数。

本文将包含:

  • 什么是回调
  • 回调的优势
  • 回调地狱是什么
  • 为什么会出现回调地狱
  • 回调和Future有什么区别
  • 如何解决回调地狱

我们今天从最开始讲起,先讲讲什么是回调函数。

什么是回调函数?

在百度百科上,是这么说的:

回调函数就是一个通过函数指针调用的函数。如果你把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。回调函数不是由该函数的实现方直接调用,而是在特定的事件或条件发生时由另外的一方调用的,用于对该事件或条件进行响应。 回调是任何一个被以方法为其第一个参数的其它方法的调用的方法。很多时候,回调是一个当某些事件发生时被调用的方法。

什么?不好理解?确实很难理解,并且这段解释还有指针云云,对于java用户实在是不友好。

给大家举个例子,供大家参考,也欢迎批评指正:

回调:调用方在调用被调方后,被调方还将结果反馈给调用方。(A调用B,B完成后,将结果反馈给A)

举个例子:老板安排员工一项工作,员工去完成。员工完成工作后,给老板反馈工作结果。这个过程就叫回调。

Java如何实现一个回调地狱(Callback Hell)?
回调示例

这下容易理解很多了吧!Talk is cheap, Show me the code! 好,我们就用这个写一个简单的例子。

回调的例子

Callback接口

首先,我们先写一个如下的 Callback 接口,接口只包含一个方法,用于callback操作。

/**
 * @author yangzijing
 */
public interface Callback<T> {

    /**
     * 具体实现
     * @param t
     */
    public void callback(T t);

}
复制代码

Boss类

老板是被反馈的对象,于是需要实现 Callback 这个接口,重载 callback 方法;对于老板具体要干什么,当然是做大生意,于是有了 makeBigDeals 方法;老板当然不能是光杆司令,他需要一个员工,我们再构造方法里给他添加一个员工 Worker ,稍后我们来实现Worker类。

public class Boss implements Callback<String> {

    private Worker worker;

    public Boss(Worker worker) {
        this.worker = worker;
    }

    @Override
    public void callback(String s) {
    }

    public void makeBigDeals(final String someDetail) {
		worker.work(someDetail);
    }

}
复制代码

Worker类

员工类,很简单,出入一个工作,完成就好了,返回结果即可。但是如何完成回调?

public class Worker {
    public String work(String someWork) {
		return 'result';
    }
}
复制代码

我们很容易想到就是这个思路,非常符合思维的逻辑,但是在回调中,我们需要做一些改变。

让代码回调起来

对于员工来说,需要知道两点,谁是老板,需要干啥。于是,输入两个参数,分别是老板和工作内容。具体内容分两步,首先完成任务,之后则是汇报给老板。

public class Worker {
    public void work(Callback<String> boss, String someWork) {
		String result = someWork + 'is done!'; // 做一些具体的处理
		boss.callback(result); // 反馈结果给老板
    }
}
复制代码

接下来,我们完成Boss类。在 callback 方法中,接收到传来的结果,并对结果进行处理,我们这里仅打印出来;在 makeBigDeals 方法中,老板分配工作,员工去完成,如果 完成过程是异步 ,则是 异步调用如果是同步的 ,则是 同步回调 ,我们这里采用异步方式。

在新建线程中,我们执行 worker.work(Boss.this, someDetail) ,其中 Boss.this 即为当前对象,在这里,我们正式完成了 回调

public class Boss implements Callback<String> {
    ……
    @Override
    public void callback(String result) { // 参数为worker输出的结果
		logger.info("Boss got: {}", result) // 接到完成的结果,并做处理,在这里我们仅打印出来
	}

    public void makeBigDeals(final String someDetail) {
		logger.info("分配工作");
		new Thread(() -> worker.work(Boss.this, someDetail)); // 异步完成任务
		logger.info("分配完成");
		logger.info("老板下班。。");
    }
}
复制代码

回调结果

Show me the result! 好,跑一下代码试一下。

Worker worker = new Worker();
Boss boss = new Boss(worker); // 给老板指派员工
boss.makeBigDeals("coding"); // 老板有一个代码要写
复制代码

结果如下。在结果中可以看到,老板在分配完工作后就下班了,在下班后,另一个线程通知老板收到反馈"coding is done"。至此,我们完成了 异步回调 整个过程。

INFO  2019 九月 20 11:30:54,780 [main]  - 分配工作
 INFO  2019 九月 20 11:30:54,784 [main]  - 分配完成
 INFO  2019 九月 20 11:30:54,784 [main]  - 老板下班。。
 INFO  2019 九月 20 11:30:54,787 [Thread-0]  - Boss got: coding is done!
复制代码

我将代码示例传至Github,供大家参考。 callback代码示例

回调的优势

  • 解耦 ,回调将 子过程主过程 中解耦。 对于相同的输入,可能对其有不同的处理方式。在回调函数,我们完成主流程(例如上面的 Boss 类),对于过程中的子流程(例如上面的 Worker 类)从主流程中分离出来。对于主流程,我们只关心子过程的输入和输出,输入在上面的例子中即为 Worker.work 中的参数,而子过程的输出则是主过程的 callback 方法的参数。
  • 异步回调 不会阻塞 主线程。上面的例子清晰可以看到,员工没有完成工作之前老板就已经下班,当工作完成后,会通过另一个线程通知老板。老板在这个过程无需等待子过程。

回调地狱

总体设计

我们将上述功能扩展,老板先将工作交给产品经理进行设计;设计完成后,交给程序员完成编码。流程示意如图。

Java如何实现一个回调地狱(Callback Hell)?
回调地狱

将任务交给产品经理

首先,写一个Callback,内部new一个产品经理的的 Worker ,在 makeBigDeal 方法实现主任务,将任务交给产品经理;在重载的 callback 方法中,获取产品经理的输出。

new Callback<String>() {
            private Worker productManager = new Worker();
            @Override
            public void callback(String s) {
                System.out.println("产品经理 output: " + s); // 获取产品经理的输出
            }

            public void makeBigDeals(String bigDeal) {
                System.out.println("Boss将任务交给产品");
                new Thread(() -> {
                    this.productManager.work(this, bigDeal); // 异步调用产品经理处理过程
                }).start();
            }
        }.makeBigDeals("design");
复制代码

再将产品经理输出交给开发

在拿到产品经理的输出之后,再将输出交给开发。于是我们在再次实现一个 Callback 接口。同样的,在 Callback 中,new一个开发的 Worker ,在 coding 方法中,调用 Worker 进行开发;在重载的 callback 方法中,获取开发处理后的结果。

@Override
public void callback(String s) {
	System.out.println("产品经理 output: " + s); // 产品经理的输出
	String midResult = s + " coding";
	System.out.println("产品经理设计完成,再将任务交给开发");
	new Callback<String>() {
		private Worker coder = new Worker();

		@Override
		public void callback(String s) {
			System.out.println("result: " + s); // 获取开发后的结果
		}

		public void coding(String coding) {
			new Thread(() -> coder.work(this, coding)).start(); // 调用开发的Worker进行开发
		}
	}.coding(midResult); // 将产品经理的输出交给开发
}
复制代码

完整的实现

new Callback<String>() {
            private Worker productManager = new Worker();
            @Override
            public void apply(String s) {
                System.out.println("产品经理 output: " + s);
                String midResult = s + " coding";
                System.out.println("产品经理设计完成,再将任务交给开发");
                new Callback<String>() {
					private Worker coder = new Worker();
                    @Override
                    public void apply(String s) {
                        System.out.println("result: " + s);
                    }
                    public void coding(String coding) {
                        new Thread(() -> coder.work(this, coding)).start();
                    }
                }.coding(midResult);
            }

            public void makeBigDeals(String bigDeal) {
                System.out.println("Boss将任务交给产品");
                new Thread(() -> this.productManager.work(this, bigDeal)).start();
            }
        }.makeBigDeals("design");
复制代码

好了,一个简单的回调地狱完成了。Show me the result!

Boss将任务交给产品
产品经理 output: design is done!
产品经理设计完成,再将任务交给开发
result: design is done! coding is done!
复制代码

回调地狱带来了什么?

到底什么是回调地狱?简单的说,回调地狱就是Callback里面又套了一个Callback,但是如果嵌套层数过多,仿佛掉入地狱,于是有了回调地狱的说法。

优势:回调地狱给我们带来什么?事实上, 回调的代码如同管道一样接收输入,并将处理后的内容输出至下一步 。而回调地狱,则是多个管道连接,形成的一个流程,而各个子流程(管道)相互独立。前端的朋友可能会更熟悉一些,例如 Promise.then().then().then() ,则是多个处理管道形成的流程。

劣势:回调的方法虽然将子过程解耦,但是回调代码的可读性降低、复杂性大大增加。

Callback Hell示例: Callback Hell

和Future对比

在上面,我们提到异步回调不会阻塞主线程,那么使用Future也不会阻塞,和异步回调的差别在哪?

我们写一个使用Future来异步调用的示例:

logger.info("分配工作...");
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> worker.work(someDetail));
logger.info("分配完工作。");
logger.info("老板下班回家了。。。");
logger.info("boss got the feedback from worker: {}", future.get());
复制代码

在上面的代码,我们可以看到,虽然 Worker 工作是异步的,但是老板获取工作的结果( future.get() )的时候却需要等待,而这个等待的过程是阻塞的。这是回调和Future一个显著的区别。

回调和Future的对比: callback和future对比

如何解决

如何解决回调地狱的问题,最常用的就是反应式编程 RxJava 和 Reactor ,还有Kotlin的Coroutine协程,OpenJDK搞的Project Loom。其中各有优势,按下不表。

原文  https://juejin.im/post/5d84a0f26fb9a06acf2b94ea
正文到此结束
Loading...