代理人模式(Proxy Pattern)
1: //飲料機
2: public class DrinkMachine
3: {
4: public bool state {get;set;}
5: int count{get;set;}
6: string location; //位置資訊
7:
8: public DrinkMachine(string location)
9: {
10: this.location = location;
11: }
12:
13: public string getLocation()
14: {
15: return location;
16: }
17:
18: public int getCount()
19: {
20: return count;
21: }
22:
23: public bool getState()
24: {
25: return state;
26: }
27:
28: public void saleDrink()
29: {
30: if (count>0)
31: {
32: count--;
33: Console.WriteLine("謝謝您,飲料售出");
34: }else{
35: Console.WriteLine("不好意思,飲料售完");
36: }
37: }
38:
39: public void Replenishment(int count)
40: {
41: this.count += count;
42: if (this.count>10)
43: {
44: this.count=10;
45: }
46: Console.WriteLine(string.Format("補貨完畢,目前有{0}罐",this.count));
47: }
48: }
以上是一台飲料機的簡單實作,裡面有簡單的庫存機制、販賣機制以及相關的屬性。
現在飲料公司為了準備報表,想要建立飲料機的監控類別,透過一個叫作DrinkMachineMonitor
只要傳入放在各地的飲料機,就可以來快速的取得各個飲料機的位置資訊,並列印出報告。
這樣的需求我們可以馬上進行開發
1: public class DrinkMachineReporter
2: {
3: DrinkMachine machine;
4:
5: public DrinkMachineReporter(DrinkMachine drinkmachine)
6: {
7: this.machine = drinkmachine;
8: }
9:
10: public void Report(){
11: Console.WriteLine("飲料機的位置:"+machine.getLocation());
12: Console.WriteLine("飲料機的狀態:"+machine.getState());
13: Console.WriteLine("飲料機的庫存:"+machine.getCount());
14: }
15:
16: }
頭腦一動,我們馬上就可以寫出類似上面的程式碼。
簡單測試一下就可以運行這個簡單的監控程式
上述,我們透過了監視器的開發建立了一個糖果機的另一個View
簡而言之,只是建立了另一個畫面,讓使用者看到
但這樣出現了什麼問題呢?我們假設每個物件都是一台真實的飲料機的話…
那我們這個監視器不是必須將所有的飲料機搬回公司,才能夠透過「本地」端的飲料機
進行資訊的取得,並取得報表的資訊嗎?
其實…更進一步的需求就是這樣:透過類似網路的環境,能夠將遠在別處的飲料機的資訊
透過服務的方式來取得!
第一個關鍵字是「遠端」、第二個關鍵字是「代理人」
所謂遠端就是,我們希望在辦公室遠端監控這些機器,所以我們可以不要更動原先的Monitor程式
但是不要將飲料機傳入Monitor,而是將飲料機的代理人交給Monitor程式
而代理人是指(Proxy)某個真實的物件,就像是飲料機一樣,但是它不是飲料機,幕後是利用網路
和一個遠端的飲料機溝通。
整體來說,在軟體架構中,遠端代理人就是「遠端物件的本地代表」
遠端物件是一種活在記憶體的Heap(堆積中)的物件
而本地代表是在本地端調用的物件,其調用的行為會被轉到遠端物件中執行。
客戶端的程式碼會以為他溝通的物件對象是真正在遠端的物件
但其實它只有接觸到代理人,再由代理人透過網路和真正的物件溝通。
代理人可以假裝自己是遠端的物件,但其實只是一個中間角色。
在這邊,遠端的物件是真正重要的角色,它才是真正的做事者。
客戶物件運作起來就像是在呼叫遠端,但其實是呼叫本地Heap區中的「代理人」物件,再交由代理人處理網路溝通的低階細節。
首先先讓飲料機準備好當一個遠端的服務吧:
1.為飲料機建立一個遠端介面,介面內包括了一組可以遠端調用的方法。
2.在具象類別中實踐此介面。
我們先來定義一個遠端介面,好供飲料機來提供服務,而且讓監視器,可以取得服務的共同介面
(這邊要注意,因為要分為遠端與本地端,因此,這個共同的服務介面,我們要另外開一個專案出來)
簡單而言如下:
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5:
6: namespace com.DrinkCompany
7: {
8: //定義遠端服務接口的介面,
9: public interface DrinkMachineRemote
10: {
11: string getLocation();
12: int getCount();
13: bool getState();
14: }
15: }
從我們報表為例,我們需要這三個服務,因此我們先定義出來。並建置成一個dll檔案
(以下實作採用的是.NET的Remoting架構,在Java中也可以Import RMI來進行實作,目的一致)
接著我們先由遠端的飲料機來實現這個服務介面吧(其實我們飲料機早就有了,我們等於是為飲料機設計服務介面)
因此直接在飲料機的程式碼中實作即可,完全不需要改程式碼:(為了引入共同的介面定義,因此要參考先前建置完dll檔案)
然後因為同一家公司,自然使用相同的Namespace,另外,為了實作底下的網路層我們必須讓這個飲料機實作MarshalByRefObject物件
詳細MarshalByRefObject的原理可以看MSDN的介紹(點我)
1: using com.DrinkCompany; //定義的共同遠端服務介面
2:
3: namespace com.DrinkCompany
4: {
5: //飲料機
6: public class DrinkMachine : MarshalByRefObject, DrinkMachineRemote
7: {
8: public bool state { get; set; }
9: int count { get; set; }
10: string location; //位置資訊
11:
12: public DrinkMachine(string location)
13: {
14: this.location = location;
15: }
16:
17: public DrinkMachine()
18: {
19: this.count = 3;
20: this.location = "Taipei";
21: this.state = true;
22: }
23:
24: public string getLocation()
25: {
26: return location;
27: }
28:
29: public int getCount()
30: {
31: return count;
32: }
33:
34: public bool getState()
35: {
36: return state;
37: }
38:
39: public void saleDrink()
40: {
41: if (count > 0)
42: {
43: count--;
44: Console.WriteLine("謝謝您,飲料售出");
45: }
46: else
47: {
48: Console.WriteLine("不好意思,飲料售完");
49: }
50: }
51:
52: public void Replenishment(int count)
53: {
54: this.count += count;
55: if (this.count > 10)
56: {
57: this.count = 10;
58: }
59: Console.WriteLine(string.Format("補貨完畢,目前有{0}罐", this.count));
60: }
61: }
接著,我們實作完飲料機的服務了(其實沒有實作,只是拿來直接用而己)
為了讓這個飲料機能夠直接提供服務,因此我們必須定義出服務的底層模式…其實方法很多重,也可以列很多篇幅來討論
不過這邊我們直接實作TCP的協定,在Server端飲料機的這邊實作以下程式碼:
Server端
1: class Program
2: {
3: static void Main(string[] args)
4: {
5: TcpChannel channel = new TcpChannel(9876); //定義Port
6: ChannelServices.RegisterChannel(channel,false); //註冊通道(與Java的RMI同原理)
7:
8: //服務類別的註冊
9: RemotingConfiguration.RegisterWellKnownServiceType(
10: typeof(DrinkMachine), "remotePin", WellKnownObjectMode.SingleCall);
11:
12: System.Console.WriteLine("The Server is ready .... Press the enter key to exit...");
13: System.Console.ReadLine();
14: }
15: }
其中,對於服務註冊的含義各位可以參考這篇 點我 (像是這個可以定義WellKnowOjbectMode,可以定義SingleCall或是Singleton)
這關係到提供的服務是否為同一個物件,不過這次的分享主軸在代理人模式,所以我們先不討論網路層的細節。
OK了,我們接著執行這個飲料機服務
然後等著我們的監控程式來呼叫服務了,現在我們不需要依賴具體類別了
這我們直接依賴共用的服務介面,因此也要將先前建置的dll加入到客戶監控程式的專案檔
1: public class DrinkMachineReporter
2: {
3: //換成依賴遠端介面,而不是直接使用具象的DrinkMachine類別
4: public DrinkMachineRemote machine;
5:
6: public DrinkMachineReporter(DrinkMachineRemote drinkmachine)
7: {
8: this.machine = drinkmachine;
9: }
10:
11: public void Report(){
12: try
13: {
14: Console.WriteLine("飲料機的位置:" + machine.getLocation());
15: Console.WriteLine("飲料機的狀態:" + machine.getState());
16: Console.WriteLine("飲料機的庫存:" + machine.getCount());
17: }
18: catch (Exception ex)
19: {
20: throw;
21: }
22: }
23: }
以上可以看到先前的具體飲料機都被換成了服務介面,但是我們報表的程式仍然完全不用改
直接就像操作飲料機拿到資訊一樣似的。(以下實作呼叫服務網路底層,有興趣可以看先前.NET的RMI實作那篇)
主要與服務相關的就是他們要實作共同的介面(In dll中),然後第12行呼叫的協定(TCP)、通道(9876)與URL位置(remotePin)都是在Server端程式定義好的。
Client端
1:
2: /// <summary>
3: /// Entry point into console application.
4: /// </summary>
5: static void Main(string[] args)
6: {
7: TcpChannel channel = new TcpChannel();
8: ChannelServices.RegisterChannel(channel,false);
9:
10: // Create an instance of the remote object
11: DrinkMachineRemote obj = (DrinkMachineRemote)Activator.GetObject(
12: typeof(DrinkMachineRemote), "tcp://localhost:9876/remotePin");
13: // localhost OR your server name
14: if (obj.Equals(null))
15: {
16: System.Console.WriteLine("Error: unable to locate server");
17: }
18: else
19: {
20: String strArgs;
21: if (args.Length == 0)
22: {
23: strArgs = "Client";
24: }
25: else
26: {
27: strArgs = args[0];
28: }
29: DrinkMachineReporter tReporter = new DrinkMachineReporter(obj);
30: tReporter.Report();
31: }
因為透過介面來呼叫,因此本地端的DrinkMachineReporter 可以直接將遠端的服務物件,傳入監控程式使用。
所以呼叫了Repoter函式後,可以得到以下結果。
來定義代理人模式吧,在Head First Design Pattern中定義為:
讓某個物件具有一個替身,以控制外界對此物件的接觸。
類別圖如下:
實例:
遠端飲料機(RealSubject)、本地飲料機監控(Proxy)、飲料機服務介面(Subject)
我們具備有一個 飲料機服務介面 的介面,供 遠端飲料機 與 本地飲料機監控 共用
好讓 本地飲料機監控 可以取代遠端飲料機的功能(取得資訊),而不用大老遠跑到飲料機所在地
遠端飲料機 是真正做事的物件,它是被 本地飲料機監控 代理的對象,被控制存取的對象。
在某些案例中,本地飲料機監控還要負責 遠端飲料機 的建立與消毀。客戶必須透過Proxy才能與RealSubject互動
也就無形的控制了存取權限。因此被代理的物件可以是遠端的物件、建立成本高的物件或者是需要安控的物件…
上述就是一般代理人(遠端)的模式
代理人模式還有許多的變型:虛擬代理人、保護代理人、動態代理人、防火牆代理人、同步化代理人、備用代理人…etc
但這邊不一一分享,未來若有機會再分享唄(下一次,代理人模式的變型:虛擬代理人)
參考資料:
Head First Design Pattern Book
.Net Remoting http://www.iiiedu.org.tw/knowledge/knowledge20030430_2.htm
Migrating Java RMI to .NET Remoting http://www.c-sharpcorner.com/UploadFile/pk_khuman/RMIto.NETRemoting01162006052210AM/RMIto.NETRemoting.aspx
MSDN