第三章 函式—Clean Code 閱讀筆記

MowLi 微風
微風飛翔
Published in
8 min readJul 18, 2021
Photo by fotografierende on Unsplash

簡介

本篇主要目標為介紹如何寫出一個好讀易懂的函式,Clean Code 書中提供了許多觀點,我取出書中內容的六個大項,依照內容分類到各個項目中,並附上一些程式碼範例幫助理解。

前言

本章 Clean Code 提供一個約 3 分鐘可以看完的程式範例,不過 Uncle Bob 相信大部分人看完都不太能理解,因為有太多詭異的函式呼叫。經過重構之後, Uncle Bob 把這段冗長的程式碼變成一個 6 行就能看懂的程式碼,其中的關鍵到底是什麼?

一、除了簡短,還是簡短

這是本章最重要的概念,根據 Uncle Bob 看過無數函式的經驗,他給出了一個認為核心的準則:除了簡短,還是簡短

這跟大家愛看懶人包一樣,很多時候我們對繁雜的細節並不感興趣,直接給我重點就好,而『重點』往往象徵著用一兩句話便能詮釋整個事件的一種描述,Uncle Bob 希望函式也應該如此

請以最短且精確的程式碼來說明函式到底想做什麼,這能有效提升函式的易讀性。

(一) 函式通常只做一件事情,並且把這件事情做好

我們不會把宣告、初始化、渲染 (render) 等邏輯全然放到一起,很顯然的各別屬於不同概念的東西。

如何知道自己有沒有做超過一件事情可以從兩個方向判斷:

  1. 函式內做的事是不是同一個層次的概念
    借韓導的名言「我們談的是大海,你問我漱口杯的問題」;在組織結構圖中,你覺得自己會跟主管放在同一階層嗎?大概是這個概念,不要把大方向跟細節放在一起。
  2. 該函式是不是還能提煉出另外一個新函式。

(二) 縮排和區塊

  1. if, else, while 之中的敘述應該只有一行,而那一行通常是呼叫函式。

2. 縮排最好 (盡量) 不要多於兩層,因為複雜的巢狀結構會影響閱讀和理解。下面這個範例還沒到三層就讓人有點眼花了。

(三) 降層準則

寫程式跟撰寫一篇文章一樣,你必須帶領你的閱讀者一步一步進入你的思考邏輯,你不能自顧自地寫得很爽,看的人會覺得很賭爛,這讓我想到一個經常讓我審稿的哲學系友人。

因此,書中建議由上往下依序敘事,我自己覺得也可以理解成 Top-Down 或 Hierarchy 的概念,每一個函式後面應該緊接著『下一層次』的函式,把你要做的事情慢慢從綜觀的角度講到細節,使閱讀的人可以順著讀下來。

1. 我今天剛睡醒要準備上班,呼叫 prepareGoToWork()
2. 上班前其中一件事是刷牙,呼叫 brashTeeth()
3. 拿起牙刷並擠牙膏,呼叫 squeezeToothpaste()
4. … [下一層次]

二、具備自我描述的名稱 — 花 1 秒 v.s. 花 1 分鐘

其實前一章已經有提到命名的重要性,函式命名有一個重要觀點:別害怕去取一個較長的函式名稱。

  • 名稱長但表達清楚的函式只會「多花一秒」的時間去看它的名稱。
  • 但是,你有可能因為一個名稱過短的函式「多花了一分鐘」去理解這段 Code 到底在幹嘛。

三、傳入型參數的數量與輸出型參數

Uncle Bob 的三個觀點:

  1. 函式的傳入參數具有透露概念的能力,你可能會先關注到當下不需要知道的細節。
  2. 並不是用越多就越不好,作者想表達的是:當你的傳入的參數越多,越容易讓人模糊,也提高了發生錯誤的可能
  3. 函式傳入參數的理想數量為 0 個,最多請勿超過 3 ,除非有特殊理由。

(一) 1 個傳入參數

  • 若函式有回傳值,命名必須符合閱讀者的預期結果
    名稱如果有 is 或 exist,大多預期會回傳 boolean,假設今天它回傳 String 的話會讓人很困惑。
  • 事件命名要盡量讓閱讀的人意識到這是一個事件 (event)
    傳入單一參數並不回傳值的函式,稱為事件,通常是用來更改系統的狀態,setter 就是事件的一種。

(二) 2 個傳入參數

  • 有時參數會搞混先後順序,可以用命名表示。
    isEquals(expected, actual);
    改成 ↓
    isExpectEqualsActual(expect, actual);
  • 允許的話,盡量縮減參數。有以下方式:
    1. 將方法變為類別方法。
    2. 將參數當作類別的建構子,並在類別提供要使用的方法。

