如何使用 F# 實現 Strategy Pattern ?
Strategy Pattern 是 OOP 中最著名的 Design Pattern,幾乎可以說是 OOP 中 『解耦合』最經典的應用,F# 既然是 Function First Language,就讓我們以 function 的角度重新思考什麼是 『解耦合』。
Version
macOS High Sierra 10.13.3
.NET Core SDK 2.1.101
Rider 2017.3.1
F# 4.1
User Story
假設你在處理訂單,訂單的折扣方式有兩種
- 超過 1000 元,則
滿千送百
- 不到 1000 元,則
全館8折
Task
直接使用 FP 的思維完成需求。
Definition
Strategy Pattern
將不同演算法抽象化成相同 interface,讓高階模組與實際演算法解耦合,而彼此僅相依於 interface,進而可動態切換演算法
首先思考 Strategy Pattern 的本質:
Strategy
必須與Context
解耦合Strategy
必須能動態切換
只要能達到這兩個目標,就算完成了 Strategy Pattern。
OOP 思考方式
OOP 強調是 data 與 function 合一,認為什麼都是物件,所以 strategy 也是物件。
要將不同的 strategy 抽象化
看成相同的物件,才能使用 多型
操作,所以要設計 interface
訂定 抽象化
的標準。
也就是 OOP 是將焦點放在 不同的部分
,進而將 不同的部分
抽象化成 Inteface。
FP 思考方式
FP 強調是 data 與 function 分開,data 有 Type System,function 有 Higher Order Function、Function Composition,因為 strategy 只是功能,所以是 function。
要將相同的部分抽出為 Higher Order Function,不同 strategy 抽成獨立 funciton,再將 strategy 以參數的方式傳入 Higher Order Function。
也就是 FP 是將焦點放在 相同的部分
,進而將 相同的部分
抽成 Higher Order Function。
Implemetation
Strategy.fs
1 | namespace OrderLibrary |
將 買千送百
以 rebateStrategy()
表示。
將 全館8折
以 discountStrategy()
表示。
特別注意 rebateStrategy()
與 discountStrategy()
的 singnature 並不一樣,傳統 OOP 在使用 Strategy Pattern 時,必須先定義 strategy interface,但只要遇到 strategy 間 signature 不同時就很困擾,甚至要動用 Adapter Pattern。
但因為 FP 沒有 interface 概念,所以不需要為不同的 strategy 抽象化
成相同 interface,故也沒有 interface 很難開的困擾。
OrderService.fs
1 | namespace OrderLibrary |
第 4 行
1 | let strategyFactory price = |
既然有不同的 strategy,就會有選擇 strategy 的邏輯,所以 factory 少不了,只是從 OOP 的 factory class 退化成 factory function。
Strategy 的選擇,使用 FP 的 Pattern Matching 最適合,根據不同的條件回傳不同的 function。
F# 雖然不用寫型別,但對於型別檢查依然非常嚴格,strategyFactory()
被 Type Inference 推導為 float -> (float -> float)
,也就是我們必須回傳一個 float -> float
的 function。
在 C# 我們必須明確使用 delegate
或 Func<float, float>
定義 strategy 的 signature,但在 F# 都省了,因此程式碼變得非常精簡,兼具強型別語言與弱型別語言的優點。
但 rebateStrategy()
與 discountStrategy()
的 signature 畢竟不一樣,因此在使用 Pattern Matching 時,必須先將 float -> float
整理好,因爲 F# 支援 Currying,discountStrategy 0.8
會自動回傳 float -> float
的 function,如此將符合 Pattern Matching 對型別的要求。
FP 不用 interface,不代表沒有型別要求,透過 Currying,可以解決 OOP 因為需求不同難開 interface 的老問題,只要在最後 signature 一樣即可,並不要求設計 function 時都要有相同的 signature,可使用 Currying 逐步完成 signature 要求
第 9 行
1 | let getPrice price = |
將 price 以 Pipeline 方式傳給對的 strategy 計算,其中 strategyFactory price
將傳回對的 strategy。
Program.fs
1 | open System |
將各種 price 以 Pipeline 方式傳給 OrderService.getPrice()
計算,並將結果傳給 printfn()
顯示。
Summary
回想 Strategy Pattern 的本質:
Strategy
必須與Context
解耦合Strategy
必須能動態切換
FP 雖然沒有定義 interface,但 strategy 已經與 context 實質解耦合,strategy 都必須嚴格遵守 float -> float
的 signature,任何 float -> float
的 function 都可視為 strategy。
只要遵守 float -> float
的 strategy,就能被 Pattern Matching 動態切換。
所以 FP 版的 Strategy Pattern 雖然沒有 interface 也沒有 多型
,但本質與 OOP 是相同的。
Conclusion
- 不用很糾結一定要使用 interface 與
多型
,重點在於解耦合
與動態切換
,FP 使用 Higher Order Function 與 Pattern Matching 也能達成相同的目標 - FP 的 Higher Order Function,可以實現
DRY
原則,FP 設計時要將焦點放在相同的部分
,抽出 Higher Order Function 後,再透過 Function Composition 組合成新的 function - FP 的 Pattern Matching,可以實現 OOP 的
多型
,藉此達到動態切換
需求
Sample Code
完整的範例可以在我的 GitHub 上找到