防御式編程(Defensive Programming)

寫(xiě)在前面

新的一年開(kāi)始了,大家都立下了什么新年flag呢?好久沒(méi)有更新簡(jiǎn)書(shū)了,最近在看《代碼大全》對(duì)于第八章的防御式編程頗有感慨,正好最近又是在準(zhǔn)備公司的技術(shù)分享,索性用md寫(xiě)了篇博文,總結(jié)了一下又增加了一些我個(gè)人的理解,在這里跟各位分享一下。

Defensive Programming

防御式編程(Defensive Programming)是提高軟件質(zhì)量技術(shù)的有益輔助手段

怎么理解呢?防御式編程思想的理解可以參考防御式駕駛:

在防御式駕駛中要建立這樣一種思維,那就是你水遠(yuǎn)也不能確定另一位司機(jī)將要做什么。這樣才能確保在其他人做出危險(xiǎn)動(dòng)作時(shí)你也不會(huì)受到傷害。你要承擔(dān)起保護(hù)自己的責(zé)任,哪怕是其他司機(jī)犯的錯(cuò)誤。

防御式編程的主要思想是:

子程序應(yīng)該不因傳入錯(cuò)誤數(shù)據(jù)而被破壞,哪怕是由其他子程序產(chǎn)生的錯(cuò)誤數(shù)據(jù)。

更一般地說(shuō),其核心想法是要承認(rèn)程序都會(huì)有問(wèn)題,都需要被修改,聰明的程序員應(yīng)該根據(jù)這一點(diǎn)來(lái)編程序,這種思想是將可能出現(xiàn)的錯(cuò)誤造成的影響控制在有限的范圍內(nèi)。

保護(hù)程序免遭非法輸入數(shù)據(jù)的破壞

計(jì)算機(jī)領(lǐng)域有著一句GIGO(Garbage In Garbage Out)俗語(yǔ),翻譯過(guò)來(lái)就是垃圾進(jìn),垃圾出,意思就是有垃圾數(shù)據(jù)進(jìn)來(lái)后,出來(lái)的也是垃圾數(shù)據(jù)。

而就目前而言,對(duì)于已經(jīng)成型的產(chǎn)品可能單單是這種原則并不適用,而是應(yīng)該做到垃圾進(jìn),什么也不出、垃圾進(jìn),出去的是錯(cuò)誤提示、垃圾進(jìn),經(jīng)過(guò)篩選提取,出去的是有用信息或是不許垃圾進(jìn)來(lái)。換句話(huà)說(shuō),GIGO于今天的標(biāo)準(zhǔn)看來(lái)已然是差勁程序的標(biāo)志了。

防御式編程針對(duì)垃圾進(jìn)這種情況,有以下三種方法處理:

1、檢查所有來(lái)源于外部的數(shù)據(jù)

當(dāng)從文件、用戶(hù)、網(wǎng)絡(luò)或其他外部接口中獲取數(shù)據(jù)時(shí),應(yīng)檢查所獲得的數(shù)據(jù)值,以確保它在允許的范圍內(nèi)。

2、檢查子程序所有輸入?yún)?shù)的值

檢查子程序輸入?yún)?shù)的值,事實(shí)上和檢查來(lái)源于外部的數(shù)據(jù)一樣,只不過(guò)數(shù)據(jù)來(lái)源于其他子程序而非外部接口。

3、決定如何處理錯(cuò)誤的輸入數(shù)據(jù)

一旦檢測(cè)到非法的參數(shù),你該如何處理它呢?根據(jù)情況的不同,你可以選擇適合你的錯(cuò)誤處理技術(shù)斷言來(lái)處理。

接下來(lái)我們將針對(duì)以上所說(shuō)的情況講解防御式編程中需要掌握的方式:

斷言

斷言是指在開(kāi)發(fā)期間使用的、讓程序在運(yùn)行時(shí)進(jìn)行自檢的代碼(通常為宏或一個(gè)子程序)。斷言為真則程序正常運(yùn)行,斷言為假則意味著代碼中發(fā)生了錯(cuò)誤。

舉個(gè)例子:一份客戶(hù)信息程序要求包含記錄數(shù)不超過(guò)10,我們加一個(gè)斷言。當(dāng)記錄數(shù)小于10,斷言會(huì)默默無(wú)語(yǔ)兩眼淚,當(dāng)超過(guò)10,斷言就會(huì)大聲的說(shuō)程序中存在一個(gè)錯(cuò)誤!

斷言對(duì)于大型復(fù)雜程序或可靠性要求極高的程序來(lái)說(shuō)尤為重要。通過(guò)使用斷言,程序員能更快速排查出因修改代碼或者別的原因,而弄進(jìn)程序里不匹配的接口和錯(cuò)誤等。

