使用 FP 將有不同的實現方式

Proxy Pattern 是 OOP 中著名的 Design Pattern,尤其可在不改變 interface 的前提下,就能控制該物件的使用,隨著 FP 逐漸受到重視, Proxy Pattern 在實作上也有了新的面貌。

Version


macOS High Sierra 10.13.3
.NET Core SDK 2.1.101
JetBrains Rider 2017.3.1
C# 7.2

User Story 1


假設你在處理訂單,為了慶祝週年慶,全館八折

Task
OrderService 包在 OrderLibrary assembly 內。

Architecture
proxy004

Implementation
Client

proxy004

Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using OrderLibrary;

namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{

PriceInterface orderService = new OrderService();
var realPrice = orderService.GetPrice(0.8, 800);

Console.WriteLine("Real price : {0}", realPrice);
}
}
}

與一般使用 service 寫法相同,建立 OrderService 物件,並執行 getPrice() 計算 全館八折

PriceInterface

proxy004

PriceInterface.cs

1
2
3
4
5
6
7
namespace OrderLibrary
{
public interface PriceInterface
{
double GetPrice(double discount, double price);
}
}

PriceInterface 定義 OrderService 的 interface,只有 GetPrice() 一個 method。

OrderService
proxy007

OrderService.cs

1
2
3
4
5
6
7
8
9
10
namespace OrderLibrary
{
public class OrderService : PriceInterface
{
public double GetPrice(double discount, double price)
{

return discount * price;
}
}
}

OrderService 實踐 PriceInterfaceGetPrice(),計算 全館八折

目前 PriceInterfaceOrderService 都包在 OrderLibrary assembly 內,與 ClientConsoleApp 是不同 assembly。

User Story 2


週年慶過後,只有 會員 才享有 全館八折,一般消費者維持原價。

Task

  • 由於 開放封閉原則OrderLibrary 不再修改與編譯
  • 在相同的 PriceInterface 下,要使 Client 的修改程式碼降到最低

Inheritance

Architecture

proxy002

  • ClientOrderService 的 user,實務上可能是 component 或 controller
  • PriceInterface:定義 OrderService 的 interface
  • OrderService:實現 全館八折
  • MemberOrderService:根據 MemberService 判斷 是否為會員,並且因為 code reuse 繼承 OrderService
  • MemberService:判斷 是否為會員

Implementation

MemberService

proxy012

MemberService.cs

1
2
3
4
5
6
7
8
9
10
namespace MemberLibrary
{
public class MemberService
{
public bool IsMember(string account)
{

return account == "Sam";
}
}
}

因為要判斷是否為 會員,新增 MemberService

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

MemberOrderService

proxy013

MemberOrderService.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using MemberLibrary;

namespace OrderLibrary
{
public class MemberOrderService : OrderService
{
private readonly MemberService _memberService = new MemberService();

public override double GetPrice(double discount, double price)
{

return _memberService.IsMember("Sam") ?
base.GetPrice(price, discount) :
price;
}
}
}

第 5 行

1
public class MemberOrderService : OrderService

由於只是多了 是否為會員 判斷,全館八折 的邏輯還是一樣,因此最直覺的寫法就是直接繼承 OrderService 在加以 override

第 7 行

1
private readonly MemberService _memberService = new MemberService();

由於需使用 MemberService 判斷 是否為會員,因此宣告 MemberService 為 private field。

實務上不應該直接 new service,而應該使用 DI 注入解耦合,因為本文主是談 Proxy Pattern,因此就偷懶直接 new 了。

11 行

1
2
3
return _memberService.IsMember("Sam") ? 
base.GetPrice(price, discount) :
price;

由於 MemberOrderService 繼承 OrderService,因此使用 base.GetPrice() 呼叫父類別 OrderServiceGetPrice()

OrderService

proxy014

OrderService.cs

