# B0002 易讀程式碼的特性:直觀 ## 直接,不要省略 關於程式碼是否易讀,除了**白話**之外,**「直觀」** 也是很重要的特性。所謂的**直觀**,就是程式碼的字面邏輯要符合直覺,想做的事情都直接表達,不要省略。 ### 忍不住要問為什麼 請嘗試閱讀以下敘述,感受一下文字表達是否足夠直觀。 _敘述組1_: * 方向盤向右轉讓汽車向左轉。 * 按2次向上音量鍵,才讓音量增加。 * 跑道起跑線後退2公尺為起跑點。 * 輪值的第2輪排班從第5個人開始。 相信在看到這些敘述後,心理肯定是忍不住要問 **「為什麼要這樣做」** 吧?這些敘述都很不直覺,閱讀起來很莫名其妙。對於無法理解原因的敘述,是無法看懂的;如果程式碼所呈現的邏輯就像這類的敘述,即使已經寫得很白話了,易讀性還是不佳。 ### 直覺就不用問為什麼 看完 _敘述組1_ ,可以對比一下另一組類似內容的敘述,如下。 _敘述組2_: * 方向盤向右轉讓汽車向右轉。 * 按1次向上音量鍵,讓音量增加。 * 跑道起跑線為起跑點。 * 輪值每一輪的排班都是所有人列入排班。 是不是覺得 _敘述組2_ 既自然又直覺呢?想必一般人看到這樣的敘述,都能輕易理解敘述所代表的邏輯,不會去問為什麼要這樣做。如果程式碼所表達的邏輯都不需要再問為什麼,那麼就能建立很好的易讀性。 ### 填補因果關係 然而,_敘述組1_ 與 _敘述組2_ 畢竟還是代表了不同的程式碼行為,這樣的比較略為不公平。倘若一定要完成 _敘述組1_ 想要達成的行為,要如何描述會比較好呢?答案不難,只要將**因果關係**明確呈現即可。 請再嘗試閱讀下面填補**因果關係**後的新敘述,感受一下文字表達是否足夠直觀。 _敘述組3_: * **由於事故當中判斷右轉有危險**,因此即使方向盤向右轉,也要讓汽車向左轉。 * **由於架構限制,1次按鍵只能對應一個反應,而在選單還未跳出時,按音量鍵會先讓選單跳出**,因此按2次向上音量鍵,才讓音量增加。 * **由於要從排位賽成績調整起跑點,因此排位賽第2名的**跑道起跑線後退2公尺為起跑點。 * **由於職員表前4位為業務較多的主管,每月的輪值只需參與1次,因此**輪值的第2輪排班從第5個人開始。 在 _敘述組3_ 裡可以發現,_敘述組1_ 的主要敘述沒有更動,僅在前面填補原因。當原因有清楚明白地提供出來,讀起來就不會覺得莫名其妙。如果程式碼能明確表達因果關係,讓邏輯脈絡鉅細靡遺地呈現,易讀性就能因直觀而大大地提升。這就是**直觀**的核心概念。 ## 邏輯翻譯機 如果說不夠白話的程式碼需要**字面翻譯機**[^1],那不直觀的程式碼就需要**邏輯翻譯機**。仔細來說,不直觀的程式碼會有一個背後理由,而這個理由沒有在程式碼中明確表達出來。因為不明確表達,導致看到相關的程式碼,都必須在腦中把這個理由補進來,才能讀懂邏輯。這就是**邏輯翻譯機**。 [^1]: [B0001 易讀程式碼的特性:白話](/bdQxzTadSP-HIfXlQZHxLg) 一樣都是進行翻譯,但**邏輯翻譯機**其實比字面翻譯機更難以駕馭。主要的理由有3個面向:**複雜**的對應關係、**難以反推**的對應關係、與**多層次**的對應關係。 ### 複雜的對應關係 一般而言,字面翻譯機通常是簡單的對應關係,像是數字1、2、3分別代表綠燈、黃燈、紅燈,或者數字 4、1、5分別代表綠燈、黃燈、紅燈維持的秒數,對於腦力、記憶力超好的人來說,還是可處理的範圍。然而,**邏輯翻譯機**往往是複雜的對應關係,從「職員表前4位為業務較多的主管,每月的輪值只需參與1次」這樣複雜的理由可以看出,單一個翻譯機對作者或讀者的負擔之重,不是一個字面翻譯機能比擬的。 ### 難以反推的對應關係 由於字面翻譯機是簡單的對應關係,對經驗豐富的程式設計師人員來說,遇到需要字面翻譯機的情況,是有機會不需要原作者說明,就猜到對應關係。相反地,邏輯翻譯機的**對應關係幾乎不可能直接猜到**。回顧前面例子,應該是沒人能透過 _敘述組1_ 直接猜到 _敘述組3_ 的吧?! ### 多層次的對應關係 基本上,字面翻譯機就是一對一的對應關係,像「1代表綠燈」就是一個數字對應一個意義,不太會有另一層理由去決定這個1;而邏輯翻譯機的對應關係可能是一連串因果關係所建立的。一樣拿 _敘述組3_ 的例子來說,「職員表前4位為業務較多的主管」可能是因為「職員表原本就按照員工編號由小到大排列,而且小編號為主管」,甚至員工編號也可能不是真實編號,而是「系統基於某個設計給予每個員工編號,方便做處理」。換句話說,當我們看到「輪值的第2輪排班從第5個人開始」這樣的程式碼,背後所代表的程式碼意義應包括「系統的員工編號規則」、「職員表的排列方式」等一連串的因果關係,最終才是看到的排班程式碼。 如果把每一個細節分拆清楚,可以建立如下圖**多層次的對應關係**。從關係圖可以看到,需要清楚編號、規則、排序、知識、考量等直接或間接的因果關係,才能完成最終實作。如果程式碼不將這些概念清楚明白地表達出來,可見需要多少腦力才能記清楚這麼多細節。 ```mermaid graph TD; ENUM["編號:系統給予員工編號"] RULE["規則:編號先派給主管,再派給基層員工"]; SORT["排序:職員表照編號由小到大排列"]; KNOW["知識:主管有4位"]; CONC["考量:主管業務較多,只輪值1次"]; IMPL["實作:輪值的第2輪排班從第5個人開始"]; ENUM --> SORT; RULE --> SORT; SORT --> IMPL; KNOW --> IMPL; CONC --> IMPL; ``` ## 不直觀的原因 對於有經驗的程式設計人員,在學習到要避免的問題後,像是程式碼壞氣味[^2],通常會設法去避免。然而,即使已經知道**不直觀**的程式碼相當不易讀、會對腦力造成很大的負擔,卻還是不自覺地寫出了這樣的程式碼。人們會很自然地完成工作流程,包括完成程式碼、進行 Debug、通過測試、量產、維護等等,卻沒有發覺自己在處理程式碼的過程中,消耗了大量的腦力在做邏輯翻譯。 [^2]: 提到程式碼壞氣味的資料、文獻有許多,這裡僅提供一篇不錯的整理文章供作參考。未來會另外撰文探討壞氣味。 https://carger.tips/%E9%87%8D%E6%A7%8B-refactoring-%E5%AD%B8%E7%BF%92%E5%BF%83%E5%BE%97%E7%AD%86%E8%A8%98-%E5%A3%9E%E5%91%B3%E9%81%93-bad-smell-code-smell 為什麼會這樣呢?可能要歸咎於**不寫原因的慣性**與**不易檢查的盲點**。 ### 不寫原因的慣性 試想一下,我們是不是都有這樣的慣性:「經過思考並設計好一個方案後,直覺地將方案的結論撰寫出來,而忘了把原因寫清楚」呢?拿輪值的例子來說,在思考過編號、規則、排序、知識、考量等狀況後,想出了「輪值的第2輪排班從第5個人開始」的解決方案;接著,很自然地把解決方案寫完之後,就沒有然後了。如果沒有回頭檢查是否有漏了什麼,一旦程式碼運作正常,應該很難察覺自己沒有把原因寫進去吧! ### 不易檢查的盲點 就算有回頭檢查好了,其實也很容易有**盲點**。透過對比程式碼壞氣味,大概可以看出為什麼會有**盲點**。 程式碼壞氣味,像是參數太多、太多行、霰彈修改、耦合性太高等等,都是直接現象、明確指標,這些就像是線索,可以通過檢查是否存在線索,然後對程式碼進行改進。可是程式碼**不直觀**的關鍵在於沒有將**原因**明確地寫進程式碼裡,而**原因**本身是邏輯推演的過程,不是一種線索,也就很難直接被檢查出來。更何況,作者在撰寫的當下,是帶著邏輯翻譯機在敲程式碼的,會把原因當成理所當然,就不覺得要再補充什麼,這就是**盲點**。 ## 如何往直觀程式碼邁進呢? 在知道不直觀的原因之後,就應該思考怎麼解決。 首先,要破除**不寫原因的慣性**,就需要先養成**回頭檢查**的習慣。所謂回頭檢查,是指別讓自己陷在當下撰寫的一小塊程式碼裡,跳脫到外層看一下程式碼的流程、順一下邏輯,釐清是否有不通順的地方,再設法將邏輯補齊。要知道,程式碼本身就是在處理邏輯,而不通順的地方很可能是邏輯漏洞;也就是說,**回頭檢查**不只可以把邏輯補完整,甚至有機會早期就排除邏輯漏洞有關的 Bug。 接著,要排除**不易檢查的盲點**,一樣也是要透過**回頭檢查**的習慣,特別是**過一段時間後**再回頭檢查。理由是,一般撰寫程式碼時,對於細節的記憶力通常很短,過一段時間回頭看程式碼就像是別人寫的了[^3]。這麼做等於是忘了自己原本已知的**邏輯翻譯機**,就不會有理所當然的**盲點**了。當然,不排除有人記憶力超強,任何程式碼細節都記得清清楚楚,沒法徹底忘掉已知的邏輯翻譯機;這時就只能藉由程式碼審查(Code Review)找人幫忙了。由於審查者不是作者本人,本來就不清楚實作的前因後果,不會有**邏輯翻譯機**有沒有忘記的問題。透過別人的眼睛與腦袋,就更容易排除盲點了! [^3]: Eagleson's Law 是這麼說的:「自己寫的程式碼超過6個月沒看的話,就像是別人寫的一樣」。英文原文為 "Any code of your own that you haven't looked at for six or more months might as well have been written by someone else." 總而言之,程式碼要直觀,就是要不斷問為什麼要這樣寫,只要還能問出為什麼,就把原因補進程式碼裡。如果自己已經覺得沒問題了,可以再找其他人幫忙,確保沒有盲點。假以時日練習,就能把清楚寫出邏輯的動作變成習慣,寫出的程式碼就都能直觀了! ## 延伸閱讀 [[C0002 不直觀的程式碼:反應遲鈍]] (撰寫中) [[C0003 不直觀的程式碼:你說的黑不是黑]] (撰寫中) [[C0004 不直觀的程式碼:逆向行駛]] (撰寫中) [[C0005 不直觀的程式碼:就是要特權]] (撰寫中)