代理模式——結構型模式(7)


前言

今天我們將介紹的最后一種結構型模式——代理模式,在介紹它之前,讓我們先回顧下上一篇博文對享元模式的學習。享元模式主要是通過運用共享技術有效支持大量細粒度的對象,其本質在於兩方面:分離和共享。簡單地說,分離的是對象狀態中變與不變的部分,其中不變的部分設置為對象的內部狀態,而隨應用場景隨之發生變化的部分為對象的外部狀態;而共享指的就是對對象狀態中不變部分的共享,因為內部狀態不會隨外部狀態的改變而改變,從而也就具備了共享的條件,否則也就失去了共享的意義呢。再者,就是需要通過享元工廠來管理可共享的對象,也是各種具體的享元對象,享元工廠完全可以設計成單例,實現全局唯一性。之前,我們說過,結構型模式大部分都遵循Favor Composition Over Inheritance原則,而在享元模式里並沒有太多體現。復習就到此為此吧,讓我們步入今天的主題——代理模式的學習。

動機

在實際的軟件系統開發中,我們經常面臨着對一個對象進行訪問控制問題。有些對象由於跨越網絡或者安全方面等原因,不能夠直接或者不需要直接被訪問,直接訪問的代價會給系統帶來不必要的復雜性,比如可能得考慮訪問延遲、運行效率低下等問題。如何在客戶端程序與目標對象之間增加一層中間層,通過它來代替目標對象來完成客戶程序對目標對象的各種操作要求,如此,客戶程序就可以透明地與目標對象進行交互呢。這便是代理模式能解決的類似場景的擅長之處呢。是時候,該代理模式粉墨登場呢!

意圖

為其他對象提供一種代理以控制對這個對象的訪問。

結構圖

image

  1. 代理(Proxy)角色:代理對象,實現與具體目標對象一致的接口,這樣就可以使用代理來代替具體的目標對象。同時保存一個指向具體目標對象的引用,可以在需要的時候調用具體的目標對象。此外,亦可以控制對目標對象的訪問,並通過代理對象來負責對目標對象的創建和銷毀。
  2. 目標接口(Subject)角色:目標接口,定義代理和具體目標對象的接口,這樣就可以在任何使用具體目標對象的地方使用代理對象。
  3. 具體目標對象(RealSubject)角色:具體的目標對象,真正實現目標接口功能。
  4. 客戶(Client)角色:通過代理中間層與具體目標對象進行交互。

代碼示例

 1:  public abstract class Subject{
 2:      public abstract void Request();
 3:  }
 4:   
 5:  public class RealSubject extends Subject{
 6:      public void Request(){
 7:          //執行具體的功能處理
 8:          System.out.println("Request of RealSubject is done!");
 9:      }
10:  }
11:   
12:  public class Proxy extends Subject{
13:      private RealSubject realSubject=null;
14:   
15:      //通過參數完成對具體目標對象的創建,不過一般不推薦,因為客戶端通常也無法直接構造具體目標對象
16:      public Proxy(RealSubject realSubject){
17:          this.realSubject=realSubject;
18:      }
19:   
20:      //直接通過默認構造器來完成對具體目標對象的創建
21:      public Proxy(){
22:          this.realSubject=new RealSubject();
23:      }
24:   
25:      public void Request(){
26:          //在轉調具體的目標對象之前,可以執行一些預處理
27:          
28:          realSubject.Request();
29:   
30:          //在轉調具體的目標對象之后中,可以執行一些后處理
31:      }
32:  }
33:   
34:  public class Client{
35:      public static void main(String[] args){
36:          Subject subject=new Proxy();
37:          subject.Request();
38:      }
39:  }    

從上述示例代碼中,我們可以大概明了代理模式的基本實現。目標接口Subject定義了代理和具體目標對象都應該實現的接口,這樣客戶端使用的時候只需要統一和目標接口打交道即可。目標對象RealSubject真正實現了目標接口功能,而代理Proxy雖然也實現了目標接口,但是它的實現主要還是將功能委派給具體的目標對象來完成,所以代理必須保存一個具體目標對象的引用,在需要的時候能夠調用它。而在客戶端程序里,我們看到,雖然完成功能的是具體的目標對象,但是其只與代理打交道,因為代理會把客戶端的操作請求委派給具體的目標對象的,之所以只與代理打交道,這也就回到了代理模式的終極目的呢:代理模式在客戶與具體目標對象之間,引入一定程度的間接性,客戶直接使用代理,讓代理來與具體的目標對象進行交互,這正因為這層間接性,我們可以完成各種各樣的功能,也就可以實現不同類型的代理方式呢,下面對此會有更細致的介紹。從實現上來說,代理模式主要使用了對象的組合和委托,這點從示例代碼中已經清楚地表現出來呢,主要體現在代理Proxy類結構中。下面,讓我們來看看比較貼切真實的場景例子吧!

