【笔记】编程的原则:改善代码质量的101个方法
代碼必然被修改
Code will be changed
代碼不是寫完就結束了,它在日后必然會被修改。沒有寫完就扔的一次性代碼。
在編寫代碼的時候,我們應將“代碼會被修改”這一點作為進行判斷和選擇時的優先考慮事項。
為什么?
軟件在本質上具有復雜性,這就決定了它不可能是完美無缺的。軟件在發布后必然會發生故障,這時我們就需要對故障進行修復。
另外,用戶可能在軟件發布后產生新的需求,因為有些問題只有等到用戶實際使用軟件之后才能被發現。任何軟件都不可能在首次發布時就滿足用戶所有的需求。
除用戶自身之外,用戶所在商務環境的變化也會導致需求發生變化。軟件必須迎合這種變化。如果執著于最初編寫的程序,做出沒有人用的軟件,那么一切都是徒勞。
怎么做?
編程中的任何一個判斷都要以代碼會被修改為前提。也就是說,編寫的代碼要經得起修改。
因此,提高代碼的可讀性就顯得尤為重要了。代碼這種東西,讀遠比寫要費時間。如果以代碼會被修改為前提,那么不管寫代碼需要耗費多少時間,只要讀代碼的時間能夠縮短,我們就能把消耗在寫代碼上的時間賺回來。
特別是接手其他人寫的代碼時尤為明顯,有一次我接手了一個反編譯后得到的.NET工程,由于是反編譯后得到的代碼,所以是沒有任何注釋的,當時花了2,3天的時間才弄懂涉及到新需求的邏輯,之后為了完成需求而做出的修改所花費的時間比閱讀代碼的時間少的多。要是代碼里有注釋應該能節省不少時間。
KISS原則
Keep It Short And Simple(讓代碼保持整潔)
編寫代碼時,要優先保證代碼的簡潔性。
不管是從零開始編寫代碼,還是修復故障或擴展功能,都要注意保持代碼簡潔。
為什么?
隨意修改代碼會使代碼變得越來越復雜,越來越沒有秩序。
復雜的代碼可讀性較差且難以修改。強行修改不僅會降低代碼的質量,還會浪費時間。這樣一來,我們就無法保證能在合適的時間發布修正版或者對軟件進行更新。如果我們沒有重視這個問題,依舊強行修改代碼,代碼就會變得沒有人能看懂,最終腐化為無用之物。
而一份簡潔的代碼,其各個組成要素也是簡潔的,各要素承擔的職責也都降到了最小,各要素之間的關系也比較簡單。因此,簡潔的代碼可讀性高,容易理解,便于修改。各要素職責明確,使得測試也變得簡單易行。程序員之間能更加輕松地通過代碼進行交流,減少了在現實世界中多余的對話,節約了交流成本。這樣,我們就能保證在不降低開發速度的情況下對軟件進行長期維護。
代碼必然會被修改,因此易于修改的特性對代碼來說不可或缺。保持代碼簡潔可以使代碼擁有易于修改的特性。
怎么做?
下面幾種情況會讓代碼變得復雜,應該盡量避免:
1. 試圖使用新學會的技術
學會一門新技術后,人們傾向于使用新技術寫出一些無謂的代碼。
但是,代碼并不是用來炫耀聰明才智的,它的作用是給用戶提供價值。我們不能在代碼上耍聰明。
我們要多多斟酌代碼的寫法,努力保持代碼簡潔。
2. 以備將來之需
有時人們覺得將來會用到某些功能,認為最好趁現在寫下來,于是編寫了過剩的代碼。
現在用不到的東西就不應該現在寫,因為在大多數情況下,這些東西將來也用不到。
我們應該只寫當前需要的代碼,保持代碼簡潔。
3. 擅自增加需求
程序員有時會擅自增加需求,添加多余的代碼。他們覺得,某個需求必要與否、正確與否,與其找用戶確認,不如自己直接寫出來。但是,需求是由用戶決定的,程序員不可以擅自增加。
一旦添加了不必要的代碼,花費在維護上的時間就會像滾雪球一樣增加。不寫多余的代碼是保證代碼簡潔的秘訣。
DRY
Don't Repeat Yourself(不要重復)
將整個邏輯隨便復制粘貼到其他地方去用是造成代碼重復的主要原因。這樣一來,同一個邏輯將出現在多個地方。
直接將常量寫入代碼也會造成代碼重復。如果意義相同的常量在多處使用,常量表達的信息就會重復出現多次。
為什么?
代碼一旦出現重復,故障修復、添加功能等,代碼的改善措施就會變得難以實施。具體來說,我們會遇到以下困難:
1. 代碼的可讀性下降
相同的代碼出現多次,從量的角度來看是“代碼量變大”,從質的角度來看是“復雜度變高”。顯然,代碼的可讀性會下降。
無法準確理解代碼就無法確立修改方針。
2. 代碼難以修改
當相同的代碼出現在多處時,只有正確修改每一處代碼,才能確保整體的一致性。稍有不慎,修改就會出現遺漏。
另外,即使代碼完全相同,有時某些地方也用不著修改。在這種情況下,我們就需要閱讀前后代碼,判斷這一處是否需要修改。
若當前重復的代碼之間存在細微差別,我們就需要更加深入地閱讀各個位置的代碼??刂普Z句的條件內容或條件數量只要存在一點點差別,理解的難度就會進一步增大。弄不好代碼會因無法解讀而得不到改善。
3. 沒有測試
出現重復的代碼大多是遺留代碼,也就是說,這部分代碼沒有經過任何測試。
在沒有測試的狀態下,就算我們拼盡全力去修改遺留代碼,發生新故障的概率還是很大。
就算克服了上述所有困難,費盡九牛二虎之力完成修改,這些代碼也會因為動了多個“大手術”而變得更加混亂。長此以往,當混亂蔓延至所有代碼時,修改就會變成一個不可能完成的任務。
怎么做?
我們可以通過對代碼執行抽象化操作來消除重復。
對代碼的邏輯執行抽象化操作,其實就是給整個處理命名,將其函數化、模塊化。至于數據,則需要起個名字定義為常量。最后將重復的部分全部置換為抽象后的內容。
抽象化有以下幾個優點:
- 減少了代碼量,減輕了閱讀負擔
- 因為邏輯和數據有了名稱,所以代碼的可讀性變高了
- 重復的代碼集中到了一處,我們只對這一處進行修改即可。于是,代碼的修改操作變得簡單,代碼的質量也得到了保證
- 抽象化的部分易于重復使用。在添加新功能的時候,重復使用代碼可以更快、更好地完成編程
不過,執行抽象化操作需要我們跨越心理方面的障礙。比如將邏輯轉化為函數的操作就相當費時間,我們需要有足夠的耐心。另外,由于我們修改的是原本可以運行的代碼,所以修改后的代碼存在不能正常運行的風險。抽象化操作還有一個最明顯的缺點,那就是太麻煩。
然而,避免重復這一點沒有商量的余地。從長遠看來,避免重復的利大于弊,這是歷史總結出來的結論。所以,即便要花時間重構,即便要花時間消除代碼不能正常運行的風險,即便操作起來有些麻煩,我們也要消除重復的代碼。
設計模式就是具有代表性的一種設計手法,它提供了代碼結構模式以達到重復使用代碼的目的。從另一個方面來看,設計模式也可以說是一種防止重復思考(重復思考同一問題的解決方案)的手法。
性能調優的箴言
Proverb of performance tuning
是什么?
所謂性能調優,就是編寫運行速度快的代碼。性能調優也稱為代碼優化。
很多人認為加快代碼的運行速度是一件好事。但實際上,過早優化代碼會產生各種問題。
因此,對于代碼優化,我們要遵守以下規則。
① 不要在編程之初就對代碼進行優化
② 編程之初暫時不要對代碼進行優化(適用于專家)
代碼優化并不是我們在編程之初就應該考慮的事情。在編程時,我們要注意的是代碼的正確性和可讀性,編寫高質量的代碼,而不是想方設法讓代碼的運行速度變快?!?/p>
為什么?
優化代碼需要我們付出無法接受的代價。即便完成優化,代碼也會失去一些重要的東西,比如以下幾點。
1. 可讀性變低
優化后的代碼肯定比優化前的代碼更難懂。
因為從性質上來說,優化所做的工作是修改代碼中原本簡單直接的邏輯。優化代碼后,邏輯不再簡單明快,變得難以表達意圖。也就是說,要提高性能,必須以失去邏輯清楚的設計和降低代碼的可讀性為代價。
最大限度優化過的代碼非常難看,我們很難掌握它的處理過程。
2. 質量變差
代碼復雜化會導致代碼的可讀性下降,從而降低代碼的質量。在沒有明確描述算法過程的代碼中,故障很容易被漏掉。
不論回答的速度有多快,答不出正確答案也枉然。說得諷刺一點,優化在給代碼加入難以發現的新缺陷方面算是一種切實有效的方法。
3. 復雜度增大
優化會利用特殊的后門強化模塊間的依賴性,提升代碼的結合度,讓代碼能夠利用一些平臺固有的功能。
用如此取巧的方式編寫代碼會增加代碼的復雜度,同時讓代碼失去可移植性。
慢慢地,代碼將越來越不符合優質代碼的條件。
4. 阻礙維護
代碼復雜化導致代碼的可讀性下降,從而提升了維護代碼的難度。
首先,問題難以被發現,因為代碼優化之后,不自然的描述會增加。這樣一來,我們就很難追蹤處理的流程了。也就是說,優化后的代碼是高風險的危險代碼。
再者,優化還會對代碼的可擴展性產生不好的影響。優化是在給代碼設置更多前提條件的基礎上實現的。因此,優化會限制代碼的通用性和可擴展性。
5. 與環境相沖突
在大多數情況下,優化只能在特定的環境中發揮作用。在某個特定環境下對代碼進行優化后,代碼在其他環境中運行的效率可能會變低。
比如我們針對某個特定種類的處理器選用了最合適的數據類型。這種做法就可能會導致軟件在其他處理器上執行的速度變慢。
6. 工作量增多
對代碼進行優化就等于多加了一項工作。
程序員要做的工作非常多。代碼如果能成功運行起來,我們就應該先去處理其他緊急的工作,而不是去對代碼進行優化。
優化是一項非常耗時的工作。找到問題出現的原因并對代碼進行優化并不是一件容易的事情。一旦弄錯優化對象,就會浪費大量寶貴的勞力。
怎么做?
我們要先寫高質量的代碼,然后根據需要進行優化。
高質量的代碼是在信息隱藏的原則下寫出來的。因為各個決定只會在局部范圍產生影響,所以代碼的修改不會影響到其他部分。先寫高質量的代碼再調節性能效率更佳。
況且在大多數情況下,“高質量”與“高性能”并不矛盾。按照上述優先順序寫出來的代碼只要滿足高質量代碼的要求,優化時就不會產生多少新的工作。而且代碼質量高,我們在做添加工作時也會輕松一些。
另外,寫完高質量的代碼之后,如果要進行優化,一定要思考其必要性。優化在很多時候不值得我們花費那么多的時間和成本。是否進行優化,要在與解決故障、添加新功能和發布產品等重要工作相比較之后再決定。
影響軟件的性能的幾個因素:
從整體來看,除了代碼,軟件的性能還受到很多因素的影響。比如以下幾個因素。
- 執行環境
- 部署的設置或者安裝的設置
- 使用的中間件
- 使用的庫
- 相互運用的舊系統
- 架構
這樣一看,一行一行的代碼對軟件整體的影響十分渺小。除了各行代碼之外,還有很多影響性能的因素?!?/p>
性能調優的流程:
在實際工作中,很多時候我們會因為軟件的特性而需要對代碼進行優化。
在對代碼進行優化(性能調優)時,我們需要在流程方面遵守幾項規則。
1. 證明優化的必要性
首先要再三確認優化的必要性。有時候用戶對某部分性能的需求并沒有程序員想的那么高。
2. 測量性能,找出瓶頸
確認需要優化后,我們要先找出瓶頸所在。
性能出現問題并不代表所有代碼的運行速度都很慢,大多是某個特定部分占用了較長時間。這個占去大部分處理時間的部分稱為“熱點”。
我們要全身心地尋找這個熱點。
3. 優化瓶頸部分的代碼
發現熱點之后要對其進行修改。
4. 測量性能,確認優化效果
不管是代碼優化前還是代碼優化后,我們都必須對性能進行測量。
性能差的部分是無法推測出來的。優化的效果也只能通過測量得知。
5. 驗證優化后的代碼是否存在運行問題
優化可能會使代碼出現一些新的問題。對代碼進行優化后,必須認真檢查代碼是否存在運行問題。
這里再說一下尋找熱點的方法。尋找熱點時,應利用分析工具,盡可能仔細且準確地檢查代碼。
另外,由于優化過程中要多次對代碼的性能進行測量,所以為了提高效率,我們最好對這部分工作執行自動化處理。
一步一步走
One by one
是什么?
編程時要一次只做一件小事。
一件一件做,一點一點來,就像上臺階一樣一步一步走。不要一次性處理多項工作。
完成一個小任務后認真檢查,沒有問題后再開始下一個任務,如此循環?!?/p>
為什么?
一次處理一項工作的工作方式更有效率,最終產品的質量也更好。
一步一步進行編程,最后一步操作撤銷起來也會比較容易。
一步一步進行編程,工作檢查起來也比較簡單。
一步一步進行編程,新舊代碼的替換也會更安全。
一步一步編程意味著程序員能夠掌握和控制代碼的狀態。這樣做能去除不確定因素,讓人安心工作。
在有心理壓力時,人很難像平時一樣做出準確的判斷。控制好自己的狀態也是寫出優質代碼的必要條件之一。
怎么做?
不一次性處理多項工作
邏輯思考的秘訣
關于邏輯思考,有幾個關鍵點需要我們了解。
- 想立刻獲得答案的態度是不正確的。一眼看不出答案時應當繼續思考
- 沒有經過深思熟慮就下結論的做法是錯誤的。發現某個東西可以滿足條件時不能想當然,要探討其他的可能性
- 避免反復思考同一件事
- 直接用腦子思考有些困難,不如邊寫邊思考。邊寫邊思考能產生額外的效果。對于想不明白的地方,有時候寫下來一看就明白了
- 直覺對邏輯思考來說也很重要。比如,當我們感覺“創建矩陣有助于整理信息”時,不妨先試一試。不過,直覺只能用在思考的過程中。僅憑直覺來獲取答案的行為只能說是瞎猜,這可不是一個好習慣
布魯克斯法則
是什么?
增員等于“火上澆油”
對于開發進度滯后的軟件開發項目,如果為了趕進度而在開發后半程添加人手,反而會使延遲情況進一步加重。
在項目尾聲,當我們發現產品無法如期交付時,常會投入更多的人手。但這種做法只會火上澆油。
為什么?
人數和月數是無法交換的
項目的工時是用人數和月數換算的,也就是幾個人用幾個月完成某個項目,所以用“人數×月數”來計算項目工時。
這里要注意的是,該乘法運算與數值的乘法運算不同,人數和月數不能調換。也就是說,“人數×月數= 月數×人數”的式子是不成立的。
比如一個12人月的項目,客戶要求6個月內開發完成,那么我們只要投入2人即可。如果人數和月數可以調換,那么當客戶說這個項目比較急,需要在2個月之內完成時,我們只要投入6個人就行了。
然而在現實中,“6×2”和“2×6”并不相同。2人工作的效率與6人工作的效率不可同日而語。
理由如下。
1. 因存在依賴關系而產生額外的負擔
如果每個人的工作相互獨立,那么在人數是原來3倍的情況下,生產效率也會變為原來的3倍。
然而一般來講,工作分割之后,各項工作之間會產生依賴關系。
如此一來就會產生一些新的負擔,如任務的分割、各項確認工作的出現以及通信路徑的增加等。
即便追加人手,這些額外的負擔也會拖慢項目的進度。
2. 培訓新人會占用一定時間
在追加人手時,為了能讓這些人發揮作用,必須讓他們學習當前項目固有的各種知識、信息以及技術。也就是說,要花時間對新人進行培訓。此外,負責培訓的人是同一個項目內的成員,這就導致新團隊的整體生產效率下滑。
在新人真正發揮作用之前,整個項目的進度都是滯后的。
怎么做?
重新制訂時間表
無條件地投入更多人手來趕上進度是一種不明智的做法。
強行給當前成員增加負擔也只會對項目造成損害。
進度滯后最好的解決方法是重新制訂時間表。在此過程中,要與用戶做好協調,同時決定各個功能的優先程度,進行階段式發布。
拓展:
人與人也不可交換
前面說過,人數與月數不可交換。從某種意義上講,人與人也是不可交換的。
一個程序員離開了,并不是再補一個程序員就行。之所以這么說,是因為程序員的水平參差不齊。
在物理空間內的生產效率方面,有能力的人與沒能力的人之間的差距最多也就幾倍。但像程序員這種以信息空間為主戰場的人,由于不受物理方面的制約,各個程序員之間的生產效率有很大的差別。據說能差30倍。
不過,“同樣的時間內能寫出多少代碼”這種生產效率上的差距并不是造成上述現象最根本的因素。某些方面的差距更根本且更巨大。
比如以下幾個方面。
- 有能力 / 沒能力
有些人寫出的代碼能用,有些人寫出的代碼不能用。這是一個有與無的比較,計算差距已經沒有意義了。
- bug多 / bug少
有些人寫出的代碼沒有bug,有些人寫出的代碼到處都是bug。
二者的維護成本會出現巨大的差別。
- 執行速度快 / 執行速度慢
有些人寫出的代碼執行速度快,有些人寫出的代碼執行速度慢。代碼的執行速度慢意味著會浪費用戶的時間。軟件的目的是實現業務的高效化,為用戶節省更多的時間。代碼執行速度慢的話就違背了這一目的。
況且,代碼執行速度慢還會引來用戶的投訴。這時,我們不僅要花時間應對用戶投訴,還會失去用戶的信任。
- 代碼可讀性高 / 代碼可讀性低
有些人寫出的代碼可讀性高,有些人寫出的代碼可讀性低。
另外,有些人寫出的代碼便于修改,有些人寫出的代碼一經修改就會出問題。
二者由此產生的優化成本大不相同。代碼質量差到一定程度時甚至無法優化。
綜合上面幾點來看,有能力的程序員和沒能力的程序員確實差出好幾個檔次。
有能力的程序員在項目中起到的作用非常大。對于這些有能力的程序員,我們不可以將他們視為可交換的“1人月”,要把他們留在項目中承擔固定的職責?!?/p>
防御性編程
是什么?
防患于未然的程序設計
我們在編程的時候不要想當然。
防御性編程與開車時的防御性駕駛是同一種思路。
在采取防御性駕駛這一駕駛方式的情況下,我們總抱有一種不知道其他駕駛員會做出什么事情的心態。也就是說,自己不認為駕駛的過程是百分之百安全的,覺得中途可能會發生什么事。這樣一來,當其他駕駛員做出一些危險的行為時,自己就能做好充分的準備不受傷害。即便過失在其他駕駛員身上,自己的命也要由自己來保護。防御性駕駛體現的就是這樣一種心理。
與此類似,當函數接收到非法數據時,即便問題出在其他函數身上,我們也應準備好“防御性”的代碼以避免函數受到損害。為此,編程時要注意以下幾點內容。
1. 確認外部代碼傳來的數據輸入值(檢測“預想之內的錯誤”)
在從文件、用戶接口、網絡以及其他外部接口獲取數據時,要確認數據是否在合法范圍內。比如檢查數值是否在有效范圍內、字符串的長度是否符合規定等。
盡量在較早的階段檢測出無效輸入。檢測出無效輸入后要迅速對其進行適當的錯誤處理。
2. 確認參數的值(檢測“預想之外的錯誤”)
確認其他函數傳來的參數的值。與檢測外部代碼傳來的數據不同,這里如果檢測出無效輸入,就意味著代碼存在bug。
我們可以使用斷言確認參數,在發現非法值時立刻停止程序。
為什么?
開發與運維中的“安全駕駛”
開發中的“安全駕駛”
提早發現非法數據能提升調試的效率,因為提早檢測出非法數據,并以明確的形式進行通知,可以幫助我們立刻找到出現問題的地方。這樣一來,代碼的調查與修改都變得非常容易。
反過來,如果沒能提早檢測出非法數據,那么故障就會蔓延到其他地方,這時我們就需要花費更多的時間來尋找根本原因。
運維中的“安全駕駛”
盡早處理非法數據能防止運維中出現的問題進一步擴大。在較早的階段處理掉問題,能防止問題的蔓延。
錯誤如果處理得不徹底就會蔓延到其他處理中,問題會變得越來越大。特別是當錯誤的數據進入軟件深處時,軟件的運行可能會發生錯誤,或者錯誤的數據會進入數據庫中,這將造成無法挽回的后果。
其中最棘手的當屬安全問題。黑客在入侵系統時,喜歡利用沒有徹底處理錯誤的地方。可見,不完備的錯誤處理也可能會成為安全漏洞。
怎么做?
路障戰術
我們需要采用“路障戰術”。建立路障,將損害控制在一定的區域內。
船體由多個相互隔離的區域組成,這與路障戰術是同一種戰略思想。即便船撞上冰山,船體破損,只要隔離破損的區域,整個船體就不會有沉沒的危險。
另外,建筑物中的防火墻與路障戰術也有異曲同工之妙。防火墻的作用在于防止火勢蔓延。
為了在代碼中建立路障,我們需要將特定的接口用作安全地帶與非安全地帶的分界線。檢驗通過這條分界線的數據,一旦發現非法數據,立即采取適當的措施。
這就好比手術室,所有東西都必須經過消毒才能拿進去。因此,通過大門進入手術室的東西都是安全的。
以門(= 路障)為界,分界線的左側是“臟房間”,右側是“干凈的房間”?!?/p>
在代碼設計中,我們要明確“哪些東西可以進入手術室”“哪些東西不能進入手術室”,以及“門的位置”,也就是對安全地帶里面的模塊、安全地帶外面的模塊和在中間負責消毒的模塊進行分工。
拓展1:
錯誤處理的變種
對于預想之內的錯誤,不同的情況有不同的處理方式。具體來說有以下幾種處理方式。
- 返回無害的值
在確認某值無害的情況下,返回該值。
比如在數值計算的情況下返回0,在字符串計算的情況下返回空字符串,在指針計算的情況下返回NULL。
- 使用下一個數據
在處理一連串數據的情況下,返回下一個有效數據。
以從數據庫讀取記錄為例,如果記錄無效,則一直讀取,直到發現有效記錄。
- 返回和前面一樣的值
如果不會對結果造成重大影響,則返回和前面一樣的值。
以1秒內讀取100次溫度計的代碼為例,如果其中有一次讀取失敗,在這種情況下,即使返回失敗前最后一次讀取的值,也不會有什么問題。
- 使用近似值
在滿足一定的嚴密性的前提下,返回近似值。
比如在能顯示0℃~100℃的溫度顯示畫面中,溫度低于0℃時顯示0℃,高于100℃時顯示100℃。
- 在日志中記錄警告信息
在日志文件中記錄警告信息后繼續執行處理。
當發生微小的錯誤時,忽略錯誤繼續執行處理有時是一個很好的選擇。不過,發生過的錯誤一定要記錄下來。
- 返回錯誤
為了調用上游函數來處理錯誤,我們要將檢測出來的錯誤記錄在報告中。
在這種情況下,決定讓代碼的哪個部分負責處理錯誤,哪個部分負責報告錯誤就變得至關重要。
我們可以使用模塊的狀態變量、函數的返回值,或者通過拋出異常來報告錯誤。
- 調用錯誤處理函數
錯誤處理要交給共同的錯誤處理函數來完成。
將錯誤處理的責任一元化能降低調試的難度。不過,這個一元化的功能會使代碼整體產生較高的耦合度。因此,如果想把一部分代碼用到其他系統中,就需要連同錯誤處理算法一起“搬家”。
- 顯示錯誤信息
在發生錯誤的地方顯示錯誤信息。
將錯誤處理的開銷抑制到最小。不過,由于信息會分散在軟件各處,所以創建具有統一性的用戶接口、區分用戶接口與其他部分、將軟件轉換為其他語言等工作變得難以實施。
- 終止處理
檢測到錯誤后終止處理。
這個方法對重視安全性的軟件來說非常有效。在關鍵任務系統中,比起帶著錯誤繼續處理,很多時候重新啟動程序會比較好。
- 各部分選擇最合適的方式處理錯誤
選擇何種方式處理錯誤,由負責設計與實現錯誤發生部分的程序員來決定。
這給了程序員很大自由,但從軟件整體來看,錯誤處理將失去統一性。
拓展2:
錯誤處理中的“正當性”和“堅固性”
錯誤處理中有“正當性”和“堅固性”兩種思路。
正當性指一定不返回不正確的結果。與其返回不正確的結果,不如什么都不返回。
而堅固性指為了讓軟件繼續運行而不擇手段。即使會產生不正確的結果,也要讓軟件繼續運行下去。
以哪種思路為先,就要看軟件的目的是什么了。
重視安全性的軟件要以正當性為先。與其返回錯誤結果,不如直接停止軟件。以醫療相關的管理軟件為例,相較于返回錯誤結果繼續處理,通知錯誤并停止軟件更能防止重大事故的發生。
而對于提供給用戶的軟件,堅固性就要優先于正當性了。以文字處理軟件為例,比起軟件突然關閉導致大量寶貴的輸入數據丟失,帶著錯誤繼續運行所造成的損失更小。
拓展3:
不忽視錯誤代碼
不忽視錯誤代碼是防御性編程的鐵則。
即使函數返回錯誤代碼,接收方也有可能會忽視掉它。但是,我們一定要養成評價函數返回值的習慣。即便某個函數在理論上不會發生錯誤,保險起見我們也要對其進行檢查。因為防御性編程的目的就是防止預料之外的情況出現。
自己編寫的函數不能忽視錯誤,系統函數同樣不能。每次進行系統調用都要檢查錯誤代碼。
發現錯誤之后,要在日志中輸出錯誤編號以及錯誤的詳細內容?! ?/p>
破窗效應
是什么?
不好的代碼是“蟻穴”
如果大樓這類建筑物上有一扇長期未被修理的窗戶,這棟大樓就會給人一種“被遺棄”的感覺。人們便不會再留心這棟大樓的狀態。
這樣的話,還會有窗戶繼續碎掉。接著是垃圾亂倒,滿墻涂鴉。別看只是破了一扇窗戶,如果放置不管,整棟建筑也會遭到嚴重的破壞。
軟件也會發生這樣的事情。如果對軟件的“破窗”,也就是那些不好的設計、錯誤的決定或不好的代碼放置不管,那么不論它多么微不足道,也能在很短的時間內讓整個軟件腐爛。
為什么?
不好的代碼會帶來邪念
軟件中一旦存在“破窗”,程序員的腦中就會不自覺地產生“剩下的代碼肯定也是一團糟,隨便改一改算了”的想法。
關于這種現象,有一個叫作“信箱實驗”的著名心理學實驗。如果自家信箱附近的墻壁上有涂鴉,或者信箱附近有垃圾,那么該信箱中信件被盜的概率就會達到25%。僅僅是一些垃圾和涂鴉,就能將許多正直人士變成小偷。
除了從眾心理之外,我們也可以用“莫名的不安”這種心理來解釋為什么會出現這種現象。一扇被棄之不管的破窗戶,會讓人產生“在這附近遇到危險的話肯定沒人來救”的想法,隨之讓人產生不安的情緒。即便是一些細枝末節的東西,如果總是以一種沒有得到處理的狀態擺在人們眼前,也會讓人漸漸變得神經質,使人的交感神經處于緊張狀態, 甚至促使人付諸暴力。
也就是說,出現這種現象的關鍵原因,與其說是“破窗戶”本身,不如說是小小的問題被棄之不管而帶來的“不安”。相較于時間短強度大的精神壓力,人們對時間長強度小的精神壓力更加敏感。當某些有違社會道德的現象一直出現在我們的眼前時,人就會暴露出脆弱性。
怎么做?
保持代碼整潔
我們不能對代碼的“破窗”,也就是代碼不好的部分放置不管,要在發現“破窗”的時候立即進行修補。沒有了“破窗”,代碼就能保持整潔的狀態,這樣一來,程序員便會小心翼翼地對待這些代碼,避免弄臟它們。就算交付日期近在眼前,也沒人愿意當第一個弄臟代碼的人。
另外,如果沒有足夠的時間修復代碼,至少要簡單明了地指出“這段代碼不好”。
比如對于自己認為不好的地方,可以添加帶標簽的注釋以顯示在IDE(Integrated Development Environment,集成開發環境)的任務列表里。這么做的目的是強調這些不好的地方已經得到了管理,防止損害進一步擴大。
擴展:
人會模仿人
破窗效應既與“莫名不安”的心理因素有關,也與“反射性模仿他人行為”的人類自身特性有關。
心理學中已經證實,人類在嬰兒時期就已經具備“反射性模仿他人行為”的特性了。不過,這個特性需要有足夠長的時間才會顯現出來。如果人們長期處于一種低素質的“習慣性懈怠”的狀態,就會去模仿他人不好的行為,最終陷入惡性循環,這也可能是破窗效應出現的原因。
不過,不管是因為“莫名不安”還是“反射性模仿他人行為”,及時解決不好的代碼都是不變的應對策略。
熵增原理
是什么?
代碼會自然而然地開始腐壞
熵是物理學術語,表示體系的混亂程度。根據熱力學法則,人們證明了全宇宙的熵處于增加狀態。
軟件開發可以超越大部分的物理法則,卻逃不出熵增原理的束縛。如果不對代碼進行管理,其混亂程度就會不斷加深,直到突破極限。也就是說,代碼會逐漸轉向腐壞。
為什么?
代碼會向著混亂的方向轉變
代碼變得越來越混亂是軟件開發中自然而然的事情。
不管開頭多么有序,只要過上一陣子,代碼就會開始腐壞。就像生肉放久了會變質一樣,隨著時間的推移,代碼的腐壞程度會越來越深。臃腫的代碼越積越多,使得維護難度不斷增大。用不了多久,即便是很小的修改都需要耗費大量勞力,迫使我們不得不重新設計軟件。
在這種情況下,重新設計軟件很難一帆風順。如今的軟件日新月異,新的設計必須能跟得上時代的變遷才行。
也就是說,我們就算有非常明確的目標,也難免會跟不上步調,因為我們在實際工作時打的是“移動的靶子”。
怎么做?
抓住代碼腐壞的征兆
代碼開始腐壞時有幾個征兆。不要放過這些征兆,發現它們后立刻處理。
- 刻板
刻板指不容易修改代碼。
僅僅因為一處修改,就需要對所有與其存在依賴關系的模塊進行修改,我們稱這種代碼設計為刻板的設計。
刻板的設計會給我們帶來很多困擾。比如我們接到委托,要對代碼做一個很簡單的修改,于是簡單調查了需要修改的地方,預估了工作量。然而在實際工作時,隨著工作的推進,我們還是要對其他預想之外的地方進行修改。結果,工作量遠遠超出預估,我們只能在規模龐大的代碼中追查需要修改的地方。
- 脆弱
脆弱指一處修改會對其他部分的代碼造成很大損害。脆弱的代碼甚至會損壞與其完全不相關的代碼。因此,程序員在處理新問題時就可能會引發其他問題,這就使程序員陷入追著自己尾巴跑的狀態。
毫不夸張地說,脆弱的模塊并不罕見。這類模塊很容易辨認。那些需要經常修復的模塊、常年出現在故障列表中的模塊、程序員認為需要重新設計的模塊,以及越修復質量越差的模塊等就屬于脆弱的模塊。
- 可移植性差
可移植性差指軟件難以移植到其他環境中。
如果軟件在任何環境下分離可運行部分和依賴環境的部分都會出現困難并伴隨風險,我們就可以說該軟件不具備可移植性。
- 難以掌控
難以掌控指代碼難以掌控和開發環境難以掌控。
代碼難以掌控是指設計結構不具備靈活性。我們無法在保持設計結構的前提下輕松修改難以掌控的代碼。相較于能保持設計結構的方法,使用投機取巧的方法更能輕松地完成修改。在代碼難以掌控的狀態下,做錯事容易,做對事反而難。
而開發環境難以掌控常發生在開發環境效率低下的時候。比如,當編譯需要花費大量時間時,即使我們知道已經無法保持設計結構了,還是會傾向于采用能避免大規模編譯的修改方式。如果提交確認兩三個文件需要花費好幾個小時,我們就不會再思考保持設計結構的方法了,而是會尋找更節約時間的修改方式。
- 復雜
復雜指不必要的元素過多。
當程序員預判規格說明書會發生變更,在代碼中事先埋下應對機制時,就容易使代碼變得復雜。這類做法總給人一種好的印象。很多人認為預見未來并提早做出準備就能保持代碼的靈活性,防止今后苦于修改。
然而很遺憾,這樣做只會帶來相反的效果。為應對更多不測,我們會在代碼中留下大量一次都用不上的結構。這會讓代碼變得復雜,變得難以理解。
- 重復
重復指同樣的代碼出現多次。
在寫文檔時,復制粘貼是一個很好用的方法,但在編輯代碼時,使用復制粘貼則會招來很嚴重的后果。在代碼出現重復的情況下,修改軟件將成為一項勞神費力的工作。如果在重復的部分發現故障,就需要修改代碼中所有相同的部分。
況且,代碼有時候看上去相同,但實際上有著細微的差別,這時修改方式就可能不同了。
如果這種看上去相同但存在細微差別的代碼在軟件中大量出現,就表示程序員沒有做抽象化工作。如果能找出所有重復的部分,將其適當抽象化,消除重復,系統將更容易理解且更容易維護。
- 不透明
不透明指代碼難以理解。
代碼有時候很難讓人理解。而頻繁修改的代碼會隨著時間的流逝越來越難以讓人理解。
在剛寫完代碼時,代碼對編碼者本人來說是非常明了的,因為編碼者沉浸于開發,熟悉該項目的每個地方。然而過一段時間再回過頭來看,編碼者就會覺得自己怎么能寫出如此不堪的代碼。
為了防止此類情況發生,編碼者需要站在代碼閱讀者的立場思考,寫出別人能夠理解的代碼。讓別人來看自己寫的代碼是一個行之有效的方法。
80-10-10原則
是什么?
編程沒有萬能藥
我們在用高水平的工具或語言開發軟件時,可以在非常短的時間內實現用戶80% 的需求。而在剩下20%的需求中,有10的需求需要通 過一定努力才能實現,另10%則完全不可能實現。
因此,如果要100%滿足用戶的需求,開發就會陷入進退兩難的境地。
如果此時已經開發一部分內容了,那么拋棄原有工具重新開發就顯得不切實際。這時,我們就得放棄使用工具,用最笨拙的方式來滿足某部分需求。
為什么?
編程的問題領域太廣
軟件行業從20世紀90年代中期起,舉整個行業之力花費十幾年做了一場實驗。實驗內容是創造一款能夠讓能力平庸的技術人員的生產效率飛躍性提升的“萬能工具”,比如模型驅動開發、4GL(第四代語言)等。
實驗的結果顯示,使用單一工具很難在所有領域都獲得完美的成果。
人們創建這種工具是為了開發出更人性化、質量更好的軟件。因此,為了防止能力平庸的技術人員引發問題,人們對語言施加了相當強的功能限制。結果,工具產生了自己的“防守范圍”。
但軟件要處理的問題范圍是無限大的。用一個工具解決所有問題的“萬能藥”路線顯然走不通。
第二系統綜合征
是什么?
第二次發布總會出現功能過多的情況
由發布第一版軟件的程序員設計的第二版軟件會成為最危險的一個版本。
第二版軟件有功能過多、質量差以及功能的使用體驗較差等傾向。
為什么?
人在適應開發后會傾向于“多功能主義”
在開發第一版軟件時,由于未知的情況很多,風險較高,所以我們在進行判斷時會比較慎重。即便想到了好的功能,也會留到下一次再實現。
然而,在開發第二版軟件時,我們掌握了更多的信息,也有了自信,所以傾向于把之前保留的功能以及新想到的功能一股腦兒加進去。
添加過多功能之后,代碼變得復雜,不易維護。功能本身也變得復雜,使用體驗變差,結果添加的功能也沒能得到人們的青睞。不管是代碼還是實現的功能,質量都較以前有所下降。
另外,那些暫時保留的功能在第一版軟件中也許是比較實用的,但在第二版軟件中,這些功能可能已經失去了必要性,或者落后于時代了。也就是說,把這部分功能放到第二版軟件中實現是一種浪費時間的做法。
怎么做?
考慮用戶
程序員要有自制力,避免陷入多功能主義的怪圈。
要做到這一點,一個有效的做法就是重新對用戶進行定義并將用戶具象化。此時不論是有意識的還是無意識的,程序員對用戶的印象都會對程序員的判斷產生影響。這就給程序員添加新功能的欲望帶上了“枷鎖”。
具體做法就是在編程時多想想以下問題。
- 用戶是誰
- 用戶需要什么
- 用戶認為什么是必要的
- 用戶想要什么
拓展:
第二系統后綜合征
前面說程序員容易在第二版軟件中產生多功能主義的傾向,但實際上,第二版以后的版本也會出現同樣的情況。
特別是數據包軟件等需要持續發布的軟件,隨著一次次版本升級,沒用的功能會越來越多。
出現這種現象的原因可能是用戶群體不固定,程序員很難對用戶進行具象化。而且功能一旦發布就很難有機會刪除,所以只能越積越多。
不可否認,添加功能可以提升軟件的魅力。但是,相較于新功能,用戶往往希望基本功能是穩定的,或者基本功能的使用體驗能得到改善。
功能蔓延:
功能的過分擴張不能全部歸罪于程序員的一己私欲,毫無原則地滿足用戶的需求也是重要原因之一。
無條件滿足用戶的愿望,就會在軟件中增加大多數用戶用不到的功能,還要準備用于控制該功能的復雜的設置畫面以及相關設置文件。如此一來,軟件就會變得難以維護,故障頻出。
這種功能肆意增多的現象稱為功能蔓延(feature creep),該現象意味著軟件開始邁向破滅(或者已經破滅了)。
軟件設計的終極之美是“簡單”。越是簡單優質且擁有眾多用戶的軟件,越容易出現更多的需求。如果忠實地滿足這些需求,將所有功能都開發出來,軟件將失去簡單性,變成一款沒人用的軟件。這時我們就會陷入進退兩難的窘境。
避免出現這種悲劇的關鍵是要有勇氣對需求說“NO”。對于那些與軟件核心無關、需要與其他軟件組合才能實現的功能,我們要明確地說“NO”。只有這樣,才能產生優秀的設計,才能讓軟件保持簡單性。
不過,有時候我們很難拒絕用戶強烈的訴求。這時,我們不要直接在軟件主體中實現該功能,而是要圍繞軟件主體進行擴展,或者以插件的形式在不改變軟件核心代碼的前提下修改軟件的運行模式,以此來保持軟件主體的簡單性。
重新發明車輪
是什么?
制作已有的東西
有時候對于某種功能,明明有現成的代碼或庫可以使用,人們卻還要自己重新開發相同的功能。這就像專門花時間又重新發明一遍世上早就有的車輪一樣,是一種無用功。
有現成的東西,卻要去重新發明一個,這是在浪費時間。當開發規模足夠大時,其危害也是非常大的。想要一個“能運行各種服務的服務器”,于是專門把Web 服務器這種規模極大的軟件重新開發了一遍。這種做法會浪費非常多的時間。
而且在大部分情況下,相較于重新發明出來的東西,既有產品的質量更好。比如相較于我們現寫出來的庫,既有的標準庫更好,因為它不僅能反映出提供標準庫的專家的知識,還能反映出人們在使用過程中積累的經驗。標準庫還有一個好處,那就是就算我們什么都不做,隨著時間的推移,其中的故障、功能和性能也會自行改善。
另外,如果忽視標準規格,根據自己的協議編寫代碼,將來就只能走自己的路線了。僅靠本地的幾個程序員是不可能跟得上世間的主流的。另外,由于所有的插口都是獨創的,所以將來也無法實現替換。
為什么?
不知道車輪和想制作車輪
重新發明車輪的原因有以下兩種。
- 不知道車輪
程序員不知道車輪的存在。也就是說,這種發明不是程序員有意而為的。
這歸咎于程序員的知識不足和學習不足。編寫與語言標準庫功能相同的代碼,或者在有標準協議的情況下用獨創的格式編寫通信功能的代碼等都屬于這種情況。
- 想制作車輪
程序員有制作車輪的欲望。也就是說,這種發明是程序員有意而為的。
這是一種叫作“非我發明”(Not Invented Here,NIH)綜合征的問題。具體表現為某個東西原本沒有重新制作的必要,程序員卻出于對技術的興趣或排斥他人制作的東西而想重新制作一遍。
怎么做?
關注車輪之外的東西
我們要避免重新發明車輪,將重點放在本來應該做的工作上。
為此,在編寫代碼之前,一定要先確認是否存在相同功能的標準庫、開源庫,是否存在標準協議等。
另外,要借助團隊會議等機會從其他程序員處獲取信息。這樣就能避免團隊內出現重復勞動的情況。
同時,在團隊中徹底清除利己主義的思想。
因為想做而做,這是程序員自私的一面。然而,軟件的目的不是滿足程序員的欲望,而是滿足用戶的需求。為了用戶,為了在質量、開發時長和費用等方面做到最好,我們應該時常調查哪些東西可供使用,掌握高質量的開源工具或商用工具。
拓展:
許重新發明車輪的情況
有時我們也需要大膽地重新發明車輪。
- 商業目的
商業上的核心部分必須由自己制作。
在使用已有的東西時,必然會對該部分產生依賴。依賴則意味著對該部分失去了控制權。
即便知道其中潛藏著致命的問題,我們也無法主動去修改。就算可以委托他人修復,何時能夠發布,是否真的能得到改善,都是未知數。質量和交付期方面的問題很可能在商業上造成無可挽回的損害。
況且,使用已有的東西就意味著放棄該部分的“差別化”。因此,商業上的核心部分,從原則上來講都應該由自己制作。只有自己制作出這部分內容,并且花心思做出個性,從中積累經驗,才能開發出獨特的、能貢獻于世界的軟件。
- 學習目的
要成為優秀的程序員,就得不斷積累高質量的經驗。
軟件開發的模式、設計和編程等方面的好書有很多,然而讀書和實踐之間有很大的差別。
同樣,借用已有的代碼與自己從零設計、測試軟件,解決故障,提高軟件質量得來的經驗有天壤之別。
不過,有機會編寫軟件核心部分代碼的程序員少之又少。大部分程序員只能借用已有代碼。在這種情況下,我們不知道代碼內部是如何運作的,因此和使用“黑箱”沒什么區別。
只看水面的話,我們是無法得知水下隱藏著何種危險的。如果不知道水底究竟發生了什么,就不能靈活運用水流。自己親手制作是一種必要的經歷。為此而“重新發明車輪”是程序員學習、提高技術非常有效的一個方法。
當然,我們免不了失敗,但這種經歷也比直接拿現成的使用要寶貴。
親手從零開始寫代碼,進行各種嘗試,從一次次失敗中學習,能帶來不同于閱讀技術類圖書的好處。不過,讀書與實踐同等重要,它們對程序員來說都是不可或缺的?!?/p>
給牦牛剃毛
是什么?
抓不住問題的本質
有種家畜叫牦牛。它是牛的一種,特征是身上長著厚厚的毛。每當臨近夏天,牦牛就需要剃毛。我們需要給牦牛剃去相當多的毛才能讓它的皮膚露出來。
我們處理某些問題時就像給牦牛剃毛一樣,在解決問題的過程中總會有新的問題冒出來,讓我們難以抓住問題的本質。
這種狀態如果持續太久,人們就可能會忘記原本要解決的問題是什么。
為什么?
問題會接二連三地出現
問題總是接二連三地出現。
假設我們想導入在Web 服務器上運行的任務自動化工具,以提高工作效率。
“先下載Web服務器程序?!?/p>
“文件太大了,沒有辦法下載?!?/p>
“那就導入下載工具?!?/p>
“下載工具怎么不運行呀?”
“原來需要前置模塊啊?!?/p>
“那就下載前置模塊?!?/p>
“需要注冊用戶?!?/p>
“那就注冊一個吧。”
“誒?用戶注冊頁面不動了?!?/p>
“原來是瀏覽器版本太老了?!?/p>
“升級了瀏覽器,注冊了用戶,模塊也下載好了。”
“怎么下載工具還是不運行?”
“哎呀,需要操作系統的補丁包。”
(后面依然沒完沒了。)
這種像給牦牛剃毛一樣的情況會造成時間上的浪費。有時候,就算我們預估了工作所需時間,也沒有辦法在預估的時間內完成工作,這種情況發生的原因就是我們把時間耗費在了給牦牛剃毛上。
另外,在給牦牛剃毛的狀態下,人非常容易積攢壓力。我們很難推測出需要花多長時間才能把牦牛身上的長毛剃光。如果這種無法達成目標的狀態一直持續下去,人就會產生挫敗感。
怎么做?
盡早收手
當我們發覺自己已經陷入給牦牛剃毛的狀態時,應停下腳步,回想自己原本要實現的目標是什么。如果發現自己已經偏離了目標,或者從時間、成本的角度來看不適合再繼續操作下去了,應立刻停止工作。因為在這種情況下,尋找其他出路往往會帶來更好的結果。
另外,為防止其他人也陷入同樣的狀態,我們應將整個過程分享給團隊成員。在一個全員共享的空間留下一份筆記,能夠防止他人浪費時間?!?/p>
拓展:
勇于面對“給牦牛剃毛”
一般來說,見到要給牦牛剃毛的情況應該繞著走。但是,出于一些有價值的目的,或者因為緊急故障等,有時我們必須跨越“給牦牛剃毛”的障礙,解決問題。
這個時候最麻煩的是我們大腦解決問題的速度跟不上問題出現的速度。由于前一個問題尚未解決就冒出了下一個問題,所以我們的大腦在解決問題時往往像使用棧一樣,先讓問題入棧,再一個一個出棧解決(同時讓繼發的新問題入棧)。這就是給牦牛剃毛的狀態。在這種情況下,問題通常會接二連三地發生,出棧速度趕不上入棧速度,導致腦內棧溢出。
為了防止這類情況的發生,我們要記住不能只在腦中解決問題。應當把問題寫下來,一個一個地解決。
編程中的“給牦牛剃毛”
給牦牛剃毛的情況常出現在搭建環境的過程中。不過,編程中也會遇到類似的情況。
比如,在寫代碼時,由一個問題聯想到其他問題,離最初要解決的問題越來越遠。在最壞的情況下,我們甚至會忘記最初或中間想到的問題是什么。
又比如,在讀代碼時,由于代碼未整理,所以我們很難找到當初想知道的東西。在梳理錯綜復雜的調用關系時,一不小心就會忘記代碼讀到了哪里,或者讀代碼的目的是什么。
為防止這類情況發生,我們在讀寫復雜的代碼時,要一邊做記錄一邊操作。特別是在寫代碼時,我們需要思考的部分比實際操作的部分要多,不做記錄的話就可能會有陷入循環思考的狀態。
總結
以上是生活随笔為你收集整理的【笔记】编程的原则:改善代码质量的101个方法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: kubernetes之ReplicaSe
- 下一篇: 使用xetex直接由围棋棋谱文件创建pd