OC中內(nèi)置的斷言:(iOS每個(gè)線(xiàn)程都可以指定斷言處理器。想設(shè)置一個(gè) NSAssertionHandler 的子類(lèi)來(lái)處理失敗的斷言,在線(xiàn)程的 threadDictionary 對(duì)象中設(shè)置 NSAssertionHandlerKey 字段即可

對(duì)NSAssertionHandler有興趣的童鞋請(qǐng)移步:傳送門(mén)

#define NSAssert(condition, desc, ...)  \
    do {                \
    __PRAGMA_PUSH_NO_EXTRA_ARG_WARNINGS \
    if (__builtin_expect(!(condition), 0)) {        \
            NSString *__assert_file__ = [NSString stringWithUTF8String:__FILE__]; \
            __assert_file__ = __assert_file__ ? __assert_file__ : @"<Unknown File>"; \
        [[NSAssertionHandler currentHandler] handleFailureInMethod:_cmd \
        object:self file:__assert_file__ \
            lineNumber:__LINE__ description:(desc), ##__VA_ARGS__]; \
    }               \
        __PRAGMA_POP_NO_EXTRA_ARG_WARNINGS \
    } while(0)
#endif

下面將介紹一下斷言使用時(shí)的建議:

1、建立自己的斷言機(jī)制

很多時(shí)候,系統(tǒng)自帶的斷言無(wú)法滿(mǎn)足我們的需求,比如iOS斷言在release模式下會(huì)失效,那么我們可以自定義斷言來(lái)適應(yīng)我們的項(xiàng)目

下面是C++的斷言宏示例:


#define ASSERT(condition, message) {    \
    if (!condition) {                   \
        Log("ERROR ",condition,message);\
        exit( EXIT_FAILURE );           \
    }                                   \
}                                       \

OC中示例:

#define WYHAssert(condition, desc)  \
if (DEBUG) {                        \
   NSAssert(condition, desc);       \
}else {                             \
   NSString *app_build = [[NSBundle mainBundle].infoDictionary objectForKey:@"CFBundleVersion"]; \
NSLog(@"Assert Error condition:%@ (desc: %@) \n Occur in <%s><第%d行> , AppBuildVersion:%@",condition,desc,__FILE__,__LINE__,app_build); \
   [LogModule postLog];                            \
} \

2、用錯(cuò)誤處理代碼處理預(yù)期發(fā)生的狀況,用斷言去處理那些不可發(fā)生的錯(cuò)誤!

斷言和錯(cuò)誤處理代碼的區(qū)別:

斷言是用來(lái)檢查永遠(yuǎn)不該發(fā)生的情況,而錯(cuò)誤處理代碼(error-handling code)是用來(lái)檢查不太可能經(jīng)常發(fā)生的情況,這些情況是能在寫(xiě)代碼時(shí)被預(yù)料的,且在產(chǎn)品正式上線(xiàn)時(shí)也要處理這些情況,因而說(shuō)錯(cuò)誤處理通常用來(lái)檢查有害的輸入數(shù)據(jù),而斷言是用于檢查代碼中的bug !

有種方式可以讓你更好理解斷言:

把斷言看做是可執(zhí)行的注解,你不能依賴(lài)它來(lái)讓代碼正常工作,但與編程語(yǔ)言中的注解相比,它更能主動(dòng)地對(duì)程序中的假定做出說(shuō)明。

3、利用斷言來(lái)注解前條件和后條件

前條件(先驗(yàn)條件)和后條件(后驗(yàn)條件)專(zhuān)有名詞最初來(lái)自于契約式設(shè)計(jì)(Design by Contract)(DbC),使用契約式設(shè)計(jì)時(shí),每個(gè)子程序或類(lèi)與程序的其余部分都形成了一份契約。

很多語(yǔ)言都有對(duì)這種斷言的支持。然而DbC認(rèn)為這些契約對(duì)于軟件的正確性至關(guān)重要,它們應(yīng)當(dāng)是設(shè)計(jì)過(guò)程的一部分。實(shí)際上,DbC提倡首先寫(xiě)斷言。(百度百科)

前條件:子程序或類(lèi)的調(diào)用方代碼再調(diào)用子程序或?qū)嵗瘜?duì)象之前要確保為真的屬性。前條件是調(diào)用方對(duì)其所調(diào)用的代碼要承擔(dān)的義務(wù)。

后條件:子程序或類(lèi)在執(zhí)行結(jié)束后要確保為真的屬性,后條件是子程序或類(lèi)對(duì)調(diào)用方代碼所承擔(dān)的責(zé)任。

而斷言是用來(lái)說(shuō)明前后條件的有利工具。

下面舉個(gè)例子說(shuō)明:

/// 警報(bào)站坐標(biāo)
private class EStationCoordinate: NSObject {

    var latitude: Float?
    
    var longitude: Float?
    
    var elevation: Float = 0.0
    
    init(_ latitude: Float,_ longitude: Float,_ elevation: Float) {
        super.init()
        
        self.latitude = latitude
        self.longitude = longitude
        self.elevation = elevation
    }
}


