編寫(xiě)函數(shù)的實(shí)踐

2018-02-24 16:17 更新

我們已經(jīng)討論了如何處理異常,那么當(dāng)你在編寫(xiě)新的函數(shù)的時(shí)候,怎么才能向調(diào)用者傳遞錯(cuò)誤呢?

最最重要的一點(diǎn)是為你的函數(shù)寫(xiě)好文檔,包括它接受的參數(shù)(附上類(lèi)型和其它約束),返回值,可能發(fā)生的錯(cuò)誤,以及這些錯(cuò)誤意味著什么。?如果你不知道會(huì)導(dǎo)致什么錯(cuò)誤或者不了解錯(cuò)誤的含義,那你的應(yīng)用程序正常工作就是一個(gè)巧合。?所以,當(dāng)你編寫(xiě)新的函數(shù)的時(shí)候,一定要告訴調(diào)用者可能發(fā)生哪些錯(cuò)誤和錯(cuò)誤的含義。

Throw, Callback 還是 EventEmitter

函數(shù)有三種基本的傳遞錯(cuò)誤的模式。

  • throw以同步的方式傳遞異常--也就是在函數(shù)被調(diào)用處的相同的上下文。如果調(diào)用者(或者調(diào)用者的調(diào)用者)用了try/catch,則異常可以捕獲。如果所有的調(diào)用者都沒(méi)有用,那么程序通常情況下會(huì)崩潰(異常也可能會(huì)被domains或者進(jìn)程級(jí)的uncaughtException捕捉到,詳見(jiàn)下文)。

  • Callback 是最基礎(chǔ)的異步傳遞事件的一種方式。用戶傳進(jìn)來(lái)一個(gè)函數(shù)(callback),之后當(dāng)某個(gè)異步操作完成后調(diào)用這個(gè) callback。通常 callback 會(huì)以callback(err,result)的形式被調(diào)用,這種情況下, err和 result必然有一個(gè)是非空的,取決于操作是成功還是失敗。

  • 更復(fù)雜的情形是,函數(shù)沒(méi)有用 Callback 而是返回一個(gè) EventEmitter 對(duì)象,調(diào)用者需要監(jiān)聽(tīng)這個(gè)對(duì)象的 error事件。這種方式在兩種情況下很有用。

  • 當(dāng)你在做一個(gè)可能會(huì)產(chǎn)生多個(gè)錯(cuò)誤或多個(gè)結(jié)果的復(fù)雜操作的時(shí)候。比如,有一個(gè)請(qǐng)求一邊從數(shù)據(jù)庫(kù)取數(shù)據(jù)一邊把數(shù)據(jù)發(fā)送回客戶端,而不是等待所有的結(jié)果一起到達(dá)。在這個(gè)例子里,沒(méi)有用 callback,而是返回了一個(gè) EventEmitter,每個(gè)結(jié)果會(huì)觸發(fā)一個(gè)row?事件,當(dāng)所有結(jié)果發(fā)送完畢后會(huì)觸發(fā)end事件,出現(xiàn)錯(cuò)誤時(shí)會(huì)觸發(fā)一個(gè)error事件。

  • 用在那些具有復(fù)雜狀態(tài)機(jī)的對(duì)象上,這些對(duì)象往往伴隨著大量的異步事件。例如,一個(gè)套接字是一個(gè)EventEmitter,它可能會(huì)觸發(fā)“connect“,”end“,”timeout“,”drain“,”close“事件。這樣,很自然地可以把”error“作為另外一種可以被觸發(fā)的事件。在這種情況下,清楚知道”error“還有其它事件何時(shí)被觸發(fā)很重要,同時(shí)被觸發(fā)的還有什么事件(例如”close“),觸發(fā)的順序,還有套接字是否在結(jié)束的時(shí)候處于關(guān)閉狀態(tài)。

在大多數(shù)情況下,我們會(huì)把 callback 和 event emitter 歸到同一個(gè)“異步錯(cuò)誤傳遞”籃子里。如果你有傳遞異步錯(cuò)誤的需要,你通常只要用其中的一種而不是同時(shí)使用。

