CODE COMPLETE 2 軟體開發實務指南 第7章 高品質的子程式

      第七章的內容是子程式,可與《Clean Code》第三章對照看,兩位大師對於函式的看法略有不同。


子程式是為實現一個特定的目的而編寫的一個可被呼叫的方法或程序。


作者從反方向說明何謂高品質的子程式,書中舉出的惡例有以下缺點:


子程式名稱HandleStuff()沒有明確說明程式做什麼事。


沒有說明文件。


佈局不好。(?)


輸入變數inputRec的值被改變了,C++應該被定義為const。如果變數會修改,不要命名為inputRec。


子程式讀寫了全域變數 ,子程式應該與其他子程式通訊。


沒有單一目的,做了太多事情。


有可能出現除零的情況。


出現過多魔術數字。


有變數宣告之後沒有使用。


參數傳遞方式錯誤,prevColor被標為參考參數(&),程式中卻傳數值。


子程式參數過多。


子程式參數混亂且沒有註解。


 


子程式最明顯的好處是避免重複的程式碼,還有其他使用子程式的原因。


7.1 建立子程式的正當理由


降低複雜度


隱藏細節。


內部迴圈或條件判斷的巢狀層次很深時,意味要擷取新的程式,降低外圍子程式複雜度。


引入中間的、易懂的抽象


從範例可看出提取子程式「函式名稱」增加程式碼可讀性,也降低程式複雜度。


避免程式碼重複


幫助子類別的設計


隱藏程序


看範例比較易懂,假設有兩行程式碼先讀取堆疊資料,再減少stackTop的數值,可將這兩行程式放入PopStack(),隱藏執行順序。


隱藏指標操作


提高可移植性


可用子程式隔離程式中不可移植的部份例如非標準功能、對硬體的依賴、以及對作業系統的依賴。


簡化複雜的邏輯判斷


將複雜的布林判斷寫成一個子程式可隱藏細節、增加程式碼可讀性、降低複雜度。


改善效能


作者指出不一定子程式越小越好,要根據當時情況。


似乎過於簡單而沒有必要寫成子程序的操作


最大的心理障礙是不想為了簡單的目的寫一個簡單的程式。


精巧的子程式可以有效提升程式碼的可讀性。


總結:建立子程式的理由


理由


 


降低複雜度


引入中間的、易懂的抽象


避免程式碼重複


幫助子類別的設計


隱藏程序


隱藏指標操作


提高可移植性


簡化複雜的邏輯判斷


改善效能


 


建立類別同時也是建立子程式的理由


 


隔離複雜度


隱藏實作細節


限制變化帶來的影響


隱藏全域資料


形成中央控制點


促成可重用的程式碼


達到特定的重構目的


7.2  在子程式層上設計


內聚力(cohesion)是指子程式中各種操作之間聯繫的緊密程度。


Cosine()餘弦函式極為內聚,因為函式只完成一項功能。CosineAndTan() 餘弦與正切函式的內聚力較弱,因為不只做一件事。


功能內聚力(functional cohesion)


最強也最好的一種內聚力,讓一個子程式只做一件事。


還有一些是被認為不理想的內聚力:


順序內聚力(sequential cohesion)


子程式內包含有需要按照特定順序執行的操做,這些步驟需要共享資料,全部執行完畢才能完成一項完整的資料。


例如子程式先得到出生日期計算出年齡,在從年齡計算出退休時間,子程式就有了順序內聚力。


子程式從出生日期分別計算出年齡與退休時間,兩次計算使用相同的出生日期,子程式具有通訊內聚力(communicational cohesion)。


如何設計有功能內聚力的子程式?可分拆為兩個子程式,第一個子程式從出生日期計算年齡,第二個子程式呼叫第一個子程式計算出退休時間。


通訊內聚力(communicational cohesion)


子程序不同操作使用同一種資料。


暫時的內聚力 (temporal cohesion)


有需要同時執行才放在一起的子程式,例如Startup()、Shutdown()。Startup()之中有可能會出現不相關的程式碼。


以下為不可取的內聚力:


程序內聚力 (procedural cohesion)


子程序的操作是按照特定的順序進行。


為了得到更好的內聚力,可把不同的操作納入各自的子程式中,讓子程式具有單一完整的功能。


邏輯內聚力(logical cohesion)


若干操作被放入同一個子程式,透過控制旗標執行其中某項操作,它們被包在if 或 case 之中。


這段內容建議將各種不同的功能寫成子程式,而不是用一個程式處理 if/else 和 switch/case。


如果子程式唯一的功能就是發佈各種命令,自身並不做任何處理,這也是不錯的設計。例如event handler。


巧合的內聚力(coincidental cohesion)


子程式內各種操作沒有任何關連,有稱為無內聚力或混亂的內聚力。


7.3  好的子程式名稱


描述子程式所做的所有事情


避免使用無意義的、模糊或表述不清的動詞


不要僅使用數字來形成不同的子程式名稱


根據需要確定子程式名稱的長度


給函式命名時要對回傳值有所描述


函式有回傳值,要針對回傳值來進行函式的命名。


給程序命名時使用語氣強烈的動詞加受詞的形式


準確使用反義詞


下面列出一些常見的正反義詞組:


add/remove   increment/decrement  open/close


begin/end       insert/delete               show/hide


create/destroy   lock/unlock source/target


first/last   min/max start/stop