/// 取得報(bào)警站坐標(biāo)
///
/// - Parameters:
///   - latitude: <#latitude description#>
///   - longitude: <#longitude description#>
///   - elevation: <#elevation description#>
/// - Returns: <#return value description#>
private func createEmergencyCoordinate(_ latitude: Float,_ longitude: Float,_ elevation: Float) -> EStationCoordinate {
    
    // precondition
    assert(-90 <= latitude && latitude <= 90, "latitude must within range !");
    
    assert(0 <= longitude && longitude < 360, "longitude must within range !");
    
    assert(100 <= elevation && elevation < 500, "elevation must within range !");
    
    // handle .... searching in local
    
    // postcondition
    assert(isContain, "local not contain this coordinate !")
    
    var coordinate = EStationCoordinate(latitude,longitude,elevation)
    
    return coordinate
    
}

如果變量latitude、longitude和elevation都是來(lái)源于系統(tǒng)外部,那么就應(yīng)該用錯(cuò)誤處理代碼來(lái)檢查和處理非法的數(shù)值,而如果變量的值是源于可信的系統(tǒng)內(nèi)部,并且這段程序是基于這些值不會(huì)超出合法范圍的假定而設(shè)計(jì),使用斷言則是非常合適的。

4、避免將需要執(zhí)行的子程序放到斷言中

如果把需要執(zhí)行的子程序代碼寫(xiě)在斷言的codition條件里,那么當(dāng)你關(guān)閉斷言功能時(shí),編譯器很可能就把這些代碼排除在外了,下面舉一個(gè)例子:

- (void)postFileToServer {
    
    // .... make file
    
    NSAssert([self compressFileToZip], @"File can't be compressed !");
    
    // ... post to server 
}

- (BOOL)compressFileToZip {
    
    //... compress file and create a zip path !
    if (zipPath.length > 0) {
        
        return YES;
    }
    return NO;
}

這樣如果未編譯斷言,則condition語(yǔ)句的子程序也將不會(huì)執(zhí)行,應(yīng)修改為以下:

- (void)postFileToServer {
    
    // .... make file
    BOOL isCompressSuccess = [self compressFileToZip];
    
    NSAssert(isCompressSuccess, @"File can't be compressed !");
    
    // ... post to server
}

錯(cuò)誤處理技術(shù)

前面我們提過(guò)了,斷言是處理程序代碼中那些不應(yīng)發(fā)生的錯(cuò)誤,那么又如何處理那些我們預(yù)料之內(nèi)的可能發(fā)生的錯(cuò)誤呢?

首先我們要明確對(duì)于程序而言,處理錯(cuò)誤最恰當(dāng)?shù)姆绞绞且鶕?jù)程序軟件的類(lèi)別而定,進(jìn)而言之就是對(duì)于程序的兩個(gè)概念:健壯性與正確性

程序的健壯性

定義:健壯性具體指的是系統(tǒng)在不正常的輸入或不正常的外部環(huán)境下仍能表現(xiàn)出正常的程度。

健壯性的原則:

  • 不斷嘗試采取措施來(lái)包容錯(cuò)誤的輸入以此讓程序正常運(yùn)轉(zhuǎn)(對(duì)自己的代碼要保守,對(duì)用戶(hù)的行為要開(kāi)放)
  • 考慮各種各樣的極端情況,沒(méi)有impossible
  • 即使終止執(zhí)行,也要準(zhǔn)確/無(wú)歧義的向用戶(hù)展示全面的錯(cuò)誤信息
  • 錯(cuò)誤信息有助于進(jìn)行debug

例如:視頻游戲中的繪圖子程序接收到了一個(gè)錯(cuò)誤的顏色輸入,那么在設(shè)計(jì)的時(shí)候可以針對(duì)這種情況,采用它的默認(rèn)背景色或前景色繼續(xù)繪制,而不是讓程序崩潰或退出。

程序的正確性

定義:正確性意味著程序永不返回不準(zhǔn)確的結(jié)果,即使這樣做會(huì)不返回結(jié)果或是直接退出程序。

例如:在設(shè)計(jì)控制治療癌癥病人的放療設(shè)備的軟件時(shí),當(dāng)軟件接收到錯(cuò)誤的放射劑量,那么也許直接關(guān)閉程序就是最佳的選擇,哪怕重啟也比冒險(xiǎn)施放錯(cuò)誤的放射劑量要好的多。

總結(jié),兩者之間的區(qū)別在于:

  • 正確性:永不給用戶(hù)錯(cuò)誤的結(jié)果,哪怕是退出程序
  • 健壯性:盡可能的保持軟件運(yùn)行而不是總是退出

了解了程序的健壯性與正確性,我們就可以采用以下幾種手段,或結(jié)合起來(lái)使用錯(cuò)誤處理技術(shù):

1、返回中立值:

有時(shí),處理錯(cuò)誤的最佳做法就是繼續(xù)執(zhí)行操作并簡(jiǎn)單的返回一個(gè)沒(méi)有危害的值。

