如何使用 C# 實現 Proxy Pattern ?
Proxy Pattern 是 OOP 中著名的 Design Pattern,尤其可在不改變 interface 的前提下,就能控制該物件的使用,隨著 FP 逐漸受到重視, Proxy Pattern 在實作上也有了新的面貌。
Version
macOS High Sierra 10.13.3
.NET Core SDK 2.1.101
Rider 2017.3.1
C# 7.2
User Story 1
假設你在處理訂單,為了慶祝週年慶,全館八折
。
Task
將 OrderService
包在 OrderLibrary
assembly 內。
Architecture
Implementation
Client
Program.cs1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16using 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
PriceInterface.cs1
2
3
4
5
6
7namespace OrderLibrary
{
public interface PriceInterface
{
double GetPrice(double discount, double price);
}
}
PriceInterface
定義 OrderService
的 interface,只有 GetPrice()
一個 method。
OrderService
OrderService.cs1
2
3
4
5
6
7
8
9
10namespace OrderLibrary
{
public class OrderService : PriceInterface
{
public double GetPrice(double discount, double price)
{
return discount * price;
}
}
}
OrderService
實踐 PriceInterface
的 GetPrice()
,計算 全館八折
。
目前
PriceInterface
與OrderService
都包在OrderLibrary
assembly 內,與Client
的ConsoleApp
是不同 assembly。
User Story 2
週年慶過後,只有 會員
才享有 全館八折
,一般消費者維持原價。
Task
- 由於
開放封閉原則
,OrderLibrary
不再修改與編譯 - 在相同的
PriceInterface
下,要使Client
的修改程式碼降到最低
Inheritance
Architecture
- Client:
OrderService
的 user,實務上可能是 component 或 controller - PriceInterface:定義
OrderService
的 interface - OrderService:實現
全館八折
- MemberOrderService:根據
MemberService
判斷是否為會員
,並且因為 code reuse 繼承OrderService
- MemberService:判斷
是否為會員
Implementation
MemberService
MemberService.cs
1 | namespace MemberLibrary |
因為要判斷是否為 會員
,新增 MemberService
。
實務上判斷
是否為會員
與資料庫有關,本文主要是談 Proxy Pattern,就只簡單的判斷會員是否為Sam
即可。
MemberOrderService
MemberOrderService.cs
1 | using MemberLibrary; |
第 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 | return _memberService.IsMember("Sam") ? |
由於 MemberOrderService
繼承 OrderService
,因此使用 base.GetPrice()
呼叫父類別 OrderService
的 GetPrice()
。
OrderService
OrderService.cs
1 | namespace OrderLibrary |
因為 MemberOrderService
要 override GetPrice()
,所以在 OrderService
必須宣告成 virtual
。
Q:使用
繼承
有什麼問題?
OrderService.GetPrice()
必須改成virtual
,OrderLibrary
必須修改與重新編譯,違反OrderLibrary 不再修改與編譯
的需求,也違反開放封閉原則
MemberOrderService
必須加在OrderLibrary
assembly,違反OrderLibrary 不再修改與編譯
的需求,也違反開放封閉原則
繼承
就該符合里氏替換原則
,也就是無論怎麼繼承,GetPrice()
都該只是計算 price,但MemberOrderService.GetPrice()
卻還去判斷IsMember()
,這已經超出GetPrice()
的職責,也就是違反單一職責原則
MemberOrderService
完全以 code reuse 的角度去繼承OrderService
,而不是以多型
的角度去繼承OrderService
,這也違反里氏替換原則
MemberOrderService
與OrderService
強耦合,將來在OrderService
新增任何 field 與 method,都會污染到MemberOrderService
,違反介面隔離原則
繼承
不是不能用,若真的沒有駕馭繼承
的功力,建議就遵循里氏替換原則
Proxy Pattern
Definition
Proxy Pattern
在不改變原有 interface 的前提下,控制 service 的使用
- Client:
Proxy
的 user,實務上可能是 component 或 controller - Subject:定義
Proxy
與RealSubject
的共用 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
- Client:
OrderProxy
的 user,實務上可能是 component 或 controller - PriceInterface:定義
OrderProxy
與OrderService
的共用 interface,相當於Subject
- OrderProxy:控制
OrderService
的使用,相當於Proxy
- OrderService:實際計算
全館八折
,相當於RealSubject
- MemberService:判斷是否為會員,為
OrderService
所依賴的物件
Implementation
OrderProxy
OrderProxy.cs1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19using 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
2private readonly PriceInterface _orderService = new OrderService();
private readonly MemberService _memberService = new MemberService();
由於 OrderProxy
有兩個相依的 service:OrderService
與 MemberService
,因此宣告為 private field 儲存。
實務上不應該直接
new
service,而應該使用 DI 注入解耦合,因為本文主是談 Proxy Pattern,因此就偷懶直接new
了。
11 行
1 | public double GetPrice(double price, double discount) |
先判斷是否為會員,在執行 OrderService.GetPrice()
。
重點在於在呼叫 service 的之前與之後,我們都可以加上自己的邏輯,而不是直接呼叫 service,這就是 proxy 本質。
Client
Program.cs1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16using 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 的 Rename
將 orderService
重構成 orderProxy
。
其他都不用改,達成 使 Client 的修改程式碼降到最低
的要求。
Q:
OrderProxy.GetPrice()
與MemberOrderService.GetPrice()
的程式碼差異不大,為什麼寫在 proxy 就可以,寫在繼承
就不行?
就功能而言,Proxy 與 繼承
都能達到需求,但語意不一樣:
- Proxy 目的就是
控制 service 的使用
,所以加上IsMember()
判斷就天經地義,這就是它的職責 OrderProxy
並沒有使用繼承
,因此不受里式替換原則
約束,interface 只要遵循介面隔離原則
即可
Func
雖然先定義好 PriceInterface
,讓 OrderService
與 OrderProxy
共用 interface 的理念很好,但 …
- 只有
GetPrice()
一個 method,真的需要開 interface 嗎? - 真的有必要特別實作
OrderProxy
嗎?
Architecture
Implementation
OrderService
OrderService.cs
1 | namespace OrderLibrary |
由於 OrderService
都沒用到 field (state), 是 pure function,因此可以將 GetPrice()
與 OrderService
全部改成 static。
MemberService
MemberService.cs1
2
3
4
5
6
7
8
9
10namespace MemberLibrary
{
public static class MemberService
{
public static bool IsMember(string account)
{
return account == "Sam";
}
}
}
由於 MemberService
也沒用到 field (state),也是 pure function,因此將 IsMember()
與 MemberService
全部改成 static。
Client
Program.cs1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21using 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 | Func<double, double, double> OrderProxy(Predicate<string> isMember) => |
將 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 Method
到 OrderProxy
class,那也非常簡單。
OrderProxy
OrderProxy.cs1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16using 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 Method
到 OrderProxy
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
Program.cs1
2
3
4
5
6
7
8
9
10
11
12
13
14
15using 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
Decorator Pattern
以 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 上找到