《代碼大全第二版》學習筆記(三)

第五部分 代碼改善

第二十章 軟件質量概述

20.1 軟件質量的特性

1.         正確性:系統規範、設計和實現方面的錯誤的稀少程度。

2.         可用性:用戶學習和使用一個系統的容易程度。

3.         效率:軟件是否儘可能少地佔用系統資源,包括內存和執行時間。

4.         可靠性:在指定的必需條件下,一個系統完成所需要功能的能力——應該有很長的平均無故障時間。

5.         完整性:系統阻止對程序或者數據進行未經驗證或者不正確訪問的能力。

6.         適應性:爲特定的應用或者環境設計的系統,在不做修改的情況下,能夠在其他應用或者環境中使用的範圍。

7.         精確性:輸出結果的誤差程度。

8.         健壯性:系統在接收無效輸入或者處於壓力環境時繼續正常運行的能力。

軟件內在的質量特性:

1.         可維護性:是否能夠很容易對系統進行修改、改變或者增加功能、提高性能,以及修正缺陷。

2.         靈活性:假如一個系統是爲特定用途或者環境設計的,那麼當該系統被用於其他目的或者環境的時候,需要對該系統做修改的程度。

3.         可移植性:爲了在原來設計的特定環境之外運行,對系統所進行修改的難易程度。

4.         可重用性:系統的某些部分可被應用到其他系統中的程度,以及此項工作的難易程度。

5.         可讀性:閱讀並理解代碼的難易程度,尤其在細節語句層次上。

6.         可測試性:可進行何種程度的單元測試或系統測試。

7.         可理解性:在系統組織和細節語句的層次上理解整個系統的難易程度。

20.3 不同質量保障技術的相對效能

1.         對所有的需求、架構以及系統關鍵部分的設計進行正式檢查。

2.         建模或者創建原型。

3.         代碼閱讀或者檢查。

4.         執行測試

20.4 什麼時候進行質量保證工作

錯誤越早引入到軟件當中,問題就會越複雜。

第二十一章 協同構建

21.1 協同開發實踐概要

工作中開發人員總會對某些錯誤點視而不見,而其他人不會有相同的盲點,所以開發人員讓其他人來檢查自己的工作是很有好處的。

21.2 結對編程

1.         用編碼規範來支持結對編程。

2.         不要讓結對編程編程旁觀。

3.         不要強迫在簡單的問題上使用結對編程。

4.         有規律地對結對人員和分配的工作任務進行輪換。

5.         鼓勵雙方跟上對方的步伐。

6.         確認兩個人都能夠看到顯示器。

7.         不要強迫程序員與自己關係緊張的人組對。

8.         避免新手組合。

9.         指定一個組長。

21.3 正式檢查

進行詳查的目的是發現設計或者代碼中的缺陷,而不是探索替代方案,或者爭論誰對誰錯,其目的絕不應該是批評作者的設計或者代碼。

第二十二章 開發者測試

1.         單元測試Unit testing

2.         組件測試Component testing

3.         集成測試Integration testing

22.2 開發者測試的推薦方法

1.         對每一項相關的需求進行測試,以確保需求都已經被實現。

2.         對每一個相關的設計關注點進行測試,以確保設計已經被實現。

3.         用基礎測試basis testing來擴充針對需求和設計的詳細測試用例。增加數據流測試data-flow test,然後補充其他所需的測試用例。

4.         使用一個檢查表,其中記錄着你在本項目迄今爲止所犯的,以及在過去的項目中所犯的錯誤類型。

在設計產品的時候設計測試用例,這樣可以幫助避免在需求和設計中產生錯誤,修正這些錯誤的代價往往比修正編碼錯誤更昂貴。儘可能早地對測試進行規劃並找出缺陷。

測試先行還是測試後行

1.         在開始寫代碼之前先寫測試用例,並不比之後再寫要多花功夫,只是調整了下測試用例編寫活動的工作順序而已。

2.         加入你首先編寫測試用例,那麼你將可以更早發現缺陷,同時也更容易修正它們。

3.         首先編寫測試用例,將迫使你在開始寫代碼之前至少思考一下需求和設計,而這往往會催生更高質量的代碼。

