Callback 雜談

  過去在開發東西的時候,其實最常做的事情都是使用 library,把重心放在照顧好業務邏輯上。不過,這陣子因為工作需要,正跟同事一起開發一個 library。我發現第一件事情:「確認需求」還真的蠻困難的。畢竟,光是只有一個「滿足 library 應用場域的需求」這個大方向真的不夠啊!大的命題還是不能告訴我們,到底 library 應該要具備那些條件以及提供哪些功能?所以,接下來就是要學著說故事,說幾個應用的使用案例或情境,藉此去推演出「需求的細節,一條一條、一個一個」。

  下一步是根據這些需求,規劃 APIs,並且先把每支 API 的介面契約訂好,契約除了輸入輸出之外,我們還會包含名命等等跟 style 相關的規則那些事。雖然這些契約,跟著開發的腳步,有百分之一千萬的機會再修改;不過一開始先做初步統一還是好的,畢竟還要跟別人 co-work。

  在訂契約的同時,還有一個困擾產生,就是常常要回過頭去調整需求的細節。例如說,某「需求 Z」 需要 libray 提供一支 API-Z 來滿足,可是後來發現要滿足需求 Z,其實由 API-X  API-Y 共同合作就可被滿足,這時候就會去思考到底需不需要拿 API-X 跟 API-Y 來 facade 包裝成一支 API-Z。畢竟,library 的 APIs 太多支,感覺也是很煩人的啊!我跟同事的經驗是,如果預期這支 API-Z 是屬於那種會被經常使用的,那就包吧!如果不是很常被使用,那麼就乾脆抽掉,不用再多此一舉了。

這篇文章想談談 Callback


  接觸 node.js 也有一段時間了,也漸漸習慣它的非同步、事件驅動的環境。在 javascript (JS) 程式設計中,常常會遇到一大堆 callbacks,特別是 function 在 JS 中是一等公民,所以它一天到晚被拿來傳來傳去也不是什麼很奇怪的事情,callback 正是很常被傳入另一個函式的函式(很繞口啊...)。關於那些對 JS function 的說明,例如什麼相當於是 C 的 function pointer 的觀念啦,什麼有 functional programming 的意味啦!就請大家自己估狗一下了.... 因為我個人並不懂 functional programming,只稍稍知道一點皮毛,不敢胡亂說明,誤導大眾。

  在我的觀念裡,callback 是一種很廣義的說法。比較狹義的說法就像 event handler 或 listener 這類東西。例如,GUI 設計中的按鈕 clicked 事件,你可以掛一支 listener 到這個事件,一旦使用者按了按鈕,引發 clicked 事件,程式就會去調用你的監聽器,執行監聽器的內容。當然 handler 跟 listener 本身也可能只是一支 dispatcher,繼續剖析訊息後再分派出去。分派出去的手法可能是再用 event 發射出去,也有可能是直接分派給一支 message handler。

  這裡帶出了 callback 最常用被使用的情境:

1. 非同步


  非同步的意思是說,某事件不知道什麼時候會發生,可能下一秒就發生,可能在任意時刻發生,也可能永遠不會發生(永遠不發生也太哭夭了)。另一種思考是,推遲(defer)執行。推遲可以是定時推遲,例如推遲10秒後再執行,或者是推遲到「某件事情做完了」再執行。
  對於定時推遲是不是非同步,有點不好說,畢竟如果是週期性、或是推遲時間是可決定的,嚴格來說應該就不能說是非同步的。我覺得還是得看整個程式的上下文到底在幹嘛,會比較好認定。這一點,我自己不能很肯定,原諒我無法精確地說明啊~~ (如果在 function return 之後再執行,應該是認定為非同步的。在 node 裡面大多數 setTimeout 都是這些情境....)

