以 FP 重新思考 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 的使用

ecorator00

首先思考 Proxy Pattern 的本質:

  1. Proxy 與 RealSubject 之間的 interface 必須相同
  2. 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 … elsePattern Matchingtry catch 一定會錯
  • Proxy 與 RealSubject 都是 function,既然 interface 都相同,由 RealSubject 切換到 Proxy 就不用修改程式碼
  • Proxy function 可加上邏輯,控制 RealSubject function 的使用

Implementation


MemberService.fs

1
2
3
4
namespace MemberLibrary

module MemberService =
let isMember account = account = "Sam"

isMember() 判斷是否為 會員

實務上判斷 是否為會員 與資料庫有關,本文主要是談 Proxy Pattern,就只簡單的判斷會員是否為 Sam 即可。

OrderService.fs

1
2
3
4
namespace OrderLibrary

module OrderService =
let getPrice discount price = price * discount * 1.0

計算 全館八折,之所以加上 * 1.0,是為了讓 Type Inference 推導出 discountprice 的型別為 float

當使用 function pipeline 時,最後一個參數可以自動被 pipeline,所以設計 function parameter 時,將要使用 pipeline 的 value 放在最後一個 parameter,才能發揮 pipeline 的優勢

Program.fs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
open MemberLibrary
open OrderLibrary
open System

[<EntryPoint>]
let main argv =
let account = "Sam"
let originalPrice = 800.0
let discount = 0.8

let isMember = MemberService.isMember account

let orderProxy =
match isMember with
| true -> OrderService.getPrice
| false -> fun _ price -> price

originalPrice
|> orderProxy discount
|> printf "Real price : %0.0f"

0 // return an integer exit code

// Real price : 640

11 行

1
let isMember = MemberService.isMember account

由於會由 MemberService.isMember() 判斷是否為會員,先判斷並將結果 binding 到 isMember

13 行

1
2
3
4
let orderProxy =
match isMember with
| true -> OrderService.getPrice
| false -> fun _ price -> price

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
2
3
originalPrice
|> orderProxy discount
|> printf "Real price : %0.0f"

originalPrice 傳給 orderProxy() 計算,這是個已經考慮 是否為會員orderService(),最後再傳給 printf() 顯示。

Summary


回想 Proxy Pattern 的本質:

  1. Proxy 與 RealSubject 之間的 interface 必須相同
  2. 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 上找到

2018-04-13