# 虎年行大運 ~ Clean Code 系列 - 函式 ## 【前言】 最近開始看 Clean Code 了,接下來會做關於書中內容的心得與個人經驗整理的分享文。 ## 【主文】 上一篇講述了命名的部分,這一篇來講關於函式,函式是構成功能的最小單位,一個一個的函式組成可以確立許多不同的業務邏輯,因此函式本身的程式長短與規劃,占了舉足輕重的地位。 ## 【書中的大道至簡】 - **行數的簡化** 首先講求的便是越少的程式碼,讓開發人員相對的越好閱讀和理解,因此能夠減少不必要的物件、重複邏輯、甚至是多功用的設計等,都可以簡潔單個函式的整體。 - **區塊** 為了讓開發人員能夠快速理解,程式碼的區塊應該做好處理進度的分區,每區間隔一行,有必要的話甚至可以在每一段開頭標明註解,簡單說明該段的處理目的。 (雖然這種狀況應該作拆分的處理比較好...) 而像是需要使用到大括號的部分,例如:條件式、迴圈、例外處理等,則一律建議添加完整,這是為了讓整體更容易看懂哪些區塊屬於這些處理的部分,減少開發人員判讀的時間。 - **縮排** 當程式碼被類別、方法(函式)、條件式、迴圈、例外處理等包覆的話,則內容應自動縮減一排,讓整體更易看懂究竟哪些部分被包覆在內。 - **縮排方式** 這一部分大概會挑起 **空白**與**TAB**的戰爭吧... 但 Kai 的建議是擇一使用之後就不要再做更改,讓整體的使用與觀感保持一致性。 - **禁止重複** 這應該是不用提醒的一件事情,當有重複的處理邏輯時,盡量提出為獨立函式,避免本身冗長的處理邏輯,且重複出現的程式碼容易消磨掉開發人員理解函式的精神。 - **符合單一職責原則** 函式作為構成功能最小的單位,應該盡可能的達成一個函式一個功用的設計原則。不該讓一個函式夾帶太多的功用,這會讓函式在後續應用上處理過多的狀況,日後開發上更易遇到因應新需求的新邏輯,這會造成程式碼逐漸肥大、不易拆分、不易維護等狀況。 - **函式的段落** 接續的說單一職則原則的部分,一個設計良好的函式,是無法被切分段落的。當然這僅是一個準則,現實情況往往錯綜複雜。但盡可能地遵循原則是可以讓函式分責更仔細。 更嚴格的要求甚至是會把宣告或初始也當成個別的段落進行切分。 - **保持一層抽象概念結構** 這部分比較不好理解,可以把他想像成:如果一個函式都是負責在呼叫其他函式,則就保持這樣子的作法,不要再有任何處理資料的程式碼;反之如果有負責處理資料的部分,則盡量減少呼叫其他函式的狀況。因為進行呼叫這個動作本身就可能是一件事情,或是需要例外處理的話也是一件可以獨立設計的事情。 - **降層準則** 這是一種容易理解的流程設計法,1. **由上而下閱讀程式碼** 2.**該行的結果可以接到下一行的使用上**。若符合這兩個條件則符合降層準則的理念,一行接一行的有序處理,使整體函式的處理邏輯能夠被容易閱讀。 - **If...Else... / Switch** 條件陳述式大概是程式碼中較難拆分的部分,我們不得避免會使用到這些東西,畢竟現實業務邏輯千奇百怪,一兩個東西差一點就要重寫一份新的函式或類別也太累人。 但這就是一開始設計上應該注意到的地方。我們應該透過作出更多實作來代替冗長的條件陳述式,獨立的實作也可以避免互相干擾,且在擴展上也更方便。可參考 Builder 設計模式。 Switch 被抽出來講是因為他的設計比 if...else... 更容易出現兩個以上的條件狀況。 (若沒有兩個以上也不會特地來使用 Switch 了) 應盡量將其放在較低抽象層的類別裡實作,或是 Builder 設計模式的做法,針對不同資料集合或物件實體的方式去產出對應的實作物件進行作業處理。 - **符合 S.O.L.I.D 設計原則** 無論是否採取 Builder 設計模式做開發,或透過工廠模式將條件陳述式放到較低抽象層進行實做,其設計概念應盡量符合 S.O.L.I.D,這是因為條件陳述式本身就會夾帶太多需要處理的面向:判斷式、通用判斷式、類別高耦合風險、多抽象層風險、空物件風險、例外處理等。長久下來會累積很多弊端。應當能減少就減少,讓其保持只有執行判斷的處理是較佳的做法。 - **使用具備描述能力的名稱** 這部分在命名中提過了,函式的命名盡量以動詞做開頭,後續接著名詞等描述實際處理的邏輯、或產出的結果等。保持描述與產出一致的狀況,避免混淆、或需要思維轉換的情況。 - **動詞和關鍵詞** 已經在命名提過但還是重提一次,有許多業界常用的字詞可以當作優先選擇 - get: 取得、取出 - set: 賦予、修改 - add: 增加、增添 - remove: 排除 - is: 判定 - do: 執行邏輯 - save: 儲存 (與DB操作較相關) - update: 修正、更新 (與DB操作較相關) - delete: 刪除、排除 (與DB操作較相關) - **參數** 最理想的函式參數為 0,最多不超過 3 個。參數一多,開發者就要花費更多的時間在了解這些參數的意義與進入程式後的邏輯處理流程,也更不易寫測試。 - **單一參數** 是最常見的函式類型,進行包含像修改值、返回新實體、返回回應等動作。只要函式本身的邏輯行數不多,一般來說不難理解,在需要做 Clean Code 的優先順序是排較後面的。 - **布林參數 / 旗標參數** 這兩個放一起講,這代表整個函式完全可以進行拆分,秉持單一職責原則,不應該在單一函式中處理條件判斷與業務邏輯。可以分拆成負責處理邏輯的部分與執行業務的部分。甚至參照前面針對 if...else / Switch 的部份,直接做成 Builder 設計模式都行,總之設計上越少的判斷式越好。 - **兩個參數 / 包含但不限於三個以上參數** 若無法減少放入參數的話,則參數的順序非常重要,這是幫助開發人員快速理解函式的重要依據。 - **物件型態參數** 很多時候過多的參數是可以透過包成一個物件的方式傳入使用,例如設計成 POJO 類的物件等等。可以減少送入的函式。 - **無副作用** 函式既被稱為功能最小單位,在追求單一職則原則的情況下,意即只會做一件事情,切莫在程式碼中額外執行其他操作。例如: 做計算的函式,就不要再去管參數的問題,若有拋出錯誤則讓外部接住就好,千萬不要在程式中自己給設定值,以免後續除錯非常難追蹤。 - **輸出型參數** 若開發者對於那些輸入後會返回一個結果值的函式非常依賴,則在每次開發階段可能都會耗費過多的時間在檢查這些函式的輸入與輸出結果是否符合預期,這些花費的時間累積起來十分可觀,歸根究底的是因為這些處理的函式並不那麼透明化,做為主要取得結果的輸出型函式在過往是必要的存在,但在現今可以使用 **this** 作為替代。且如果你設計這類型的函式僅是為了改變物件中某一個參數的值或狀態的話,為何你不直接改變他們本身就好呢? ```java= public String appendFoot(String footString){ return this.string.append(footString); } 記憶過多的輸出型參數函式後,開發人員可能需要每次進行邏輯上的確認與輸入輸出的處理 對比以下 public void appendFoot(){ this.string.append(this.footString); } 開發者僅需要做的是確認好 this.string 與 this.footString 的值即可 ``` - **控制與查詢的動作分離** 遵循單一職責原則,不該讓一個涵式同時擁有多個功用,例如:改變物件狀態、取得結果、執行陳述邏輯等。雖然這些複合式的設計處理得當可以得到 1+1 > 2 的效果,但站在一個維護的立場而言這類型需要思考轉換的設計應當盡量小心謹慎。 我們可以拿 Java 的 Collection 類為例:List, Map 等會把是否已存在物件與放入物件的函式切分,這就是一個非常典型的設計,充分讓開發人員理解到兩方法的不同且不會混用,在使用上可以將程式碼的處理切分得乾淨;Set 卻在放入物件的函式中同時做物件是否已存在判斷,並會回傳一個布林結果,考慮到 Set 的特性,多給一個回傳的功用就是上面提到的好設計 1+1 > 2 的類型,但你是否曾經,曾經有過對這種設計方式感到不那麼簡單易懂、有種彆扭的感覺呢?這就是同時處理控制與查詢兩件事情的混亂感。 - **例外處理** 例外處理該被視為一個事件,因此在設計上來說,他就該是一個獨立的函式。 - **提出 Try/Catch 區塊** 如上述所提到的,遵循單一職責原則,例外處理就該是一個獨立的函式,因此在 try{} 內部執行的陳述式都必須提出為新的函式,並讓他們可以拋出錯誤,而在例外處理的函式就可以完全不用顧慮內在邏輯這件事情了,且別忘了 Try/Catch 後還可能會有一個 finally 的區塊,這會佔據了不少行數,讓程式碼的閱讀判斷變得困難,拖慢開發速度,因此慎重地切分例外處理與業務邏輯,可以簡潔程式碼上的雜亂,並提升閱讀、開發速度。 - **少用回傳代碼的方式** Java 有著針對錯誤的捕捉與拋出等機制,因此減少或者完全不需要使用私設的錯誤代碼,避免開發人員過多的思考轉換過程,因為比起一般程式碼的處理來說,通常碰上例外處理的機會特別高,若此時還有著私設的錯誤代碼機制,那真的會非常耗費時間在理解這些東西上。 Java 提供的為數眾多 Exception 類已經足夠應付大部分的情況,實在是不需要特別加設更多的錯誤代碼。 - **結構化程式設計準則** **Edsger Dijkstra** 提出,亦即在每個函式、甚至每個函式中的區塊,應當只有一個進入點和一個離開點。任何遵守了這個準則的函式必定只有一個進入點,且只有一個 return 當作離開點、迴圈內不能有任何的 continue, break 甚至 goto 指令。 雖然這種設計準則能夠在比較大型的函式中有所助益,但在小型的函式中,善用 continue, break 也未嘗不可,因此比起只有一個進入點、禁止使用 continue, break, goto 來說,我更願意遵循的是只有一個離開點的準則部分,也就是保持只有一個 return,這會讓整個函式的邏輯更淺顯易懂。 ## 【結語】 章節的結語說道,開發系統就像在寫一本書,每一個章節就是一道功能、組成這些功能的是將從你手中描繪出的字詞,這些字詞都是程式碼,美麗的成語、典範引言、編排方式就是你透過字詞組成的函式,你要如何把一句句的內文寫好?將其組織成一個個美麗的段落?並在最後成就一本完整的書?都是透過不斷的練習、一次次小小的成功與失敗的經驗,最終累積起來的成果。 沒有人一開始就辦的到這些事情,但不著手要求自己實行的話,便永遠沒有進展! 首頁 [Kai 個人技術 Hackmd](/2G-RoB0QTrKzkftH2uLueA) ###### tags: `Clean Code 無瑕的程式碼`