2. 這個事件發生了,你想要幹嘛


  你可以把「這個事件發生時,你想做的事情」包裝起來成為一支函式向系統(或框架)註冊。這樣子只要事件發生,系統就會回過頭去調用你準備好的 callback,執行你想做的事。例如,clicked 事件發生時,你想要改變畫面的背景顏色。

  在單晶片嵌入式的領域中,中斷跟中斷服務常式(ISR)就是非同步的好例子。只是中斷編號是固定的,ISR 的記憶體位址也大多是固定的,沒有像高層次的事件/handler那麼靈活。ISR 就是一種 callback,當對應的中斷發生時,CPU 就會跑去執行那一段程式碼。因為 CPU 不知道那個中斷發生時,你到底想幹嘛,所以你如果有使用到該中斷,那麼你就要自己實作ISR的內容。

  說到第2點:「這個事件發生了,你想要幹嘛」,另一種類似的是,當什麼狀態或條件出現時,你想要幹嘛(這跟是不是非同步沒有關係)。這個可以推廣成,系統(或框架)會先自己規劃好當某狀態或條件出現時,會去調用一支 function,但是這支 function 的內容可能是空的,或是只實作了某一小部分,它留有一些空間,等待你去填滿。填滿的內容,就是「你想幹嘛」或是「你要怎麼做」。這就是反轉控制(inversion of control, ioc)的思考,應該隨便估狗一下,會看到類似的說法:「你不用 call 我,我會 call 你」。當系統需要你的時候,他就會回過頭去摳摳你。

  我甚至有看過,有人認為 C++ 的 virtual 或 Java 的 abstract  functions 那些留空等待實作的函式(方法)也是 callback。不過,這說法好像有點爭議,我不敢亂評論。若有人很懂的話,或許可以跟我們分享一下關於這件事的想法。

同步的 Callback

  上面說的是 callback 很常出現的非同步情境。這裡要說的也是 callback 很常出現的情境,只不過是同步的。例如以前在學 C 的時候遇到的 sort() 函式,又或者是你用列舉當作旗標,根據列舉的順序準備好對樣的執行函式(一個 function pointer的陣列),在程式執行期間遇到代表某狀態的旗標成立,就去找出對應位置的 function 起來執行。

  如果你常使用像 underscore 或 lodash 這種工具函式庫的話,你一定會碰到一大堆同步的 callback,只是它不會用 callback 這麼廣義的字眼,而是會用比較狹義的字眼,例如 predicate、iteratee。隨便撈一支 API: _.find() 來看一下好了,你可以在它引數 predicate  塞入你自己的預測器,自訂你想要 find 的條件。這就是一種 callback,因為 _.find() 不知道你的自訂規則是什麼,所以你必須告訴它你到底想幹嘛(怎麼比較)。

  再舉另一個 iterator (迭代器) 的例子,_.forEach(),它會在繞行 collection (或 array) 物件時的每一次 iteration,都去調用你給他的 iteratee (就是一支callback),API 文件會告訴你 iteratee 的介面契約,它會拋給你該次 iteration 所抽出來的元素,像 _.forEach() 就是拋給你的 callback 該次抽出的 value 跟 key。當然, _.forEahc() 不知道你想對每一個元素作什麼事,所以 iteratee 的內容就是你該負責實作的囉!

後記

  其實我本來是想要翻譯一篇「談同步和非同步callback的文章」跟大家分享,結果自己就先屁一堆啦!真的很歹勢~ 不過屁就屁啦!翻譯的文章就下次再說吧!其實已經快翻譯完啦!

  對於非同步的程式流程控制,我的經驗是,當我搞懂了 Promise,就發現對原本的非同步 callback 那些東西變得更有感覺。而且,Promise真的好用的要死.... 每次用它都會令人高潮啊!除了 chainable style 不用在那邊 callback 進 callback 出之外,對程式流程控制的脈絡也比較清晰一點。然後 nodeify 一下就可直接支援 error-back callback 的 style....  三不五時就想要把promise 物件丟來丟去的.... 這用 callback 可不是很容易搞的啊....

  以上是個人一點小經驗,可能還是有一點以管窺天。若文中有任何錯誤,或需要修正之處,很歡迎大家提供指正!因為在軟體方面,我時常不能知道自己的盲點在哪呀!很需要磨練磨練~


乾蝦大家收看!我們下次見!

補充一點東西,是我原本預計要翻譯的文章對 sync 與 sync callback的說明,我覺得很好理解

1. 同步的callback:在 function return 之前被執行
2. 非同步的 callback:(有機會) 在 function return 之後才被執行 (推遲執行)

1 則留言:

技術提供:Blogger.