比如,一個(gè)基于輸入顏色的繪圖子程序接收到了一個(gè)錯(cuò)誤的顏色輸入,它可以忽略這個(gè)錯(cuò)誤的顏色,而是采用默認(rèn)的底色或前景色繼續(xù)進(jìn)行繪制,而不是直接崩潰。

2、換用下一個(gè)正確的數(shù)據(jù)

在處理輪詢(xún)查詢(xún)狀態(tài)的子程序時(shí),如果某次查詢(xún)出的輸出數(shù)據(jù)錯(cuò)誤或有誤,大可以忽略本次錯(cuò)誤的數(shù)據(jù),繼續(xù)等待下一次輪詢(xún)時(shí)讀取正確的數(shù)據(jù)
(例如,如果你以每秒100次的速度讀取體溫計(jì)的數(shù)據(jù),如果某一次得到的數(shù)據(jù)有誤,我們可以再等上1/100秒后繼續(xù)讀取正確的數(shù)據(jù))

3、返回與前次相同的數(shù)據(jù)

還是舉上一個(gè)例子,如果體溫計(jì)在1/100秒讀取到的是一個(gè)錯(cuò)誤數(shù)據(jù),那么大可以返回上一次正確的數(shù)據(jù),因?yàn)闇囟仍?/100秒內(nèi)變化不會(huì)太大。

4、換用最接近的合法值

比如,當(dāng)我們?cè)诰帉?xiě)一個(gè)滑塊在規(guī)定區(qū)域內(nèi)滑動(dòng)的程序時(shí),如果滑塊超過(guò)規(guī)定區(qū)域,我們可以取最接近于超過(guò)區(qū)域的安全數(shù)值返回。

5、把警告信息記錄到日志文件中

在檢測(cè)到錯(cuò)誤數(shù)據(jù)時(shí),可以選擇在日志文件中記錄一條警告信息,然后繼續(xù)執(zhí)行。

6、返回一個(gè)錯(cuò)誤狀態(tài)碼

可以決定只讓徐彤的某些部分處理錯(cuò)誤,其他部分則不在局部處理錯(cuò)誤,而是簡(jiǎn)單的返回一個(gè)錯(cuò)誤碼。

比如在用戶(hù)信息編輯頁(yè)面有一個(gè)保存按鈕,當(dāng)某些信息填寫(xiě)錯(cuò)誤時(shí),這時(shí)只是記錄一個(gè)錯(cuò)誤碼,當(dāng)點(diǎn)擊保存按鈕時(shí)才去判斷驗(yàn)證這個(gè)錯(cuò)誤碼是否存在,決定是否允許用戶(hù)執(zhí)行下一步操作

7、調(diào)用錯(cuò)誤處理子程序或?qū)ο?/strong>

把錯(cuò)誤處理都集中在一個(gè)全局的錯(cuò)誤處理子程序或?qū)ο笾?,這種方法優(yōu)點(diǎn)在于能把錯(cuò)誤處理的職責(zé)集中到一起,從而讓調(diào)試變得更簡(jiǎn)單。而代價(jià)則是整個(gè)程序都要知道這個(gè)集中點(diǎn),并與之緊密耦合。

什么意思呢?比如在一系列有上下文關(guān)系的請(qǐng)求中,針對(duì)所有的請(qǐng)求錯(cuò)誤,我們只封裝一個(gè)錯(cuò)誤管理類(lèi)來(lái)集中管理這些錯(cuò)誤。

8、當(dāng)錯(cuò)誤發(fā)生時(shí)顯示出錯(cuò)消息

這種方法可以把錯(cuò)誤處理的開(kāi)銷(xiāo)減到最小,然而你需要衡量此時(shí)的錯(cuò)誤消息對(duì)于用戶(hù)而言是否是友善的,相反對(duì)于攻擊者而言,盡量不要讓他們利用錯(cuò)誤信息來(lái)發(fā)現(xiàn)如何攻擊這個(gè)系統(tǒng)。

9、關(guān)閉程序

有一些更偏向于正確性的程序,當(dāng)檢測(cè)到錯(cuò)誤發(fā)生時(shí),也許關(guān)閉程序是最佳的選擇。

如上面談到的癌癥病人的放療設(shè)備的軟件

異常

異常是把代碼中的錯(cuò)誤或異常事件傳遞給調(diào)用方代碼的一種特殊手段。

異常處理,英文名為exceptional handling, 是代替日漸衰落的error code方法的新法,提供error code 所未能具體的優(yōu)勢(shì)。異常處理分離了接收和處理錯(cuò)誤代碼。這個(gè)功能理清了編程者的思緒,也幫助代碼增強(qiáng)了可讀性,方便了維護(hù)者的閱讀和理解。 異常處理(又稱(chēng)為錯(cuò)誤處理)功能提供了處理程序運(yùn)行時(shí)出現(xiàn)的任何意外或異常情況的方法。異常處理使用 try、catch 和 finally 關(guān)鍵字來(lái)嘗試可能未成功的操作,處理失敗,以及在事后清理資源。(百度百科)

