Re: [心得]以策略模式重構switch case或if (影片)
終於有空來加入討論啦~
這邊有 markdown 好讀版:https://hackmd.io/@rayshih/SyAAwbxkd
這邊我也來提一下我的看法。為了閱讀方便我把一些 code snippet 複製在這邊:
```java=
public double shippingFee(String shipper, double length, double width, doubleheight, double weight) {
if (shipper.equals("black cat")) {
return // some calculation
} else if (shipper.equals("hsinchu")) {
return // some calculation
} else if (shipper.equals("post office")) {
return // some calculation
} else {
throw new IllegalArgumentException("shipper not exist");
}
}
```
然後以下是 refactor 過後的程式碼片段:
首先你訂了個 interface
```java=
public interface Shipper {
double calculateFee(Product product);
}
```
然後所有 shipper 都會實作這個 interface:
```java=
public class BlackCat implements Shipper {
public BlackCat() {
}
@Override
public double calculateFee(Product product) {
return // some calculation
}
}
```
於是本來的 function shippingFee 被 refactor 成這樣:
```java=
public class Cart {
private final HashMap<String, Shipper> shippers = new HashMap<>() {{
put("black cat", new BlackCat());
put("hsinchu", new Hsinchu());
put("post office", new PostOffice());
}};
public double shippingFee(String shipperName, Product product) {
if (shippers.containsKey(shipperName)) {
return shippers.get(shipperName).calculateFee(product);
}
throw new IllegalArgumentException("shipper not exist");
}
}
```
首先,這根本不能算「策略模式」,只能算是一般的多型應用,不過我這邊不是很想討
論 strategy pattern 本身,有興趣的可以去 wiki 比較一下差在哪裡。
## 所以原本的 code 到底有什麼問題?
基本上有兩點可以討論:
1. 不同計算方法都被寫在同一個 function 裡
2. 如果 caller 丟了一個不認識的 shipperName,這 function 就會丟出 exception
### 1. 不同計算方法都被寫在同一個 function 裡
原 solution 定義了一個 interface,所以要實作這個 function 必須建立一個 class
來實作這個 interface,所以算是有解決到這個問題。但其實單純的為不同的 shipper
建立相對應的 function 就行了,並沒有必要多一個 interface:
```java=
private double blackCatShippingFee(Product product) {
return // The calculation for black cat
}
// hsinchuShippingFee and postOfficeShippingFee are similar
public double shippingFee(String shipper, Product product) {
if (shipper.equals("black cat")) {
return this.blackCatShippingFee(product);
} else if (shipper.equals("hsinchu")) {
return this.hsinchuShippingFee(product);
} else if (shipper.equals("post office")) {
return this.postOfficeShippingFee(product);
} else {
throw new IllegalArgumentException("shipper not exist");
}
}
```
### 2. 如果 caller 丟了一個不認識的 shipperName,這 function 就會丟出
exception
假設今天,我們新增了一個貨運商,工程師記得要建立一個新的 class 並實作 Shipperinterface,但是他忘了把它加入 shippers hashmap,又剛好沒寫測試,於是 rollout
之後就觸發了 exception,就 QQ 惹。
有沒有方法可以保證不會有例外呢?這問題就有點有趣了,但首先讓我們先換一個語言
kotlin:
```kotlin=
sealed class Shipper {
object BlackCat: Shipper()
object Hsinchu: Shipper()
object PostOffice: Shipper()
}
data class Product(
val length: Double,
val height: Double,
val weight: Double,
)
fun shippingFee(shipper: Shipper, product: Product): Double {
return when(shipper) {
is Shipper.BlackCat -> {
// computation here
}
is Shipper.Hsinchu -> {
// computation here
}
is Shipper.PostOffice -> {
// computation here
}
}
}
fun main(args: Array<String>) {
val product = Product(7.0, 8.0, 9.0)
println(shippingFee(Shipper.BlackCat, product))
}
```
因為 kotlin 的 when 有提供 exhausive check 的功能。只要使用 sealed class,
compiler 就會幫你檢查你有沒有漏掉的 case。所以假設我們新增一個新的 case 像這樣:
```kotlin=
sealed class Shipper {
object BlackCat: Shipper()
object Hsinchu: Shipper()
object PostOffice: Shipper()
object Lalamove: Shipper() // 新增的部分
}
```
Compiler 會直接吐一個 error 給你:
```
main.kt:15:10: error: 'when' expression must be exhaustive, add necessary
'Lalamove' branch or 'else' branch instead
return when(shipper) {
```
這時,工程師就可以根據 compiler 的提醒來做相對應的修正。
所以其實可以去掉 exception, or can't I?
## 如果 Shipper 是由 user 輸入的資料決定的呢?
因為這邊 Shipper 是自定義的 data type,所以需要有個過程把 user input 轉換成這些 data type
```kotlin=
val userInput = // some function return user input
val shipper = when (userInput) {
'black cat' -> Shipper.BlackCat
'hsinchu' -> Shipper.Hsinchu
'post office' -> Shipper.PostOffice
else -> throw IllegalArgumentException('shipper not exist')
}
println(shippingFee(shipper, product))
```
只有在 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,我們也可以很快地知道到底是從哪裡開始錯的。
## Boundary between Safe and Unsafe
這個架構基本上可以被視為兩個部分
1. conversion and check
2. execution
而其中因為 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 就是一個例子。
--
sealed class真D爽
同意 Design pattern 是為了解決特定語言的缺陷
沒 syntax highlight 還是寫在部落格比較好XD
應該說語言特性可以用design pattern補救吧?
如果這個interface有三個func 原本是三個長得很像的if e
lse,變成沒有,也不會有人忘掉三個地方都要實作
alihue 最上面有 markdown 連結喔~
我認同其實只是多型取代 condition logic 唷
其實我想說... Pattern像是武學的內功心法 如果你想練
可以嘗試著不要用套件 自己寫 寫不通的時候參考別人怎麼寫
例如說可以參考Apache tomcat怎麼實作filter
或是spring怎麼建構整個架構 為什麼annotation可以有作用
每個套件裡面pattern都用超兇的 不注意思考還會以為理所當然
Ray大大必推
個人覺得什麼像內功心法之類的,都是似懂非懂才會用的
「形容詞」,而且光看別人怎麼用 pattern 大概也很容
易就學個皮毛而已
學 design pattern 的方式就是要自己試著寫框架給別人
用 框架作者及使用者雙方都不能修改對方的程式碼 而且
要有彈性能讓人插入客制化邏輯 如果常有 breaking cha
nge 沒人受得了 如果你的專案程式碼都是自己寫的 而且
習慣想改就改 那有沒有套 pattern 就不是必要的了
同意樓上
不過以型態來說所有design pattern都是oo的一些應用跟寫法
我倒是蠻想知道你所謂的策略模式長怎樣
不是所有 design pattern 都要從 OO 出發
我所謂的策略模式就是 wiki 上面寫的
看完wiki感覺你只是自以為比較懂策略模式而已
嗯,的確不是全部,只能說幾乎。context 對不太上,只能先這
樣。
yfr 你這個就 OO 本位主義啊 XD
fantasychese 你有看 wiki 範例就會知道差別。不過如果
你要說沒有違反文字描述,那照這個思路,我也可以說所
有有用到多型的都算「策略模式」:p
我大概懂你的意思了,少了一個context傳入策略的這個封裝。
的確,如果按照這個定義你說的沒錯。不過那篇文章的本意也不
在這名詞的定義就是,要完成策略模式需要再加點工
文字沒有溫度,無法表達語氣,以上討論沒有不敬的意思
可能跟我是C跟Java出身的有關,通常不太OO的語言,Design Pa
ttern我都看不太懂 (無奈
你是不是擅長函數式語言呢?
還行,FP 水很深
不過我寫這篇本來就沒有要討論策略模式,我都明寫了 :p
網路討論就是這樣,比較難抓到討論的點(笑
一個東西大家可能都有不同看法
可以先說你看的是中文還是英文wiki,原po缺的是什麼嗎?
因為你說原文不算是,只是缺少context傳入?還是更多,
這篇文章對未來讀者的價值就少了一段,建議補足
我認為兩篇文章都是懂得人在寫,兩方都省略說法,到最後
不懂的人看了還是不懂,對新手沒什麼效益
其實我在原文的底下推文連結 就有解決你說的shipperName
問題了
vi000246 你這個也是 strategy pattern 只不過用的是
generic aka type level application 去做 injection
是個滿好的方式,但還是沒有辦法避免 conversion 喔~
尤其是在轉換的時候在 c# 應該會用到 reflection
wulouise 我再強調一次,我寫這篇文沒有要討論策略模式
我想討論的是本來的 code 有什麼樣的問題以及如何重構
感覺很多人還是太聚焦在策略模式上 XD
如果一開始就學一個沒啥缺陷的語言就沒這問題
1
原原 PO 用 interface 的好處是,shipper 有新的行為時。 可以很簡單的在 interface 加新的 function。 同時可以檢查有 implement Shipper 的 class 要加入新的 function。 感覺上,彈性更好。 缺點嘛... 如果 shipper 很多時每個都要再補 function 是比較累一點。3
因為有朋友想要 Python 的版本, 簡單的 legacy code 也可以讓他們玩玩 team build 練練手, 所以我就順手整理了 Python 的版本了。 - GitHub Repo & commit history: - 用 PyCharm 重構的影片,YouTube:3
上回用 Java + IntelliJ 來重構一堆 if/else 的計算運費範例, 這次改用 C# + Rider 來重構一樣的例子,方便習慣 C# 的朋友參考與練習, 不過這次刻意改用 Func<T> 來當作 strategy 的實作內容, 以 function 來取代,省去 class + interface 的部份。 兩種作法適用場景不同,東西夠小夠單純,想要少一點 class/interface 等 elements,6
恕刪 策略模式不就是一個戰鬥機器人 防禦模式就護甲值+20 攻擊模式就攻擊力+50 閃避率-10% 回復模式就自動補血+5hp18
首Po最近在客戶那邊一起 pair 重構 legacy code, 碰到了一大段 if/else statement,用來判斷什麼時候該使用哪一種cache, 並依照不同 cache 的邏輯來決定回傳的內容。 發現還是有蠻多風氣比較封閉的公司對這類型的基本功跟處理不是很熟悉, 可能是對 code smell 不熟,對重構不熟,對 design pattern 不熟,對工具不熟。
39
Re: [心得] (轉)軟體開發六年後我改變想法的事情這篇滿有意思的,牽涉的主題很廣,不過有些事情只有一句很難講清楚 針對中間幾點,分享一些我自己的理解 ※ 引述《alihue (wanda wanda)》之銘言: : 看到不錯的文章 翻譯分享一下 : 原文:33
Re: [討論] Unit test 的撰寫請益先說結論,先都不要寫。 Legacy system 要先補大範圍的 integration test,確定整體的行為是對的。 如果 code 沒有要再改,不用補細部 unit tests。 原因是因為,原本 API 可能因為設計不良,導致無法寫 unit test 得先 refactor 才有辦法讓它變成 testable,這情況就要先 refactor 再補 UT20
Re: [請益] Spring boot的依賴注入降低耦合的例子在這個時代依賴注入最重要的用途,特別是在後端開發是讓Application 在多個不同的 環境下(Development, Production, local, etc) 能夠根據profile 組出能正確執行的Application 多型在這裡當然有他的地位,但是一般來說,大部分不接觸system boundary的service objects 是不太需要多型的,如果是java,那種一個interface 只有一個implementation16
[請益] 如何有效益的維護data loader如題 目前做的project架構長這樣 Loader1 Loader2 Loader3 ........... Loader30 Area 1 Area 214
[請益] 台積IT部門請教今天收到台積寄來的面試邀約,不過從內容還是有點無法判斷是哪個部門~ 不知道有沒有比較了解的大大可以指點迷津? 另外有個問題,職務上班地點有寫在新竹或台南, 不過因為個人家庭因素,必須要留在台中, 想問問是否能夠提出想在中科上班?4
Re: [請益] 多型用在哪ㄛ現在ㄉ想法4 沒有多型 只有介面 多型的用例之一 for(auto p_actor : actors) p_actor->act() 對ㄛ來縮 p_actor實際上到底是什麼 並不重要- 這個我很有經驗 遊戲中每個物件都會有自己的 update function, 去實作 base class 在我仔細比較過雷克斯與兩個女角的效能成本後 從一個健康男性技術員的角度出發 我猜想Rex的實作應該面臨這樣的挑戰 class Rex : public Fighter {