如何使用 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都包在OrderLibraryassembly 內,與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。
實務上不應該直接
newservice,而應該使用 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必須加在OrderLibraryassembly,違反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 儲存。
實務上不應該直接
newservice,而應該使用 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 上找到