閒談軟體設計:Switch 壞味道

更新於 發佈於 閱讀時間約 7 分鐘

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 陳述式裡面) 在許多不同的地方出現。
raw-image

後記

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


avatar-img
53會員
104內容數
這是從 Medium 開始的一個專題,主要是想用輕鬆閒談的方式,分享這幾年軟體開發的心得,原本比較侷限於軟體架構,但這幾年的文章不僅限於架構,也聊不少流程相關的心得,所以趁換平台,順勢換成閒談軟體設計。
留言0
查看全部
avatar-img
發表第一個留言支持創作者!
Spirit的沙龍 的其他內容
長遠的角度來看,內部函式庫還是值得投資的公司資產,只是它需要時間、人力與管理才能做得好。若有不錯的內部函式庫也可以回饋給open-source社群,畢竟,現在開發軟體已經不太可能沒有用到任何open-source的東西。雖然說是將公司資產以 open-source 釋出,但換取的利益卻不見得是零。
整結來說,受到幾種語言的影響,我個人設計 API 時,除了合乎該語言的 convention、上述的穩定性及一致性外,大致還會注意幾點:語意清楚、相近的顆粒度、簡單的文件、讓程式能像文章般閱讀。
第三方套件用了Promise或是Reactive,導致所有business logic都要做調整,這就違反「只能有對內的相依方向」的原則。business logic大多數情況下與效能優化無關,通常需要優化的是I/O的存取,這些既然都在外層,就應該在外層做優化,外層的優化不該影響核心,這才是好架構。
任何語言特性用與不用,其實要看是否提升了生產力?是否提升可讀性?是否提升可維護性?這些都是在三個月甚至半年後回來修改程式時,才能明顯感受到的,而不是寫程式的當下。Java 8 的 CompletableFuture、Stream 和 Optional 都很好,但用的不好反而畫蛇添足又沒提高可讀性。
不管用哪種語言開發軟體,除非是那種一個 function 寫個幾萬行的人 (來人啊,把這種人拖去砍了),不然,一般都會根據某些因素,切割成模組或是特定功能的區塊 (一個 class 或是一個 function),但要完成一個特定功能,這些模組或區塊勢必要一起合作,因此這些模組與區塊就發生了關係。
不重新造輪子,我們使用第三方函式庫,聽起來很合理,但每個被引入的函式庫意味著一種 coupling,看到套件管理工具下載眾多第三方函式庫,意味著不用重寫這些東西,開發效率能提升數倍甚至數百倍,但我們真的都能掌握這些 coupling 嗎?當這其中任何一個環節出錯,我們的系統架構真的很優雅地應付嗎?
長遠的角度來看,內部函式庫還是值得投資的公司資產,只是它需要時間、人力與管理才能做得好。若有不錯的內部函式庫也可以回饋給open-source社群,畢竟,現在開發軟體已經不太可能沒有用到任何open-source的東西。雖然說是將公司資產以 open-source 釋出,但換取的利益卻不見得是零。
整結來說,受到幾種語言的影響,我個人設計 API 時,除了合乎該語言的 convention、上述的穩定性及一致性外,大致還會注意幾點:語意清楚、相近的顆粒度、簡單的文件、讓程式能像文章般閱讀。
第三方套件用了Promise或是Reactive,導致所有business logic都要做調整,這就違反「只能有對內的相依方向」的原則。business logic大多數情況下與效能優化無關,通常需要優化的是I/O的存取,這些既然都在外層,就應該在外層做優化,外層的優化不該影響核心,這才是好架構。
任何語言特性用與不用,其實要看是否提升了生產力?是否提升可讀性?是否提升可維護性?這些都是在三個月甚至半年後回來修改程式時,才能明顯感受到的,而不是寫程式的當下。Java 8 的 CompletableFuture、Stream 和 Optional 都很好,但用的不好反而畫蛇添足又沒提高可讀性。
不管用哪種語言開發軟體,除非是那種一個 function 寫個幾萬行的人 (來人啊,把這種人拖去砍了),不然,一般都會根據某些因素,切割成模組或是特定功能的區塊 (一個 class 或是一個 function),但要完成一個特定功能,這些模組或區塊勢必要一起合作,因此這些模組與區塊就發生了關係。
不重新造輪子,我們使用第三方函式庫,聽起來很合理,但每個被引入的函式庫意味著一種 coupling,看到套件管理工具下載眾多第三方函式庫,意味著不用重寫這些東西,開發效率能提升數倍甚至數百倍,但我們真的都能掌握這些 coupling 嗎?當這其中任何一個環節出錯,我們的系統架構真的很優雅地應付嗎?
你可能也想看
Google News 追蹤
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
這篇內容,將會講解什麼是「switch」,以及與「switch」相關的知識。包括switch的簡介、switch、break。
Thumbnail
本章節為Swift程式語言的異常處理介紹,說明了為何需要進行異常處理以及如何進行異常處理。提供了使用do、try、catch和throw關鍵字進行異常處理的基本語法並展示了其在實際程式中的應用。同時也說明了Swift中的一些常見異常類型,並且教導了如何主動觸發異常訊息和定義自己的異常類型。
Thumbnail
本篇介紹了Swift程式語言中的各種流程控制元素,包括條件語句(如if, else if, else),三元運算子,多條件分支判斷的switch語句,以及各種迴圈(如for迴圈,while迴圈,以及repeat-while迴圈)。同時也詳細解釋了如何進行迴圈嵌套,以及如何使用控制迴圈語句。
※ switch用法: ​switch是 JavaScript 中的一個控制結構,是一種更結構化的方法來替代多個 if...else 語句,特別是當需要根據同一變數的多個值進行不同操作時非常有用。 ※ switch語法: switch 語句首先評估括號內的表達式 (expression)。
Thumbnail
在程式世界裡,if 條件句是我們的好朋友,幫我們做各種決策。如果不注意可能會讓我們掉進小陷阱。文中透過幾個例子,在使用 if 時可能會遇到的一些常見問題,像是不必要的 if、過於複雜的條件、忘了用嚴格比較,還有嵌套太深的 if。透過這篇文章,你將學到如何避免這些小錯誤,寫出更乾淨、更有效率的程式碼。
讓我在這篇文章總結一下前面對物件導向設計的討論,我們討論了物件導向的四個特性:繼承、抽象、多型、封裝,分析了它們的問題,並跟函數式編程的思維做比較。我們引入了與之相對應的特性:泛型、特性系統、模組化,有些特性雖然跟那四個特性很像,但在一些細微的地方有不同的詮釋,使得整體思考方式很不一樣。 「繼
Thumbnail
在人生中總是會遇到許多選擇,做了不同的選擇產生的結果也會不一樣,大多時候都是在為了要吃什麼在抉擇,小孩才做選擇大人全部都要,全部都要何嘗也不是一個選擇。 在Python程式語言中也有選擇的語法,就是If Else,如果是就做什麼,不是就做什麼,有別於其他程式語言,他不一定要有else,可以只有If
Thumbnail
本文將介紹自定函式及應用,利用程式範例解釋為什麼要用到自定函式 自定函式好處當然就是,讓你的程式碼看起來比較簡潔,在重複使用到的程式碼區塊,可以包裝成函式,讓你重複使用它。
Thumbnail
IF,Switch,三元運算子語法說明 IF條件選擇結構說明 IF為布林條件,當()內條件式滿足True執行if區塊的程式碼,不滿足則執行else區塊的程式碼,若無else也行。
Thumbnail
嘿,大家新年快樂~ 新年大家都在做什麼呢? 跨年夜的我趕工製作某個外包設計案,在工作告一段落時趕上倒數。 然後和兩個小孩過了一個忙亂的元旦。在深夜時刻,看到朋友傳來的解籤網站,興致勃勃熬夜體驗了一下,覺得非常好玩,或許有人玩過了,但還是想寫上來分享紀錄一下~
Thumbnail
這篇內容,將會講解什麼是「switch」,以及與「switch」相關的知識。包括switch的簡介、switch、break。
Thumbnail
本章節為Swift程式語言的異常處理介紹,說明了為何需要進行異常處理以及如何進行異常處理。提供了使用do、try、catch和throw關鍵字進行異常處理的基本語法並展示了其在實際程式中的應用。同時也說明了Swift中的一些常見異常類型,並且教導了如何主動觸發異常訊息和定義自己的異常類型。
Thumbnail
本篇介紹了Swift程式語言中的各種流程控制元素,包括條件語句(如if, else if, else),三元運算子,多條件分支判斷的switch語句,以及各種迴圈(如for迴圈,while迴圈,以及repeat-while迴圈)。同時也詳細解釋了如何進行迴圈嵌套,以及如何使用控制迴圈語句。
※ switch用法: ​switch是 JavaScript 中的一個控制結構,是一種更結構化的方法來替代多個 if...else 語句,特別是當需要根據同一變數的多個值進行不同操作時非常有用。 ※ switch語法: switch 語句首先評估括號內的表達式 (expression)。
Thumbnail
在程式世界裡,if 條件句是我們的好朋友,幫我們做各種決策。如果不注意可能會讓我們掉進小陷阱。文中透過幾個例子,在使用 if 時可能會遇到的一些常見問題,像是不必要的 if、過於複雜的條件、忘了用嚴格比較,還有嵌套太深的 if。透過這篇文章,你將學到如何避免這些小錯誤,寫出更乾淨、更有效率的程式碼。
讓我在這篇文章總結一下前面對物件導向設計的討論,我們討論了物件導向的四個特性:繼承、抽象、多型、封裝,分析了它們的問題,並跟函數式編程的思維做比較。我們引入了與之相對應的特性:泛型、特性系統、模組化,有些特性雖然跟那四個特性很像,但在一些細微的地方有不同的詮釋,使得整體思考方式很不一樣。 「繼
Thumbnail
在人生中總是會遇到許多選擇,做了不同的選擇產生的結果也會不一樣,大多時候都是在為了要吃什麼在抉擇,小孩才做選擇大人全部都要,全部都要何嘗也不是一個選擇。 在Python程式語言中也有選擇的語法,就是If Else,如果是就做什麼,不是就做什麼,有別於其他程式語言,他不一定要有else,可以只有If
Thumbnail
本文將介紹自定函式及應用,利用程式範例解釋為什麼要用到自定函式 自定函式好處當然就是,讓你的程式碼看起來比較簡潔,在重複使用到的程式碼區塊,可以包裝成函式,讓你重複使用它。
Thumbnail
IF,Switch,三元運算子語法說明 IF條件選擇結構說明 IF為布林條件,當()內條件式滿足True執行if區塊的程式碼,不滿足則執行else區塊的程式碼,若無else也行。