以下是依據書中想法寫的 pseudo code,實際上書中只有給一行,其他都用文字描述,所以我只能用猜出其他部分是怎麼寫的。

說明:writeField(sourceStream, name) 主要將 sourceStream 寫入一個名為 name 的新欄位,而writeField我猜測是寫在 public class 的靜態方法。

補充:至於為何一開始要以這種方式去 call 函式我也不太清楚,至少我不會這樣用 (而且用 outputStream 舉例好像有點不妥,所以我換成 sourceStream)

(三) 3 個傳入參數

理念與大致上都差不多,若允許的話,盡量縮減參數,有以下方法:

1. 傳入物件型態的參數
將圓形的 x , y 直接變成物件的方式傳入。

2. 參數串列
當參數的型態是一樣但數量不固定時,等同於傳入一個 List 型態的參數,在 Java 之中稱為 Variable Arguments (Varargs),它會以 List 方式存取變動長度的參數。

(四) 輸出型參數

以文字來形容可能會有點抽象,我對輸出型參數的理解大致上是:

  • 丟進去的參數會去操作別人,而不是被別人操作。
  • 輸出型參數最後會更新自己的狀態。

appendFooter(report) 函式的意義是「將 Footer 加到 report 後面」,傳入的 report 可能會做一些如 report.append("...") 的動作,不過大家也有可能把它誤解成「某個東西後面接了 report 這個 Footer」。

如同前面所提到的方法,能將 appedFooter() 變成 report 類別的方法,這樣你就可以直接用 report.appedFooter()

書中建議「盡量不要」用這種會讓人停下來思考的輸出型參數(但我好像不經意地會使用到😢)

四、以例外處理取代錯誤碼

以下引用書中的範例作為說明,直接看程式碼會比較快。

1. 錯誤碼寫法

2. try/catch 寫法

錯誤碼的劣勢就是一但你寫了一種情況,你立刻要用 else 處理沒有發生的情況,這樣寫下去會遇到俗稱的「波動拳」;但 try-catch 直接用 catch 來統一處理錯誤,既簡潔又好用,而且你還可以再把邏輯抽成一個函式來使用 (例如:deleteCodeAndAllReferences)。

五、避免副作用 (Side Effect)

Side Effect 指的是你命令這個人掃地,但實際上他卻連拖地跟倒垃圾也一起做了。如果現實世界真的有這種人是蠻好的啦,不過你會發現事情的結果不會照著你的計畫走。

切回程式的角度,假設有一個public boolean checkUserPassword()函式,可以預期這個函式會檢查密碼正不正確並回傳布林值,但如果它檢查完卻順便初始化了使用者的 Session 或 Cookie呢?

這會讓你之後 Debug 很痛苦,因為你可能找了半天才發現初始化有可能做了兩次,而且你一開始不會來這個函式找,因為函式的名稱「完全不會讓你懷疑」它會做初始化的動作。

蝦米!我會犯這種錯嗎?哦,因為有時候你讀得並不是自己寫的程式啦,所以我認為只能盡量避免自己犯這種 Side Effect。

六、DRY 原則

DRY 原則 (Don’t Repeat Yourself) 指的是不要重複自己。軟體發展從過去到現在,許多技術/規則都致力於消除程式碼的重複性,例如:SQL 的正規化、物件導向的類別、剖面導向設計…等等,你只要發明一種消除程式碼冗餘的方法就足以成為大師了,可見這件事有多重要。

我對於 DRY 有一些見解,本身會用內聚性 (Cohesion) 的概念來解釋它。冗餘 (redundant) 程式碼是「內聚性低」的代表,如果改了一個地方,其他五個地方也要跟著改就是內聚性太爛了,那你應該要把他們「收納在一個盒子裡」,這個盒子可以是一個函式。

筆者自己會試著觀察有哪些東西改了就會改動其他程式碼,通常都是可以下手的地方。但有些不是表面上可看的到的,而是邏輯上的重複,這只能靠自己多閱讀程式碼跟重構的經驗了。

補充:記得也要注意耦合性 (Coupling),不要把程式碼一昧地都搬在一起。

總結

函式是組成程式的重要基礎,也是程式裡面的動詞,若沒辦法寫好一個函式,其他人甚至是你自己也無法預期執行之後會發生什麼行為。

本篇文章介紹了六個如何將你的函式寫成一個易讀好懂的函式,包括:簡短、具備自我描述的名稱、傳入/出型參數的注意事項、try-catch例外處理、避免副作用及 Dry 原則,希望有幫助到大家對寫出一個好函式有更進一步的理解。

本篇是自己對內容的理解,若有任何問題,歡迎大家幫忙指正,謝謝 😃

--

--