如果在一個(gè)子程序中遇到了預(yù)料之外的情況,但并不知道如何處理的話(huà),你就可以選擇拋出一個(gè)異常。

異常的基本結(jié)構(gòu)是:子程序使用throw拋出一個(gè)異常對(duì)象,再被調(diào)用鏈上層其他子程序的try-catch語(yǔ)句捕獲。(內(nèi)建的異常機(jī)制都是沿著函數(shù)調(diào)用棧的函數(shù)調(diào)用逆向搜索,直到遇到異常處理代碼為止)

我知道聽(tīng)到這,肯定有人懵逼了,我們來(lái)看下面的例子:

+ (void)tryFirstException {
    @try {
        // 1
        [self trySecondException];
        
    } @catch (NSException *exception) {
        //2
        NSLog(@"First reason:%@",exception.reason);
        
    } @finally {
        //3
    }
    //4
}

+ (void)trySecondException {
    @try {
        //5
        [self tryThirldException];
        
    } @catch (NSException *exception) {
        //6
        @throw exception; //如果將這段代碼注釋后又會(huì)怎樣?
        NSLog(@"Second reason:%@",exception.reason);
    } @finally {
        //7
    }
    //8
}

+ (void)tryThirldException {
    //9
    @throw [NSException exceptionWithName:@"Exc" reason:@"no reason!" userInfo:nil];
}

有人知道程序應(yīng)該怎么執(zhí)行嗎?

許多常見(jiàn)的程序設(shè)計(jì)語(yǔ)言,包括Actionscript,Ada,BlitzMax,C++,C#,D,ECMAScript,Eiffel,Java,ML,Object Pascal(如Delphi,F(xiàn)ree Pascal等),Objective-C,Ocaml,PHP(version 5),PL/1,Prolog,Python,REALbasic,Ruby,Visual Prolog以及大多數(shù).NET程序設(shè)計(jì)語(yǔ)言,內(nèi)建的異常機(jī)制都是沿著函數(shù)調(diào)用棧的函數(shù)調(diào)用逆向搜索,直到遇到異常處理代碼為止。一般在這個(gè)異常處理代碼的搜索過(guò)程中逐級(jí)完成棧卷回(stack unwinding)。但Common Lisp是個(gè)例外,它不采取棧卷回,因此允許異常處理完后在拋出異常的代碼處原地恢復(fù)執(zhí)行。

下面將給予一些使用異常的建議:

1、用異常通知程序的其他部分,發(fā)生了不可忽略的錯(cuò)誤

異常機(jī)制的優(yōu)越之處,在于它能提供一種無(wú)法被忽略的錯(cuò)誤通知機(jī)制。其他錯(cuò)誤處理技術(shù)有可能會(huì)導(dǎo)致錯(cuò)誤在不知不覺(jué)中向外擴(kuò)散,而異常則消除了這種可能性。

2、只在真正例外的情況下才拋出異常

僅在真正例外的情況下才使用異?!獡Q句話(huà)說(shuō),就是僅在其他編碼實(shí)踐方法無(wú)法解決的情況下才使用異常。這種情況跟斷言相似————都是用來(lái)處理那些不僅罕見(jiàn)甚至永遠(yuǎn)不該發(fā)生的情況。

3、不能用異常來(lái)推卸責(zé)任

如果某種錯(cuò)誤情況可以在局部處理,那就應(yīng)該在局部處理它。不要把本可以處理的錯(cuò)誤當(dāng)成一個(gè)未被捕獲的異常拋出去。

4、避免在構(gòu)造函數(shù)和析構(gòu)函數(shù)中拋出異常,除非你在同一個(gè)地方把它們捕獲

如果嘗試在構(gòu)造函數(shù)或析構(gòu)函數(shù)中拋出異常,則處理異常將變得非常復(fù)雜。
比如在C++中,只有對(duì)象在完全構(gòu)造之后才能調(diào)用析構(gòu)函數(shù),也就是說(shuō)如果再構(gòu)造函數(shù)中拋出異常,就不會(huì)調(diào)用析構(gòu)函數(shù),從而造成潛在的資源泄漏。

5、在恰當(dāng)?shù)某橄髮哟螔伋霎惓?/h4>

當(dāng)你決定把一個(gè)異常傳給調(diào)用方時(shí),請(qǐng)確保異常的抽象層次與子程序的接口抽象層次一致。
(比如在A(yíng)類(lèi)的某一子程序中,有一個(gè)getDefenseId的方法,在方法中的某些步驟中,我們拋出了一個(gè)文件讀寫(xiě)錯(cuò)誤的異常,這本應(yīng)由層次更低的內(nèi)部類(lèi)F專(zhuān)職去做的事,卻錯(cuò)誤的在A(yíng)類(lèi)中拋出異常,破壞了封裝性,也暴露了一些私有的實(shí)現(xiàn)細(xì)節(jié),這顯然不是我們想要的)

6、在異常消息中加入關(guān)于導(dǎo)致異常發(fā)生的全部信息

