如何使用 F# 實現 Proxy Pattern?
Proxy Pattern 是 OOP 中著名的 Design Pattern,尤其可在不改變 interface 的前提下,就能控制該物件的使用,F# 既然是 Function First Language,就讓我們以 function 的角度重新實現 Proxy Pattern。
Version
macOS High Sierra 10.13.3
.NET Core SDK 2.1.101
Rider 2017.3.1
F# 4.1
User Story
假設你在處理訂單,只有會員才能享受 全館八折
,其他人都只能原價購買。
- 目前已經有
MemberService.isMember()
判斷是否為會員 - 目前已經有
OrderService.getPrice()
可根據折購計算售價 - 在
MemberService.isMember()
與OrderService.getPrice()
不修改的前提下 (開放封閉原則
),計算出售價
Task
直接使用 FP 的思維完成需求。
Definition
Proxy Pattern
在不改變原有 interface 的前提下,控制 service 的使用
首先思考 Proxy Pattern 的本質:
- Proxy 與 RealSubject 之間的 interface 必須相同
- Client 藉由 Proxy 控制 RealSubject 的使用
只要能達到這兩個目標,就算完成了 Proxy Pattern。
OOP 思考方式
- 為了讓 Proxy 與 RealSubject 的 interface 要相同,所以必須訂出共同的 interface
- Proxy 必須以 state 記住 RealSubject,client 才能透過 Proxy 使用 RealSubject
- Proxy 可以加上邏輯,控制 RealSubject 的使用
FP 思考方式
- Proxy 與 RealSubject 都是 function,不用事先定義 interface,反正只要 interface 不同,在
if … else
、Pattern Matching
或try catch
一定會錯 - Proxy 與 RealSubject 都是 function,既然 interface 都相同,由 RealSubject 切換到 Proxy 就不用修改程式碼
- Proxy function 可加上邏輯,控制 RealSubject function 的使用
Implementation
MemberService.fs
1 | namespace MemberLibrary |
isMember()
判斷是否為 會員
。
實務上判斷
是否為會員
與資料庫有關,本文主要是談 Proxy Pattern,就只簡單的判斷會員是否為Sam
即可。
OrderService.fs
1 | namespace OrderLibrary |
計算 全館八折
,之所以加上 * 1.0
,是為了讓 Type Inference 推導出 discount
與 price
的型別為 float
。
當使用 function pipeline 時,最後一個參數可以自動被 pipeline,所以設計 function parameter 時,將要使用 pipeline 的 value 放在最後一個 parameter,才能發揮 pipeline 的優勢
Program.fs
1 | open MemberLibrary |
11 行
1 | let isMember = MemberService.isMember account |
由於會由 MemberService.isMember()
判斷是否為會員,先判斷並將結果 binding 到 isMember
。
13 行
1 | let orderProxy = |
將 OrderProxy
class 退化成 orderProxy()
function,其 interface 仍然為 float -> float -> float
,Type Inference 會自動推導,若有違反,Pattern Matching 就會報錯。
orderProxy()
本質為 function,由於 closure 機制,可以自然使用到 function 外面的 isMember
,因此不必使用 parameter 方式傳入。
Pattern Matching 根據 isMember
結果回傳 OrderService.getPrice()
,或者全新的 Lambda function。
1 | | false -> fun _ price -> price |
由於要 return 的 Lambda function 並沒有使用到 discount
計算,使用 _
代表即可。
16 行
1 | originalPrice |
將 originalPrice
傳給 orderProxy()
計算,這是個已經考慮 是否為會員
的 orderService()
,最後再傳給 printf()
顯示。
Summary
回想 Proxy Pattern 的本質:
- Proxy 與 RealSubject 之間的 interface 必須相同
- Client 藉由 Proxy 控制 RealSubject 的使用
雖然沒有特別定義 interface,但 orderProxy()
與 OrderService.getPrice()
的 signature 都是 float -> float -> float
,若 function 的 signature 不同,在 Pattern Matching 就會編譯錯誤,與原本 Proxy Pattern 定義 interface 的本質相同。
FP 則藉由 Proxy Function 控制 RealSubject function 的使用,與原本 Proxy Pattern 藉由 Proxy 控制 RealSubject 使用的本質相同。
Conclusion
- Proxy Pattern 本質就是 delegation,但 object 的 delegation 沒 function 簡單直覺,所以才需要搭配 interface;但若純 function,連 interface 都不需要,而且也能享受 strong type 的編譯檢查
Sample Code
完整的範例可以在我的 GitHub 上找到