那么,什么時(shí)候用throw,什么時(shí)候用callback,什么時(shí)候又用 EventEmitter 呢?這取決于兩件事:

  • 這是操作失敗還是程序員的失誤?
  • 這個(gè)函數(shù)本身是同步的還是異步的。

直到目前,最常見(jiàn)的例子是在異步函數(shù)里發(fā)生了操作失敗。在大多數(shù)情況下,你需要寫(xiě)一個(gè)以回調(diào)函數(shù)作為參數(shù)的函數(shù),然后你會(huì)把異常傳遞給這個(gè)回調(diào)函數(shù)。這種方式工作的很好,并且被廣泛使用。例子可參照 NodeJS 的fs模塊。如果你的場(chǎng)景比上面這個(gè)還復(fù)雜,那么你可能就得換用 EventEmitter 了,不過(guò)你也還是在用異步方式傳遞這個(gè)錯(cuò)誤。

其次常見(jiàn)的一個(gè)例子是像JSON.parse這樣的函數(shù)同步產(chǎn)生了一個(gè)異常。對(duì)這些函數(shù)而言,如果遇到操作失?。ū热鐭o(wú)效輸入),你得用同步的方式傳遞它。你可以拋出(更加常見(jiàn))或者返回它。

對(duì)于給定的函數(shù),如果有一個(gè)異步傳遞的異常,那么所有的異常都應(yīng)該被異步傳遞??赡苡羞@樣的情況,請(qǐng)求一到來(lái)你就知道它會(huì)失敗,并且知道不是因?yàn)槌绦騿T的失誤??赡艿那樾问悄憔彺媪朔祷亟o最近請(qǐng)求的錯(cuò)誤。雖然你知道請(qǐng)求一定失敗,但是你還是應(yīng)該用異步的方式傳遞它。

通用的準(zhǔn)則就是?你即可以同步傳遞錯(cuò)誤(拋出),也可以異步傳遞錯(cuò)誤(通過(guò)傳給一個(gè)回調(diào)函數(shù)或者觸發(fā)EventEmitter的?error事件),但是不用同時(shí)使用。以這種方式,用戶處理異常的時(shí)候可以選擇用回調(diào)函數(shù)還是用try/catch,但是不需要兩種都用。具體用哪一個(gè)取決于異常是怎么傳遞的,這點(diǎn)得在文檔里說(shuō)明清楚。

差點(diǎn)忘了程序員的失誤?;貞浺幌?,它們其實(shí)是Bug。在函數(shù)開(kāi)頭通過(guò)檢查參數(shù)的類(lèi)型(或是其它約束)就可以被立即發(fā)現(xiàn)。一個(gè)退化的例子是,某人調(diào)用了一個(gè)異步的函數(shù),但是沒(méi)有傳回調(diào)函數(shù)。你應(yīng)該立刻把這個(gè)錯(cuò)拋出,因?yàn)槌绦蛞呀?jīng)出錯(cuò)而在這個(gè)點(diǎn)上最好的調(diào)試的機(jī)會(huì)就是得到一個(gè)堆棧信息,如果有內(nèi)核信息就更好了。

因?yàn)槌绦騿T的失誤永遠(yuǎn)不應(yīng)該被處理,上面提到的調(diào)用者只能用try/catch或者回調(diào)函數(shù)(或者 EventEmitter)其中一種處理異常的準(zhǔn)則并沒(méi)有因?yàn)檫@條意見(jiàn)而改變。如果你想知道更多,請(qǐng)見(jiàn)上面的 (不要)處理程序員的失誤。

下表以 NodeJS 核心模塊的常見(jiàn)函數(shù)為例,做了一個(gè)總結(jié),大致按照每種問(wèn)題出現(xiàn)的頻率來(lái)排列:

函數(shù) 類(lèi)型 錯(cuò)誤 錯(cuò)誤類(lèi)型 傳遞方式 調(diào)用者
fs.stat 異步 file not found 操作失敗 callback handle
JSON.parse 同步 bad user input 操作失敗 throw try/catch
fs.stat 異步 null for filename 失誤 throw none (crash)

異步函數(shù)里出現(xiàn)操作錯(cuò)誤的例子(第一行)是最常見(jiàn)的。在同步函數(shù)里發(fā)生操作失?。ǖ诙校┍容^少見(jiàn),除非是驗(yàn)證用戶輸入。程序員失誤(第三行)除非是在開(kāi)發(fā)環(huán)境下,否則永遠(yuǎn)都不應(yīng)該出現(xiàn)。