比如因?yàn)橐粋€(gè)索引值錯(cuò)誤而拋出的,就應(yīng)該在異常消息中包含索引的上下界限一級(jí)非法的索引下標(biāo)值等信息。

7、避免使用空的catch語(yǔ)句

不要視圖敷衍一個(gè)不知該如何處理的異常

8、考慮創(chuàng)建一個(gè)集中的異常報(bào)告機(jī)制

封裝一個(gè)異常報(bào)告的子程序(或基類(lèi))專(zhuān)門(mén)快速方便的報(bào)告程序中需要主動(dòng)拋出的異常(異常處理器),將對(duì)于異常的使用更加標(biāo)準(zhǔn)化

9、考慮異常的替換機(jī)制

雖然一些編程語(yǔ)言對(duì)于異常的支持已有5~10年甚至更久的歷史,但關(guān)于如何安全使用異常的經(jīng)驗(yàn)仍然還是很少。

拿iOS舉例,Apple.inc雖然同時(shí)提供了錯(cuò)誤處理(NSError)和異常處理(Exception)兩種機(jī)制,但是Apple不建議我們主動(dòng)去使用Exception,更加提倡開(kāi)發(fā)者使用NSError來(lái)處理程序運(yùn)行中可恢復(fù)的錯(cuò)誤。而異常被推薦用來(lái)處理不可恢復(fù)的錯(cuò)誤。

Important: In many environments, use of exceptions is fairly commonplace. For example, you might throw an exception to signal that a routine could not execute normally—such as when a file is missing or data could not be parsed correctly. Exceptions are resource-intensive in Objective-C. You should not use exceptions for general flow-control, or simply to signify errors. Instead you should use the return value of a method or function to indicate that an error has occurred, and provide information about the problem in an error object. For more information, see Error Handling Programming Guide.

(developer.apple.com)

感興趣的童鞋請(qǐng)移步蘋(píng)果官網(wǎng) 傳送門(mén)

多說(shuō)一句,雖然apple不推薦我們經(jīng)常主動(dòng)使用Exception,但針對(duì)于crash的異常捕捉,iOS是可以通過(guò)NSSetUncaughtExceptionHandler來(lái)捕獲大部分crash的,但值得注意的是無(wú)法捕獲那些由于內(nèi)存溢出、野指針BAD_ACCESS導(dǎo)致的crash,比如Flurry中對(duì)crash處理就是這么運(yùn)作的。

    - (void) uncaughtExceptionHandler(NSException *exception) 
    {
        [Flurry logError:@"Uncaught" message:@"Crash!" exception:exception];
    }
 
    - (void)applicationDidFinishLaunching:(UIApplication *)application
    {
        NSSetUncaughtExceptionHandler(&uncaughtExceptionHandler);
        [Flurry startSession:@"YOUR_API_KEY"];
        // ....
    }

隔欄

隔欄是防御式編程中的一種容損策略,舉個(gè)例子,大家可以這樣來(lái)理解:

船體外殼上裝備隔離艙,如果船只與冰山相撞導(dǎo)致船體破裂,隔離艙就會(huì)被封閉起來(lái),從而保護(hù)船體的其余部分不會(huì)受到影響。
隔欄過(guò)去叫防火墻,但現(xiàn)在防火墻這一術(shù)語(yǔ)常用來(lái)代指阻止惡意網(wǎng)絡(luò)攻擊)

如下圖:

5毛錢(qián)特效翻譯過(guò)來(lái)就是:

左側(cè)外部接口數(shù)據(jù)假定是骯臟的不可信的,中間這些類(lèi)(子程序)構(gòu)成隔欄,負(fù)責(zé)清理和驗(yàn)證數(shù)據(jù)并返回可信的數(shù)據(jù),最右側(cè)的類(lèi)(子程序)全部在假定數(shù)據(jù)干凈(安全)的基礎(chǔ)上工作,這樣可以讓大部分的代碼無(wú)須再擔(dān)負(fù)檢查錯(cuò)誤數(shù)據(jù)的職責(zé)!

這種策略可以擬一個(gè)比較生動(dòng)的例子,可以看做是手術(shù)室里使用的一種策略。
任何東西在允許進(jìn)入手術(shù)室之前都要經(jīng)過(guò)消毒處理,因此手術(shù)室里的任何東西都可以認(rèn)為是安全的。這其中最核心的設(shè)計(jì)決策是規(guī)定什么可以進(jìn)入手術(shù)室,什么不可以,還有把手術(shù)室的門(mén)設(shè)在哪里

在編程中也是如此,約定哪些子程序可認(rèn)為是在安全區(qū)域里的,哪些又是在安全區(qū)域外的,哪些負(fù)責(zé)清理數(shù)據(jù)(完成這一工作最簡(jiǎn)單的方法是在得到外部數(shù)據(jù)時(shí),立即進(jìn)行清理,不過(guò)數(shù)據(jù)往往需要經(jīng)過(guò)一層以上的清理,因此多層清理有時(shí)也是必須的)