4.         在編寫代碼之前先編寫測試用例,能更早地把需求上的問題暴露出來。

5.         如果你保存了最初編寫的測試用例——這是你應該做的,那麼先進行測試並非唯一選擇,你仍然可以最後再進行測試。

於是,測試先行。

22.4 典型錯誤

三種最爲常見錯誤的源頭:缺乏應用領域知識、頻繁變動且相互矛盾的需求,溝通和協調的失效。

第二十三章 調試

23.2 尋找缺陷

科學的調試方法

1.         將錯誤狀態穩定下來。

2.         確定錯誤的來源

a)         收集產生缺陷的相關數據。

b)         分析所收集的數據,並構造對缺陷的假設。

c)         確定怎樣去證實或證僞這個假設,可以對程序進行測試或是通過檢查代碼。

d)         按照2c確定的方法對假設做出最終結論。

3.         修補缺陷。

4.         對所修補的地方進行測試。

5.         查找是否還有類似錯誤。

尋找缺陷的一些小建議

1.         在構造假設時考慮所有的可用數據。

2.         提煉產生錯誤的測試用例。

3.         在自己的單元測試族unit test suite中測試代碼。

4.         利用可用的工具。

5.         採用多種不同的方法重現錯誤。

6.         用更多的數據生成更多的假設。

7.         利用否定性測試用例的結果。

8.         對可能的假設嘗試頭腦風暴。

9.         在桌上放一個筆記本,把需要嘗試的事情逐條列出。

10.     縮小嫌疑代碼的範圍。

11.     對之前出現過缺陷的類和子程序保持警惕。

12.     檢查最近修改過的代碼。

13.     擴展嫌疑代碼的範圍。

14.     增量式繼承。

15.     檢查常見缺陷。

16.     同其他人討論問題。

17.     拋開問題,休息一下。

語法錯誤

1.         不要過分信任編譯器信息中的行號。

2.         不要迷信編譯器信息。

3.         不要輕信編譯器的第二條信息。

23.3 修正缺陷

1.         在動手之前先要理解問題。

2.         理解程序本身,而不僅僅是問題。

3.         驗證對錯誤的分析。

4.         放鬆一下。

5.         保留最初的源代碼。

6.         治本,而不是治標。

7.         修改代碼時一定要有恰當的理由。

8.         一次只做一個改動。

9.         檢查自己的改動。

10.     增加能暴露問題的單元測試。

11.     搜索類似的缺陷。

23.5 調試工具——明顯的和不那麼明顯的

1.         源代碼比較工具 Source-Code Comparators

2.         編譯器的警告消息 Compiler Warning Messages

3.         增強的語法檢查和邏輯檢查 Extended Syntax and Logic Checking

4.         執行性能剖測器 Execution Profilers

5.         測試框架/腳手架 Test Frameworks/Scaffolding

6.         調試器 Debuggers

第二十四章 重構

24.2 重構簡介

重構的理由

1.         代碼重複。

2.         冗長的子程序。

3.         循環過長或嵌套過深。

4.         內聚性太差的類。

5.         類的接口未能提供層次一致的抽象。

6.         擁有太多參數的參數列表。

7.         類的內部修改往往被侷限於某個部分。

8.         變化導致對多個類的相同修改。

9.         對繼承體系的同樣修改。

10.     case語句需要做相同的修改。

11.     同時使用的相關數據並未以類的方式進行組織。

12.     成員函數使用其他類的特徵比使用自身類的特徵還要多。

13.     過多使用基本數據類型。

14.     某個類無所事事。

15.     一系列傳遞流浪數據的子程序。

16.     中間人對象無事可做。

17.     某個類同其他類關係過於親密。

18.     子程序命名不恰當。

19.     數據成員被設置爲公用。

20.     某個派生類僅使用了基類的很少一部分成員函數。

21.     註釋被用於解釋難懂的代碼。

22.     使用了全局變量。

23.     在子程序調用前使用了設置代碼,或在調用後使用了收尾代碼。

24.     程序中的一些代碼似乎是在將來的某個時候纔會用到的。

24.3 特定的重構