吐槽:程序員失誤還是操作失敗?

你怎么知道是程序員的失誤還是操作失敗呢?很簡(jiǎn)單,你自己來(lái)定義并且記在文檔里,包括允許什么類(lèi)型的函數(shù),怎樣打斷它的執(zhí)行。如果你得到的異常不是文檔里能接受的,那就是一個(gè)程序員失誤。如果在文檔里寫(xiě)明接受但是暫時(shí)處理不了的,那就是一個(gè)操作失敗。

你得用你的判斷力去決定你想做到多嚴(yán)格,但是我們會(huì)給你一定的意見(jiàn)。具體一些,想象有個(gè)函數(shù)叫做“connect”,它接受一個(gè)IP地址和一個(gè)回調(diào)函數(shù)作為參數(shù),這個(gè)回調(diào)函數(shù)會(huì)在成功或者失敗的時(shí)候被調(diào)用。現(xiàn)在假設(shè)用戶傳進(jìn)來(lái)一個(gè)明顯不是IP地址的參數(shù),比如“bob”,這個(gè)時(shí)候你有幾種選擇:

  • 在文檔里寫(xiě)清楚只接受有效的IPV4的地址,當(dāng)用戶傳進(jìn)來(lái)“bob”的時(shí)候拋出一個(gè)異常。強(qiáng)烈推薦這種做法。
  • 在文檔里寫(xiě)上接受任何string類(lèi)型的參數(shù)。如果用戶傳的是“bob”,觸發(fā)一個(gè)異步錯(cuò)誤指明無(wú)法連接到“bob”這個(gè)IP地址。

這兩種方式和我們上面提到的關(guān)于操作失敗和程序員失誤的指導(dǎo)原則是一致的。你決定了這樣的輸入算是程序員的失誤還是操作失敗。通常,用戶輸入的校驗(yàn)是很松的,為了證明這點(diǎn),可以看Date.parse這個(gè)例子,它接受很多類(lèi)型的輸入。但是對(duì)于大多數(shù)其它函數(shù),我們強(qiáng)烈建議你偏向更嚴(yán)格而不是更松。你的程序越是猜測(cè)用戶的本意(使用隱式的轉(zhuǎn)換,無(wú)論是JavaScript語(yǔ)言本身這么做還是有意為之),就越是容易猜錯(cuò)。本意是想讓開(kāi)發(fā)者在使用的時(shí)候不用更加具體,結(jié)果卻耗費(fèi)了人家好幾個(gè)小時(shí)在Debug上。再說(shuō)了,如果你覺(jué)得這是個(gè)好主意,你也可以在未來(lái)的版本里讓函數(shù)不那么嚴(yán)格,但是如果你發(fā)現(xiàn)由于猜測(cè)用戶的意圖導(dǎo)致了很多惱人的bug,要修復(fù)它的時(shí)候想保持兼容性就不大可能了。

所以如果一個(gè)值怎么都不可能是有效的(本該是string卻得到一個(gè)undefined,本該是string類(lèi)型的IP但明顯不是),你應(yīng)該在文檔里寫(xiě)明是這不允許的并且立刻拋出一個(gè)異常。只要你在文檔里寫(xiě)的清清楚楚,那這就是一個(gè)程序員的失誤而不是操作失敗。立即拋出可以把Bug帶來(lái)的損失降到最小,并且保存了開(kāi)發(fā)者可以用來(lái)調(diào)試這個(gè)問(wèn)題的信息(例如,調(diào)用堆棧,如果用內(nèi)核文件還可以得到參數(shù)和內(nèi)存分布)。

那么?domains?和?process.on('uncaughtException')?呢?

操作失敗總是可以被顯示的機(jī)制所處理的:捕獲一個(gè)異常,在回調(diào)里處理錯(cuò)誤,或者處理EventEmitter的“error”事件等等。Domains以及進(jìn)程級(jí)別的‘uncaughtException’主要是用來(lái)從未料到的程序錯(cuò)誤恢復(fù)的。由于上面我們所討論的原因,這兩種方式都不鼓勵(lì)。

以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)