前一陣子看到 PTT Soft Job 上面有人寫了一篇文描述怎麼用 Strategy Pattern 來 refactor 某段 code,我在這邊為了方便閱讀先節錄一部分:
from https://www.ptt.cc/bbs/Soft_Job/M.1607866053.A.4BB.html
然後以下是 refactor 過後的程式碼片段:
首先他訂了個 interface
然後所有 shipper 都會實作這個 interface:
於是本來的 function shippingFee 被 refactor 成這樣:
首先,這根本不能算「策略模式」,只能算是一般的多型應用,不過我這邊不是很想討論 strategy pattern 本身,有興趣的可以去 wiki 比較一下差在哪裡。
基本上有兩點可以討論:
原 solution 定義了一個 interface,所以要實作這個 function 必須建立一個 class 來實作這個 interface,所以算是有解決到這個問題。但其實單純的為不同的 shipper 建立相對應的 function 就行了,並沒有必要多一個 interface:
假設今天,我們新增了一個貨運商,工程師記得要建立一個新的 class 並實作 Shipper interface,但是他忘了把它加入 shippers hashmap,又剛好沒寫測試,於是 rollout 之後就觸發了 exception,就 QQ 惹。
有沒有方法可以保證不會有例外呢?這問題就有點有趣了,但首先讓我們先換一個語言kotlin:
因為 kotlin 的 when 有提供 exhausive check 的功能。只要使用 sealed class,compiler 就會幫你檢查你有沒有漏掉的 case。所以假設我們新增一個新的 case 像這樣:
Compiler 會直接吐一個 error 給你:
這時,工程師就可以根據 compiler 的提醒來做相對應的修正。
所以其實可以去掉 exception, or can't I?
因為這邊 Shipper 是自定義的 data type,所以需要有個過程把 user input 轉換成這些 data type
只有在 shipper 事先定義好的情況下才能去掉 exception,所以如果 shipper 是由 user input 決定的話,就還是會有 user 輸入沒有合作的貨運商的狀況,這個時候還是需要 throw exception 才行。
這時,你可能會問,既然還是要 throw exception,那有差嗎?有的。
想像一下如果你的 shippingFee 是在整個 callstack 裡面的第十層,也就是 shipperName 就這樣一路的被傳了十層。一旦 exception 出現,你也要花不少時間去 trace code 才會知道這個 shipperName 是在什麼情況下變成不支援的 value。
相較之下,因為上面提到的 conversion from user input to Shipper data type 本身就是一個檢查的過程,所以如果我們在 user input 之後馬上做 conversion,那萬一出現了 exception,我們也可以很快地知道到底是從哪裡開始錯的。
這個架構基本上可以被視為兩個部分
而其中因為 exhausive check 的關係,第二部分可以說是 safe 的,相較之下第一部分就是 unsafe。也就是我們可以通過這個手法把系統切割成 safe 跟 unsafe。我們可以對 unsafe 的部分做更完整的測試。safe 的部分就可以相對放心。
當然,你也可以把 fee computation 放進 shipper 並封裝成一個 interface/function,不過我覺得這相對來說比較不重要,有興趣的可以自己查查資料看要怎麼做。
可惜的是 Java 並沒有這樣的設計,所以如果使用的是 Java,有很大的機率你還是需要各種 throw new Exception。
雖然現在 Design Pattern 看起來是個顯學,不過在使用 Design Pattern 之前,要先能夠先了解問題本身的性質,再去看看使否有模式可以解決,不然很容易會變成為套而套,而所謂的 refactor 也不一定能帶來什麼實質的效用。
另外,有一些人會倡導 Design Pattern 是 language agnostic,其實並不盡然,這篇使用 kotlin 的 sealed class 就是一個例子。