1. 程式人生 > 程式設計 >Java如何實現一個回撥地獄(Callback Hell)?

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對比

如何解決

如何解決回撥地獄的問題,最常用的就是反應式程式設計RxJavaReactor,還有Kotlin的Coroutine協程,OpenJDK搞的Project Loom。其中各有優勢,按下不表。

總結

總結一下:

  1. 什麼是回撥。回撥是呼叫方在呼叫被調方後,被調方還將結果反饋給呼叫方。(A呼叫B,B完成後,將結果反饋給A)
  2. 回撥的優勢。1)子過程和主過程解耦。2)非同步呼叫並且不會阻塞主執行緒。
  3. 回撥地獄是什麼。回撥地獄是回撥函式多層巢狀,多到看不清=。=
  4. 為什麼會出現回撥地獄。每一個回撥像一個管道,接受輸出,處理後將結果輸出到下一管道。各個管道處理過程獨立,多個管道組成整個處理過程。
  5. 回撥和Future有什麼區別。1)兩者機制不同;2)Future在等待結果時會阻塞,而回調不會阻塞。
  6. 如何解決回撥地獄。最常見的則是反應式程式設計RxJava和Reactor。