數量級的重構Data-Level Refactorings

1.         用具名常量代替神祕數值。

2.         使變量的名字更爲清晰且傳遞更多信息。

3.         將表達式內聯化。把與一箇中間變量換成給它賦值的那個表達式本身。

4.         用函數來代替表達式。

5.         引入中間變量。中間變量的命名應能準確概括表達式的用途。

6.         用多個單一用途變量代替某個多用途變量。

7.         在局部用途中使用局部變量而不是參數。

8.         將基礎數據類型轉化爲類。

9.         將一組類型碼type codes轉化爲類或枚舉類型。

10.     將一組類型碼轉換爲一個基類及其相應派生類。

11.     將數組轉換爲對象。

12.     把羣集collection封裝起來。

13.     用數據類來替代傳統記錄。

語句級的重構

1.         分解布爾表達式。引入命名準確的中間變量。

2.         將複雜布爾表達式轉換成命名準確的布爾函數。

3.         合併條件語句不同部分中的重複代碼片段。

4.         使用breakreturn而不是循環控制變量。

5.         在嵌套的if-then-else語句中一旦知道答案就立即返回,而不是去賦一個返回值。

6.         用多態來替代條件語句(尤其是重複的case語句)。

7.         創建和使用null對象而不是去檢測空值。

子程序級重構

1.         提取子程序或者方法。

2.         將子程序的代碼內聯化。

3.         將冗長的子程序轉換爲類。

4.         用簡單算法替代複雜算法。

5.         增加參數。

6.         刪除參數。

7.         將查詢操作從修改操作中獨立出來。

8.         合併相似的子程序,通過參數區分它們的功能。

9.         將行爲取決於參數的子程序拆分開來。

10.     傳遞整個對象而非特定成員。如果發現有同一對象的多個值被傳遞給了一個子程序。

11.     傳遞特定成員而非整個對象。如果發現創建對象的唯一理由只是你需要將它傳入某個子程序。

12.     包裝向下轉型的操作。檔子程序返回一個對象時,應當返回其已知的最精確的對象類型。

類實現的重構

1.         將值對象轉化爲引用對象。針對大型對象。

2.         將飲用對象轉化爲值對象。針對小型對象。

3.         用數據初始化替代虛函數。如果一組派生類,差別僅僅是虛函數返回的常量不同。

4.         改變成員函數或成員數據的位置。考慮對類的繼承體系做出修改。

減少派生類的重複工作:

a)         將子程序上移到基類中。

b)         將成員上移到基類中。

c)         將構造函數中的部分代碼上移到基類中。

反之,則是對派生類進行特殊化。

5.         將特殊代碼提取爲派生類。

6.         將相似的代碼結合起來放置到基類中。

類接口的重構

1.         將成員函數放到另一個類中。

2.         將一個類變成兩個。

3.         刪除類。

4.         去除委託關係。

5.         去掉中間人。

6.         用委託代替繼承。如果某類需要用到另一個類,但有打算獲取對該類接口更多的控制權,可以讓基類成爲原派生類的一個成員,並公開它的一組成員函數,以完成一種內聚的抽象。

7.         用繼承代替委託。如果某個類公開了委託類(成員類)所有的成員函數,就改成繼承。

8.         引入外部的成員函數。如果一個客戶類需要被調用類的某個額外的成員函數,而你又無法去修改被調用類,那麼可以通過在客戶類中創建新成員函數的方式來提供此功能。

9.         引入擴展類。如果一個類需要多個額外的成員函數,你同樣無法修改該類,可以創建一個新類,包括原類的功能以及新增功能。

10.     對暴露在外的成員變量進行封裝。public改爲private

11.     對於不能修改的類成員,刪除相關的set()成員函數。

12.     隱藏那些不會再類之外被用到的成員函數。

13.     封裝不使用的成員函數。

14.     合併那些實現非常類似的基類和派生類。

系統級重構

1.         爲無法控制的數據創建明確的索引源。

2.         將單向的類聯繫改爲雙向的類聯繫。各自需要用到對方的功能。

3.         將雙向的類聯繫改爲單向的類聯繫。只有一個類需要訪問另一個類。

