# 如何正確理解 Design Pattern -- Strategy Pattern ## 看了一堆解釋就是看不懂 Design Pattern? 你可能看了好幾個教學,但就是覺得好像抓不到某個 Design Pattern 的要領?這系列文應該可以幫助到你。 那麼這系列會有什麼不同呢? 通常 Design Pattern 都會用 Class diagram 來描述結構,然後以文字來輔助說明。這系列文我會直接用 type system 來做說明。 ## Type System 是什麼? Type System 可以算是一種邏輯表示方法。跟在學校學的邏輯符號不一樣的地方是,它更靠近程式語言,所以他同時有邏輯符號的嚴謹性跟程式語言的易用性。另外現在很多程式語言都有支援不錯的 type system,不過語法上可能會有些差異。為了方便理解,我也會附上 TypeScript 的範例來比較。 讓我們先來舉幾個簡單的例子,以下是常見的 type: - `String` -- 字串 - `Int` -- 整數 - `Double` -- 浮點數 - `[String]` -- 字串陣列 然後我們可以用這些來標示某個物件或是變數的 type,例如: ```haskell= helloStr = "hello" :: String ``` 雙冒號的左邊是字串物件,右邊是標示「左邊的物件是什麼 type」 ```typescript= // in TypeScript const helloStr : string = "hello"; ``` 這些幾乎在所有程式語言都有類似的 type,基本上只有名字會不一樣。但其實 type 可以再更複雜/抽象一點,例如: - `Car` - `Animal` - `Bar` ```haskell= cat :: Animal ``` ```typescript= // in typescript cat : Animal ``` ### Function Type 如果你看過一些 OOP 或 Design Pattern 的教學的話,這些應該都算是常提到的 class name。是的,基本上 OOP 的 class 就是一種 type。 跟 OOP 不一樣的是,還有一種常用 type 不常被 OOP 相關文章書籍提到:function type。例如: ```haskell= Int -> Int ``` 這代表一個有一個 Int 為 input 的 function,而其 return type 是 Int。你可以很輕易地想出一些例子,譬如說 `addOne`, `minusOne`。 ```haskell= addOne :: Int -> Int ``` 這個代表一個 function 叫做 addOne,而它吃一個 Int 為參數,回傳一個 Int。 ```typescript= // in typescript addOne : ((a: number) => number) // full implementation might look like const addOne : ((a: number) => number) = a => a + 1 // or use ordinary syntax function addOne(a: number): number { return a + 1 } ``` 要表達 function invocation 的話,可以寫成這樣 ```haskell= result = addOne 3 ``` ```typescript= // in typescript const result = addOne(3); ``` 這樣代表,把 3 塞進 addOne function 當 input 另一個例子:把整數轉成字串 ```haskell= -- given toString :: Int -> String -- you can invoke result = toString 4 ``` ```typescript= // in typescript // given const toString : ((n: number) => string) = n => n.toString(); // you can invoke const result = toString(4); ``` 不熟悉這個形式的話,只要記得以下幾點就好: 1. 大寫開頭通常是個 type,小寫開頭一定是個 value。 2. 只要出現 `::`,那就代表是描述左邊的 value 是右邊的 type。上面的例子來說的話。`toString` 是一個 value,它的 type 是 `Int -> String`。(沒錯,function 也是一種 value) 3. 一個 value 後面隔著一個空白接另外一個 value,一定代表前者是個 function,而整體是一個 function invocation。 當然 function 應該要可以有多個參數: ```haskell= -- given add :: Int -> Int -> Int -- invoke result = add 3 4 ``` (參數跟參數中間直接用空格格開就好) ```typescript= // given const add : ((a: number, b: number) => number) = (a, b) => a + b; // you can invoke const result = add(3, 4); ``` 更多例子: ```haskell= -- 吃一個整數字串,回傳值是加總 sum :: [Int] -> Int -- 把一個數字加到陣列的後方 append :: Int -> [Int] -> [Int] ``` ### Function Type 並不這麼特別 Function type 其實沒有什麼特別的,跟 Int 一樣,它也只是一種 type。所以你可以在 function type 裡面塞 function type ```haskell= map :: (Int -> Int) -> [Int] -> [Int] ``` 注意有個括號,所以這個 map function 的第一個參數其實是 `(Int -> Int)`,也就是前面提到的其中一種 function type。 ### Type Alias 為了方便,我們可以為 type 取別名: ```haskell= type FuncInt1 = Int -> Int ``` 這樣我們就可以把上面的 map function type 改成 ```haskell= map :: FuncInt1 -> [Int] -> [Int] ``` 這樣就很清楚整個 `Int -> Int` 是一個參數了 使用上則是: ```haskell= result = map addOne [1, 2, 3] ``` 對照 TypeScript ```typescript= // map implementation type FuncInt1 = (n: number) => number; const map : ((f: FuncInt1, list: number[]) => number[]) = (f, list) => { let r = []; for (let item of list) { r.push(f(item)); } return r; } const result = map(addOne, [1, 2, 3]); ``` ### 一些不合法的寫法 以下的 statement 是不合法的: ```haskell= 3 :: String -- 3 是個 Int 不是 String Int :: Int -- Int 不是 value String :: 3 -- 3 不是一個 type ``` 然後,以上面的 map 為例,這樣也是不合法的: ```haskell= map FuncInt1 [1, 2, 3] ``` `FuncInt1` 不是 value ,所以不能餵進 map function 當第一個參數 ### 宣告一個 type 在有些狀況下,我們需要宣告一個全新的 type,就跟在 Java 裡面我們可以宣告一個新的 class 一樣: ```haskell= data Animal ``` 這樣就是宣告了一個叫做 Animal 的 type。 ```typescript= // in typescript class Animal {}; ``` ### Type 之抽象封裝 你可能會説「一直跟我說 type,給我看實作啊!」但實作細節應該是「封裝」好的,所以要理解這個程式結構,應該是不需要看實作的。這也是 OOP/Design Pattern 常常提到的觀念。舉例來說,假設你要實作一個酒吧結帳系統(借用 wiki [Strategy Pattern](https://en.wikipedia.org/wiki/Strategy_pattern) 範例),那麼你會需要計算一筆「單」要收多少錢,那麼我們只需要知道有 "Order" 的 type 跟 `getPrice` 這個 function 就行了,用上面介紹的表示方法就是: ```haskell= data OrderItem type Order = [OrderItem] getTotalPrice :: Order -> Int ``` ```typescript= // in typescript class OrderItem { /* ignore implementation */ } type Order = OrderItem[]; const getTotalPrice: ((order: Order) => number) = order => { // ignore implementation return 0; } ``` 介紹完 type system,接下來我們就可以來討論如何用這個工具來理解 Strategy Pattern 了! ## Strategy Pattern explained in Type 延續上面講的,結帳的計算功能,假設這間店有 happy hour 時段,在這個時段內有些東西半價,那在計算的時候就要根據不同時段來採用不同的計算「策略」。簡單的做法可能是像這樣: ```haskell= getNormalTotalPrice :: Order -> Int getHappyHourTotalPrice :: Order -> Int ``` 也就是根據現在是什麼時段來決定使用那個 function。雖然簡單,但這個做法有個問題是:假設今天 happy hour 的時段有分好幾種呢?例如 Happy Friday、Crazy Saturday,那我們是不是每次有新的 happy hour 就要多寫一個 get price function 呢?這樣應該會有不少重複的邏輯吧? 所以我們可以把計算單個 OrderItem 計算抽出來,也就是 getTotalPrice 的實作不會包含 item price 計算,把這個計算委託給另外指定的 function。那這個「委託」應該要怎麼做呢?很簡單,讓「OrderItem 的價錢計算」變成一個參數就好。 ```haskell= type BillingStrategy = OrderItem -> Int getTotalPrice :: BillingStrategy -> Order -> Int happyFridayStrategy :: BillingStrategy crazySaturdayStrategy :: BillingStrategy -- same as above but after inlining BillingStrategy getTotalPrice :: (OrderItem -> Int) -> Order -> Int happyFridayStrategy :: OrderItem -> Int crazySaturdayStrategy :: OrderItem -> Int -- actuall usage order :: Order priceWhenHappyFriday = getTotalPrice happyFridayStrategy order priceWhenCrazySaturday = getTotalPrice crazySaturdayStrategy order ``` ```typescript= // in typescript type BillingStrategy = (item: OrderItem) => number; const getTotalPrice: ((strategy: BillingStrategy, order: Order) => number) = (strategy, order) => { let total = 0; for (let item in order) { total += strategy(item); } return total; } const happyFridayStrategy: BillingStrategy = (item) => { const priceOfItme = 0; // ignore implementation return priceOfItme; } const crazySaturdayStrategy: BillingStrategy = (item) => { const priceOfItme = 0; // ignore implementation return priceOfItme; } const priceWhenHappyFriday = getTotalPrice(happyFridayStrategy, order); const priceWhenCrazySaturday = getTotalPrice(crazySaturdayStrategy, order); ``` 在這個版本中,我們加入了新的參數:一個可以把 OrderItem 轉成整數的 function。這樣我們就可以在不修改 getTotalPrice 的情況下抽換 item price 的計算了! ## 所以,什麼是 Strategy Pattern? 基本上一個物件或函數,其中的重要邏輯是由 caller 遞進去的,我們就可以稱他為 Strategy Pattern。而它帶來的好處是:可以更高程度的共用邏輯,並且可以動態的抽換邏輯,例如上述 `getTotalPrice` 中,`OrderItem` 的價錢計算是在最後面才「置入」的。 ## 與 Class Diagram 的比較 在 Class Diagram 中,首先你需要理解什麼是 Class,然後理解什麼是繼承或是介面,然後畫出三個方框跟一些箭頭來表示他們之間的靜態關係,但並沒有描述它們之間會怎麼動態互動。 用 Type System 的話,我們可以用短短的一行來描述:要做這樣的計算,我們需要一個「計算 OrderItem 的價錢的方法」跟 Order。在同樣不描述實作細節的情況下,這個方法更能夠讓 dependency 變得更明顯。 ## To Be Continued Strategy Pattern 應該算是簡單的 Design Pattern,所以你可能會認為 Type System 的這種表示方法並沒有帶來太多的差異。我保證其他更複雜的就會更明顯了。接下來我會用這種方式來帶大家重新理解其他的 Design Pattern。