異常處理是我們?nèi)粘i_發(fā)中關(guān)注比較少的一塊筋讨,雖然很多時候并不起眼谆棺,但是如果處理不當(dāng)叹螟,很容易使精心設(shè)計(jì)的程序變得不堪一擊魄宏。通過學(xué)習(xí)軟件強(qiáng)健度等級劃分的概念及常用的異常處理方法秸侣,能讓我們可以根據(jù)用戶不同的需求實(shí)現(xiàn)不同程度的異常處理,使得系統(tǒng)結(jié)構(gòu)更清晰宠互,代碼更加簡潔味榛,軟件功能更加健壯。
寫在前面
這篇文章探討的主題是異常處理中的等級劃分及異常處理的重構(gòu)方法予跌,具體參考了陳建村所著的《笑談軟件工程:異常處理的設(shè)計(jì)與重構(gòu)》搏色,同時也借鑒了《clean code》對異常處理的部分內(nèi)容。我對書中體會較深的部分列舉出來匕得,算是讀書筆記與體會继榆。
為什么要談?wù)劗惓L幚?/h2>
作為一名軟件開發(fā)人員,從小到大你可能學(xué)過各式各樣的軟件設(shè)計(jì)與方法汁掠,從最基礎(chǔ)的程序語言略吨、數(shù)據(jù)結(jié)構(gòu)與算法,到面向?qū)ο蠓治雠c設(shè)計(jì)考阱、設(shè)計(jì)模式翠忠、軟件架構(gòu)以及各種敏捷開發(fā)實(shí)踐,包含自動化測試乞榨、測試驅(qū)動開發(fā)秽之、持續(xù)集成和敏捷設(shè)計(jì)原則等当娱。以上,所有的“大師”費(fèi)盡心力考榨,都在告訴你“如何設(shè)計(jì)軟件的光明面”跨细。
但是,世界是對立的河质,有光明就有黑暗冀惭,黑暗面就是“異常行為”(abnormal behavior),軟件設(shè)計(jì)忽略任何一方掀鹅,都可能讓原本精心規(guī)劃的設(shè)計(jì)變得不堪一擊散休。
強(qiáng)健度等級與異常處理策略
這里定義了軟件的強(qiáng)健度等級,總共分為四個等級乐尊。
等級0:未定義
未定義(undefined)表示當(dāng)某個服務(wù)發(fā)生錯誤的時候戚丸,可能會讓調(diào)用者指導(dǎo)錯誤發(fā)生,也有可能會假裝沒事扔嵌。也就是說限府,使用該服務(wù)的人無法確切知道它是否成功達(dá)成任務(wù)。當(dāng)錯誤發(fā)生的時候对人,服務(wù)處于不明或是錯誤狀態(tài)谣殊。異常發(fā)生時系統(tǒng)可能會終止,也可能繼續(xù)執(zhí)行牺弄。
等級1:錯誤報(bào)告
錯誤報(bào)告(error-reorting)代表當(dāng)某個服務(wù)發(fā)生錯誤的時候姻几,一定要讓調(diào)用者知道,絕對不能假裝沒事势告。要達(dá)到錯誤報(bào)告強(qiáng)健度等級很簡單蛇捌,就是把所有的異常都往外丟,然后在主程序(整個系統(tǒng)最外層的那個程序)捕捉所有的異常并報(bào)告給使用者或開發(fā)人員知道咱台。錯誤報(bào)告又稱為“早死早投胎”络拌。
等級2:狀態(tài)恢復(fù)
狀態(tài)恢復(fù)除了要求等級1錯誤報(bào)告以外,還要求當(dāng)錯誤發(fā)生之后回溺,服務(wù)必須保證系統(tǒng)仍然處于正確狀態(tài)春贸。由于整個系統(tǒng)的狀態(tài)還是正確的,因此遗遵,當(dāng)異常發(fā)生之后萍恕,系統(tǒng)仍可以執(zhí)行。
要達(dá)到這個等級车要,必須要多做兩件事情允粤。首先是錯誤處理(error handling),讓系統(tǒng)恢復(fù)到一個正確的狀態(tài)。假設(shè)有一個服務(wù)修改了數(shù)據(jù)庫內(nèi)容类垫,當(dāng)異常發(fā)生時就要執(zhí)行回滾(roolback)動作司光。其次是釋放資源(cleanup)。例如把要來的內(nèi)存悉患、文件残家、數(shù)據(jù)庫聯(lián)機(jī)等資源釋放。狀態(tài)恢復(fù)又稱為弱容錯(weakly tolerant)购撼。
等級3:行為恢復(fù)
這個等級核心就是服務(wù)的使命必達(dá)跪削。因此,當(dāng)某個服務(wù)執(zhí)行失敗的時候迂求,便要想辦法排除困難,總之就是要達(dá)成原本被賦予的任務(wù)晃跺。當(dāng)發(fā)生錯誤的時候揩局,除了達(dá)成等級2的狀態(tài)恢復(fù)之外,還需要“想其他方法來達(dá)成原本的任務(wù)”掀虎,如重試與設(shè)計(jì)多樣性凌盯、功能多樣性、數(shù)據(jù)多樣性烹玉、時序多樣性等設(shè)計(jì)技巧驰怎,嘗試?yán)^續(xù)提供服務(wù)。
強(qiáng)健度等級的推廣
- 首先二打,在團(tuán)隊(duì)中對團(tuán)隊(duì)成員倡導(dǎo)強(qiáng)健度等級觀念县忌。沒錯,只需要花一兩個小時继效,因?yàn)橛^念本身很簡單症杏,不需要長時間訓(xùn)練便可了解。
- 在沒有特殊情況下瑞信,默認(rèn)所有函數(shù)一定達(dá)到強(qiáng)健度等級1厉颤。乍看起來,等級1好像“很不負(fù)責(zé)任”把所有異常都往外丟凡简,但是如此一來逼友,反而可在開發(fā)階段及時發(fā)現(xiàn)問題并加以修復(fù)。將問題暴露之后秤涩,便有足夠的情境可以決定該異常的處理方式帜乞,是否將等級1升級到更高的等級。
- 對于特定函數(shù)而言(如數(shù)據(jù)庫處理)溉仑,因?yàn)閿?shù)據(jù)庫錯誤會給使用者造成很大的困擾挖函,加上數(shù)據(jù)庫本身有事務(wù)功能,因此默認(rèn)應(yīng)該達(dá)到強(qiáng)健度2等級。
- 除非客戶特別要求或是不達(dá)到等級3會使系統(tǒng)變得很難用(例如網(wǎng)絡(luò)數(shù)據(jù)傳遞怨喘,如果不能自動保證數(shù)據(jù)可以完整無誤地傳遞到遠(yuǎn)程津畸,系統(tǒng)將變得很難用),否則必怜,不會特別要求函數(shù)做到行為恢復(fù)肉拓。
異常處理壞味道與重構(gòu)方案
- 用異常代替錯誤碼
// 壞味道
public synchronized int withdraw(int amount){
if(amount > this.balance){
return -1;
} else {
this.balance = this.balance - amount;
return this.balance;
}
}
// 重構(gòu)后代碼
public synchronized int withdraw(int amount) throws NotEnoughMoneyException {
if(amount > this.balance){
throw new NotEnoughMoneyException();
}
this.balance = this.balance - amount;
return this.balance;
}
動機(jī):以傳回值來代表錯誤狀況,會讓軟件組件連強(qiáng)健度等級1(錯誤報(bào)告)的標(biāo)準(zhǔn)都達(dá)不到梳庆,因?yàn)檎{(diào)用者通常傾向忽略傳回值得檢查暖途,所以也忽略了錯誤狀況。因此膏执,若采用異常來取代錯誤碼驻售,可以使軟件達(dá)到強(qiáng)健度1。
- 以未查異常代替忽略已查異常/空的處理程序
// 壞味道
catch(IOException e){
/* ignoring the exception */
e.printStackTrace();
}
// 重構(gòu)后代碼
catch(IOException e){
throw new UnhandledException(e);
}
動機(jī):常用的IDE如eclipse對于Checked異常的處理就是選擇忽略:
catch(IOException e) {
/* TODO */
e.printStackTrace();
};
雖然寫了TODO注釋來提醒自己要“抽空”回來再續(xù)這段孽緣更米,但是注釋本身沒有什么約束力欺栗,絕大多數(shù)的時候會選擇遺忘,連強(qiáng)健度1的要求都達(dá)不到征峦。因此迟几,與其捕捉這一類已查異常并忽略它,不如在捕獲后轉(zhuǎn)拋一個未查異常栏笆。這時候类腮,你就可以專心處理正常邏輯,而暫時不被這一類已查異常干擾蛉加。
- 使用最外層try語句避免意外終止
// 壞味道
static public void main(String[] args){
/*
* 做一大堆事情的主程序
*/
}
// 重構(gòu)后代碼
try{
/*
* 做一大堆事情的主程序
*/
} catch(Throwable e){
logger.log(e);
dialog.show(Dialog.CRITICAL, e.getMessage());
}
動機(jī):按照強(qiáng)健度1要求蚜枢,所有異常都往外丟,未被捕捉的異常最終都會傳遞到主程序(或線程)上七婴。要是主程序也沒有捕捉這些異常祟偷,整個應(yīng)用程序就會被迫終止,這就是大家非常熟悉的老朋友“程序當(dāng)?shù)簟薄?br> 因此打厘,為了避免應(yīng)用程序不預(yù)期地終止修肠,將最外層程序代碼以一個try語句包住,捕捉所有異常類户盯,在頁面上顯示清楚且容易理解的錯誤信息嵌施,并視需要記錄詳細(xì)出錯信息到日志文件中,最后結(jié)束應(yīng)用程序的執(zhí)行莽鸭,讓它“死得好看一點(diǎn)”吗伤。
- 以函數(shù)取代嵌套的try語句
// 壞味道
finally{
try{
if(in != null) in.close();
} catch(IOException e){
// log the exception
}
}
// 重構(gòu)后代碼
finally {
cleanup(in);
}
動機(jī):嵌套的循環(huán)使得代碼結(jié)構(gòu)復(fù)雜,難于理解與維護(hù)硫眨。
- 引入Checkpoint類
// 原代碼
public void moveFiles(String srcFolder, String destFolder) throws IOException {
try {
// 復(fù)制srcFolder 所有文件到 destFolder
// 可能發(fā)生 IOException
} finally {
// 釋放資源
}
}
// 重構(gòu)后代碼
public void moveFiles(String srcFolder, String destFolder) throws IOException {
FolderCheckpoint fscp = null;
try {
fscp = new FolderCheckpoint();
fscp.establish(srcFolder);
// 復(fù)制 srcFolder 所有檔案到 destFolder足淆,
// 可能會發(fā)生 IOException
} catch(Exception e) {
fscp.restore();
throw e;
} finally {
fscp.drop();
// 釋放資源
}
}
動機(jī):為了達(dá)到強(qiáng)健度2的要求,在程序發(fā)生異常的時候能使系統(tǒng)狀態(tài)恢復(fù)到正常的狀態(tài)并持續(xù)運(yùn)作下去,可使用checkpoint方法巧号。checkpoint就是“快照”(snapshot)的觀念族奢,一般有三個基本函數(shù),分別是產(chǎn)生檢查點(diǎn)establish丹鸿,恢復(fù)數(shù)據(jù)的restore以及丟棄檢查點(diǎn)的drop越走。
- 引入多才多藝的try塊
// 壞味道
public User readUser(String name) throws ReadUserException {
try {
} catch(Exception e){
try {
return readFromLDAP(name);
} catch(IOException ex) {
throw new ReadUserException(ex);
}
}
}
// 重構(gòu)后代碼
public User readUser(String name) throws ReadUserException {
final int maxAttempt = 3;
int attempt = 1;
while(true) {
try {
if(attempt <= 2) return readFromDatabase(name);
else return readFromLDAP(name);
} catch(Exception e) {
if(++attempt > maxAttempt)
throw new ReadUserExcetption(e);
}
}
}
動機(jī):為了達(dá)到強(qiáng)健度3的要求,重試也是一種很常見也很有用的異常處理策略靠欢。但很多人將catch塊當(dāng)成try塊的“備胎”廊敌,等同于在catch塊中執(zhí)行重試。這種做法的主要缺點(diǎn)在于“只能重試一次”门怪,如果要多次重試骡澈,勢必會寫出具備嵌套try語句(Nested Try Statement)的程序,者又是另外一個異常處理壞味道掷空。
因此秧廉,重新思考try塊與catch塊的責(zé)任分工,讓try塊負(fù)責(zé)實(shí)現(xiàn)程序正常邏輯(包含主要方案與替代方案)拣帽,將“備胎(替代方案)”從catch塊移到try塊,讓catch塊負(fù)責(zé)控制重試終止條件與錯誤報(bào)告嚼锄。
總結(jié)
市面上關(guān)于異常處理的書籍不多减拭,《異常處理的設(shè)計(jì)與重構(gòu)》算是鳳毛麟角中的一本。除了上面講到的強(qiáng)健度等級劃分和異常處理重構(gòu)区丑,書中還講了異常處理的基本觀念及java的異常處理機(jī)制等內(nèi)容拧粪。這里我對書中最實(shí)用的章節(jié)進(jìn)行了整理與歸納,同學(xué)可根據(jù)興趣閱讀其他章節(jié)沧侥,會對上述異常重構(gòu)的方法有更加深刻的認(rèn)識可霎。