4.         Factory Method模式而不是簡單地構造函數。在需要基於類型碼創建對象,或者希望使用引用對象而非值對象的時候,應當使用factory Method

5.         用異常取代錯誤處理代碼,或者做相反方向的變換。

24.4 安全的重構

1.         保存初始代碼。

2.         重構的步伐請小些。

3.         同一時間只做一項重構。

4.         把要做的事情一條條列出來。

5.         設置一個停車場。現在不急將來要做的事情。

6.         多使用檢查點。

7.         利用編譯器警告信息。

8.         重新測試。

9.         增加測試用例。

10.     檢查對代碼的修改。

11.     根據重構風險級別來調整重構方法。

不宜重構的情況

1.         不要把重構當做先寫後改的代名詞。

2.         避免用重構代替重寫。

24.5 重構策略

1.         在增加子程序時進行重構。

2.         在添加類的時候進行重構。

3.         在修補缺陷的時候進行重構。

4.         關注易於出錯的模塊。

5.         關注高度複雜的模塊。

6.         在維護環境下,改善你手中正在處理的代碼。

7.         定義清楚乾淨代碼和拙劣代碼之間的邊界,然後嘗試把代碼移過這條邊界。

第二十五章 代碼調整策略

25.1 性能概述

對用戶來說,程序員按時交付軟件,提供一個清爽的用戶界面,避免系統死機常常比程序的性能更爲重要。

性能和代碼調整

1.         程序需求。在花費時間處理一個性能問題之前,想清楚你的確是在解決一個確實需要解決的問題。

2.         程序的設計。如果程序的資源佔用量和速度至關主要,設計時就應該優先考慮整體性能。

3.         類和子程序設計。合適的數據類型和算法。

4.         同操作系統的交互。

5.         代碼編譯。選擇優秀的編譯器。

6.         硬件。

7.         代碼調整。Tuning

25.2 代碼調整簡介

但是,高效的代碼不一定就是“更好”的代碼。

完美是優良之大敵。越是追求完美,越有可能完不成任務。程序員們首先應該實現程序應該具備的所有功能,然後再使程序臻於完美。

一些無稽之談

1.         在高級語言中,減少代碼的行數就可以提升所生成的機器代碼的運行速度,或是減少其資源佔用——錯誤!

2.         特定運算可能比其他的快,代碼規模也較小——錯誤!

3.         應當隨時隨地進行優化——錯誤!

4.         程序的運行速度同其正確性同等重要——錯誤!

何時調整代碼

程序員應當使用高質量的設計,把程序編寫正確。使之模塊化並易於修改,將讓後期的維護工作變得很容易。在程序已經完成並正確之後,再去檢查系統的性能。

除非你對需要完成的工作一清二楚,否則絕不要對程序做優化。

25.3 蜜糖和哥斯拉

常見的低效率之源

1.         輸入/輸出操作。如果你可以選擇在內存中處理文件,就不要費力通過磁盤、數據庫、或是跨越網絡訪問相同的文件。除非程序對空間佔用非常敏感,否則數據都應放在內存裏面。

2.         分頁。引發操作系統交換內存頁面的運算會比在內存同一頁中進行的運算慢許多。

3.         系統調用。編寫自己的服務程序/避免進入系統。

4.         解釋型語言。

5.         錯誤。比如,沒有去掉調試代碼(例如繼續吧調試信息記錄到文件中去)、忘了釋放內存、數據庫表設計失誤、輪詢並不存在的設備直至超時,等等。

常見操作的相對效率

絕大部分常用操作所耗費的時間相差無幾——成員函數調用、賦值、整型運算和浮點運算等等之間都差不多。超越函數(浮點方根、浮點正弦、浮點對數、浮點指數)的計算則會用去非常多的時間。多態函數調用比其他函數調用略微費時。

25.4 性能測量

底層往往已經做過優化了。如果你認爲沒有必要通過測量來證實哪種方法更爲有效,那麼同樣也沒有必要犧牲代碼的可讀性,而把賭注押在性能的提高上。

性能測量應使用專門的性能剖析工具。

25.6 代碼調整方法總結

