# C0001 不白話的程式碼:文字加密 ## 別讓程式碼加密了 回顧[B0001 易讀程式碼的特性:白話](/bdQxzTadSP-HIfXlQZHxLg),我們提到了易讀程式碼的特性:**白話**;藉由簡單的例子來比較白話與不白話的差異,也將不白話的程式碼比喻是需要**翻譯機**做對應才能理解。 在這裡我們進一步比喻,這些需要翻譯機的程式碼,就像是很怕人(也包括作者自己)一眼就看懂的樣子,於是把文字給**加密**了。如果不想讓自己的程式碼需要翻譯機,就要學習怎麼別將文字加密。說來有趣,人們寫程式竟然直覺是寫出加密的程式碼,反倒是要學習技巧來讓程式碼不要加密。 下面提供一些參考方法,以避免寫出加密的程式碼。 ## 方法1:避免魔術數字 **魔術數字(Magic Number)**,是指程式碼中難以辨識其物理意義的數字。我們用[B0001 易讀程式碼的特性:白話](/bdQxzTadSP-HIfXlQZHxLg)出現過的例子說明魔術數字的概念。 ```c++ if (counter_green < 4) { ++counter_green; return 1; } ``` 這段程式碼的`4`與`1`是無法直接辨識其物理意義的,他們就是魔術數字。 該例子用了2個手段處理: * 使用額外 Variable 來儲存數值,透過**有意義的 Variable 名稱**來明確表示該數字所代表的意義。 * 使用`enum`的 Symbol 替換返回用的數值,透過**有意義的 Symbol 名稱**來明確表示該數字所代表的意義。 所以可以看到修改後的程式碼如下: ```c++ if (counter_green < duration_green) { ++counter_green; return light_green; } ``` 避免魔術數字的核心概念,在於用**有意義的名稱來取代無意義的數字**[^1];因此,只要能符合這個核心概念的手段,都是可行的。比方說: [^1]: Steve McConnell, _Code Complete: A Practical Handbook of Software Construction, Second Edition_, 2004 書中第11、12章有談到魔術數字與有意義命名等相關細節。 * 使用`#define`來定義 Symbol,透過**有意義的 Symbol 名稱**來明確表示該數字所代表的意義[^2]。 ```c++ #define duration_green 4 #define light_green 1 if (counter_green < duration_green) { ++counter_green; return light_green; } ``` [^2]: 針對這個案例使用 `#define` 是可行的手段,但如果沒其他考量的話,可能是較不好的選擇。這牽涉到`#define`更多的觀念,需另外再探討,就不在此文章發散討論。 * 使用 Function 來返回常數,透過**有意義的 Function 名稱**來明確表示該數字所代表的意義[^3]。 [^3]: 一般而言,Function 會是比`enum`或常數更厲害的工具,但這個案例如果沒其他考量的話,不需要做到這麼複雜。這牽涉到 Function 更多的觀念,需另外再探討,就不在此文章發散討論。 ```c++ static inline int duration_green() { return 4; } static inline int light_green() { return 1; } if (counter_green < duration_green()) { ++counter_green; return light_green(); } ``` ## 方法2:新增 Variable 以代換 if 判斷式 在閱讀程式碼的時候,很容易遇到「不確定`if`條件是要判斷什麼事情」的情況;倒不是判斷式裡出現魔術數字導致不容易判斷,而是這個判斷的意義可能不好解讀。 為了讓人更容易了解`if`做了什麼事情,有的人會利用註解做說明。舉例來說: ```c++ if (p1_left > p2_right || p1_right < p2_left) { // segments are not overlapped ... } ``` 然而,在讓程式碼易讀的手段裡,加註解雖然是可行的,但常常是缺點相對較多的方式[^4];因此,對於要賦予無直接意義的`if`判斷式,建議可以使用 Variable。修改如下: ```c++ const bool notOverlapped = p1_left > p2_right || p1_right < p2_left; if (notOverlapped) { ... } ``` [^4]: Steve McConnell, _Code Complete: A Practical Handbook of Software Construction, Second Edition_, 2004 書中第32章有談到註解的問題。 這裡可以看到`notOverlapped`直接就表達了這個`if`所做的判斷意義,就不必額外寫註解了。藉由這個方法我們能理解到,透過**有意義的名稱來取代不容易直接理解的判斷式**,可以加強白話的特性。 不過這個方法是有好有壞的,建議在使用時可以判斷一下是否適用自己手邊的情境。 ### 優點:Variable 通常不會過時 這個優點是相對於註解而言。註解畢竟不是直接的程式碼,所以當時間久了,判斷式被調整了或意義改變了,註解很可能不會被同時改寫,這樣註解就**過時**了。而 Variable 通常不會有這樣的問題,因為一旦判斷式被調整了,對應的程式碼都可能跟著變動,用來代表意義的 Variable 很難不跟著調整名稱,否則程式碼就不好維護了。所以說,**Variable 通常不會過時**[^5]。 [^5]: 這裡用「通常」,是因為有些人的撰碼習慣不好;即使變數的意義變了,也不去調整變數名稱,甚至對相關程式碼的邏輯也不重新整理過,也就是會出現名不符實的程式碼。這觀念會在未來的文章 _C0003 不直觀的程式碼:你說的黑不是黑_ 討論。 ### 優點:Variable 可以複用 身為 Variable。如果有一個以上的`if`都要做相同判斷的話,就可以直接利用這個 Variable,而不用重複寫判斷式。不重複寫除了容易維護以外,該判斷式不用被重複運算也是另一個好處。 ### 缺點:可能有些微效能問題 如果有2層以上的 Variable 加入,可能會讓判斷式運算無法提早完成。 舉例來說,如果將案例改寫如下: ```c++ const bool p1_at_right = p1_left > p2_right; const bool p2_at_right = p1_right < p2_left; const bool notOverlapped = p1_at_right || p2_at_right; if (notOverlapped) { ... } ``` 從改寫後的程式碼可以看到,2層以上的 Variable 是可以提升白話程度,但這也表示`p1_left > p2_right`與`p1_right < p2_left`都必須運算過之後,才能組合出`notOverlapped`;而原本1層 Variable 的作法,在遇到`p1_left > p2_right`是`True`的時候,就不會再運算`p1_right < p2_left`了。 雖然存在運算效能問題,但一般來說,除非這個判斷式是在許多層的 Loop 裡運作、或者 Variable 的運算是高複雜度的,否則這樣的些微效能損失通常可以接受,以換取更好的程式碼易讀性。 ## 方法3:打包行為 在上面提到如何避免魔術數字的手段中,有利用 Function 返回常數值的方式來讓程式碼更白話。這個觀念其實可以從單一個數值的處理,擴展到一整段的程式碼。具體而言,是透過**有意義的 Function 名稱來明確表示一段程式碼所代表的意義**;而實作上,就是將一段程式碼行為打包到 Function 中,然後呼叫 Function 來執行被打包的程式碼。 我們用這個概念改寫[[B0001 易讀程式碼的特性:白話]]的例子。請參考調整後的程式碼。 ```c++ static inline bool process_light(int& counter, const int duration) { const bool ret = counter < duration; if (ret) { ++counter; } return ret; } static inline void reset_light(int& counter_green, int& counter_yellow, int& counter_red) { counter_green = 1; counter_yellow = 0; counter_red = 0; } static LightId getLight_4() { const int duration_green = 4; const int duration_yellow = 1; const int duration_red = 5; static int counter_green = 0; static int counter_yellow = 0; static int counter_red = 0; if (process_light(counter_green, duration_green)) { return light_green; } else if (process_light(counter_yellow, duration_yellow)) { return light_yellow; } else if (process_light(counter_red, duration_red)) { return light_red; } else { reset_light(counter_green, counter_yellow, counter_red); return light_green; } } ``` 在調整後的程式碼中,我們可以明確看到數個`if-else`區塊都被呼叫 Function 所取代;而 Function 名稱與參數的組合,恰恰明確告知了該區塊正在進行的工作。仔細來說,我們不用再透過細讀程式碼以判斷前3個`if`區塊分別是屬於3個燈號的處理、及最後區塊是屬於重設狀態以進行下一輪的綠燈、黃燈、紅燈循環[^6]。 [^6]: 這個修改僅展示如何將行為打包,而不是完善這段程式碼,所以會看到 Variable 的存取位置不合理、Function 參數設計有權責疑慮等問題。為專注在本篇文章的重點,這些問題就不在此發散討論。 要注意的是,很多時候使用 Function 的目的在於複用,但不要被這個觀念給綁死了。對於不需要複用的程式碼,一樣也可以用 Function 打包的。這裡的核心觀念,是透過**有意義的 Function 名稱來明確表示該段程式碼所代表的意義**,複用與不複用都與這個核心觀念無關。所以在這個例子中,為了可以獲得更白話的程式碼,可以看到`reset_light`即使只被一個地方呼叫,仍然要打包到 Function 中[^7]。 [^7]: 「打包行為」不只是可以讓程式碼更白話,也是抽象化(Abstraction)、封裝(Encapsulation)等技術的重要基礎觀念。請特別注意這個觀念,未來文章探討會很常出現。 ## 見山不是山 所以,是不是看到常數、看到`if`判斷式沒有用 Variable、看到沒打包的程式碼,就要修正呢?答案是否定的,要小心掉入**見山不是山**[^8]的陷阱裡。 [^8]: [A0002 見山是山,見山不是山,見山只是山](/xiinN0T5QQ2a_V_rCFhavg) 請先複習一下本篇討論過的核心觀念: * 用**有意義的名稱**來取代無意義的數字 * 用**有意義的名稱**來取代不容易直接理解的判斷式 * 用**有意義的名稱**來取代一段程式碼所代表的意義 這些觀念是可以換句話說的: * 有意義的數字不用取代 * 能直接理解的判斷式不用取代 * 清楚意義的程式碼不用取代 根據這樣的換句話說,可以了解到像下面這樣的程式碼就**不適合**再做加強白話特性的處置: ```c++ int sum = 0; for(int i=0; i<size; ++i) { sum += value[i]; } if(sum > limit) { sum = limit; } ``` 這個案例中,已經一目了然地看到了3段概念: * 加總數值歸零 * 對所有數值加總 * 加總超過上限則只保留到上限 對這麼清楚概念的程式碼還要做處理,可能就會變這樣: ```c++ const int zero = 0; int sum = zero; //method 1 add_to(value, size, sum); //method 3 const bool hit_limit = sum > limit; //method 2 if(hit_limit) { sum = limit; } ``` 是不是覺得這樣很畫蛇添足呢?雖然讓程式碼更白話是重要的,但在不合適的情況下硬是要調整,反而更不易讀了。見山不是山,不可不慎!