防禦式編程

防禦式編程:這個概念其實來源於駕駛員,簡單來說,當你開車上路的時候要時刻保持警覺,假設路上你遇到的每一輛車都有可能向你撞過來造成危險。在coding裏面,要假設每個輸入都不一定符合設計之初的假設,要使用一定的語句對輸入進行限定。儘量做到”垃圾進,什麼都不出“。
使用防禦式編程常見的語句有:斷言和錯誤處理語句

斷言:
assert(condition)功能語句是一種可以保證其condition爲真的語句,是一種在調試階段可以發現問題的好工具.其用於保證內部子程序的某些變量必須爲真的情況.

在實際的工作中,斷言使用的情況並不少。使用斷言的一個好處就是可是在開發的過程中儘早地暴露軟件的缺陷。實際的開發中有一些看起來莫名其妙的錯誤是很難被複現的,而這些錯誤很有可能是因爲軟件中的某個部分並不在預期的範圍內:

//這是一個很簡單的按照工齡計算工資的函數
//@param age 員工的工齡

double cal_salary(int age){
    double salary;

    //拼命計算工資,而且工資與工齡掛鉤... 

    return 0.5*age*salary;
}

假設老闆某總一天突發奇想,用這個函數(這個函數還真奇葩的)來計算員工的工資,突然碼農A跑過來說爲什麼我的工資是負的?我辛辛苦苦幹了這麼久沒功勞也有苦勞啊,爲什麼,爲什麼。。。
仔細看一下這個計算工資的函數,其輸入工齡在現實世界裏面肯定是一個正數,但在這個函數裏面卻沒有體現,一旦輸入錯誤則導致輸出一個負的工資,這顯然與現實世界不相符。所以這個函數在開發的時候應該是這樣的:

double cal_salary(int age){
    assert(age>0);
    double salary;

    //拼命計算工資,而且工資與工齡掛鉤... 

    return 0.5*age*salary;
}

又譬如在面試中很有可能遇到的字符串拷貝函數問題,下面是一個常見的版本:

void strcpy(char *dst, const char *src){
    assert(dst && src);
    while( (*dst++ = *src++) != '\0')
}

注意:斷言只能在調試模式下有效,在正式版的release下斷言功能無效,故不要把函數調用語句放在斷言內,而應該將某一個值放在斷言內檢查

除了斷言,另一種防禦式編程常見的手段是錯誤處理語句,這在代碼裏面更加常見

  斷言與錯誤處理語句:
  錯誤處理語句用於處理已經預測得到的,可以預知的情況,而斷言則處理絕對不應該發生的情況.錯誤處理語句用於檢查有害的輸入數據,而斷言則用於檢查代碼中的BUG.前者處理反常情況時程序員可以從容應對,而後者出現問題時(程序觸發斷言時)則需要好好修改程序並重新編譯了

對於大型,複雜,健壯性要求高的程序.應先使用斷言再使用錯誤處理語句.因爲大型程序的生命週期長,在放出一個release版本之後也會繼續更新,故應使用斷言.

其實在應用軟件中,錯誤處理語句用的比斷言會更爲廣泛,在今天的互聯網時代,用戶體驗非常重要(其實一直都是很重要,只不過現在有了互聯網換一個同類型軟件的成本比以前大爲下降,所以用戶體驗在提高用戶忠誠這方面很有作用),我們總不能希望用戶在使用軟件的時候老出現崩潰需要重啓軟件吧?所以一旦軟件的某個部分一旦出現小錯誤,可以用錯誤處理語句把錯誤控制在一個較小的範圍內,就像現代輪船上面的隔離門一樣:

現代輪船有水密艙,當輪船某個地方進水的時候可以將進水的區域隔離在一定範圍內

在實際的敲代碼過程中,我們可以利用錯誤處理語句這麼來coding.還是利用上面的奇葩工資計算函數:

//還是這個奇葩的工資計算函數
double cal_salary(int age){
    double salary;

    //錯誤處理代碼
    if(age<0)
        return 0;
    if(age==0)
        return basic_salary;

    //拼命計算工資,而且工資與工齡掛鉤... 

    return 0.5*age*salary;
}

這樣一來員工再也不會找老闆的麻煩啦!

健壯性與正確性: 健壯性:不斷嘗試一些措施,使得軟件無論如何都能運行下去,即使有一些不正確的結果.常見於消費類軟件. 正確性:永遠不返回不正確的結果,哪怕不返回結果也比返回錯誤結果好. 常見於人命攸關的軟件,銀行軟件等

關於錯誤處理:在一整個程序的開發中,一旦確定了某種錯誤處理方法,就應該一如既往的在整個軟件開發過程中使用此錯誤處理方法,如一旦確定由上層應用處理錯誤,則底層只需要報告錯誤類型以及給出錯誤信息即可,並且保證在整個軟件開發過程中均使用此錯誤處理方法.

其實不論是錯誤處理語句,還是斷言,其本身就是一種代碼編寫的習慣與風格,在實際項目中最好開始就討論出來對於錯誤處理的原則,這樣相對比較方便交流

關於異常:慎重的使用異常可以減低軟件複雜度,這正是編程的主要要旨之一.異常是用來應付那些在其他編程方法都無法處理的情況.

注意:避免在構造函數和析構函數中使用異常.在構造函數中拋出異常則不會調用析構函數.而在析構函數中調用異常則可能會造成資源泄露

拋出異常的時候主要要在抽象層次一致的情況下拋出異常,例如:某讀入用戶數據的程序調用一接口,實現接口的代碼需要打開磁盤上的文件,此時接口不適宜返回EOF文件異常,而應該將此異常映射到自定義異常,如無法讀取用戶數據之類的異常.這樣子可以保證管理的協調性,同時,拋出異常需要給出異常出現的詳細信息,例如,拋出一個數組上下界問題的異常,則需要在異常中說明數組的上界,下界,以及出現異常時試圖訪問數組中的哪個地方.用try…catch…語句時,若遇到無法處理的異常,至少要記錄一下LogError()

其實在C語言裏面也可以利用setjmp和longjmp兩個函數做自己的異常處理機制,在工作中我遇到過這樣的代碼,日後有機會貼出思路.

隔離程序: 必要時,可以新建一系列接口,專門用於檢查數據是否合法.接口一端的地方數據被視爲是骯髒的.而另一端的數據被視爲安全的可以隨意調用的.

隔離程序在形式上更加像是辦公樓裏面的防火門:

//檢驗數據是否有效,若數據爲非負則有效,否則返回-1
int verify_data(int data){
    if(data<0){
        return -1;
    }else { 
        return data;
    }
}

關於這幾種技術的總結: 隔離層外部的地方(數據可能是骯髒的)應使用錯誤處理技術,而隔離層裏面的地方應該使用斷言,若斷言檢測出有錯誤的數據,則可以肯定是隔離層內部的子程序出了問題而不是外部程序出了問題.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章