# 如何正確理解 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。