代理人模式(Proxy Pattern)

  • 5199
  • 0

代理人模式(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: }

頭腦一動,我們馬上就可以寫出類似上面的程式碼。

簡單測試一下就可以運行這個簡單的監控程式

image

 

上述,我們透過了監視器的開發建立了一個糖果機的另一個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了,我們接著執行這個飲料機服務

image

 

然後等著我們的監控程式來呼叫服務了,現在我們不需要依賴具體類別了

這我們直接依賴共用的服務介面,因此也要將先前建置的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函式後,可以得到以下結果。

image

 

來定義代理人模式吧,在Head First Design Pattern中定義為:

讓某個物件具有一個替身,以控制外界對此物件的接觸。

類別圖如下:

image

實例:

遠端飲料機(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