程式碼應易於理解。
因此撰寫程式時,應將讀者理解所需的時間降到最低,意即程式碼需具有最佳可讀性。此處所謂的完全理解是指讀者有能力修改,debug,並知道程式與其他部分互動的方式。程式的可讀性與效能最佳化兩者間未必衝突,甚至具有良好可讀性的程式因易於被理解、測試,將更有利於最佳化。然而可讀性的困難之處在於必須時時考量某位虛擬讀者能否理解,故初期(未養成習慣之前)會耗費更多時間,一旦建立風格後,將可大幅縮短額外耗費的時間。
可讀性的改善可由下列兩種概念著手:1. 表層改善。使程式碼在視覺上具有良好的可讀性,使讀者不會抗拒閱讀程式碼。2. 簡化迴圈與邏輯。使程式碼在心理上具有良好的可讀性,過於複雜的迴圈與邏輯、大量的變數都會提高讀者的心理負擔,需要更縝密思考和記憶更多變數才能理解程式碼,不利閱讀。
<表層改善>
表層改善的方法主要有三種:1.挑選合適的程式(變數)名稱,可使讀者迅速了解程式的目的。2.良好的註解,可適時的幫助讀者了解程式的運作。3. 具備良好程式美學,簡潔整齊具美感的程式碼能提升讀者的耐心。
- 合適的程式(變數)名稱
一個好的命名除了不會被誤解外,還要能提供足夠資訊使讀者瞭解,因此命名時需讓程式(變數)名稱具有意義,使名稱可成為註解的一部分。在完整表達含意之餘,如能兼具高資訊空間比將會是個成功的命名。
選擇詞彙時,須選擇意義明確並避免使用空洞的詞彙。因此善用英文的同義字,找出更合適、詞意更鮮明的字彙是命名時需費心的地方。同時,避免使用過於通用的名稱(如tmp),這些變數不僅有可能在程式碼中重複出現造成讀者混淆外,也不能提供額外的訊息,因此並不是理想的命名。然而,若此變數的生命期僅有數行,且此變數不具其他責任,不會被傳遞至其他函數,亦不會被重設或重複使用,在不影響讀者理解的前提下,為了撰寫者的便利還是允許使用這類變數。
最佳的命名應能說明變數的目的以及(或是)包含實體數值的訊息。如迴圈循環子(interator)的命名一般都是使用i,j,k,故在程式碼中若出現這些變數,讀者會直覺聯想到是interator,此種命名便清楚說明變數目的(但若是將i,j,k用在其他用途,就可能是個失敗的命名方式)。有時不得不在迴圈系統中使用許多巢狀結構時,適當命名interator也可協助讀者了解目前正處於哪段迴圈中。例如有三層迴圈,分別是觀測資料的迴圈、模式水平格點與垂直格點的迴圈,若將interator命名成i_obs、j_x、k_z,將有助讀者理解。
有時變數所存取的資料會有不同單位(或是不同屬性),如在命名時就提供這些資訊將可減輕讀者負擔,亦能幫助撰寫者除錯。例如時間相關變數,若單純命名為time,則可能在A段程式碼中讀取的時間單位是秒(sec),但B段中讀入的時間單位是毫秒(ms),如此不僅會混淆讀者,甚至是個潛在的bug。如將單位加入變數名稱,如time_s、time_ms,便能方便辨識。
因此,良好的命名不僅可增加可讀性,亦可避免bug的發生。命名時需跳脫撰寫者的身分,發揮自身想像力思考名稱是否具足夠資訊協助讀者了解此變數/程式碼的目的,並審慎思考該名稱是否過於空泛、模糊、間接,使讀者不僅無法一眼看出目的外,甚至造成讀者誤解。
為使命名能提供足夠訊息,在合適範圍內增加名稱的字元數不是壞事。一般命名原則是較小的範圍內可考慮使用較短的變數,若變數跨越的範圍較長,則可透過較長的名稱賦予較多訊息以利讀者理解。因為若變數生命期只有數行,名稱卻有20個字元,其所提供的資訊空間比就較低,不是理想的命名方式。較長的名稱對撰寫者而言無疑是個負擔,但大多數的編輯器都內建「識別字補齊」的功能,如在vi中,便可使用ctrl-p 幫忙補齊字元,在註解中也同樣適用。但要注意,並不是長的變數名稱就一定好,有時消除變數中的某些詞彙若不影響名稱所含的資訊量,就建議移除(如fromNumberToString,若拿掉from並不影響意思,故寫成number2string或num2str即可)。在不同型態的變數使用不同的命名規則也有助於讀者理解,如在類別名稱使用駝峰式大小寫,變數名稱使用底線分隔也是種方法,總之只要確立命名原則並妥善遵守(或是提供註解說明)都能增加可讀性。
有些撰寫者在命名時會使用縮寫,如rmse,若這種縮寫對同領域/專案的使用者而言相當常見(如在資料同化中,使用B代表背景誤差協方差矩陣),那就可被接受(然而,在程式碼中若直接將背景誤差協方差矩陣命名為B也不洽當),否則,還是盡量避免。
為避免造成使用者誤解,有些基本的命名原可作為參考,若數值包含邊界,可加上max_或min_,便可清楚知道數值的特性;變數名稱最好避免使用否定敘述,以免增加閱讀負擔;如能在命名時便告知使用者大概的計算量,那也是相當有用的資訊,例如在進行龐大計算時,可使用compute而非get(computeMean)。
- 良好的註解
註解的目的是協助讀者能快速地了解撰寫者的想法,同時提供撰寫過程中的經驗。一般而言,程式碼的名稱應能使讀者對此程式具有基本概念,然後程式碼的開端可提供詳細、更進一步的文字說明(全局註解)。每個大段落間也可透過註解告知讀者此段程式碼的目的,避免讀者深陷於細節之中。在撰寫註解時,並不一定得留下正式完整的文句,只要能提供重要的訊息,簡單的幾句話也可以。
程式碼中的註解除幫助讀者瞭解各區塊的目的外,也提供平台供撰寫者與讀者對話,如果撰寫方式因某些原因而和一般習慣用法不同,即可透過註解說明;如果撰寫者針對某部分的效能已有一些測試結果,同樣可以註解方式說明,避免維護者或後續使用者重複無謂的測試;定義常數時,對各常數的使用環境以及採用特定數值的原因均可透過註解使讀者瞭解;撰寫過程中,有時為了時效而忽略排版或效能,這些不足之處可通過註解說明改善方向以供後續使用者(可能是數週後的自己)跟進。一個良好有用的程式碼應會不斷被使用,也會持續地被改善,故於撰寫過程中忠實記錄發現的缺憾或改善的構想能讓讀者瞭解程式的狀態及品質。許多程式設計師會使用下列標記紀錄不足的部分:
TODO:紀錄作者尚未處理的部分 (若是大寫表示須優先處理,小寫則表示問題較小)。
FIXME:已知的問題。
HACK:承認解決方式不夠優雅。
XXX:危險! 相當重要的問題,需要立即解決。
然而,並非每一步驟都需要註解。技術上來說,若註解沒有提供新資訊,或註解的部分只需簡單觀察程式碼即可輕易知道工作內容,那這個註解就是無用的註解,並不需要為了註解而註解。如何決定一個註解是有意義的?除了上述的情況外,程式設計師會試圖從第三者的角度來重讀程式碼,預期讀者可能會出現的問題。若在重新閱讀時,發現某些程式碼的內容需要額外仔細思考,或對某些變數的存在感到疑惑時,就非得要註解不可(如果是對變數的命名感到疑惑,此時需要的不是註解,而是重新命名)。
簡單的說,適時寫下心中有意義的想法,就算只有幾個字也好,都可有效幫助讀者理解程式碼。但一個好的註解應具有「高資訊空間比」。與命名的原則相同,能妥善使用包含大量資訊的詞彙將可使註解更為簡潔;撰寫註解時需極力避免模稜兩可的的代名詞(如修好它,讀者並不一定知道"它"表示哪一部分);使用非內建或陌生函數時,必須精確描述函數的行為,最好提供輸入值與輸出值的範例(以及變數型態)以增進讀者瞭解,同時亦可幫助除錯。
- 程式美學
良好的程式碼應能使讀者賞心悅目,因此程式碼編排方式對可讀性相當重要,基本上可藉由一致的排版、將相似(關)的程式碼調整成有相似外觀以及將程式碼分段使程式碼具有基本美學架構。
為了排版的一致,在整個程式或專案中,最好維持相同的排版原則,如巢狀結構中空白字元的數量、段落間空白幾行等。為使整體版面能維持整潔,多幾行程式碼無傷大雅。
將相似(關)的程式碼調整成有相似外觀,如此除能使讀者一眼便可了解此區塊的程式碼具相同結構外,由於對應的位置相似,若出現打字錯誤的bug,也能夠快速找出。然而,若要將大量的程式碼調整成「列對齊」以提供較佳的視覺邊界,對撰寫者而言也是不小的負擔。當維護時,原本只是修改一行程式碼,卻為了使整體版面具有相似外觀反而需改動更多行程式碼。雖然麻煩,但這樣做的好處應遠大於所花費的時間。萬一程式碼真的過於龐大不易調整,到時再放棄就好。
適當的分段能讓讀者清楚知道目前所在位置,同時也易於閱讀,若能於每段都放入合適的註解將可大幅縮短讀者的理解時間。在某些情況下,程式碼的順序並不影響正確性,然若能賦予有意義的順序對程式維護與內容理解將有正面助益,透過重要性遞減排序,或是單純依照字母排序都是不錯的方式。
程式美學的標準見仁見智,但在專案中,保持一致的風格會比「正確」的風格更為重要。
<簡化迴圈與邏輯>
如果程式碼中沒有任何的迴圈或邏輯判斷,讀起來會非常容易,過於複雜的迴圈與邏輯判斷將會增加讀者的負擔,因此如何簡化程式碼中的迴圈與邏輯敘述對可讀性的提升相當重要。設計迴圈與邏輯敘述時,應使敘述盡量自然,讓讀者在閱讀過程中不需停下來重新思考。例如一般的程式設計者在設計迴圈與邏輯的條件敘述時,習慣將會改變的變數(也就是欲比較的對象)放在左側,而固定的常數(也就是比較的基準)放在右側,如果違背這個原則,讀者在閱讀時將會因程式碼的「不自然」敘述而停下腳步,雖不至於無法理解,但會降低閱讀速度。在撰寫if/else敘述時,通常能自由交換各區塊的順序,但若能先處裡肯定條件而非否定條件、先處裡簡單的情況、先處裡較有趣或明顯的情況,將可避免條件敘述是以較難理解的順序呈現,也是增進可讀性的好技巧。
有些程式設計師喜歡將複雜的概念用一行程式碼或是獨特的程式碼來表示,前者對讀者而言需要花費許多心力才可能領悟撰寫者的巧思;至於後者,雖然寫法獨特的程式碼看起來很新穎,也許能縮減程式碼行數,但會對讀者以及後續使用者造成困擾,如果是個人練習的實作無可厚非,但若是需要分享的程式碼,還是以增進程式可讀性為優先,縮短他人理解所需的時間比減少程式碼還重要。
多層巢狀結構對讀者相當不友善,每層巢狀結構都會造成讀者額外的心理負擔,因為當讀者看到某層巢狀結構結尾時,很難回憶起這部分屬於哪層巢狀結構以及其所表示的意義。所以除非必較,撰寫時盡量避免使用過於複雜的巢狀結構。巢狀結構的增長往往出現在修改的過程,重複的增修經常會導致最後出現複雜的巢狀結構。因此修改程式碼時,需以全新的角度重新審視,以整體的角度考慮程式碼的修改方式。一般來說,非得使用巢狀結構時,最好能及早跳脫並清除巢狀結構,使程式碼更為簡潔,因為每多一層巢狀結構,讀者就必須記憶更多的環境資訊。盡量使用線性的程式流程,避免巢狀結構。
和100行程式碼相比,一次閱讀3000行程式碼對讀者造成的心理負擔更重,研究顯示人類一次只能思考三到四種東西,因此程式碼越長就越難理解。在撰寫程式碼時,最好將巨大的內容分解成更容易消化的大小。這個概念相當重要,可應用於任何層級的程式碼中。最低階的層級如某行需要計算的程式碼,假設該行程式碼的工作是先讀取某個變數,再進行條件邏輯判斷,若只用一行表示會增加讀者閱讀負擔,故可使用「解釋性變數」先行讀取變數,並以變數名稱告知讀者正在進行的事,再使用第二行程式碼進行邏輯判斷,雖然這會多出一個變數及一行程式碼,但對整體程式碼的負擔並不顯著,且能大幅提升可讀性;相同的概念,假設某個變數陣列中有五個變數,但計算過程中只需使用其中某變數,此時便可利用「摘要變數」來增加可讀性。摘要變數目的是用較短、較易了解的名稱,於小範圍程式碼中取代較長或複雜的變數名稱。利用這兩種技巧,可避免在程式碼中不斷重複相同的步驟(DRY, Do not Repeat Yourself),不僅能避免重複打字造成的錯誤外,也可大幅減少程式碼的負擔,維護時若欲修改名稱也只需更改一次。
更高的層級是思考該如何重新組織整個程式碼,找出程式碼中會重複執行的區塊,並試圖用副程式來處理。這樣的好處不只能縮短程式碼,將程式內容以副程式獨立出來也易於維護與測試,甚至副程式還能被其他程式呼叫,成為通用的副程式碼。在重新組織程式碼時,有兩個很重要的概念:1.積極尋找並抽離不相關的子問題;2.一次處理一個問題。
在程式撰寫的過程中,有個相當重要的基本原則就是將大問題分解成小問題,再將小問題的解答組合成原本大問題的解答。因此如何找尋可分離且值得分離的部分就是個值得程式設計師認真思索的問題。初學者的話,可先閱讀特定的函數或程式區塊,並自問這段程式的主要目的為何?接著再細看每行程式碼,自問每行程式的工作內容與這段程式碼的工作目的是否直接相關?還是這行程式是為了處理與目的無關的子問題?若用以解決子問題所花費的行數過多,就值得抽離並寫成獨立的函數或副程式。例如在計算兩點間距時,需處理球面的影響,座標轉換的過程相當複雜且一再重複,所花費的程式行數也相當龐大,故若將此部分以副程式表示,即可簡潔程式碼,並使撰寫者能專注於高階目的,不需一直耗費心思處理幾何公式,且這個副程式也可被重複使用,是個相當值得的投資。同時,將子問題抽離後,也較容易加入新功能、改善可靠度或進行效能測試。因此一個理想的專案,應會將許多常見的基本子問題抽離,並放置於特定資料夾供所有撰寫者使用。
但須注意,萬物皆過猶不及。若將程式碼分解過頭反而會造成讀者的負擔,讀者必須記憶各副程式輸入與輸出的訊息,也可能需要在執行的路徑上來回跳躍。因此撰寫者必須清楚了解加入新的副程式必須付出微小(但明確且無可避免)的可讀性成本,若付出的成本並無法獲得相對應的收穫,就不需要立刻將此部分寫成副程式,可等到其他部分也面臨同樣需求時再來考慮。
如前所述,人的大腦一次能思考的東西並不多,若在程式碼中同時進行許多工作,將對讀者造成嚴重負擔,對撰寫者而言也可能是個潛在bug。故應將程式碼重新組織為一次只做一件事(包含副程式),將一大段的程式碼依邏輯獨立性分拆成不同段落。實作前可先列出需進行工作內容(盡量詳細),再依工作間的相關性盡量將各工作獨立進行,使程式碼的結構在不顯著影響效能與保有正確性的前提下能更為單純簡潔。分離的形式並不重要,可以是抽離成新的副程式,也可以獨立成段,重點在於能確實分離。然而分離工作並非件容易的事,有時可能有五項工作,但囿於效能或其他原因使能將其分離出三項,但也是相當不錯的收穫了。
在希望增進可讀性之前,我們必須知道,可讀性最高的程式碼就是沒有任何程式碼。也就是說,一旦開始撰寫程式,就會立即面臨可讀性的問題。若想達到最佳可讀性,那程式碼自然是越自然、越短越好。要使程式碼更為自然,上述的抽離子問題與一次處裡一個問題的原則都可使讀者很自然地了解撰寫者的構思。但要使程式碼變短,並不能像存錢一樣,東省一兩行、西省一兩行,而是要更有組織,從結構上處理才是根本之道。因此必須掌握一個原則:不需要的功能不必開發。
經常在一個專案開始時,設計者都會很興奮地設想許多附加的功能,希冀這個完美的程式碼能夠具有無限擴充性,但往往反而會忽略專案真正的核心功能。最後這些附加的功能可能來不及完成,或根本沒人使用,反而增加程式的複雜度。因此想要縮短程式碼,必須從一開始設計時,就很清楚程式工作的核心目的,以及潛在的擴充方向,以完成核心部分為主要方向的前提下,再合理地維持擴充性。
開發過程中,隨著系統的成長,維持系統所需的複雜度會以等比級數增加。因此最好的方式是維持程式碼的小而美,也就是在開發過程中,盡量使用通用的副程式(適度的抽離子問題)以消除重複的程式碼,同時須果斷移除未使用的程式碼與功能,並隨時注意程式的份量。前者對大多數有觀念的撰寫者而言並非難事,但要求撰寫者自行刪除程式碼卻非易事。一旦程式碼被寫下之後,這些程式碼就代表工作的成果,刪除程式碼意味著之前所有努力全都白費,在心理上不易接受。當下定決心刪除程式碼後會發現,刪除獨立的函數相當容易,但有時無用的程式碼卻是散落在各個部分,不僅找尋不易,有時甚至連撰寫者都不知情。最常見的情況是程式設計之初,希望在核心功能外能搭配一些附帶功能,然而在撰寫過程中因專案目的改變或其他原因而放棄,但原先為此附帶功能而預留的空間或變數卻依然被保留,久而久之可能連設計者都忘記它們的存在,這些就是屬於無用的程式碼。
除了無用的程式碼需要刪除外,無用的變數也不需要保存。濫用變數或許能讓撰寫者快速地達到目的,但對讀者而言,變數越多越難同時記住所有變數;變數生存期越長,就必須記憶越久;變數若一直改變,就越難記得當前的數值。因此撰寫時必須刪除不必要的暫存變數,若該變數無法提供任何額外訊息,也無法簡化程式碼,就可考慮刪除此變數。在處理問題時,有時需要一些中介變數,使用這些中介變數時最好能盡量縮短它的生命週期,因此若能一次處理一件事情,並盡快完成所需的工作就能盡早刪除這些中介變數,避免這些中介變數成為潛在的bug。簡單的說,撰寫程式時,要盡量縮短變數的生命週期、減少變數出現的行數、也減少變數改變的次數,因為操作變數的地方越多就越難記得變數目前的數值。此外,也得避免使用過多的全域變數,以免全域變數汙染命名空間,使得修改區域變數的數值時,影響到全域變數的數值,產生一個潛在bug。
簡化的過程中除了依循上述三個概念外,有些不錯的小技巧也可幫助寫出簡潔的程式碼。例如在處理問題之前,先使用口語敘述遭遇的問題以及困難處,並說明可能的解決方法。如果發現無法用口語清楚描述,那表示可能遺漏某些東西,或仍有問題尚未被發掘。有時十分單純的問題,卻在實作過程中反而轉變為非常複雜的邏輯概念,通常遇到這種情況表示思考過程中遺漏一些重要因子,或其實有更簡單的作法只是還沒被想到。前者可透過口語敘述解決;後者則需具要些創造力找到更優雅的作法,一般可先嘗試能否從反向思考獲得解決方式。確定整體構想後,再依照構想寫下步驟並抽離子問題,養成這個習慣可幫助撰寫者在設計架構時能以更宏觀的角度思考並避免過度設計。
<參考資料>
易讀程式之美學-提升程式碼可讀性的簡單法則,2013,O'Reilly,ISBN:978-0-596-80229-5
<參考資料>
易讀程式之美學-提升程式碼可讀性的簡單法則,2013,O'Reilly,ISBN:978-0-596-80229-5