1
2
3
4
5
6
7
8
9
10
namespace OrderLibrary
{
public class OrderService : PriceInterface
{
public virtual double GetPrice(double discount, double price)
{

return discount * price;
}
}
}

因為 MemberOrderService 要 override GetPrice(),所以在 OrderService 必須宣告成 virtual

Q:使用 繼承 有什麼問題?

  1. OrderService.GetPrice() 必須改成 virtualOrderLibrary 必須修改與重新編譯,違反 OrderLibrary 不再修改與編譯 的需求,也違反 開放封閉原則
  2. MemberOrderService 必須加在 OrderLibrary assembly,違反 OrderLibrary 不再修改與編譯 的需求,也違反 開放封閉原則
  3. 繼承 就該符合 里氏替換原則,也就是無論怎麼繼承, GetPrice() 都該只是計算 price,但 MemberOrderService.GetPrice() 卻還去判斷 IsMember(),這已經超出 GetPrice() 的職責,也就是違反 單一職責原則
  4. MemberOrderService 完全以 code reuse 的角度去繼承 OrderService,而不是以 多型 的角度去繼承 OrderService,這也違反 里氏替換原則
  5. MemberOrderServiceOrderService 強耦合,將來在 OrderService 新增任何 field 與 method,都會污染到 MemberOrderService,違反 介面隔離原則

繼承 不是不能用,若真的沒有駕馭 繼承 的功力,建議就遵循 里氏替換原則

Proxy Pattern


Definition

Proxy Pattern

在不改變原有 interface 的前提下,控制 service 的使用

proxy015

  • ClientProxy 的 user,實務上可能是 component 或 controller
  • Subject:定義 ProxyRealSubject 的共用 interface
  • Proxy : RealSubject 的替身,Client 實際只接觸 Proxy, 再由 Proxy 呼叫 RealSubject
  • RealSubject:實際功能的物件

適用時機

  • 原有 interface 不能變動而想控制 service 使用
  • 原來的 class 在不同 assembly,無法修改 code 卻想控制 service 使用
  • 原本使用繼承而造成強耦合

優點

  • Interface 沒有變動,因此 client 程式不用修改 (如 .NET Core 的 Type Forwarding 設計)

在 GoF 的 Proxy Pattern,有提到四種 proxy,分別為 Remote Proxy、Virtual Proxy、Protection Proxy、Smart Reference,本文只針對最常用的 Protection Proxy 討論

Architecture
proxy000

  • ClientOrderProxy 的 user,實務上可能是 component 或 controller
  • PriceInterface:定義 OrderProxyOrderService 的共用 interface,相當於 Subject
  • OrderProxy:控制 OrderService 的使用,相當於 Proxy
  • OrderService:實際計算 全館八折,相當於 RealSubject
  • MemberService:判斷是否為會員,為 OrderService 所依賴的物件

Implementation

OrderProxy

proxy003

OrderProxy.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using MemberLibrary;
using OrderLibrary;

namespace ConsoleApp
{
public class OrderProxy : PriceInterface
{
private readonly PriceInterface _orderService = new OrderService();
private readonly MemberService _memberService = new MemberService();

public double GetPrice(double discount, double price)
{

return _memberService.IsMember("Sam") ?
_orderService.GetPrice(discount, price) :
price;

}
}
}

第 6 行

1
public class OrderProxy : PriceInterface

為了要達成 使 Client 的修改程式碼降到最低 的要求,OrderProxy 也繼續使用 PriceInterface

第 8 行

1
2
private readonly PriceInterface _orderService = new OrderService();
private readonly MemberService _memberService = new MemberService();

由於 OrderProxy 有兩個相依的 service:OrderServiceMemberService,因此宣告為 private field 儲存。

實務上不應該直接 new service,而應該使用 DI 注入解耦合,因為本文主是談 Proxy Pattern,因此就偷懶直接 new 了。

11 行

1
2
3
4
5
6
public double GetPrice(double price, double discount)
{

return _memberService.IsMember("Sam") ?
_orderService.GetPrice(price, discount) :
price;
}