第二十六章 代碼調整技術

26.1 邏輯

1.         在知道答案後停止判斷。

2.         按照出現的頻率來調整判斷順序。

3.         用查詢表替代複雜表達式。

4.         使用惰性求值。避免做任何事情,直到迫不得已。

26.2 循環

1.         將判斷外提。把循環放在條件語句內。

2.         合併。把兩個對相同一組元素進行操作的循環合併在一起。

3.         展開。如,把循環10次的循環展開爲10行代碼。

4.         儘可能減少在循環內部做的工作。

5.         哨兵值。當循環的判斷條件是一個複合判斷的時候,可以通過簡化判斷來節省代碼運行時間。如果該循環是一個查找循環,簡化方法之一就是使用一個哨兵值,可以把它放在循環範圍的末尾,從而保證循環一定能夠中止。

6.         把最忙的循環放在最內層。

7.         削減強度。用多次輕量級運算(例如加法)來代替一次代價高昂的運算(例如乘法)。

26.3 數據變換

1.         使用整型數而不是浮點數。

2.         數組維度儘可能少。

3.         儘可能減少數組引用。

4.         使用輔助索引。如,字符串長度索引,獨立的平行的索引結構(當每一個數據條目都很大的時候,可以創建一個輔助結構,裏面存放關鍵碼和指向詳細信息的指針。可以把關鍵碼條目存放在內存裏,而把數據存放在外部。這使所有的查找和排序都可以在內存裏完成,知道了所需訪問條目的具體地址之後,進行一次磁盤訪問就夠了)。

5.         使用緩存機制。把某些值存起來,使得最常用的值會比不太常用的值更容易被獲取。

26.4 表達式

1.         利用代數恆等式。

2.         削弱運算強度。如:

a)         用加法代替乘法。

b)         用乘法代替冪乘。

c)         利用三角恆等式代換等價的三角函數。

d)         longint來代替longlong整數。

e)         用定點數或整型數代替浮點數。

f)          用單精度數代替雙精度數。

g)         用移位操作代替整數乘2或除2

3.         編譯期初始化。如果在一個子程序調用中使用了一個具名常量或是神祕數值,而且它是子程序唯一的參數,這就是一種暗示,你應當提前計算這一數值,把它放到某個常量中,避免上面那種子程序調用。

4.         小心繫統函數。系統函數運行起來很慢,提供的精度常常也是根本不需要的。

C++示例:使用右移運算的以2爲底的對數函數

unsigned int Log2(unsigned int x) {
         unsigned int i = 0;
         while ( ( x = ( x >> 1) ) ! = 0 ) {
                   i++;
         }
         return i;
}

5.         使用正確的常量類型。所使用的具名常量和應該同被賦值的相應變量具有相同的類型。如果不同,編譯器發生類型轉換,花時間。

6.         預先算出結果。如:

a)         在程序執行之前算出結果,然後把結果寫入常量,在編譯時賦值。

b)         在程序執行之前計算結果,然後把它們硬編碼在運行時使用的變量裏。

c)         在程序執行之前計算結果,把結果存放在文件中,在運行時載入。

d)         在程序啓動時一次性計算出全部結果,每當需要時去引用。

e)         儘可能在循環開始之前計算,最大限度地減少循環內部需要做的工作。

f)          在第一次需要結果時計算,然後將結果保存起來以備後用。

7.         刪除公共子表達式。如果發現某個表達式老是在你面前出現,就把它賦給一個變量,然後在需要的地方引用該變量,而非重新計算這個表達式。

26.5 子程序

1.         將子程序重寫爲內聯。

26.6 用低級語言重寫代碼

簡單有效的彙編重編碼方法,即啓用一個能順帶輸出彙編代碼列表的編譯器。把需要調整子程序的彙編代碼提取出來,保存到單獨的源文件中。將這段彙編代碼作爲優化工作的基礎,手動調整代碼。

26.7 變得越多,事情反而越沒變

計算機發展,本章討論的性能優化提升的意義已如明日黃花。

未經測量的代碼優化對性能上的改善充其量是一次投機,然而,其對可讀性等產生的負面影響則確鑿無疑。

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