隔欄的應(yīng)用:
隔欄的使用使斷言和錯(cuò)誤處理有了清晰的區(qū)分,隔欄外部的程序應(yīng)該使用錯(cuò)誤處理技術(shù),在那里對(duì)數(shù)據(jù)做的任何處理假定都是不安全的。而隔欄內(nèi)部的程序里就應(yīng)該使用斷言技術(shù),因?yàn)閭鬟M(jìn)來(lái)的數(shù)據(jù)應(yīng)該已在通過(guò)隔欄時(shí)被清理過(guò)了。如果隔欄內(nèi)部的子程序檢測(cè)到了錯(cuò)誤的數(shù)據(jù),那么應(yīng)該是程序里的錯(cuò)誤而不是程序外的錯(cuò)誤。

輔助調(diào)試代碼

防御式編程的另一重要方面就是使用調(diào)試助手(輔助調(diào)試代碼),調(diào)試助手非常之強(qiáng)大??,可以幫助我們快速檢查錯(cuò)誤。

應(yīng)用在開(kāi)發(fā)期間應(yīng)犧牲一些速度和對(duì)資源的使用,來(lái)?yè)Q取一些可以讓開(kāi)發(fā)更順暢的內(nèi)置工具。

1、應(yīng)盡早的引入輔助調(diào)試代碼

越早進(jìn)入輔助調(diào)試代碼,它能夠提供的幫助也越大。如果你經(jīng)常遇到某些問(wèn)題,嘗試自己編寫(xiě)或引入一些輔助調(diào)試代碼,它就會(huì)自始至終在項(xiàng)目中幫助你。

2、采用進(jìn)攻式編程

什么又是進(jìn)攻式編程,其實(shí)這也是防御式編程中的一種習(xí)慣,其思想在于:

盡量讓異常的情況在開(kāi)發(fā)期間暴露出來(lái),而在產(chǎn)品上線(xiàn)時(shí)自我恢復(fù)。

比如你有一段switch case語(yǔ)句用來(lái)處理事件,在開(kāi)發(fā)期間應(yīng)盡量考慮所有你能預(yù)料得到的情況并作出處理,另外在default case語(yǔ)句中,如果是開(kāi)發(fā)階段可以采用進(jìn)攻式編程處理,而在產(chǎn)品正式上線(xiàn)期間,針對(duì)default case應(yīng)更穩(wěn)妥一些,比如記錄錯(cuò)誤日志。

下面列舉一下進(jìn)攻式編程的方法:

  • 確保斷言語(yǔ)句使程序終止運(yùn)行
  • 完全填充分配到的所有內(nèi)存
  • 完全填充己分配到的所有文件或流
  • 確保每一個(gè)case 語(yǔ)句中的default分支或else 分支都能產(chǎn)生嚴(yán)重錯(cuò)誤(如終止程序)
  • 在刪除一個(gè)對(duì)象之前把它填滿(mǎn)垃圾數(shù)據(jù)
  • 讓程序?qū)㈠e(cuò)誤日志主動(dòng)用電子郵件或推送發(fā)送給開(kāi)發(fā)者(安防目前采用)

3、計(jì)劃移除調(diào)試輔助的代碼

