搜集了一些關(guān)于“防御性編程”資料坤次,將其中一些思想備份下浙踢,學習
軟件工程師的智慧洛波,就是在于其是否開始意識到:使程序能用和使程序正確蹬挤,這兩者之間有什么樣的差別
編寫在常規(guī)情況下都能用的代碼是很容易的焰扳,只要提供常規(guī)的輸入集吨悍,這些代碼就會給出常規(guī)的輸出集育瓜。但是如果提供了一些意外的輸入躏仇,這些代碼可能就會崩潰
正確的代碼是絕不會崩潰的焰手,對于所有可能的輸入集书妻,它的輸出都將是正確的躲履,不過崇呵,所有可能輸入的集合會常常大得驚人域慷,并且難以測試
然而并不是所有的正確的代碼都是優(yōu)秀的代碼犹褒,因為有些正確的代碼的邏輯可能很難理解叠骑,其代碼可能很不自然宙枷,并且可能幾乎無法維護
因此卓囚,編寫優(yōu)秀的代碼才應(yīng)該是不懈追求的目標哪亿,優(yōu)秀的代碼是健壯的蝇棉,高效的篡殷,也是正確的贴唇。當面對不常見的輸入的時候戳气,產(chǎn)品級的代碼不會crash瓶您,也不會出現(xiàn)錯誤的結(jié)果呀袱,同時還滿足所有其他的要求夜赵,包括線程安全寇僧、時間約束和重入等
經(jīng)常面對風格奇異的遺留代碼嘁傀,那些由現(xiàn)在早已不在的代碼猴子所編寫的舊代碼
面對各種各樣的困難,怎么保證所編寫的代碼穩(wěn)定耐用
答案就是 防御性編程
設(shè)想導致我們寫出帶有缺陷的軟件笑撞,下面是一些很常見的設(shè)想:
這個函數(shù) 絕不會 被那樣調(diào)用娃殖,傳遞的參數(shù)總是有效的
這段代碼肯定會 一直 正常運行炉爆,它絕不會出現(xiàn)錯誤
如果吧這個變量標記為 僅限內(nèi)部使用 就沒有人會嘗試訪問這個變量了
在進行防御性編程的時候芬首,我們不應(yīng)該做任何設(shè)想,我們不應(yīng)該設(shè)想“那不會發(fā)生”
經(jīng)驗告訴我們耀怜,唯一可以肯定的是:代碼在某天會因為某種原因而出錯财破,有人會做出愚蠢的舉動左痢。墨菲定律這樣說:凡是可能會出錯的事俊性,準會出錯定页。防御性編程通過遇見到 或者至少是預(yù)先推測到 問題的所在典徊,斷定代碼中每個階段可能出現(xiàn)的錯誤宫峦,并做出相應(yīng)的防范措施导绷,來防止這類意外的發(fā)生
這也許是有點偏執(zhí)妥曲,但適度的偏執(zhí)并沒有什么壞處褂萧。coding從一開始就適度的偏執(zhí)导犹,可以使代碼在很長的時間內(nèi)更加健壯
防御性編程是一種細致谎痢、謹慎的編程方法节猿,為了開發(fā)可靠地軟件,需要設(shè)計系統(tǒng)中的每個組件太雨,使其盡可能的保護自己躺彬,我們通過明確的在代碼中對設(shè)想進行檢查宪拥,擊碎了未記錄下來的設(shè)想她君,這是一種努力缔刹,防止 或者至少是觀察 我們的代碼以錯誤行為的方式被調(diào)用
防御性編程使我們可以盡早發(fā)現(xiàn)較小的問題校镐,而不是等它們發(fā)展成災(zāi)難的時候才發(fā)現(xiàn)鸟廓,當然防御性編程并不能排除所有的程序錯誤牍陌,但是問題所帶來的麻煩將會減少毒涧,并易于修改契讲,防御性程序員只是抓住飄落的雪花怀泊,而不是被埋葬在錯誤的雪崩中
防御性編程是一種防衛(wèi)方式务傲,而不是一種補救形式
對防御性編程的誤解
防御性編程 并不是 檢查錯誤:如果代碼中存在可能出現(xiàn)錯誤的情況售葡,無論如何都應(yīng)該檢查這些錯誤楼雹,這并不是防御性編程贮缅,這只是一種好的做法谴供,是編寫正確的代碼的一部分
防御性編程 并不是 測試:測試代碼并不是防御桂肌,而只是開發(fā)工作的另一份典型的部分。測試工作不是防御性的谭跨,這項工作可以驗證代碼現(xiàn)在是正確的饺蚊,但不能保證代碼在經(jīng)理將來的修改之后不會出錯裕坊。即便是擁有了全世界最好的測試工具籍凝,也還是會有人對代碼進行更改饵蒂,并是代碼進入過去未測試的狀態(tài)
防御性編程并不是調(diào)試:在調(diào)試期間退盯,你可添加一些防御性代碼渊迁,不過調(diào)試是在程序出錯之后進行的,防御性編程首先是防止程序出錯的措施 或者在錯誤以不可理解的方式出現(xiàn)之前發(fā)現(xiàn)它們
防御性編程可以節(jié)省大量的調(diào)試時間箱叁,使你可以去做更有意義的事情
編寫可以正確運行耕漱、只是速度有些慢的代碼,要遠遠好過大多數(shù)時間都正常運行齐鲤,但是有時候會crash的代碼
我們可以設(shè)計一些在版本構(gòu)建中物理移除的防御性代碼给郊,以解決性能問題统锤,總之我們這里所考慮的大部分防御性措施饲窿,并不具有明顯的開銷
防御性編程避免了大量的安全問題,這是現(xiàn)代軟件開發(fā)中是一個重大的問題鸦泳,避免這些問題可以帶來很多好處
防御性編程技巧
- 使用好的編碼風格和合理的設(shè)計
我們可以通過采用良好的編程風格做鹰,來防范大多數(shù)編碼錯誤,如選用有意義的變量名喂走,或者謹慎的使用括號乎芳,在開始coding之前奈惑,先考慮大體的設(shè)計方案肴甸,從實現(xiàn)一套清晰的API友扰、一個邏輯系統(tǒng)結(jié)構(gòu)以及一些定義良好的組件角色與責任開始入手 - 不要倉促的編寫代碼
使用閃電式編程方式的程序員會很快的開發(fā)出一個函數(shù),馬上把這個函數(shù)交給編譯器來檢查已發(fā)甚负,接著運行一邊看看能不能使用梭域,然后進入下一個任務(wù)富玷。這種方式充滿了危險凌彬。相反,在寫每一行時候都三思伐蒋,可能會出現(xiàn)什么錯誤,是否已經(jīng)考慮了所有可能出現(xiàn)的邏輯分支焙畔,放慢速度,有條不紊編程雖然看上去很平凡伸但,但是的確是減少缺陷的好辦法 - 不要相信任何人
真正的用戶:意外地提供了假的輸入更胖,或者錯誤地操作了程序
惡意的用戶:故意造成不好的程序行為
客戶端代碼:使用錯誤的參數(shù)調(diào)用了你的函數(shù),或者提供了不一致的輸入
運行環(huán)境:沒有為程序提供足夠的服務(wù)
外部程序庫:運行失誤,不遵從你所依賴的接口協(xié)議 - 編碼的目標是清晰捐下,而不是簡潔
如果要你從簡潔但是可能讓人困惑的代碼和清晰但是有可能比較冗長的代碼中選擇奸柬,一定要選擇那些看上去和預(yù)期相符合的代碼廓奕,即使它不太優(yōu)雅。例如档叔,將復(fù)雜的代數(shù)運算拆分為一系列的單獨的語句桌粉,使邏輯更清晰。想一想衙四,誰會是的代碼的讀者铃肯,這些代碼也許需要一位初級程序員來維護传蹈,如果他不能理解代碼邏輯押逼,那么他肯定會犯一些錯誤,復(fù)雜的結(jié)構(gòu)或不常用的語言技巧可以證明你在運算符優(yōu)先級方面淵博的知識惦界,但是這些實際上會扼殺代碼的可維護性秕岛,請保持代碼簡單境钟。不能維護的代碼是不安全抹凳,舉一個極端的例子鸯匹,過于復(fù)雜的表達式會使編譯器生成錯誤的代碼,許多編譯器優(yōu)化的錯誤就是因此造成的 - 不要讓任何人做他們不該做的修補工作
內(nèi)部的事情應(yīng)該留在內(nèi)部灾搏,私人的東西就應(yīng)該用鎖和鑰匙保管起來显歧,不要吧你的代碼初稿公之于眾,不管你多么有禮貌的懇求确镊,主要稍不注意,別人就會篡改你的數(shù)據(jù)范删,然后嘗試條用 僅用于執(zhí)行 的例行程序蕾域。不要讓他們這么做
a. 在面向?qū)ο蟮恼Z言中,通過將屬性設(shè)為 private 來防止對內(nèi)部類數(shù)據(jù)的訪問
b. 在過程語言中旨巷,仍可以使用面向?qū)ο蟮拇虬拍钐硗瑢rivate 數(shù)據(jù)打包在不透明的類型背后,并提供可以操作它們的定義良好的公共函數(shù)
c. 將所有變量保持在盡可能小的范圍內(nèi)搁骑,不到萬不得已又固,不要聲明全局變量煤率,如果變量可以聲明為函數(shù)內(nèi)部的局部變量仰冠,就不要在文件范圍上聲明蝶糯。如果變量可以聲明為循環(huán)體內(nèi)的局部變量,就不要在函數(shù)范圍上聲明 - 編譯時候打開所有的警告開關(guān)
大多數(shù)語言的編譯器都會在你傷了它們感情的時候給出一大堆錯誤信息识虚,當這些編譯器碰到潛在的有缺陷的代碼時候担锤,它們也會給出各種各樣的警告妻献。通常情況下這些警告可以有選擇地啟用或禁用
如果代碼中充滿了危險的構(gòu)造团赁,將會得到數(shù)頁的警告信息欢摄,糟糕的是怀挠,通常的反應(yīng)是禁用編譯器的警告功能,或者干脆不理會這些信息闷畸,這兩種做法都是不可取的
在任何情況下都要打開你的編譯器的警告功能佑菩,如果代碼產(chǎn)生了任何警告信息殿漠,應(yīng)立即修正代碼绞幌,讓編譯器的報錯聲停下來。在啟用了警告功能之后一忱,不要對不能安靜地完成編譯的代碼感到滿意莲蜘。警告的出現(xiàn)總歸是有原因的菇夸,即使你認為某個警告無關(guān)緊要庄新,也不要置之不理择诈,否則羞芍,總有一天這個警告會隱藏一個確實重要的警告 - 使用靜態(tài)分析工具
編譯器警告是對代碼的一次有限的靜態(tài)分析荷科,即在程序運行之前執(zhí)行代碼的檢查的結(jié)果畏浆,還有許多獨立的靜態(tài)分析工具可以使用刻获,在日常編程工作中蝎毡,應(yīng)該包括使用這些工具來檢查你的代碼沐兵,它們會比編譯器跳出更多的錯誤 - 使用安全的數(shù)據(jù)結(jié)構(gòu)
如果做不到扎谎,就安全地使用危險的數(shù)據(jù)結(jié)構(gòu)簿透。最常見的安全隱患大概是由緩沖溢出引起的老充。緩沖溢出是由于不正確的使用固定大小的數(shù)據(jù)結(jié)構(gòu)而造成的啡浊。如果代碼在沒有檢查一個緩沖的大小之前就寫入這個緩沖巷嚣,那么寫入的內(nèi)容總是可能會超出緩沖的末尾的 - 檢查所有的返回值
如果一個函數(shù)返回一個值廷粒,它這樣做肯定是有理由的坝茎。檢查這個返回值嗤放,如果返回值是一個錯誤的代碼,就必須辨別這個代碼并處理所有的錯誤,不要讓錯誤悄無聲息的侵入程序剂公,忍受錯誤會導致不可預(yù)知的行為诬留,這既適用于用戶自定義的函數(shù)文兑,也適用于標準庫的函數(shù)腺劣,大多數(shù)難以察覺的錯誤都是因為程序員沒有檢查返回值而出現(xiàn)的橘原,不要忘記趾断,某些函數(shù)會通過不同的機制返回錯誤增显,不論合適都要在適應(yīng)的級別上捕獲和處理相應(yīng)的異常 - 謹慎的處理內(nèi)存 和其他寶貴的資源
對于在執(zhí)行期間所獲取的任何資源同云,必須徹底釋放。內(nèi)存使這類資源最常提到的一個例子,但是并不是唯一的一個禁偎。文件和縣城所也是必須小心使用的寶貴資源届垫,做一個好管家
不要因為覺得操作系統(tǒng)會在你的程序退出時候清除程序装处,就不注意關(guān)閉文件或釋放內(nèi)存妄迁。對于代碼還會執(zhí)行多長時間登淘,是否會耗盡所有的文件句柄或者占用所有的內(nèi)存黔州,你對此一無所知流妻,甚至不能肯定操作系統(tǒng)是否會完全釋放你的資源绅这,有的操作系統(tǒng)不是這樣的证薇。Java 和 .NET 使用垃圾回收機制來執(zhí)行這些繁重的清潔工作浑度,在運行時會不時的清掃俺泣,不過不要因此對安全性抱有錯誤的想法伏钠,你仍然需要思考熟掂,你必須顯示地終止對那些不再需要赴肚,或者不會被自動清除的對象的引用:不要意外的保留對對象的引用誉券。不太現(xiàn)今的垃圾回收器也很容易被循環(huán)引用蒙蔽(A引用B踊跟,B又引用A商玫,除此之外沒有對A和B的引用)拳昌,這就會導致對象永遠不會被清除炬藤;這就是一種難以發(fā)現(xiàn)的內(nèi)存泄露形式 - 在聲明位置初始化所有變量
- 盡可能推遲一些聲明變量
這樣可以使變量的聲明位置與使用它的位置盡量接近,從而防止它干擾代碼的其他部分细睡,這樣做也是的使用變量的代碼更加清晰溜徙,不再需要導出尋找變量的類型和初始化蠢壹,在附近聲明使用這些都變得非常明顯图贸。不要在多個地方重用同一個臨時變量疏日,即使每次使用都在邏輯上相互分離的區(qū)域中進行沟优。變量重用會使得以后對代碼重新完善的工作變得異常復(fù)雜宾肺,每次創(chuàng)建一個新的變量---編譯器會解決任何有關(guān)效率的問題 - 使用標準語言庫
明確的定義你正在適應(yīng)的是哪個語言版本锨用,除非項目要求增拥,否則不要將命運交給編譯器跪者,或者對該語言的任何非標準的擴展渣玲。如果該語言的某個領(lǐng)域還沒定義忘衍,就不要依賴你所使用的特定編譯器的行為,這樣會產(chǎn)生非常脆弱的代碼 - 使用好的診斷信息日志工具
當編寫新的代碼時候搀捷,常會加入許多診斷信息嫩舟,以確定程序的運行情況家厌,在調(diào)試結(jié)束后是否應(yīng)該刪除這些診斷信息吶饭于?保留這些信息對以后再次訪問代碼會帶來很多方便掰吕,特別是如果在此期間可以有選擇地禁用這些信息殖熟。有許多診斷信息日志系統(tǒng)可以幫助實現(xiàn)這種功能,這些系統(tǒng)中很多都可以使診斷信息在不需要的時候不帶來任何開銷照皆,可以有選擇的使它們不參加編譯 - 謹慎地進行強制轉(zhuǎn)換
大多數(shù)語言都允許你將數(shù)據(jù)從一種類型強轉(zhuǎn)或者轉(zhuǎn)換為另一種類型膜毁,這種操作有時候比其他操作更成功,如果試著將一個64位的整數(shù)轉(zhuǎn)換為較小的8位數(shù)據(jù)類型杂瘸,那么其他56位會怎么樣败玉,你的執(zhí)行環(huán)境可能會突然拋出異常运翼,或者悄悄地將你的數(shù)據(jù)的完整性降級,因此程序就會表現(xiàn)出不正常的行為 - 細則
低級別防御性代碼的編寫技巧有很多悠夯,這些技巧是日常編程工作的組成部分疗疟,包含在對現(xiàn)實世界的一種健康的懷疑當中策彤,下面幾條細則值得考慮:
提供默認的行為
遵從語言習慣
檢查數(shù)值的上下限
正常設(shè)置常量
約束:
我們?nèi)绾伟丫幊虝r候做的一些設(shè)想 切實地與我們的軟件聯(lián)系起來裹刮,從而使它們不再成為隨時會出現(xiàn)的問題捧弃?只需要編寫一小段額外的代碼來檢查每種可能出現(xiàn)的情況违霞。這段代碼充當每個設(shè)想的記錄买鸽,使設(shè)想從暗處走到了明處。通過這種方式看幼,就把約束編入到了程序的功能和行為當中诵姜。
當約束被破壞了茅诱,遭到破壞的約束不僅僅是一個簡單的容易找到和更正的運行時的錯誤,事實上我們已經(jīng)無時無刻不在做這種檢查和處理摆寄,它一定是存在于程序邏輯中的一個缺陷微饥,我們對程序可能做出的反應(yīng)有以下幾種:
- 對問題視而不見,期望最終不會因此出現(xiàn)任何差錯
- 做一些小改動肃续,使程序得以繼續(xù)運行始锚,如打印一份診斷報告棵里,或者將錯誤記錄到日志中
- 直接把程序打入冷宮殿怜,不讓它繼續(xù)運行下去稳捆,以立即受控或者非受控的方式終止程序
在許多不同的場景中都需要運用約束:
- 前置條件:這些條件是在輸入一段代碼之前必須保持為真的條件砖织,如果前置條件不成立侧纯,那是因為客戶端代碼的缺陷所致
- 后置條件:這些條件是編寫一段代碼之后必須保持為真的條件妹笆,如果后置條件不成立拳缠,那是因為提供者代碼的錯誤所致
- 不變條件:這些條件是每當程序的執(zhí)行到達一個特定點窟坐,如循環(huán)中,方法調(diào)用等等徙菠,時都保持為真的條件缺狠,如果不變條件不成立儒老,則意味著程序邏輯存在錯誤
- 斷言:斷言是任何其他關(guān)于程序在給定位置狀態(tài)的陳述
#如果沒有語言的支持驮樊,實施上面的所列的前兩個條件將非常困難,如果一個函數(shù)有多個退出點练湿,那么插入后置條件就會非常麻煩肥哎。Eiffel在核心語言中支持前置和后置條件,并且也可以確保約束校驗不帶有任何副作用杈女。雖然很乏味,但是代碼中所表達的好的約束可以使你的程序更加清晰吊圾,也更加易于維護达椰,由于約束構(gòu)成了代碼段之間一個不可改變的契約,所以這一技術(shù)也稱作“契約式設(shè)計”(design by contract)
- 約束內(nèi)容
a. 檢查所有的數(shù)組訪問是否都在邊界內(nèi)项乒;
b. 在廢棄的指針之前斷言指針是非零的啰劲;
c. 確保函數(shù)的參數(shù)有效檀何;
d. 在函數(shù)的結(jié)果返回之前對其進行充分的檢查呈枉;
e. 在操作對象之前證明他的狀態(tài)是一致的;
f. 警惕代碼中會寫下的注釋“不應(yīng)該執(zhí)行到這里”的任何一段埃碱;
g. 可讀性是衡量程序質(zhì)量的最佳標準猖辫,如果一個程序易于閱讀,那么這個程序就可能是一個好的程序
h. 在主要的函數(shù)中放置前置條件和后置條件砚殿,并且在關(guān)鍵的循環(huán)中放置不變條件就已足夠 - 移除約束
通常只有在程序構(gòu)建的開發(fā)和調(diào)試階段啃憎,才需要這種約束校驗,當用約束使自己確信(無論實際對錯)程序的邏輯是正確之后似炎,理論上就應(yīng)該移除它們辛萍,從而節(jié)省很多不必要的運行時的開銷
進攻性編程:需要主動嘗試打破代碼中的常規(guī)悯姊,而不只是防止問題的發(fā)生,也就是贩毕,不是保護代碼悯许,而是主動進攻代碼,這種策略也叫測試辉阶。嚴格的測試對軟件考法具有不可低估的正面影響先壕,可以極大地提高代碼質(zhì)量,并且使開發(fā)過程變得穩(wěn)定起來
都應(yīng)該成為進攻性程序員
筆者等項目時間寬松點的時候谆甜,在繼續(xù)搜集進攻性編程資料垃僚,然后再整理
總結(jié):
編寫不僅正確而且優(yōu)秀的代碼非常重要,這需要積累下所有已作出的設(shè)想规辱,這將會使得維護更加容易谆棺,也會使得錯誤減少。防御性編程是一種預(yù)想最壞的情況并為之做好準備的方法罕袋。這是一種可以防止簡單的錯誤變得難以找到的錯誤的技術(shù)
與防御性代碼一起使用編入代碼的約束改淑,可以使你的軟件更加健壯。與其他好的編碼習慣浴讯,如單元測試一樣溅固,防御性編程也是明智的并且比較早的多花一些額外的時間,以便在以后節(jié)省更多的時間兰珍,精力和成本,這樣可以使整個項目免于crash
如果錯誤攻破了你謹慎的防御询吴,你將需要一種策略來驅(qū)逐它們
防御性編程是編寫安全的軟件系統(tǒng)的關(guān)鍵技術(shù)
你必須記錄前置和后置條件掠河,不然別人怎么知道它們的存在?如果你指定了任何約束猛计,就可以添加防御性代碼來斷言它們
何時進行防御性編程唠摹?
你是否在事情不順利時候才開始這么做?或者在整理了一些你不理解的代碼的時候才開始奉瘤?這樣是不對的勾拉,應(yīng)該從始至終地使用這些防御性編程的技巧。成熟的程序員已經(jīng)從經(jīng)驗中得到教訓盗温,在吃過不止一遍的苦頭后才明白增加防御措施是明智的藕赞。
在開始編寫代碼的時候就應(yīng)用防御性策略,比改進代碼時才應(yīng)用要容易的多卖局。如果恨晚才試著將這些策略強加進去斧蜕,就不可能做到萬無一失。如果在問題出現(xiàn)后才開始添加防御性代碼砚偶,實際上是在調(diào)試批销,被動地做出反應(yīng)洒闸,而不是積極地防患于未然
然而在調(diào)試的過程中,甚至在添加新的功能時候均芽,會發(fā)現(xiàn)一些希望驗證的情況丘逸,這常常是添加防御性代碼的好時機
糟糕的程序員:
不愿意去考慮他們的代碼出錯的情況
為繼承才發(fā)布可能會出錯的代碼,并希望別人會找到錯誤
將關(guān)于如何使用他們代碼的信息緊緊攥在手里掀宋,并隨時都可能將其丟掉
很少思考他們正在編寫的代碼深纲,從而產(chǎn)生不可預(yù)知的和不可靠的代碼
優(yōu)秀的程序員:
關(guān)心他們的代碼是否健壯
確保每個設(shè)想都是顯式地體現(xiàn)在防御性代碼中
希望代碼對無用信息的而輸入有正確的行為
在編碼的時候認真思考所編寫的代碼
編寫可以保護自己不受其他人或者程序員自己 愚蠢傷害的代碼
不定期更新 不合適的地方 還請指點~ 感激不盡