現實場景

在現實的生活場景中,我們也可以隨處可見代理模式的具體運用。比如,我們通常通過支票或者銀行存單來完成對金錢的操作,換句話來說,就是它們在市場交易中作為現金的“代理人”。我們只需要通過它們就能完成對現金的兌現、存款等操作。這樣,我們就能省去身上攜帶大筆現金的不便和不安全性呢,着實給我們日常生活帶來一定程度的便利。如果通過UML圖來表示,大概就是下面的樣子:

image

看到這個圖是不是瞬間對上述場景有了更進一步的理解呢。顯然,不管是真實的現金還是具有支付功能的支票或者在意都應該有其固定的等價金額功能,這樣才能在銀行中完成對金錢的各種操作。但是支票和真正的現金還是有很多不一樣呢,畢竟它不是真正意義上的現金,所以完成真正的金錢操作還是必須依賴於真正的現金對象,也就是說銀行最終打交道的還是現金流對象,而不是支票。之所以需要支票,主要還是為了大眾對大額現金的存取方便。這點,在我們日常生活中也是深有體會。抽象成代理模式,支票其實就是代理對象,也就是真實現金的代理者,一切對現金的操作都交給支票來間接完成,從而省去了用戶對現金的直接操作,如果非得說為什么不直接通過現金進行操作,可能安全性和便利便是銀行使用支票的最重要原因呢。記住,銀行里真正處理的對象還是“真金白銀”,只是支票在這里作為其等價物,間接地完成了現金的所有操作而已。

接下來,讓我們通過一個已經被反復多次作為代理模式示例的demo程序吧——數學計算程序問題,實現簡單的加減乘除操作。直接看代碼吧!

 1:  public abstract class IMath{
 2:      public abstract double Add(double x,double y);
 3:      public abstract double Sub(double x,double y);
 4:      public abstract double Mul(double x,double y);
 5:      public abstract double Dev(double x,double y);    
 6:  }
 7:   
 8:  public class Math extends IMath{
 9:      public  double Add(double x,double y){
10:          return x+y;
11:      }
12:      public  double Sub(double x,double y){
13:          return x-y;
14:      }
15:      public  double Mul(double x,double y){
16:          return x*y;
17:      }
18:      public  double Dev(double x,double y){
19:          return x/y;//注這里就不考慮分母為零的情況呢,一切從簡:)
20:      }
21:  }
22:   
23:  public class MathProxy extends IMath{
24:      private Math math=null;
25:      public MathProxy(){
26:          this.math=new Math();
27:      }
28:   
29:      public  double Add(double x,double y){
30:          //在真正調用Math之前可能需要完成一些網絡連接等初始化操作等        
31:          return math.Add(x, y);        
32:          //完成調用Math后,同樣需要完成清理工作等
33:      }
34:      public  double Sub(double x,double y){
35:          //在真正調用Math之前可能需要完成一些網絡連接等初始化操作等
36:          return math.Sub(x, y);
37:          //完成調用Math后,同樣需要完成清理工作等
38:          
39:      }
40:      public  double Mul(double x,double y){
41:          //在真正調用Math之前可能需要完成一些網絡連接等初始化操作等
42:          return math.Mul(x, y);
43:          //完成調用Math后,同樣需要完成清理工作等
44:      }
45:      public  double Dev(double x,double y){
46:          //在真正調用Math之前可能需要完成一些網絡連接等初始化操作等
47:          return  math.Dev(x, y);
48:          //完成調用Math后,同樣需要完成清理工作等
49:      }
50:   
51:  }
52:  public class Client{
53:      public static void main(String[] args){
54:          //這里,對MathProxy對象的創建工作完全可以通過創建型模式來完成,
55:          //或者通過java配置和反射功能透明地生成相應的代理對象
56:          IMath math=new MathProxy();
57:          math.Add(1, 2);
58:          math.Sub(4, 2);
59:          math.Mul(3, 4);
60:          math.Dev(6, 3);
61:      }
62:  }    