如果是商用軟件,調(diào)試用的代碼有時(shí)會(huì)使軟件的體積變大且速度變慢,從而給程序造成巨大的性能影響。要事先做好準(zhǔn)備計(jì)劃,避免調(diào)試用的代碼和程序原代碼糾纏不清,下面列舉一些可以選擇的移除方法:

  • 使用類(lèi)似ant和make這樣的版本控制工具
    (可以從同一套源碼編譯出不同版本的程序。在開(kāi)發(fā)模式下,你可以讓make工具把所有的調(diào)試代碼都包含進(jìn)來(lái)一起編譯。而在產(chǎn)品上線(xiàn)期間,把那些調(diào)試代碼排除在外。)

  • 使用內(nèi)置的預(yù)處理器(C++ #字符為預(yù)處理器指令,包含#if、#warning、#include、#define等
    (如果你所用的編程環(huán)境里有預(yù)處理器,你可以用預(yù)處理器來(lái)包含或排除調(diào)試的代碼)

- (void)handleSomething {
    
#ifdef DEBUG
    
    [self debugF];//通常為一些debug用的耗時(shí)操作
#else
    
    [self releaseF];
#endif
    
}
  • 為應(yīng)用增加調(diào)試模式的入口

如果你的應(yīng)用需要同時(shí)支持兩種模式(發(fā)布和調(diào)試),那么我們可以自定義進(jìn)入調(diào)試模式的入口,而不是針對(duì)編譯層次的DEBUG,我們的調(diào)試代碼的嵌入也依賴(lài)于這個(gè)調(diào)試模式是否開(kāi)啟,下面將演示安防app內(nèi)定義的調(diào)試模式。

對(duì)防御式編程采取防御的姿態(tài)

說(shuō)了這么多,那么是不是防御式代碼越多越好呢?

其實(shí)也不是,多度的防御式編程也會(huì)引起問(wèn)題,如果你在每一個(gè)能想到的地方都用每一種能想到的方法來(lái)檢查參數(shù)、處理錯(cuò)誤,那么你的程序會(huì)變得臃腫而緩慢,更糟的事,多度的防御式代碼換來(lái)的是軟件的復(fù)雜度。

這說(shuō)明,防御式編程引入的代碼也并非不會(huì)有缺陷,和其他代碼一樣,你同樣能輕而易舉的在防御式編程添加的代碼中找到錯(cuò)誤,尤其是當(dāng)你隨手編寫(xiě)這些代時(shí)更是如此。

因此,要考慮好什么地方需要進(jìn)行防御,然后因地制宜地調(diào)整你進(jìn)行防御式編程的優(yōu)先級(jí)。

總結(jié)

  • 程序代碼中對(duì)錯(cuò)誤處理的方式遠(yuǎn)比GIGO復(fù)雜的多。

  • 防御式編程技術(shù)可以讓錯(cuò)誤更容易發(fā)現(xiàn)問(wèn)題、更容易修改,并減少錯(cuò)誤對(duì)產(chǎn)品代碼的破壞。

  • 遵從防御式編程的思想去開(kāi)發(fā),會(huì)讓你在開(kāi)發(fā)階段就提前處理了許多問(wèn)題,提高代碼質(zhì)量,降低測(cè)試周期,要做到主動(dòng)去發(fā)現(xiàn)錯(cuò)誤并做出處理(千萬(wàn)不要存僥幸心理,明明可以多考慮幾種情況,偏偏卻要忽略它們的可能性),而不是等到bug隱式的出現(xiàn)所帶來(lái)的未曾預(yù)料的災(zāi)難。

  • 錯(cuò)誤處理技術(shù)更適用于暴露的接口程序或類(lèi),而斷言則更強(qiáng)調(diào)不可允許的錯(cuò)誤,多適用于不暴露在外的私有方法(或內(nèi)部類(lèi))。

  • 處理錯(cuò)誤最恰當(dāng)?shù)姆绞绞且鶕?jù)程序軟件的類(lèi)別而定,更傾向于正確性還是健壯性。

  • 異常提供了一種與代碼正常流程角度不同的錯(cuò)誤處理手段,但同時(shí)也應(yīng)該在異常和其他錯(cuò)誤處理手段之間進(jìn)行權(quán)衡比較,比如iOS中就很少采用異常處理機(jī)制。

  • 合理的運(yùn)用輔助調(diào)試代碼,會(huì)讓你不管是在開(kāi)發(fā)還是發(fā)布階段都能更快速定位問(wèn)題,并從容地解決問(wèn)題。(預(yù)處理器就是個(gè)很好的選擇)

最后,我對(duì)于防御式編程的理解是,我認(rèn)為程序的好壞與其健壯性(和正確性)有很大的聯(lián)系,所有的程序開(kāi)發(fā)人員都要對(duì)它有足夠的重視,主動(dòng)去迎戰(zhàn)錯(cuò)誤,從一點(diǎn)一滴開(kāi)始做起,不要忽視任何的細(xì)節(jié),不能盲目依賴(lài)測(cè)試去發(fā)現(xiàn)bug,而是以測(cè)試驅(qū)動(dòng)編程,不斷地思考可能發(fā)生的問(wèn)題以進(jìn)行預(yù)防,做一個(gè)聰明的程序員。這才是防御式編程所告訴我們的事 !

推薦

最后列舉一下文中出現(xiàn)的引用來(lái)源及一些推薦看的文章或書(shū)籍:

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀(guān)點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 防御式編程 在防御式駕駛中要建立這樣一種思維,那就是你永遠(yuǎn)也不能確定另一位司機(jī)將要做什么。這樣才能夠確保在其他人做...
    劉碩jessie閱讀 5,590評(píng)論 1 49
  • 第一部分 打好基礎(chǔ) Laying the Foundation 第一章 歡迎進(jìn)入軟件構(gòu)建的世界 Welcome t...
    白樺葉閱讀 4,813評(píng)論 0 17
  • 搜集了一些關(guān)于“防御性編程”資料,將其中一些思想備份下,學(xué)習(xí) 軟件工程師的智慧,就是在于其是否開(kāi)始意識(shí)到:使程序能...
    申申申申申閱讀 2,089評(píng)論 1 7
  • 關(guān)于Mongodb的全面總結(jié) MongoDB的內(nèi)部構(gòu)造《MongoDB The Definitive Guide》...
    中v中閱讀 32,329評(píng)論 2 89
  • 高考結(jié)束,分?jǐn)?shù)下來(lái),填報(bào)志愿,這一切都不是自己理想的狀態(tài)??纪旰蟮囊粋€(gè)星期,每天都是躺在床上陰死陽(yáng)活,直到分...
    熙攘lily閱讀 177評(píng)論 0 0

友情鏈接更多精彩內(nèi)容