先判斷是否為會員,在執行 OrderService.GetPrice()

重點在於在呼叫 service 的之前與之後,我們都可以加上自己的邏輯,而不是直接呼叫 service,這就是 proxy 本質。

Client
proxy001

Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using OrderLibrary;

namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{

PriceInterface orderProxy = new OrderProxy();
var realPrice = orderProxy.GetPrice(0.8, 800);

Console.WriteLine("Real price : {0}", realPrice);
}
}
}

10 行

1
PriceInterface orderProxy = new OrderProxy();

OrderService 改成 OrderProxy,再透過 Refactoring 的 RenameorderService 重構成 orderProxy

其他都不用改,達成 使 Client 的修改程式碼降到最低 的要求。

Q:OrderProxy.GetPrice()MemberOrderService.GetPrice() 的程式碼差異不大,為什麼寫在 proxy 就可以,寫在 繼承 就不行?

就功能而言,Proxy 與 繼承 都能達到需求,但語意不一樣:

  • Proxy 目的就是 控制 service 的使用,所以加上 IsMember() 判斷就天經地義,這就是它的職責
  • OrderProxy 並沒有使用 繼承,因此不受 里式替換原則 約束,interface 只要遵循 介面隔離原則 即可

Func


雖然先定義好 PriceInterface,讓 OrderServiceOrderProxy 共用 interface 的理念很好,但 …

  • 只有 GetPrice() 一個 method,真的需要開 interface 嗎?
  • 真的有必要特別實作 OrderProxy 嗎?

Architecture

proxy008

Implementation

OrderService

proxy009

OrderService.cs

1
2
3
4
5
6
7
8
9
10
namespace OrderLibrary
{
public static class OrderService
{
public static double GetPrice(double discount, double price)
{

return discount * price;
}
}
}

由於 OrderService 都沒用到 field (state), 是 pure function,因此可以將 GetPrice()OrderService 全部改成 static。

MemberService
proxy010

MemberService.cs

1
2
3
4
5
6
7
8
9
10
namespace MemberLibrary
{
public static class MemberService
{
public static bool IsMember(string account)
{

return account == "Sam";
}
}
}

由於 MemberService 也沒用到 field (state),也是 pure function,因此將 IsMember()MemberService 全部改成 static。

Client

proxy011

Program.cs

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

namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{

Func<double, double, double> OrderProxy(Predicate<string> isMember) =>
isMember("Sam") ?
(Func<double, double, double>) OrderService.GetPrice :
(discount, price) => price;

var realPrice = OrderProxy(MemberService.IsMember)(0.8, 800);

Console.WriteLine("Real price : {0}", realPrice);
}
}
}

11 行

1
2
3
4
Func<double, double, double> OrderProxy(Predicate<string> isMember) =>
isMember("Sam") ?
(Func<double, double, double>) OrderService.GetPrice :
(discount, price) => price;

OrderProxy 由 class 退化成 function,但 signature 仍然是 Func<double, double, double>,與原本的 PriceInterface 的 signature 一樣。

OrderProxy class 的 dependency,我們使用 DI 解決 依賴反轉原則 要求,但 FP 該如何做呢 ?

其實很簡單,function 就是 dependency,將 function 當參數傳入就好,也就是把 orderProxy() 當成 Higher Order Function,再將 MemberService.IsMember() 當成 predicate function 傳入 orderProxy() 即可。

1
(Func<double, double, double>) OrderService.GetPrice :

需要 Func<double, double, double> 轉型是因為 C# Type Inference 需求,所以一定得加。

1
(discount, price) => price;

由於 signature 要求為 Func<double, double, double>,所以 Lambda 的 input 一定要兩個參數,儘管只使用到 price

由此可發現所以 return function 都必須符合 Func<double, double, double>,這與 class interface 的本質是一樣的

16 行

1
var realPrice = OrderProxy(MemberService.IsMember)(0.8, 800);