現在我們對上述代碼進行一個簡單的講解下。對這樣一個數學計算程序,看似簡單,但是當該計算程序位於遠程服務器上時,如果我們直接調用真實Math對象,那么每次調用相應的操作就不會像是本地調用那樣簡單直接呢,因為調用遠程對象涉及跨網絡問題,我們不得不考慮各種網絡相關的復雜操作,比如對發送的消息或者接收到的結果進行裝包或者解包等一系列操作問題。可以想像,這將是讓人頭痛的處理過程。而通過代理模式,我們將這部分工作交給本地端的代理對象MathProxy來完成,同時代理對象也會將我們的請求委派給遠程Math對象,間接地完成我們的請求。所以,現在我們只需要與本地端的代理對象打交道,而無需考慮復雜的網絡相關問題,大大簡化了客戶端對真實目標對象Math的調用過程。此外,由於代理對象也實現了相應的目標接口,所以客戶端可以像處理具體的目標對象Math一樣透明地調用代理對象來完成相應請求操作呢。

最后,讓我們來談談java中的代理吧。java對代理模式提供了內建的支持,在java.lang.reflect包下面,提供了一個Proxy的類和一個InvocationHandler的接口。上文我們實現的代理模式可以稱為java靜態代理。不過這種實現方式有一個較大的不足,當目標接口Subject發生改變時,代理類和目標對象的實現都要發生改變,並不是很靈活。而使用java內建的對代理模式支持的功能來實現則可以避免這個問題。通常將使用java內建的對代理模式支持的功能來實現的代理稱為java動態代理。與java靜態代理的明顯區別在於:在靜態代理實現的過程中,代理類要實現目標接口所定義的所有方法,而動態代理實現的過程中,即使目標接口中定義了多個方法,但是動態代理類始終只有一個invoke方法。這樣,當目標接口發生變化時,動態代理的接口也就不需要發生改變呢。需要注意的是,java的動態代理目前還只支持對接口的代理,基本的實現是依靠java的反射和動態生成class的技術,來動態生成被代理的接口的實現對象。當然,我們亦可以通過cglib庫來完成對實現類的代理,cglib是一個開源的Code Generation Library。接下來,就讓我們通過動態代理的方式來實現上述數學計算程序吧。

 1:  public interface IMath {
 2:      public  double Add(double x,double y);
 3:      public  double Sub(double x,double y);
 4:      public  double Mul(double x,double y);
 5:      public  double Dev(double x,double y);    
 6:  }
 7:   
 8:  public class Math implements IMath {
 9:      @Override
10:      public double Add(double x, double y) {
11:          // TODO Auto-generated method stub
12:          return x+y;
13:      }
14:   
15:      @Override
16:      public double Dev(double x, double y) {
17:          // TODO Auto-generated method stub
18:          return x-y;
19:      }
20:   
21:      @Override
22:      public double Mul(double x, double y) {
23:          // TODO Auto-generated method stub
24:          return x*y;
25:      }
26:   
27:      @Override
28:      public double Sub(double x, double y) {
29:          // TODO Auto-generated method stub
30:          return x/y;
31:      }
32:  }
33:   
34:  public class MathProxy implements InvocationHandler {
35:      private IMath math=null;
36:   
37:      public IMath getProxyInterface(){
38:          this.math=new Math();
39:          IMath  proxyMath=(IMath)Proxy.newProxyInstance(math.getClass().getClassLoader(),
40:                      math.getClass().getInterfaces(),this);
41:          return proxyMath;
42:      }
43:   
44:      @Override
45:      public Object invoke(Object proxy, Method method, Object[] args)
46:              throws Throwable {
47:          // 直接將請求委派給具體的Math對象
48:          return method.invoke(math, args);
49:      }
50:  }
51:  public class Client{
52:      public static void main(String[] args){
53:          MathProxy mathProxy=new MathProxy();
54:          IMath math=mathProxy.getProxyInterface();
55:   
56:          math.Add(1, 2);
57:          math.Sub(4, 2);
58:          math.Mul(3, 4);
59:          math.Dev(6, 3);
60:      }
61:  }    

