如何使用 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 上找到