orderProxy(MemberService.IsMember) 回傳為一 anonymous function,其中 MemberService.IsMember 為 function dependency,而 (discount, originalPrice) 為 anonymous function 的參數。

也就是原本是

1
var realPrice = orderService(discount, originalPrice);

因為 orderProxy() 回傳的 anonymous function 與 orderService() 的 signature 都是 Func<double, double, double>,所以其本質與 Proxy Pattern 都遵守 PriceInterface 意義是一樣的,只是 FP 使用 proxy function,而 OOP 使用 proxy class。

Refactoring


或許你會覺得 OrderProxy() 這個 local function 放在 Client 太過沈重,想另外 Move MethodOrderProxy class,那也非常簡單。

proxy016

OrderProxy
proxy017

OrderProxy.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
using System;
using OrderLibrary;

namespace ConsoleApp
{
public static class OrderProxy
{
public static Func<double, double, double> GetPrice(Predicate<string> isMember)
{
return isMember("Sam") ?
(Func<double, double, double>) OrderService.GetPrice :
(discount, price) => price;
}

}
}

OrderProxy() local function 透過 Refactoring 的 Move MethodOrderProxy class,直接將 local function 剪下貼上 即可。

Move Method 在 OOP Refactoring 雖然很基本,但實務上卻不容易達到,主要是因為 OOP 的 method 會與 field 有關,要 Move Method 可能得先 Move Field,要 Move Field 又可能得先 Self Encapsulate Field,然後又會發現更多 method 也必須跟著 Move Method,總之會是一連串的 combo 動作。

但若是 Pure Function,則 Move Method 非常容易,只是 剪下貼上 即可,因為完全不涉及 field,也不會與其他 method 有關,所以 FP 非常適合重構 Move Method

Client
proxy018

Program.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using System;
using MemberLibrary;

namespace ConsoleApp
{
class Program
{
static void Main(string[] args)
{

var realPrice = OrderProxy.GetPrice(MemberService.IsMember)(0.8, 800);

Console.WriteLine("Real price : {0}", realPrice);
}
}
}

Client 提供 OrderProxy.GetPrice() 的 dependency function 後,就可處理訂單了。

從 OOP 原本具有 垂直 特性的架構,經過 FP 重構後,出現 水平 近似 鐵道 的架構,也就是所謂的 Railway Oriented Programming,因為重構到最後的架構都是 水平 的,剛好就是 FP 所強調的 Pipeline 與 Function Composition。

Summary


Q:Proxy Pattern 與 Decorator Pattern 有什麼差異?

Proxy Pattern
proxy018

Decorator Pattern
proxy019

以 class diagram 角度,Proxy Pattern 與 Decorator Pattern 的確非常類似。

學 Design Pattern 不能以 class diagram 的角度去思考,而要以他要解決什麼問題來思考

Similarity

  • 都在解決 繼承濫用
  • Interface 都不會改變

Difference

  • Proxy 目的在對 service 做 控制存取;Decorator 目的在對 service 增加功能
  • Proxy 與 Subject 只能在 compile-time 靜態對應;Decorator 能在 run-time 動態新增

Conclusion


  • 繼承 通常是 OOP 最直覺的作法,但也是維護上最容易出問題的地方,Proxy Pattern 與 Decorator Pattern 提供了 繼承 的取代方案,讓 client 與 service 之間的依賴僅限於 interface,而不是 concrete service
  • Proxy Pattern 與 Decorator Pattern 非常類似,也都在解決 繼承 的老問題,但 Proxy Pattern 重點在於 控制存取,而 Decorator Pattern 重點在於 增加功能
  • Proxy class 亦可使用 Proxy function 取代,如此 interface 與 proxy class 都可省略,依賴注入可改用 Higher Order Function 解決。

Sample Code


完整的範例可以在我的 GitHub 上找到

Reference


Source Making, Proxy Design Pattern

2018-04-12