談?wù)凧ava接口Result設(shè)計(jì)
這篇文章醞釀了很久,一直想寫,卻一直覺得似乎要講的東西有點(diǎn)雜,又不是很容易講清楚,又怕爭(zhēng)議的地方很多,就一拖再拖。但是,每次看到不少遇到跟這個(gè)設(shè)計(jì)相關(guān)導(dǎo)致的問題,又忍不住跟人討論,但又很難一次說清楚,于是總后悔沒有及早把自己的觀點(diǎn)寫成文章。不管怎樣,觀點(diǎn)還是要表達(dá)的,無論對(duì)錯(cuò)。
故障的推手——“Result"
先說結(jié)論:接口方法,尤其是對(duì)外HSF(開源版本即dubbo) api,接口異常建議不要使用Result,而應(yīng)該使用異常。阿里內(nèi)部的java編碼,已經(jīng)習(xí)慣性對(duì)外API一股腦兒使用“Result”設(shè)計(jì)——這是導(dǎo)致許多故障的重要原因!
???一個(gè)簡(jiǎn)化的例子
// 用戶查詢的HSF服務(wù)API,使用了Result做為返回結(jié)果
public interface UserService {
Result getUserById(Long userId);
}
// 一段客戶端應(yīng)用facade的調(diào)用示例。讀寫緩存邏輯部分省略,僅做示意
public User testGetUser(Long userId) {
String userKey = "userId-" userId;
// 先查緩存,如果命中則返回緩存中的user
// cacheManager.get(123, userKey);
// ...
try{
Result result = userService.getUserById(userId);
if (result.isSuccess()) {
cacheManager.put(123, userKey, result.getData());
return result.getData();
}
// 否則緩存空對(duì)象,代表用戶不存在
cacheManager.put(123, userKey, NullCacheObject.getInstance(), 3600);
return null;
} catch (Exception e) {
// TODO log
throw new DemoException("getUserById error. userId=" userId, e);
}
}
上面的代碼很簡(jiǎn)單,客戶端應(yīng)用對(duì)User查詢服務(wù)做了個(gè)緩存。有些同學(xué)可能一眼就看出來,這里隱藏的bug:第10行的“result.isSuccess()”為false的實(shí)際含義是什么?是服務(wù)端系統(tǒng)異常嗎?還是用戶不存在?光看API是很難確定的。不得不去找服務(wù)提供方或文檔確認(rèn)其邏輯,根據(jù)錯(cuò)誤碼進(jìn)行區(qū)分。如果是服務(wù)端系統(tǒng)異常,那么第15行將導(dǎo)致線上bug,因?yàn)楹罄m(xù)1小時(shí)對(duì)該用戶的請(qǐng)求都認(rèn)為用戶不存在了。
???嚴(yán)謹(jǐn)點(diǎn)的寫法
如果要寫正確邏輯,那么代碼可能會(huì)變成這樣:
public User testGetUser(Long userId) {
String userKey = "userId-" userId;
// 先查緩存,如果命中則返回緩存中的user
// cacheManager.get(123, userKey);
// ...
try{
Result result = userService.getUserById(userId);
if (result.isSuccess()) {
cacheManager.put(123, userKey, result.getData());
return result.getData();
}
if ("USER_NOT_FOUND".equals(result.getCode())) {
// 否則緩存空對(duì)象,代表用戶不存在
cacheManager.put(123, userKey, NullCacheObject.getInstance(), 3600);
} else {
// 可能是SYSTEM_ERROR、DB_ERROR等一些系統(tǒng)性的異常,TODO log
throw new DemoException("getUserById error. userId=" userId ", result=" result);
}
} catch (DemoException e) {
throw e;
} catch (Exception e) {
// TODO log
throw new DemoException("getUserById error. userId=" userId, e);
}
return null;
}
很顯然,代碼變得復(fù)雜起來了,加上對(duì)外部調(diào)用的try catch異常處理,實(shí)際代碼變相當(dāng)復(fù)雜繁瑣。
???不使用Result的例子
public interface UserService {
User getUserById(Long userId) throws DemoAppException;
}
public User testGetUser(Long userId) {
String userKey = "userId-" userId;
// 先查緩存,如果命中則返回緩存中的user
// cacheManager.get(123, userKey);
// ...
try {
User user = userService.getUserById(userId);
if (user != null) {
cacheManager.put(123, userKey, user);
return user;
} else {
// 否則緩存空對(duì)象,代表用戶不存在
cacheManager.put(123, userKey, NullCacheObject.getInstance(), 3600);
return null;
}
} catch (Exception e) {
// TODO log
throw new DemoException("getUserById error. userId=" userId, e);
}
}
這樣一看,代碼簡(jiǎn)潔清晰很多,也更符合對(duì)普通API的調(diào)用習(xí)慣。
???使用Result的幾個(gè)問題
- 調(diào)用成本高:雖然通過對(duì)依賴的API深入了解異常設(shè)計(jì),可以寫出嚴(yán)謹(jǐn)?shù)拇a以避免出現(xiàn)bug,但是簡(jiǎn)單的邏輯,代碼卻變得復(fù)雜。換言之,調(diào)用的成本變高。但是很可惜,我們忘記判斷而寫成“一個(gè)簡(jiǎn)化的例子”這樣是往往常事。
- 無意義錯(cuò)誤碼:SYSTEM_ERROR、DB_ERROR等系統(tǒng)異常的錯(cuò)誤碼,雖然放在Result中了,但是調(diào)用方除了日志和監(jiān)控作用外,業(yè)務(wù)邏輯永遠(yuǎn)不會(huì)關(guān)心,也永遠(yuǎn)處理不了。而些錯(cuò)誤碼的處理分支,實(shí)際與拋異常的處理邏輯一樣。既然如此,為何要將這些錯(cuò)誤碼放在返回值里?
關(guān)于阿里巴巴開發(fā)規(guī)約
我們看《阿里巴巴Java開發(fā)手冊(cè)》的“異常處理”小節(jié)第13條:
【推薦】對(duì)于公司外的http/api開放接口必須使用“錯(cuò)誤碼”;跨應(yīng)用間HSF調(diào)用優(yōu)先考慮使用Result方式,封裝isSuccess()方法、“錯(cuò)誤碼”、“錯(cuò)誤簡(jiǎn)短信息”;而應(yīng)用內(nèi)部推薦異常拋出。
這條推薦非常具有誤導(dǎo)性,在2016年孤盡對(duì)于這條規(guī)范進(jìn)行調(diào)研時(shí)的帖子:《【開發(fā)規(guī)約熱議投票02】HSF服務(wù)接口定義時(shí),是Result isSuccess方式返回,還是是拋異常的方式?》有部分同學(xué)不建議使用Result,但大部分同學(xué)推薦了Result的做法。
???為什么說這條規(guī)約具有誤導(dǎo)性?
因?yàn)檫@個(gè)問題本身沒有講清楚“對(duì)什么東西的處理”要用Result還是異常的方式,即這里沒有講清楚我們要解決的問題是什么。事實(shí)上我們常說的“失敗”,往往混淆了2種含義:
- 系統(tǒng)異常:比如網(wǎng)絡(luò)超時(shí)、DB異常、緩存超時(shí)等,調(diào)用方一般不太可能基于這些錯(cuò)誤類型做不同的業(yè)務(wù)邏輯,常用用于日志和監(jiān)控,方便定位排查。
- 業(yè)務(wù)狀態(tài):比如業(yè)務(wù)規(guī)則攔截導(dǎo)致的失敗,比如發(fā)權(quán)益時(shí)庫存不足、用戶限領(lǐng)等,為方便后文敘述和理解,暫時(shí)稱為“業(yè)務(wù)失敗”。這類“失敗”,從機(jī)器層面來看,嚴(yán)格來說不能算做是失敗,這只是一種正常的業(yè)務(wù)結(jié)果,這和“調(diào)用成功”這個(gè)業(yè)務(wù)結(jié)果對(duì)系統(tǒng)來說沒有任何區(qū)別,只是一個(gè)業(yè)務(wù)狀態(tài)而已。調(diào)用方往往可能關(guān)心對(duì)應(yīng)的錯(cuò)誤碼,以完成不同的業(yè)務(wù)邏輯。
有經(jīng)驗(yàn)的開發(fā),都會(huì)意識(shí)到這2種含義的區(qū)別,這對(duì)于幫助我們理解接口的異常設(shè)計(jì)非常重要!對(duì)這條開發(fā)規(guī)約而言,如果是第2種,并沒有什么大的問題,但如果是第1種,我則持相反的意見,因?yàn)檫@違背了java語言的基本設(shè)計(jì),不符合java編碼直覺,會(huì)潛移默化造成前面案例所示的理解和使用成本的問題。
???為什么針對(duì)HSF?
當(dāng)我們討論要用Result代替Exception時(shí),經(jīng)常會(huì)以這是HSF接口為由,因?yàn)樾阅荛_銷等等。我們常說HSF這種RPC框架,設(shè)計(jì)的目的就是為了看起來像本地調(diào)用。那么,這個(gè)“看起來像本地調(diào)用”到底指的是哪方面像呢?顯然,編碼時(shí)像,運(yùn)行時(shí)不像。所以我們寫調(diào)用HSF接口的代碼時(shí),感覺像在調(diào)用本地方法,那么我們的編碼直覺和習(xí)慣也都應(yīng)該是符合java的規(guī)范的。因此,至少有幾點(diǎn)理由,對(duì)于系統(tǒng)異常,我們的HSF接口更應(yīng)該使用Exception,而非Result的方式:
- 只有同樣遵循本地方法調(diào)用的設(shè)計(jì),來設(shè)計(jì)HSF的api,才能更好做到“像本地調(diào)用一樣”,更符合HSF設(shè)計(jì)的初衷。
- HSF接口是往往用于對(duì)外部團(tuán)隊(duì)提供服務(wù),更應(yīng)該遵循java語法的設(shè)計(jì),提供清晰的接口語義,降低調(diào)用方的使用成本,減少出bug的概率。
- Result并無統(tǒng)一規(guī)范,而Exception則是語言標(biāo)準(zhǔn),有利于中間件、框架代碼的監(jiān)控發(fā)現(xiàn)和異常重試等邏輯生效。
當(dāng)然,由于“運(yùn)行時(shí)不像”,對(duì)于HSF封裝帶來的抽象泄露,我們?cè)谑褂卯惓r(shí),需要關(guān)注幾點(diǎn)問題:
- 異常要在接口顯式聲明,否則客戶端可能會(huì)反序列化失敗。
- 盡可能不帶原始堆棧,否則客戶端也可能反序列化失敗,或者堆棧過大導(dǎo)致性能問題??梢钥紤]異常中定義錯(cuò)誤碼以方便定位問題。
???結(jié)論
無論是HSF接口,還是內(nèi)部的API,都應(yīng)該遵循java語言的編碼直覺和習(xí)慣,業(yè)務(wù)結(jié)果(無論成功還是失?。┒紤?yīng)該通過返回值返回,而系統(tǒng)異常,則應(yīng)該使用拋出Exception的方式來實(shí)現(xiàn)。
關(guān)于Checked?Exception
講到這里,我們發(fā)現(xiàn),java的Checked Exception的設(shè)計(jì),作用上和反映業(yè)務(wù)失敗的Result很像。Result是強(qiáng)制調(diào)用方進(jìn)行判斷和識(shí)別,并根據(jù)不同的錯(cuò)誤碼進(jìn)行判斷和處理。而Checked Exception也是強(qiáng)制調(diào)用方進(jìn)行處理,并且可能要對(duì)不同的異常做不同的處理。但是,基于前面的結(jié)論,業(yè)務(wù)失敗應(yīng)該通過返回值來表達(dá),而不是異常;而異常是不應(yīng)該用于做業(yè)務(wù)邏輯判斷的,那么java的Checked Exception就變成奇怪的存在了。這里我明確我的觀點(diǎn),我們應(yīng)該盡可能不使用Checked?Exception。另外,《Thinking in Java》的作者 Bruce Eckel就曾經(jīng)公開表示,Java語言中的Checked Exception是一個(gè)錯(cuò)誤的決定,Java應(yīng)該移除它。C#之父Anders Hejlsberg也認(rèn)同這個(gè)觀點(diǎn),因此C#中是沒有Checked Exception的。
Reselt 的實(shí)質(zhì)是什么?
我們看看一個(gè)java方法的簽名(省略修飾符部分):
- 方法名:用于表達(dá)這個(gè)方法的功能
- 參數(shù):方法的輸入
- 返回值類型:方法的輸出
- 異常:方法中意外出現(xiàn)的錯(cuò)誤
所以,返回值和方法功能必須是配套的,返回值類型,就是這個(gè)方法的功能執(zhí)行結(jié)果的準(zhǔn)確表達(dá),即返回值必須正好就是當(dāng)前這個(gè)方法要做的事情的結(jié)果,必須滿足這個(gè)方法語義,而不應(yīng)該有超出這個(gè)語義外的東西存在。而異常,所說的“意外”,則是指超出這個(gè)方法語義之外的部分。這幾句話有點(diǎn)拗口,舉個(gè)例子來說,上面這個(gè)用戶接口,語義就是要通過用戶id查詢用戶,那么當(dāng)服務(wù)端發(fā)生DB超時(shí)錯(cuò)誤時(shí),對(duì)于“通過用戶id查詢用戶”這個(gè)語義來說,“DB超時(shí)錯(cuò)誤”沒有任何意義,使用異常是恰好合適的,如果我們把這個(gè)錯(cuò)誤做為錯(cuò)誤碼放在返回值的Result里,那么就是增加了這個(gè)方法的使用成本。
???Result的由來
到底為什么會(huì)有“Result”這樣的東西誕生呢?如果設(shè)計(jì)的方法返回值是Result類型,那么它必須能準(zhǔn)確反應(yīng)這個(gè)方法調(diào)用的結(jié)果。實(shí)際上,以上面的例子為例,這個(gè)時(shí)候的Result就是User類本身,User.status相當(dāng)于Result.code。這聽起來可能有點(diǎn)和直覺不符,這是為什么?
public class UserRegisterResult {
private String errorCode;
private String errorMsg;
private Long userId;
// ...
public boolean isSuccess() {
return errorCode == null;
}
// ...
}
UserRegisterResult registerUser(User user) throws DemoAppException;
我們?cè)賮砜纯瓷厦孢@個(gè)“注冊(cè)用戶”的方法聲明,會(huì)發(fā)現(xiàn),這個(gè)方法定義一個(gè)Result顯得很合適。這是因?yàn)榍耙粋€(gè)例子,我們的方法是一個(gè)查詢方法,返回值剛好可以用領(lǐng)域?qū)ο箢愋捅旧恚@個(gè)“注冊(cè)用戶”的方法,顯然沒有現(xiàn)成合適的類型可以使用,所以就需要定義一個(gè)新的類型來表達(dá)方法的執(zhí)行結(jié)果??吹竭@里,我們會(huì)以為,對(duì)于“寫”與“讀”類型的方法有所差異,但實(shí)際上,對(duì)于java語言或者機(jī)器來說,并無二致,第二個(gè)方法UserRegisterResult的和第一個(gè)方法的User是同等地位。所以,最重要的還是一點(diǎn):需要有一個(gè)合適的類型,做為返回值,用于準(zhǔn)確表達(dá)方法執(zhí)行的功能結(jié)果。而偏“寫”類型,或者帶業(yè)務(wù)校驗(yàn)的讀接口,往往因?yàn)闆]有現(xiàn)成的類型可用,為了方便,常常會(huì)使用Result來代替。
???是否有必要統(tǒng)一Result?
講到這里,想想,當(dāng)我們這種“需要Result”的方法有多個(gè)時(shí),我們會(huì)說“我需要一個(gè)統(tǒng)一的Result類”時(shí),實(shí)際上說的什么呢?
- 我希望各種接口方法都統(tǒng)一同樣的Result,方便使用
- 我希望有個(gè)類復(fù)用errorCode、errorMsg以及相關(guān)的getter/setter等代碼
顯然,第1點(diǎn)理由經(jīng)不起推敲,為何“統(tǒng)一就方便使用”了?如果各種方法返回類型都一樣,那就違背了“返回值要和方法功能配套”的結(jié)論,也不符合高內(nèi)聚的設(shè)計(jì)原則。恰相反,返回值越是設(shè)計(jì)得專用,對(duì)調(diào)用方來說理解和使用成本越低。所以,我們實(shí)際想要的,僅僅是如何“偷懶”,也就是第2點(diǎn)理由。所以我們真正要做的是,只是在當(dāng)前領(lǐng)域范圍內(nèi),如何既滿足讓每個(gè)方法返回值專用以便使用,同時(shí)又可以偷懶復(fù)用部分代碼即可。因此,絕不必要求大家都統(tǒng)一使用同一個(gè)Result類型。接口返回設(shè)計(jì)建議
根據(jù)前文的結(jié)論,我們知道,對(duì)于接口方法的返回值和異常處理,最重要的是需要遵循方法的語義進(jìn)行設(shè)計(jì)。以下是我梳理的一些設(shè)計(jì)上的原則和建議。
???對(duì)響應(yīng)合理分類
接口響應(yīng)按有業(yè)務(wù)結(jié)果和未知業(yè)務(wù)結(jié)果分類,業(yè)務(wù)結(jié)果不管是業(yè)務(wù)成功還是業(yè)務(wù)規(guī)則導(dǎo)致的失敗,都通過返回值返回;未知結(jié)果一般是系統(tǒng)性的異常導(dǎo)致,不要通過返回值錯(cuò)誤碼表達(dá),而是通過拋出異常來表達(dá)。這里最關(guān)鍵一點(diǎn),就是如何理解和區(qū)分某個(gè)“失敗”是屬于業(yè)務(wù)失敗,還是屬于系統(tǒng)異常。由于有時(shí)候這個(gè)區(qū)分并不是很容易,我們可以有一個(gè)比較簡(jiǎn)單的判斷標(biāo)準(zhǔn)來確定:
- 如果一個(gè)錯(cuò)誤,調(diào)用方只能通過人工介入的方式才能恢復(fù),比如修改代碼、改配置,或數(shù)據(jù)訂正等處理,則必然屬于異常
- 如果調(diào)用方無法使用代碼邏輯處理消化使得自動(dòng)恢復(fù),而是只能通過重試的方式,依賴下游的恢復(fù)才能恢復(fù),則屬于異常
???找到合適的場(chǎng)景
普通查詢接口,如無必要,不要使用Result包裝返回值??梢院?jiǎn)單分為3類做為參考:
- 普通讀接口
查詢結(jié)果即是領(lǐng)域?qū)ο?,無其他業(yè)務(wù)規(guī)則導(dǎo)致的失?。航ㄗh直接用領(lǐng)域?qū)ο箢愋妥鰹榉祷刂?。如?/span>
User getUserById(Long userId) throws DemoAppException;
- 寫接口
或者帶業(yè)務(wù)規(guī)則的讀接口:
- 理想情況是專門封裝一個(gè)返回值類,以降低調(diào)用方的使用成本。
- 可考慮將返回值類繼承Result,以復(fù)用errorCode和errorMsg等代碼,減輕開發(fā)工作量。但注意這不是必要的。
- 將本方法的錯(cuò)誤碼,直接定義到這個(gè)返回值類上(高內(nèi)聚原則)。
- 若有多個(gè)方法有共同的錯(cuò)誤碼,可以考慮通過將這部分錯(cuò)誤碼定義到一個(gè)Interface中,然后實(shí)現(xiàn)該接口。
// UserRegisterResult、UserUpdateResult可以繼承Result類,減少工作量,但調(diào)用方不需要感知Result類的存在
UserRegisterResult registerUser(User user) throws DemoAppException;
UserUpdateResult updateUser(User user) throws DemoAppException;
- 帶業(yè)務(wù)規(guī)則的的領(lǐng)域?qū)ο笞x接口
完全遵循上面第2點(diǎn),會(huì)給方法提供者帶來一定的開發(fā)成本,權(quán)衡情況下可以考慮,套R(shí)esult包裝領(lǐng)域?qū)ο笞鰹榉祷刂?。注意,?duì)外不建議,可考慮用于內(nèi)部方法。如下接口,“沒有權(quán)限”是一個(gè)正常的業(yè)務(wù)失敗,調(diào)用方可能會(huì)判斷并做一定的業(yè)務(wù)邏輯處理:
// 查詢有效用戶,如果用戶存在但狀態(tài)非有效狀態(tài)則返回“用戶狀態(tài)錯(cuò)誤”的錯(cuò)誤碼,如果不存在則返回null
Result getEffectiveUserWithStatusCheck(Long userId) throws DemoAppException ;
???內(nèi)外部區(qū)分
對(duì)外接口,尤其是HSF,由于變更成本高,更要遵循前面的原則;內(nèi)部方法,方法眾多,如果完全遵循需要編碼成本,這里需要做權(quán)衡,根據(jù)代碼規(guī)模和發(fā)展階段不斷重構(gòu)和調(diào)整即可。
???避免直接包裝原生類型
我們對(duì)外的接口,返回值要避免出現(xiàn)直接使用Result包裝一個(gè)原生類型。比如:
Result registerUser(User user) throws DemoAppException ;
這樣設(shè)計(jì)導(dǎo)致的結(jié)果是,擴(kuò)展性很差。如果registerUser方法需要增加返回除了userId以外的其他字段時(shí),就面臨幾個(gè)選擇:
- 讓Result支持?jǐn)U展參數(shù),通過map來傳遞額外字段:可讀性和使用成本很高
- 開發(fā)一個(gè)新的registerUser方法:顯然,成本很高
???避免所有錯(cuò)誤碼定義在一個(gè)類中
有人建議,做一個(gè)全局的錯(cuò)誤碼定義,以做統(tǒng)一,方便排查和定位。但這樣做真的方便嗎?這樣做實(shí)際上有幾個(gè)問題:
- 完全違背了高內(nèi)聚、低耦合的設(shè)計(jì)原則。這個(gè)“統(tǒng)一的定義”將與各個(gè)域都有耦合,同時(shí)對(duì)于某單個(gè)接口而言,則不夠內(nèi)聚。
- 這個(gè)統(tǒng)一定義的錯(cuò)誤碼,一定會(huì)爆炸式增長(zhǎng),即便我們對(duì)其進(jìn)行分類(非常依賴人的經(jīng)驗(yàn)),遲早也會(huì)變得難以維護(hù)和理解。
- 不要將系統(tǒng)異常類的錯(cuò)誤碼和業(yè)務(wù)失敗錯(cuò)誤碼放在一起,這點(diǎn)其實(shí)和方法響應(yīng)分類設(shè)計(jì)是一回事。
我們?cè)谠O(shè)計(jì)拉菲2權(quán)益平臺(tái)的錯(cuò)誤碼時(shí),就犯了這樣的錯(cuò)誤。現(xiàn)在這個(gè)“統(tǒng)一的”錯(cuò)誤碼已經(jīng)超過400個(gè),揉合了管理域、投發(fā)放域、離線域等各種不同域的業(yè)務(wù)失敗、系統(tǒng)異常的錯(cuò)誤碼,不要說調(diào)用方,即便我們自己,也梳理不清楚了。而實(shí)際上,每個(gè)域、每個(gè)方法自己的業(yè)務(wù)失敗是非常有限的,它的增長(zhǎng)一定是隨著業(yè)務(wù)需求本身的變化而增長(zhǎng)的?,F(xiàn)在如果有個(gè)業(yè)務(wù)方來問我,拉菲2的發(fā)放接口,有哪些錯(cuò)誤碼(這問的實(shí)際是業(yè)務(wù)失敗,他也只關(guān)心業(yè)務(wù)失?。?guī)缀蹼y以回答。很可惜,這塊目前即便重構(gòu),難度也很大。
???異常處理機(jī)制
- 異常錯(cuò)誤碼
前面我們講到,即便是拋異常的形式,我們也可以為我們的異常類設(shè)計(jì)錯(cuò)誤碼,異常錯(cuò)誤碼的增加會(huì)很快,往往也和當(dāng)前業(yè)務(wù)語義無關(guān),因此千萬不要和業(yè)務(wù)失敗的錯(cuò)誤碼定義在一起。異常內(nèi)的錯(cuò)誤碼主要用于日志、監(jiān)控等,核心原則就是,要方便定位問題。
- 避免層層try?catch
- 在原始發(fā)生錯(cuò)誤的地方try?catch,比如調(diào)用HSF接口的Facade層代碼,主要目的是為了記錄原始的錯(cuò)誤以及出入?yún)?,方便定位問題,一般會(huì)打日志,并轉(zhuǎn)換成本應(yīng)用的異常類上拋
- 在應(yīng)用的最頂層catch異常,打印統(tǒng)一日志,并根據(jù)“為什么針對(duì)HSF?”小節(jié)中的建議,處理成合適的異常后再拋出。對(duì)于HSF接口,可以直接實(shí)現(xiàn)HSF的“ServerFilter”來統(tǒng)一在框架層面處理。
- 中間層的代碼,不必再層層catch,比如domain層,可以讓代碼邏輯更加清晰。
- 參數(shù)錯(cuò)誤
拋異常的場(chǎng)景,除了前面說的系統(tǒng)性異常外,參數(shù)錯(cuò)誤也推薦使用異常。原因如下:
- 參數(shù)正確一般是我們當(dāng)前上下文執(zhí)行的前提條件,我們一般可以使用assert來保證其正確。即我們的后續(xù)邏輯是認(rèn)為,當(dāng)前的參數(shù)是不可能錯(cuò)誤的,我們沒必要為此寫過多繁瑣的防御性代碼。
- 一旦發(fā)生參數(shù)錯(cuò)誤,則一定是調(diào)用方有代碼bug,或者配置bug,應(yīng)該通過拋出異常的方式,充分提前在開發(fā)或測(cè)試階段暴露。
- 參數(shù)錯(cuò)誤對(duì)調(diào)用方來說,是無法處理的,程序不可能自動(dòng)恢復(fù),一定是會(huì)需要人工介入才可能恢復(fù),調(diào)用方不可能會(huì)“判斷如果是xx參數(shù)錯(cuò)誤,我就做某個(gè)業(yè)務(wù)邏輯”這樣的代碼,因此通過返回值定義參數(shù)錯(cuò)誤碼沒有意義。
- 系統(tǒng)異常和業(yè)務(wù)結(jié)果轉(zhuǎn)換
系統(tǒng)性異常并非一定是異常,因?yàn)橛行涌赡苡心芰μ幚砟承┊惓?,比如?duì)于弱依賴的接口,異常是可以吞掉,轉(zhuǎn)換成一個(gè)業(yè)務(wù)結(jié)果;相反,有些接口返回的一些業(yè)務(wù)失敗,但調(diào)用方認(rèn)為該業(yè)務(wù)失敗不可能出現(xiàn),出現(xiàn)也無法處理,那么這一層可以將其轉(zhuǎn)換成異常。
結(jié)尾
前面講了接口的響應(yīng),包括返回值Result和異常拋出的設(shè)計(jì),有很多結(jié)論是與現(xiàn)在公司內(nèi)部大家常見做法是不同的,這也是我為什么特別想要表達(dá)的,有可能正是日常我們的這些習(xí)以為常做法,才導(dǎo)致了團(tuán)隊(duì)間接口依賴調(diào)用的成本提高,也是導(dǎo)致故障的一個(gè)很重要原因。當(dāng)然,我相信,我的觀點(diǎn)也不一定都是對(duì)的,很多同學(xué)并不一定同意上面所有的結(jié)論,所以,歡迎大家在文章下面討論!