運行的結果與我們之前手動實現的靜態代理模式是一樣的。其實,java的動態代理還是AOP的一個重要手段,有興趣的同學可以查閱相關資料進行深入學習,在這里,我們對動態代理講解就說這么多呢,網上對java動態代理的細致解說的博文也有很多,比如這里這里,還有這里等等。好呢,對代理模式的應用舉例就說這么多呢。就此打住吧:)

實現要點

  1. 代理類最好也實現與具體目標對象實現的相同接口,這樣客戶端就可以透明地操作代理對象,因為它與目標對象的接口完全一致。
  2. 代理類內部保存具體目標對象引用,以便需要的時間能夠調用之,同時,也應該考慮如何才能更好地創建目標對象,是通過外部傳遞方式還是直接在構造器中完成對目標對象的創建,兩種方案需要根據具體應用場景來取舍。
  3. 如果能夠遵循第一點,那么代理類可以不總是需要知道具體目標對象類型,也就是說無須為每一個具體目標對象都生成一個代理類,代理類可以統一地處理所有的目標對象,這點很重要。

運用效果

代理模式在訪問對象時引入了一定程度的間接性,這樣根據代理的類型,附加的間接性也有會帶來不同的用途和效果,具體請參考適用性一節。

適用性

在如下的情況中建議考慮使用代理模式:

  1. 需要為一個對象在不同的地址空間提供局部代表的時候,可以使用遠程代理。它隱藏了一個對象存在於不同的地址空間的事實,即客戶通過遠程代理來訪問一個對象,根本就不用關心這個對象在哪里,以及如何通過網絡來訪問到這個對象,這些工作完全由代理幫我們完成。
  2. 需要按照需要創建開銷很大的對象的時候,可以使用虛代理。它可以根據需要來創建“大”對象,只有到必須創建的時候,虛代理才會創建對象,從而大大加快程序運行速度,節省資源。此外,通過虛代理還可以對系統進行優化。
  3. 需要控制對原始對象的訪問的時候,可以使用保護代理。它可以在訪問一個對象的前后,執行很多附加操作,除了進行權限控制之外,還可以進行很多跟業務相關的處理,而不需要修改被代理的對象。換句話來說就是通過代理來給目標對象增加額外的功能。
  4. 需要在訪問對象執行一些附加操作的時候,可以使用智能指引代理。其和保護代理類似,也是允許在訪問一個對象的前后,執行很多的附加操作,比如引用計數等操作。

上面的四種代理方式各有側重,對它們具體的應用情況大家可以查閱相關資料進行深入地學習。

相關模式

  1. 代理模式與適配器模式:兩者都是為另一個對象提供間接性的訪問,而且都是從自身以外的一個接口向這個對象轉發請求。從功能上來說,兩者是完全不一樣的。適配器模式主要用來解決接口間不匹配的問題,為所適配的對象提供一個不同的接口;而代理模式會實現和目標對象相同的接口。
  2. 代理模式與裝飾模式:兩者結構上很相似,但是彼此的目的和功能是不同的。裝飾模式是為了讓不生成子類就可以給對象添加額外的職責,即動態地添加功能;而代理模式主要是控制對對象的訪問。

總結

代理模式的本質是:控制對象訪問。代理模式通過代理對象,在客戶端與目標對象之間增加了一層中間層。也正因為這個中間層,給代理對象很多的活動空間。可以在代理對象中調用目標對象前后增加很多操作,從而實現新的功能或擴展目標對象的功能,從抽象的含義來說,也就是完成了對對象的訪問控制作用。而且通過java動態代理方式,可以實現對目標對象的完全代理,即完全替代呢。從實現過程上來看,代理模式主要使用對象的組合和委托,這點從我們實現的靜態代理方法中體現地比較明了。在這里,強烈推薦大家可以更深入地去學習理解下java或者.net語言中的動態代理實現方式,這樣會對代理模式有一個更深一步地認識。到此為止,我們已經將所有的結構型模式都介紹完畢呢,下一篇我們將會對結構型模式作一較為詳細的總結,緊接着就是對行為型模式的講解呢,敬請期待!

參考資料

  1. 程傑著《大話設計模式》一書
  2. 陳臣等著《研磨設計模式》一書
  3. GOF著《設計模式》一書
  4. Terrylee .Net設計模式系列文章
  5. 呂震宇老師 設計模式系列文章


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM