如何使用 F# 實現 Composite Pattern?
Composite Pattern 是 OOP 中著名的 Design Pattern,無論是 物件
或 容器
,都能使用相同 interface 一視同仁的操作,F# 既然是 Function First Language,就讓我們以 function 的角度重新實現 Composite Pattern。
Version
macOS High Sierra 10.13.3
.NET Core SDK 2.1.101
Rider 2018.1
F# 4.1
User Story
由購物車計算商品價錢,但在活動期間,Apple 產品組合有特惠 :
- MacBook Pro 15”:6 萬
- iPad Air:1 萬
- Apple Watch:1 萬
- Apple 套餐組合 : (Macbook Pro + iPad Air + Apple Watch) 總價打九折:
(6 + 1 + 1) * 0.9
= 7.2 萬
若同時買 MacBook Pro + iPad Air + Apple Watch + Apple 套餐為 15.2 萬。
Task
直接使用 FP 的思維完成需求。
Definition
Composite Pattern
當
單一資料
與組合資料
同時存在時,讓 client 有一致性的操作方式
首先思考 Composite Pattern 的本質:
- Leaf 與 Composite 之間的 interface 必須相同
- Client 操作 Leaf 與 Composite 都一視同仁
只要能達到這兩個目標,就算完成了 Composite Pattern。
OOP 思考方式
- 為了讓 Leaf 與 Composite 的 interface 要相同,所以必須訂出共同的 interface
- Composite 必須以 state 記住 Leaf,並根據 state 加以操作
- Client 以相同方式操作 Composite 與 Leaf
FP 思考方式
- Data 與 function 分開
- 將 Leaf 與 Composite 都設定成 type
- Composite 無需以 state 記住 Leaf,在建立 data 時就建立 Leaf
- 只要建立單一 function 操作所有 data 即可,如此 Composite 與 Leaf 很自然地 interface 都相同
- 由於 Composite 與 Leaf 的 interface 都相同,所以 Composite 會以 recursive 方式呼叫 Leaf 的 function
Implementation
Product.fs
1 | namespace ClassLibrary |
第 3 行
1 | type Apple = |
將各產品都設定成 type,其中 AppleCombo of Apple List
表示 AppleCombo
為各種 Apple 產品的組合。
10 行
1 | let macbookPro = MacBookPro 60000.0 |
設定各產品的定價。
13 行
1 | let appleCombo = (AppleCombo) [ macbookPro; ipadAir; appleWatch ] |
設定 Apple 套餐該包含哪些產品,(AppleCombo)
轉型是必要的,若沒有轉型,Type Inference 會推斷成 Apple list
,導致編譯錯誤,我們實際需要的型別為 Apple
。
再次證明 F# 寫程式不需要型別,但型別的檢查依然非常嚴格,這就是 Type Inference 優越的地方。
16 行
1 | let rec getPrice product = |
getPrice()
計算單一商品與 Apple 套餐。
1 | match product with |
Pattern Matching 會使用型別判斷,且將該型別的值 price 直接 extract 出來。
1 | | AppleCombo(products) -> products |
由 AppleCombo
extract 出 products
,其型別為 List
,可由 List.sumBy()
去計算總和,值得注意的是再度使用 getPrice()
計算每個產品的價錢,因此 getPrice()
必須宣告成 rec
。
最後再將 List.sumBy()
的結果打九折。
我們可以發現在處理
多型
的議題上,OOP 與 FP 採用不同的方式,由於 OOP 是 data 與 function 合一,因此getPrice()
會寫在各 data class 上,然後用 interface 強迫各 class 都要實作getPrice()
;但 FP 是 data 與 function 分開,因此只要一個getPrice()
,然後使用 Pattern Matching 根據不同型別有相對應的處理。但無論 OOP 或 FP,儘管處理方式不同,但對於 client 來說,只要是
Apple
型別,都使用相同的getPrice()
,符合開放封閉原則
的要求。
ShoppingCart.fs
1 | namespace ClassLibrary |
products
的型別為 Apple list
,因此每個 element 都是 Apple
型別,都可放心使用 getPrice()
,這就是 Composite Pattern 的精華。
Program.fs
1 | // Learn more about F# at http://fsharp.org |
Client 呼叫 ShoppingCart.calculatePrice()
計算購物車內所有商品。
Summary
回想 Composite Pattern 的本質:
- Leaf 與 Composite 之間的 interface 必須相同
- Client 操作 Leaf 與 Composite 都一視同仁
FP 雖然沒有特別定義 interface,但所有 Apple
type 的 data 統一都適用於 getPrice()
function,由於在 getPrice()
內使用了 Pattern Matching,只要其中有一個 type 回傳型別不同,compiler 編譯就會報錯,與原本 Composite Pattern 定義 interface 本質相同。
對於 Apple
type,統一使用 getPrice()
function 處理,與原本 Composite Pattern 在 Component
interface 下的 Leaf
與 Composite
都一視同仁的本質相同。
Conclusion
- Composite Pattern 本質就是
多型
,OOP 使用 interface 讓物件
與容器
實踐相同 interface,讓兩者能使用相同的方式操作;FP 使用單一 function,再配合 Pattern Matching 對相同 type 的各種物件有不同的實作。雖然方式不同,但讓 client 以相同方式操作的本質都相同,也符合開放封閉原則
的要求
Sample Code
完整的範例可以在我的 GitHub 上找到