以 FP 的角度思考解耦合

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,進而可動態切換演算法

trategy00

首先思考 Strategy Pattern 的本質:

  1. Strategy 必須與 Context 解耦合
  2. 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
2
3
4
5
namespace OrderLibrary

module PriceStrategy =
let rebateStrategy price = price - 100.0
let discountStrategy discount price = price * discount * 1.0

買千送百rebateStrategy() 表示。

全館8折discountStrategy() 表示。

特別注意 rebateStrategy()discountStrategy() 的 singnature 並不一樣,傳統 OOP 在使用 Strategy Pattern 時,必須先定義 strategy interface,但只要遇到 strategy 間 signature 不同時就很困擾,甚至要動用 Adapter Pattern。

但因為 FP 沒有 interface 概念,所以不需要為不同的 strategy 抽象化 成相同 interface,故也沒有 interface 很難開的困擾。

OrderService.fs

1
2
3
4
5
6
7
8
9
10
11
namespace OrderLibrary

module OrderService =
let strategyFactory price =
match (price > 1000.0) with
| true -> PriceStrategy.rebateStrategy
| false -> PriceStrategy.discountStrategy 0.8

let getPrice price =
price
|> strategyFactory price

第 4 行

1
2
3
4
let strategyFactory price = 
match (price > 1000.0) with
| true -> PriceStrategy.rebateStrategy
| false -> PriceStrategy.discountStrategy 0.8

既然有不同的 strategy,就會有選擇 strategy 的邏輯,所以 factory 少不了,只是從 OOP 的 factory class 退化成 factory function。

Strategy 的選擇,使用 FP 的 Pattern Matching 最適合,根據不同的條件回傳不同的 function。

trategy00

F# 雖然不用寫型別,但對於型別檢查依然非常嚴格,strategyFactory() 被 Type Inference 推導為 float -> (float -> float),也就是我們必須回傳一個 float -> float 的 function。

在 C# 我們必須明確使用 delegateFunc<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
2
3
let getPrice price =
price
|> strategyFactory price

將 price 以 Pipeline 方式傳給對的 strategy 計算,其中 strategyFactory price 將傳回對的 strategy。

Program.fs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
open System
open OrderLibrary

[<EntryPoint>]
let main argv =
1200.0
|> OrderService.getPrice
|> printfn "%f"

800.0
|> OrderService.getPrice
|> printfn "%f"

0 // return an integer exit code

將各種 price 以 Pipeline 方式傳給 OrderService.getPrice() 計算,並將結果傳給 printfn() 顯示。

Summary


回想 Strategy Pattern 的本質:

  1. Strategy 必須與 Context 解耦合
  2. 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 上找到

2018-03-23