get/put   next/previous   up/down


get/set   old/new


為常用操作確立命名規則


7.4  子程式可以寫多長


與其對子程式長度限制,不如考慮以下因素,如子程序內聚力、巢狀深度、變數的數量、決策點、註解數量、複雜度相關。


超過200行就要小心,已經會出現可讀性的問題。


7.5  如何使用子程式參數


子程式之間的介面是程式最容易出錯的地方之一。


以下是減少介面錯誤的指導原則


按照輸入-修改-輸出的順序來排列參數


考慮自己建立in和out關鍵字


C++可透過前置處理指令定義 IN 與 OUT ,作者認為有弊端不建議使用。


如果幾個子程式都使用了類似的一些參數,就該讓這些參數的排列順序保持一致。例如fprintf()與printf()參數排列要一致。


使用所有的參數


把狀態或出錯變數擺在最後


不要把子程式的參數當做工具用的變數


看範例就可以了解,輸入參數不可當做回傳值。


在介面中對參數的假定加以說明


應該對哪些介面參數的假定進行說明


參數是只用於輸入的、要被修改的、還是只用於輸出的。


用以表示數量的參數的單位(例如公尺)。


如果沒有用列舉類型,就應該說明狀態程式碼和錯誤值的含意。


期望的數值範圍。


不該出現的特定數值。


把子程式的參數限制在大約7個以內


考慮對參數採用某種表示輸入、修改、輸出的命名規則


為子程式傳遞用以維持其介面抽象的變數或物件


如何把物件成員傳給子程式,有兩種觀點。假設有物件有10個存取子程式,而呼叫的子程式只需要3項資料。


第一種觀點認為只要傳遞三項特定資料


第二種觀點認為應該傳遞整個物件


關鍵是子程式的介面要表達哪種抽象?


如果採用傳遞物件的方法,總是先建立物件,填入三項參數,呼叫子程式之後又再從物件中取出三項資料,說明應該傳遞三項資料而不是物件。


如果發現自己經常修改子程式參數列表,這些參數都來自同一個物件,代表應該傳遞物件。


使用具名參數


VB範例採用顯式寫法避免參數放錯位置。


確保實際參數與形式參數相匹配


形式參數指子程式定義中宣告的變數,實際參數是子程式呼叫中用到的變數、常數或運算式。


7.6  使用函式時要特別考慮的問題


程序是指沒有返回值的子程式。


程序法


report.FormatOutput(formattedReport, outputStatus)


if(outputStatus = Success) then ....


 


函式法


 


outputStatus = report.FormatOutput(formattedReport)


if(outputStatus = Success) then ....


 


什麼時候使用函式?什麼時候使用程序?


 


一個函式應該只有一個返回值,函式應該用返回值來命名,例如sin()、CustomerID()。程序可以根據需要,接受任意數量的參數。


 


設定函式的返回值


 


使用函式會存在不正確返回值的風險,當函式內有多條執行路徑,其中一條未設定返回值就會出現錯誤。


 


請依據下列建議來做:


 


檢查所有可能的返回路徑


 


不要返回指向局部資料的參考或指標


 


7.7  Macro子程式和行內子程式


 


把Macro運算式整個包在括號內


 


把含有多條述句的Macro用大括號括起來


 


使用子程式命名法給Macro命名,有需要的時候可用子程式取代Macro


 


Macro 子程式在使用上的限制


 


C++提供了大量可以取代Macro的方案:


 


const 定義常數


inline 定義可被編譯為行內程式碼的函式


template 用於類型安全方式定義各種標準操作,如min、max


enum 定義列舉類型


typedef 可用於定義簡單的類型別名


 


作者不建議使用Macro ,Macro 無法使用除錯器。


 


行內子程式


 


inline避免子程式呼叫的額外的花費,inline 可產生非常有效率的程式碼。


 


節制使用inline子程式


 


因為inline子程式違反了封裝原則。


 


核對表:高品質的子程式


大局事項


建立子程式的理由充分嗎?


所有單獨部分是否已經提取成單獨子程式


子程式名稱是否使用動詞加受詞?函式的名稱是否描述了返回值?


子程式的名稱是否描述了它所做的全部事情?


是否給常用操作建立了命名規則?


是否有強烈的內聚力?只做好一件事?


子程式之間是否有較鬆的耦合?


子程式長度是否由其功能和邏輯自然確定,而非遵循人為的編碼標準?


 


參數傳遞事宜


 


參數列表是否有整體性且一致的介面抽象?


參數排列順序是否合理?類似的子程序參數排列順序是否一致?


介面假定是否在文件說明?


子程式參數是否沒有超過7個?


是否使用每一個輸入參數?


是否使用每一個輸出參數?


使否避免把輸入參數當做工具變數使用?


函式是否在任何情況都能返回一個合法的值?


 


要點


 


建立子程式可提高程式的可管理性、節省程式碼空間、提高可讀性、可靠性和可修改性。


有時候把簡單操作寫成子程式也非常有價值。


應該讓大多數子程式具有功能上的內聚力。


子程式的名稱表現出品質,糟糕的名稱代表程式需要修改。


子程式主要的目的是得到特定結果,才應該使用函式。


儘量不要使用Macro。


 


 


留言

這個網誌中的熱門文章

概說H-Game

AOC和人生

真珠美人魚 星羅「Beautiful Wish」中文版