Try   HackMD

如何正確理解 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,例如:

helloStr = "hello" :: String

雙冒號的左邊是字串物件,右邊是標示「左邊的物件是什麼 type」

// in TypeScript const helloStr : string = "hello";

這些幾乎在所有程式語言都有類似的 type,基本上只有名字會不一樣。但其實 type 可以再更複雜/抽象一點,例如:

  • Car
  • Animal
  • Bar
cat :: Animal
// in typescript cat : Animal

Function Type

如果你看過一些 OOP 或 Design Pattern 的教學的話,這些應該都算是常提到的 class name。是的,基本上 OOP 的 class 就是一種 type。

跟 OOP 不一樣的是,還有一種常用 type 不常被 OOP 相關文章書籍提到:function type。例如:

Int -> Int

這代表一個有一個 Int 為 input 的 function,而其 return type 是 Int。你可以很輕易地想出一些例子,譬如說 addOne, minusOne

addOne :: Int -> Int

這個代表一個 function 叫做 addOne,而它吃一個 Int 為參數,回傳一個 Int。

// 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 的話,可以寫成這樣

result = addOne 3
// in typescript const result = addOne(3);

這樣代表,把 3 塞進 addOne function 當 input

另一個例子:把整數轉成字串

-- given toString :: Int -> String -- you can invoke result = toString 4
// 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 應該要可以有多個參數:

-- given add :: Int -> Int -> Int -- invoke result = add 3 4

(參數跟參數中間直接用空格格開就好)

// given const add : ((a: number, b: number) => number) = (a, b) => a + b; // you can invoke const result = add(3, 4);

更多例子:

-- 吃一個整數字串,回傳值是加總 sum :: [Int] -> Int -- 把一個數字加到陣列的後方 append :: Int -> [Int] -> [Int]

Function Type 並不這麼特別

Function type 其實沒有什麼特別的,跟 Int 一樣,它也只是一種 type。所以你可以在 function type 裡面塞 function type

map :: (Int -> Int) -> [Int] -> [Int]

注意有個括號,所以這個 map function 的第一個參數其實是 (Int -> Int),也就是前面提到的其中一種 function type。

Type Alias

為了方便,我們可以為 type 取別名

type FuncInt1 = Int -> Int

這樣我們就可以把上面的 map function type 改成

map :: FuncInt1 -> [Int] -> [Int]

這樣就很清楚整個 Int -> Int 是一個參數了

使用上則是:

result = map addOne [1, 2, 3]

對照 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 是不合法的:

3 :: String -- 3 是個 Int 不是 String Int :: Int -- Int 不是 value String :: 3 -- 3 不是一個 type

然後,以上面的 map 為例,這樣也是不合法的:

map FuncInt1 [1, 2, 3]

FuncInt1 不是 value ,所以不能餵進 map function 當第一個參數

宣告一個 type

在有些狀況下,我們需要宣告一個全新的 type,就跟在 Java 裡面我們可以宣告一個新的 class 一樣:

data Animal

這樣就是宣告了一個叫做 Animal 的 type。

// in typescript class Animal {};

Type 之抽象封裝

你可能會説「一直跟我說 type,給我看實作啊!」但實作細節應該是「封裝」好的,所以要理解這個程式結構,應該是不需要看實作的。這也是 OOP/Design Pattern 常常提到的觀念。舉例來說,假設你要實作一個酒吧結帳系統(借用 wiki Strategy Pattern 範例),那麼你會需要計算一筆「單」要收多少錢,那麼我們只需要知道有 "Order" 的 type 跟 getPrice 這個 function 就行了,用上面介紹的表示方法就是:

data OrderItem type Order = [OrderItem] getTotalPrice :: Order -> Int
// 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 時段,在這個時段內有些東西半價,那在計算的時候就要根據不同時段來採用不同的計算「策略」。簡單的做法可能是像這樣:

getNormalTotalPrice :: Order -> Int getHappyHourTotalPrice :: Order -> Int

也就是根據現在是什麼時段來決定使用那個 function。雖然簡單,但這個做法有個問題是:假設今天 happy hour 的時段有分好幾種呢?例如 Happy Friday、Crazy Saturday,那我們是不是每次有新的 happy hour 就要多寫一個 get price function 呢?這樣應該會有不少重複的邏輯吧?

所以我們可以把計算單個 OrderItem 計算抽出來,也就是 getTotalPrice 的實作不會包含 item price 計算,把這個計算委託給另外指定的 function。那這個「委託」應該要怎麼做呢?很簡單,讓「OrderItem 的價錢計算」變成一個參數就好。

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
// 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。