Java如何實現一個回撥地獄(Callback Hell)?
對於回撥地獄(Callback hell),想必大家都不陌生,尤其對於前端的朋友,當然前端的朋友通過各種辦法去避免回撥地獄,比如Promise。但是對於後端的朋友,尤其在RxJava、Reactor等反應式程式設計框架興起之後,對於回撥地獄只是聽得多,但是見得的少。
為了更好了解回撥地獄Callback hell問題在哪,我們首先需要學會怎麼寫出一個回撥地獄。在之前,我們得知道什麼是回撥函式。
本文將包含:
- 什麼是回撥
- 回撥的優勢
- 回撥地獄是什麼
- 為什麼會出現回撥地獄
- 回撥和Future有什麼區別
- 如何解決回撥地獄
我們今天從最開始講起,先講講什麼是回撥函式。
什麼是回撥函式?
在百度百科上,是這麼說的:
回撥函式就是一個通過函式指標呼叫的函式。如果你把函式的指標(地址)作為引數傳遞給另一個函式,當這個指標被用來呼叫其所指向的函式時,我們就說這是回撥函式。回撥函式不是由該函式的實現方直接呼叫,而是在特定的事件或條件發生時由另外的一方呼叫的,用於對該事件或條件進行響應。 回撥是任何一個被以方法為其第一個引數的其它方法的呼叫的方法。很多時候,回撥是一個當某些事件發生時被呼叫的方法。
什麼?不好理解?確實很難理解,並且這段解釋還有指標云云,對於java使用者實在是不友好。
給大家舉個例子,供大家參考,也歡迎批評指正:
回撥:呼叫方在呼叫被調方後,被調方還將結果反饋給呼叫方。(A呼叫B,B完成後,將結果反饋給A)
舉個例子:老闆安排員工一項工作,員工去完成。員工完成工作後,給老闆反饋工作結果。這個過程就叫回調。
回撥示例
這下容易理解很多了吧!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
方法的引數。 - 非同步回撥不會阻塞主執行緒。上面的例子清晰可以看到,員工沒有完成工作之前老闆就已經下班,當工作完成後,會通過另一個執行緒通知老闆。老闆在這個過程無需等待子過程。
回撥地獄
總體設計
我們將上述功能擴充套件,老闆先將工作交給產品經理進行設計;設計完成後,交給程式設計師完成編碼。流程示意如圖。
回撥地獄
將任務交給產品經理
首先,寫一個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。其中各有優勢,按下不表。
總結
總結一下:
- 什麼是回撥。回撥是呼叫方在呼叫被調方後,被調方還將結果反饋給呼叫方。(A呼叫B,B完成後,將結果反饋給A)
- 回撥的優勢。1)子過程和主過程解耦。2)非同步呼叫並且不會阻塞主執行緒。
- 回撥地獄是什麼。回撥地獄是回撥函式多層巢狀,多到看不清=。=
- 為什麼會出現回撥地獄。每一個回撥像一個管道,接受輸出,處理後將結果輸出到下一管道。各個管道處理過程獨立,多個管道組成整個處理過程。
- 回撥和Future有什麼區別。1)兩者機制不同;2)Future在等待結果時會阻塞,而回調不會阻塞。
- 如何解決回撥地獄。最常見的則是反應式程式設計RxJava和Reactor。