2023-10-13|閱讀時間 ‧ 約 8 分鐘

閒談軟體設計:Switch 壞味道

Switch 的問題

這篇的標題來自《重構 — 改善既有程式的設計》第一版的第三章:程式碼的壞味道,條列了 22 個程式碼常見的壞味道,像很多人朗朗上口的 duplicated codelong methodlarge classlong parameter list 等等,其中有一個壞味道倒是比較少人提到,便是這篇的主題:switch statement

想當初念博班當助教時,將這些壞味道列為程式作業扣分標準,學生被扣分恨得牙癢癢,我則是改作業改到快抓狂,但不知道為什麼那堂課學生卻越來越多人修,即使第一堂課我去勸退也沒用,不知道這些當初被扣分的學生畢業後程式寫得如何?

比較少被提到是因為幾乎所有程式語言都有 switch statement 的設計,使用它就像呼吸空氣一樣自然,但書中作者認為 switch statement 問題的本質就是重複,我想這應該很明顯,switch 通常不會單獨出現,以最近看到的一個 Moya 套件為例,當需要實作一個接 API 的 service 時,程式可以寫成如下 :

先說明一下,這裡只是用 Moya 舉例 switch statement 多半會出現重複,沒有說 Moya 不好,可能是在下愚昧看不出來好處在哪裡。

會發現到 switch statement 出現了三次,我看了許久還是無法理解這樣寫的好處是什麼?對我來說,像下面提供一個抽象層的介面 (protocol),然後提供一個實作的 RestfulUserWebService反而自然多了:

在設計這例子的介面時也猶豫過 (1) 用內建的 Result 表達結果;(2) 傳統的結果和錯誤參數分開的 closure;(3) 或是兩個獨立分開的 closures,個人比較偏好第三種,但第三種的缺點是,一個函式帶兩個 closures 會讓程式不好讀,第二種方式無法避免兩個參數都是 nil 的情況,所以使用第一種設計。

但 Result 本身就是一個 enum,所以會有很多的 switch statement,個人沒有很喜歡,偏偏非同步 API 很難用 try catch 來處理錯誤。若改用回傳 Promise [1][2] 的話會更精簡,省去 enum 和 switch statement。

重構

用上 enum,很容易就用上 switch statement,但是可能避免的,書中提到 Replace Type Code with SubclassesReplace Conditional with Polymorphism 或 Replace Type Code with State/Strategy 取代 switch statement。enum 就是一種 type code,因此可以可慮用 Replace Type Code with Subclasses,例如以前在改學生的 OOP 作業時會常看到類似這樣的 switch statement:

透過 Replace Type Code with Subclasses 拆成 RectangleTriangle 和 Circle 三個類別,並將原本寫在 switch statement 中的邏輯分別放到對應類別的函式中。

最後就能用 Replace Conditional with Polymorphism,用一行程式碼把 switch statement 取代:

有人可能會說,一定要用 OO 的方式處理嗎?當然不是,事實上這幾年工作都是寫 JavaScript (重構的第二版用很多 JavaScript 的例子),還是可以寫出不用 class 和 switch statement 的程式。

例如前陣子處理支付的功能,每種不同的支付需要呼叫的第三方服務不一樣,參數也不一樣,若以 OO 的角度想,很快會想到將不同支付的方式宣告共同的 interface,然後為每種支付方式提供一個實作該介面的類別,然後在執行期間根據付款方式生成對應的物件來執行,但用 JavaScript 可以簡化不少程式 (例子中仍有 type code,猜猜看在哪裡?):

例子中沒有 switch statement,而且每個 require 進來的程式其實也就只是一個符合規範的 function:

在 Java 8 導入 Lambda 後,其實很多 design pattern 用 Java 也可以寫得很精簡 (參閱拙譯《用 Lambda 實作設計樣式》),重要的是抽象的思維,而不是 OO 或是非 OO 的語法

上述的例子參數都是 paymentRequest,可以說型別是一樣的,但如果型別不一樣呢?例如回到 Shape 的例子,假設現在要把圖形存成一個 JSON 檔,這時候是可以用多型處理,但如果想把檔案輸出的邏輯抽到別的類別,這時就得一一檢查是哪個類別,根據類別有不同的處理方式,若不想寫 type cast (其實 type cast 也算是一種 switch statement,而且還是比較醜的那種),可能得用上 Visitor,但就是稍嫌麻煩,而且不好讀。

這時候即便是不喜歡動態型別的我,也得說 JavaScript 沒有這煩惱。

Java 也可以用這寫法,但要搭配 cast 使用,不然無法取得 concrete class。

所以說要完全避免使用 switch statement 嗎?我倒覺得也不用這麼極端,應該還是回到問題的本質:是否會造成重複?如果這個 switch statement 就只會出現這麼一次,那也許就算了,例如下面的例子,從資料庫中將不同的圖形讀出來,並生成不同類別的物件,假設整個專案只有這裡會用 switch statement,其他都能依靠多型處理,那這裡是不是一定要用成 factory 來處理,就可以斟酌。

個人偏好

當要宣告 enum 時,先想想宣告 enum 的目的是為了什麼?因為想寫 switch statement,那也許可以想想是否還有其他更好的方式,當然,OO 的語法和非 OO 的語法,實作的方式可能不一樣,但同樣的抽象都是可以用不同的語法來實現。

我個人就想過,Redux 應該是有機會不用寫 switch 才對 XD

我個人是盡可能不寫 switch statement,但觀察這幾年程式語言的趨勢,會發現許多語言把 switch statement 擴充成為實作 pattern matching 的工具,說不定以後 switch statement 會越來越廣泛使用也說不定。

為了馬克杯,把《重構 — 改善既有程式的設計》第二版和另外三本 O’Reilly 的好書一起搬回家,買回來立即翻到第三章,發現 switch statement 被改成 repeated switches,而書中的描述也和上述一樣:

如今,多型有更多含意,它已經不是 15 年前那個簡單的紅旗了,此外,許多語言都支援更精密的 switch 陳述式,不是只使用原始代碼 (primitive code) 作為基礎。所以我們現在把注意力轉到重複的 switch,也就是有同一組條件切換邏輯 (無論是在 switch/case 陳述式裡面,還是在串連起來的 if/else 陳述式裡面) 在許多不同的地方出現。

後記

正好在這篇文章寫完後沒幾天看到這一篇文章,如何用 enum 移除程式中的 if/else,但仔細一看,其實是用上 Java 在 enum 上的特殊設計,所以簡單來說,enum 只是用來規範有限的 strategy,算是一種挺有趣的寫法,可以參考看看,缺點可能就是這個 enum 檔案會比較大一點。


分享至
成為作者繼續創作的動力吧!
© 2024 vocus All rights reserved.