模組間呼叫
在一個應用系統中,無論使用何種語言開發,必然存在模組之間的呼叫,呼叫的方式分為幾種:
(1)同步呼叫
同步呼叫是最基本並且最簡單的一種呼叫方式,類A的方法a()呼叫類B的方法b(),一直等待b()方法執行完畢,a()方法繼續往下走。這種呼叫方式適用於方法b()執行時間不長的情況,因為b()方法執行時間一長或者直接阻塞的話,a()方法的餘下程式碼是無法執行下去的,這樣會造成整個流程的阻塞。
(2)非同步呼叫
非同步呼叫是為了解決同步呼叫可能出現阻塞,導致整個流程卡住而產生的一種呼叫方式。類A的方法方法a()通過新起執行緒的方式呼叫類B的方法b(),程式碼接著直接往下執行,這樣無論方法b()執行時間多久,都不會阻塞住方法a()的執行。但是這種方式,由於方法a()不等待方法b()的執行完成,在方法a()需要方法b()執行結果的情況下(視具體業務而定,有些業務比如啟非同步執行緒發個微信通知、重新整理一個快取這種就沒必要),必須通過一定的方式對方法b()的執行結果進行監聽。在Java中,可以使用Future+Callable的方式做到這一點,具體做法可以參見我的這篇文章Java多執行緒21:多執行緒下其他元件之CyclicBarrier、Callable、Future和FutureTask。
(3)回撥
最後是回撥,回撥的思想是:
- 類A的a()方法呼叫類B的b()方法
- 類B的b()方法執行完畢主動呼叫類A的callback()方法
這樣一種呼叫方式組成了上圖,也就是一種雙向的呼叫方式。
程式碼示例
接下來看一下回撥的程式碼示例,程式碼模擬的是這樣一種場景:老師問學生問題,學生思考完畢回答老師。
首先定義一個回撥介面,只有一個方法tellAnswer(int answer),即學生思考完畢告訴老師答案:
1 /** 2 * 回撥介面,原文出處http://www.cnblogs.com/xrq730/p/6424471.html 3 */ 4 public interface Callback { 5 6 public void tellAnswer(int answer); 7 8 }
定義一個老師物件,實現Callback介面:
1 /** 2 * 老師物件,原文出處http://www.cnblogs.com/xrq730/p/6424471.html 3 */ 4 public class Teacher implements Callback { 5 6 private Student student; 7 8 public Teacher(Student student) { 9 this.student = student; 10 } 11 12 public void askQuestion() { 13 student.resolveQuestion(this); 14 } 15 16 @Override 17 public void tellAnswer(int answer) { 18 System.out.println("知道了,你的答案是" + answer); 19 } 20 21 }
老師物件有兩個public方法:
(1)回撥介面tellAnswer(int answer),即學生回答完畢問題之後,老師要做的事情
(2)問問題方法askQuestion(),即向學生問問題
接著定義一個學生介面,學生當然是解決問題,但是接收一個Callback引數,這樣學生就知道解決完畢問題向誰報告:
1 /** 2 * 學生介面,原文出處http://www.cnblogs.com/xrq730/p/6424471.html 3 */ 4 public interface Student { 5 6 public void resolveQuestion(Callback callback); 7 8 }
最後定義一個具體的學生叫Ricky:
1 /** 2 * 一個名叫Ricky的同學解決老師提出的問題,原文出處http://www.cnblogs.com/xrq730/p/6424471.html 3 */ 4 public class Ricky implements Student { 5 6 @Override 7 public void resolveQuestion(Callback callback) { 8 // 模擬解決問題 9 try { 10 Thread.sleep(3000); 11 } catch (InterruptedException e) { 12 13 } 14 15 // 回撥,告訴老師作業寫了多久 16 callback.tellAnswer(3); 17 } 18 19 }
在解決完畢問題之後,第16行向老師報告答案。
寫一個測試類,比較簡單:
1 /** 2 * 回撥測試,原文出處http://www.cnblogs.com/xrq730/p/6424471.html 3 */ 4 public class CallbackTest { 5 6 @Test 7 public void testCallback() { 8 Student student = new Ricky(); 9 Teacher teacher = new Teacher(student); 10 11 teacher.askQuestion(); 12 13 } 14 15 }
程式碼執行結果就一行:
知道了,你的答案是3
簡單總結、分析一下這個例子就是:
(1)老師呼叫學生介面的方法resolveQuestion,向學生提問
(2)學生解決完畢問題之後呼叫老師的回撥方法tellAnswer
這樣一套流程,構成了一種雙向呼叫的關係。
程式碼分析
分析一下上面的程式碼,上面的程式碼我這裡做了兩層的抽象:
(1)將老師進行抽象
- 將老師進行抽象之後,對於學生來說,就不需要關心到底是哪位老師詢問我問題,只要我根據詢問的問題,得出答案,然後告訴提問的老師就可以了,即使老師換了一茬又一茬,對我學生而言都是沒有任何影響的
(2)將學生進行抽象
- 將學生進行抽象之後,對於老師這邊來說就非常靈活,因為老師未必對一個學生進行提問,可能同時對Ricky、Jack、Lucy三個學生進行提問,這樣就可以將成員變數Student改為List<Student>,這樣在提問的時候遍歷Student列表進行提問,然後得到每個學生的回答即可
這個例子是一個典型的體現介面作用的例子,之所以這麼說是因為我想到有些朋友可能不太明白介面的好處,不太明白介面好處的朋友可以重點看一下這個例子,多多理解。
總結起來,回撥的核心就是回撥方將本身即this傳遞給呼叫方,這樣呼叫方就可以在呼叫完畢之後告訴回撥方它想要知道的資訊。回撥是一種思想、是一種機制,至於具體如何實現,如何通過程式碼將回撥實現得優雅、實現得可擴充套件性比較高,一看開發者的個人水平,二看開發者對業務的理解程度。
同步回撥與非同步回撥
上面的例子,可能有人會提出這樣的疑問:
這個例子需要用什麼回撥啊,使用同步呼叫的方式,學生物件回答完畢問題之後直接把回答的答案返回給老師物件不就好了?
這個問題的提出沒有任何問題,可以從兩個角度去理解這個問題。
首先,老師不僅僅想要得到學生的答案怎麼辦?可能這個老師是個更喜歡聽學生解題思路的老師,在得到學生的答案之前,老師更想先知道學生姓名和學生的解題思路,當然有些人可以說,那我可以定義一個物件,裡面加上學生的姓名和解題思路不就好了。這個說法在我看來有兩個問題:
(1)如果老師想要的資料越來越多,那麼返回的物件得越來越大,而使用回撥則可以進行資料分離,將一批資料放在回撥方法中進行處理,至於哪些資料依具體業務而定,如果需要增加返回引數,直接在回撥方法中增加即可
(2)無法解決老師希望得到學生姓名、學生解題思路先於學生回答的答案的問題
因此我認為簡單的返回某個結果確實沒有必要使用回撥而可以直接使用同步呼叫,但是如果有多種資料需要處理且資料有主次之分,使用回撥會是一種更加合適的選擇,優先處理的資料放在回撥方法中先處理掉。
另外一個理解的角度則更加重要,就是標題說的同步回撥和非同步回撥了。例子是一個同步回撥的例子,意思是老師向Ricky問問題,Ricky給出答案,老師問下一個同學,得到答案之後繼續問下一個同學,這是一種正常的場景,但是如果我把場景改一下:
老師並不想One-By-One這樣提問,而是同時向Ricky、Mike、Lucy、Bruce、Kate五位同學提問,讓同學們自己思考,哪位同學思考好了就直接告訴老師答案即可。
這種場景相當於是說,同學思考完畢完畢問題要有一個辦法告訴老師,有兩個解決方案:
(1)使用Future+Callable的方式,等待非同步執行緒執行結果,這相當於就是同步呼叫的一種變種,因為其本質還是方法返回一個結果,即學生的回答
(2)使用非同步回撥,同學回答完畢問題,呼叫回撥介面方法告訴老師答案即可。由於老師物件被抽象成了Callback介面,因此這種做法的擴充套件性非常好,就像之前說的,即使老師換了換了一茬又一茬,對於同學來說,只關心的是呼叫Callback介面回傳必要的資訊即可