1. 程式人生 > >研磨設計模式 之 代理模式(Proxy)3——跟著cc學設計系列

研磨設計模式 之 代理模式(Proxy)3——跟著cc學設計系列

11.3  模式講解

11.3.1  認識代理模式

(1)代理模式的功能

代理模式是通過建立一個代理物件,用這個代理物件去代表真實的物件,客戶端得到這個代理物件過後,對客戶端沒有什麼影響,就跟得到了真實物件一樣來使用。

       當客戶端操作這個代理物件的時候,實際上功能最終還是會由真實的物件來完成,只不過是通過代理操作的,也就是客戶端操作代理,代理操作真正的物件。

       正是因為有代理物件夾在客戶端和被代理的真實物件中間,相當於一箇中轉,那麼在中轉的時候就有很多花招可以玩,比如:判斷一下許可權,如果沒有足夠的許可權那就不給你中轉了,等等。

(2)代理的分類

事實上代理又被分成多種,大致有如下一些:

  • 虛代理:根據需要來建立開銷很大的物件,該物件只有在需要的時候才會被真正建立
  • 遠端代理:用來在不同的地址空間上代表同一個物件,這個不同的地址空間可以是在本機,也可以在其它機器上,在Java裡面最典型的就是RMI技術
  • copy-on-write代理:在客戶端操作的時候,只有物件確實改變了,才會真的拷貝(或克隆)一個目標物件,算是虛代理的一個分支
  • 保護代理:控制對原始物件的訪問,如果有需要,可以給不同的使用者提供不同的訪問許可權,以控制他們對原始物件的訪問
  • Cache代理:為那些昂貴的操作的結果提供臨時的儲存空間,以便多個客戶端可以共享這些結果
  • 防火牆代理:保護物件不被惡意使用者訪問和操作
  • 同步代理:使多個使用者能夠同時訪問目標物件而沒有衝突
  • 智慧指引:在訪問物件時執行一些附加操作,比如:對指向實際物件的引用計數、第一次引用一個持久物件時,將它裝入記憶體等

       在這些代理型別中,最常見的是:虛代理、保護代理、遠端代理和智慧指引這幾種。本書主要討論和示例了虛代理和保護代理,這是實際開發中使用頻率最高的。

對於遠端代理,沒有去討論,因為在Java中,遠端代理的典型體現是RMI技術,要把遠端代理講述清楚,就需要把RMI講述清楚,這不在本書討論範圍之內。

對於智慧指引,基本的實現方式和保護代理的實現類似,只是實現的具體功能有所不同,因此也沒有具體去討論和示例。

(3)虛代理的示例

       前面的例子就是一個典型的虛代理的實現。

起初每個代理物件只有使用者編號和姓名的資料,直到需要的時候,才會把整個使用者的資料裝載到記憶體中來。

也就是說,要根據需要來裝載整個UserModel的資料,雖然使用者資料物件是前面已經建立好了的,但是隻有使用者編號和姓名的資料,可以看成是一個“虛”的物件,直到通過代理把所有的資料都設定好,才算是一個完整的使用者資料物件。

(4)copy-on-write

       拷貝一個大的物件是很消耗資源的,如果這個被拷貝的物件從上次操作以來,根本就沒有被修改過,那麼再拷貝這個物件是沒有必要的,白白消耗資源而已。那麼就可以使用代理來延遲拷貝的過程,可以等到物件被修改的時候才真的對它進行拷貝。

       copy-on-write可以大大降低拷貝大物件的開銷,因此它算是一種優化方式,可以根據需要來拷貝或者克隆物件。

(5)具體目標和代理的關係

       從代理模式的結構圖來看,好像是有一個具體目標類就有一個代理類,其實不是這樣的。如果代理類能完全通過介面來操作它所代理的目標物件,那麼代理物件就不需要知道具體的目標物件,這樣就無須為每一個具體目標類都建立一個代理類了。

       但是,如果代理類必須要例項化它代理的目標物件,那麼代理類就必須知道具體被代理的物件,這種情況下,一個具體目標類通常會有一個代理類。這種情況多出現在虛代理的實現裡面。

(6)代理模式呼叫順序示意圖

代理模式呼叫順序如圖11.4所示:

 

圖11.4  代理模式呼叫順序示意圖

11.3.2  保護代理

       保護代理是一種控制對原始物件訪問的代理,多用於物件應該有不同的訪問許可權的時候。保護代理會檢查呼叫者是否具有請求所必需的訪問許可權,如果沒有相應的許可權,那麼就不會呼叫目標物件,從而實現對目標物件的保護。

       還是通過一個示例來說明。

1:示例需求

現在有一個訂單系統,要求是:一旦訂單被建立,只有訂單的建立人才可以修改訂單中的資料,其他人不能修改。

相當於現在如果有了一個訂單物件例項,那麼就需要控制外部對它的訪問,滿足條件的可以訪問,而不滿足條件的就不能訪問了。

2:示例實現

(1)訂單物件的介面定義

要實現這個功能需要,先來定義訂單物件的介面,很簡單,主要是對訂單物件的屬性的getter/setter方法,示例程式碼如下:

/**

 * 訂單物件的介面定義

 */

public interface OrderApi {

    /**

     * 獲取訂單訂購的產品名稱

     * @return 訂單訂購的產品名稱

     */

    public String getProductName();

    /**

     * 設定訂單訂購的產品名稱

     * @param productName 訂單訂購的產品名稱

     * @param user 操作人員

     */

    public void setProductName(String productName,String user);

    /**

     * 獲取訂單訂購的數量

     * @return 訂單訂購的數量

     */

    public int getOrderNum();

    /**

     * 設定訂單訂購的數量

     * @param orderNum 訂單訂購的數量

     * @param user 操作人員

     */

    public void setOrderNum(int orderNum,String user);

    /**

     * 獲取建立訂單的人員

     * @return 建立訂單的人員

     */

    public String getOrderUser();

    /**

     * 設定建立訂單的人員

     * @param orderUser 建立訂單的人員

     * @param user 操作人員

     */

    public void setOrderUser(String orderUser,String user);

}

(2)訂單物件

接下來定義訂單物件,原本訂單物件需要描述的屬性很多,為了簡單,只描述三個就好了,示例程式碼如下:

/**

 * 訂單物件

 */

public class Order implements OrderApi{

    /**

     * 訂單訂購的產品名稱

     */

    private String productName;

    /**

     * 訂單訂購的數量

     */

    private int orderNum;

    /**

     * 建立訂單的人員

     */

    private String orderUser;

    /**

     * 構造方法,傳入構建需要的資料

     * @param productName 訂單訂購的產品名稱

     * @param orderNum 訂單訂購的數量

     * @param orderUser 建立訂單的人員

     */

    public Order(String productName,int orderNum,String orderUser){

       this.productName = productName;

       this.orderNum = orderNum;

       this.orderUser = orderUser;

    }

    public String getProductName() {

       return productName;

    }

    public void setProductName(String productName,String user) {

       this.productName = productName;

    }

    public int getOrderNum() {

       return orderNum;

    }

    public void setOrderNum(int orderNum,String user) {

       this.orderNum = orderNum;

    }

    public String getOrderUser() {

       return orderUser;

    }

    public void setOrderUser(String orderUser,String user) {

       this.orderUser = orderUser;

    }

}

(3)訂單物件的代理

建立好了訂單物件,需要建立對它的代理物件了。既然訂單代理就相當於一個訂單,那麼最自然的方式就是讓訂單代理跟訂單物件實現一樣的介面;要控制對訂單setter方法的訪問,那麼就需要在代理的方法裡面進行許可權判斷,有許可權就呼叫訂單物件的方法,沒有許可權就提示錯誤並返回。示例程式碼如下:

/**

 * 訂單的代理物件

 */

public class OrderProxy implements OrderApi{

    /**

     * 持有被代理的具體的目標物件

     */

    private Order order=null;

    /**

     * 構造方法,傳入被代理的具體的目標物件

     * @param realSubject 被代理的具體的目標物件

     */

    public OrderProxy(Order realSubject){

       this.order = realSubject;

    }

    public void setProductName(String productName,String user) {

       //控制訪問許可權,只有建立訂單的人員才能夠修改

       if(user!=null && user.equals(this.getOrderUser())){

           order.setProductName(productName, user);

       }else{

           System.out.println("對不起"+user

+",您無權修改訂單中的產品名稱。");

       }

    }

    public void setOrderNum(int orderNum,String user) {

       //控制訪問許可權,只有建立訂單的人員才能夠修改

       if(user!=null && user.equals(this.getOrderUser())){

           order.setOrderNum(orderNum, user);

       }else{

           System.out.println("對不起"+user

+",您無權修改訂單中的訂購數量。");

       }

    }

    public void setOrderUser(String orderUser,String user) {

       //控制訪問許可權,只有建立訂單的人員才能夠修改

       if(user!=null && user.equals(this.getOrderUser())){

           order.setOrderUser(orderUser, user);

       }else{

           System.out.println("對不起"+user

+",您無權修改訂單中的訂購人。");

       }

    }

    public int getOrderNum() {

       return this.order.getOrderNum();

    }

    public String getOrderUser() {

       return this.order.getOrderUser();

    }

    public String getProductName() {

       return this.order.getProductName();

    }

    public String toString(){

       return "productName="+this.getProductName()+",orderNum="

+this.getOrderNum()+",orderUser="+this.getOrderUser();

    }

}

(4)測試程式碼

一起來看看如何使用剛剛完成的訂單代理,示例程式碼如下:

public class Client {

    public static void main(String[] args) {

       //張三先登入系統建立了一個訂單

       OrderApi order = new OrderProxy(

new Order("設計模式",100,"張三"));

       //李四想要來修改,那就會報錯

       order.setOrderNum(123, "李四");

       //輸出order

       System.out.println("李四修改後訂單記錄沒有變化:"+order);

       //張三修改就不會有問題

       order.setOrderNum(123, "張三");

       //再次輸出order

       System.out.println("張三修改後,訂單記錄:"+order);

    }

}

       執行結果如下:

對不起李四,您無權修改訂單中的訂購數量。

李四修改後訂單記錄沒有變化:

productName=設計模式,orderNum=100,orderUser=張三

張三修改後,訂單記錄:productName=設計模式,orderNum=123,orderUser=張三

       從上面的執行結果就可以看出,在通過代理轉調目標物件的時候,在代理物件裡面,對訪問的使用者進行了許可權判斷,如果不滿足要求,就不會轉調目標物件的方法,從而保護了目標物件的方法,只讓有許可權的人操作。

11.3.3  Java中的代理

       Java對代理模式提供了內建的支援,在java.lang.reflect包下面,提供了一個Proxy的類和一個InvocationHandler的介面。

       通常把前面自己實現的代理模式,稱為Java的靜態代理。這種實現方式有一個較大的缺點,就是如果Subject介面發生變化,那麼代理類和具體的目標實現都要變化,不是很靈活,而使用Java內建的對代理模式支援的功能來實現則沒有這個問題。

       通常把使用Java內建的對代理模式支援的功能來實現的代理稱為Java的動態代理。動態代理跟靜態代理相比,明顯的變化是:靜態代理實現的時候,在Subject介面上定義很多的方法,代理類裡面自然也要實現很多方法;而動態代理實現的時候,雖然Subject介面上定義了很多方法,但是動態代理類始終只有一個invoke方法。這樣當Subject介面發生變化的時候,動態代理的介面就不需要跟著變化了。

       Java的動態代理目前只能代理介面,基本的實現是依靠Java的反射機制和動態生成class的技術,來動態生成被代理的介面的實現物件。具體的內部實現細節這裡不去討論。如果要實現類的代理,可以使用cglib(一個開源的Code Generation Library)。

       還是來看看示例,那就修改上面保護代理的示例,看看如何使用Java的動態代理來實現同樣的功能。

(1)訂單介面的定義是完全一樣的,就不去贅述了。

(2)訂單物件的實現,只是添加了一個toString,以方便測試輸出,這裡也不去示例了。在前面的示例中,toString是實現在代理類裡面了。

(3)直接看看代理類的實現,大致有如下變化:

  • 要實現InvocationHandler介面
  • 需要提供一個方法來實現:把具體的目標物件和動態代理繫結起來,並在繫結好過後,返回被代理的目標物件的介面,以利於客戶端的操作
  • 需要實現invoke方法,在這個方法裡面,去具體判斷當前是在呼叫什麼方法,需要如何處理。

示例程式碼如下:

/**

 * 使用Java中的動態代理

 */

public class DynamicProxy implements InvocationHandler{

    /**

     * 被代理的物件

     */

    private OrderApi order = null;

    /**

     * 獲取繫結好代理和具體目標物件後的目標物件的介面

     * @param order 具體的訂單物件,相當於具體目標物件

     * @return 繫結好代理和具體目標物件後的目標物件的介面

     */

    public OrderApi getProxyInterface(Order order){

       //設定被代理的物件,好方便invoke裡面的操作

       this.order = order;

       //把真正的訂單物件和動態代理關聯起來

       OrderApi orderApi = (OrderApi) Proxy.newProxyInstance(

              order.getClass().getClassLoader(),

              order.getClass().getInterfaces(),

              this);

       return orderApi;

    }

    public Object invoke(Object proxy, Method method, Object[] args)

           throws Throwable {

       //如果是呼叫setter方法就需要檢查許可權

       if(method.getName().startsWith("set")){

           //如果不是建立人,那就不能修改

           if(order.getOrderUser()!=null

&& order.getOrderUser().equals(args[1])){

              //可以操作

              return method.invoke(order, args);

           }else{

              System.out.println("對不起,"+args[1]

+",您無權修改本訂單中的資料");

           }

       }else{

           //不是呼叫的setter方法就繼續執行

           return method.invoke(order, args);

       }

       return null;

    }

}

要看明白上面的實現,需要熟悉Java反射的知識,這裡就不去展開了。

(4)看看現在的客戶端如何使用這個動態代理,示例程式碼如下:

public class Client {

    public static void main(String[] args) {

       //張三先登入系統建立了一個訂單

       Order order = new Order("設計模式",100,"張三");

       //建立一個動態代理

       DynamicProxy dynamicProxy = new DynamicProxy();     

       //然後把訂單和動態代理關聯起來

       OrderApi orderApi = dynamicProxy.getProxyInterface(order);

       //以下就需要使用被代理過的介面來操作了

       //李四想要來修改,那就會報錯

       orderApi.setOrderNum(123, "李四");

       //輸出order

       System.out.println("李四修改後訂單記錄沒有變化:"+orderApi);

       //張三修改就不會有問題

       orderApi.setOrderNum(123, "張三");

       //再次輸出order

       System.out.println("張三修改後,訂單記錄:"+orderApi);

    }

}

執行結果如下:

對不起,李四,您無權修改本訂單中的資料

李四修改後訂單記錄沒有變化:

productName=設計模式,orderNum=100,orderUser=張三

張三修改後,訂單記錄:productName=設計模式,orderNum=123,orderUser=張三

       執行的結果跟前面完全由自己實現的代理模式是一樣的。

       事實上,Java的動態代理還是實現AOP(面向方面程式設計)的一個重要手段,AOP的知識這裡暫時不去講述,大家先了解這一點就可以了。

11.3.4  代理模式的優缺點

       代理模式在客戶和被客戶訪問的物件之間,引入了一定程度的間接性,客戶是直接使用代理,讓代理來與被訪問的物件進行互動。不同的代理型別,這種附加的間接性有不同的用途,也就是有不同的特點:

  • 遠端代理:隱藏了一個物件存在於不同的地址空間的事實,也即是客戶通過遠端代理去訪問一個物件,根本就不關心這個物件在哪裡,也不關心如何通過網路去訪問到這個物件,從客戶的角度來講,它只是在使用代理物件而已。
  • 虛代理:可以根據需要來建立“大”物件,只有到必須建立物件的時候,虛代理才會建立物件,從而大大加快程式執行速度,並節省資源。通過虛代理可以對系統進行優化。
  • 保護代理:可以在訪問一個物件的前後,執行很多附加的操作,除了進行許可權控制之外,還可以進行很多跟業務相關的處理,而不需要修改被代理的物件。也就是說,可以通過代理來給目標物件增加功能。
  • 智慧指引:跟保護代理類似,也是允許在訪問一個物件的前後,執行很多附加的操作,這樣一來就可以做很多額外的事情,比如:引用計數等。

11.3.5  思考代理模式

1:代理模式的本質

       代理模式的本質:控制物件訪問

       代理模式通過代理目標物件,把代理物件插入到客戶和目標物件之間,從而為客戶和目標物件引入一定的間接性,正是這個間接性,給了代理物件很多的活動空間,代理物件可以在呼叫具體的目標物件前後,附加很多操作,從而實現新的功能或是擴充套件目標物件的功能,更狠的是,代理物件還可以不去建立和呼叫目標物件,也就是說,目標物件被完全代理掉了,或是被替換掉了。

       從實現上看,代理模式主要是使用物件的組合和委託,尤其是在靜態代理的實現裡面,會看得更清楚。但是也可以採用物件繼承的方式來實現代理,這種實現方式在某些情況下,比使用物件組合還要來得簡單。

舉個例子來說明一下,改造11.3.2保護代理的例子來說明。

(1)首先就是去掉OrderApi,現在改成繼承的方式實現代理,不再需要公共的介面了

(2)Order物件變化不大,只是去掉實現的OrderApi介面就好了,示例程式碼如下:

public class Order implements OrderApi {

    //其它的程式碼沒有任何變化,就不去贅述了

}

(3)再看看代理的實現,變化較多,大致有如下的變化:

  • 不再實現OrderApi,而改成繼承Order
  • 不需要再持有目標物件了,因為這個時候父類就是被代理的物件
  • 原來的構造方法去掉,重新實現一個傳入父類需要的資料的構造方法
  • 原來轉調目標物件的方法,現在變成呼叫父類的方法了,用super關鍵字
  • 除了幾個被保護代理的setter方法外,不再需要getter方法了

示例程式碼如下:

/**

 * 訂單的代理物件

 */

public class OrderProxy extends Order{

    public OrderProxy(String productName

,int orderNum,String orderUser){

       super(productName,orderNum,orderUser);

    }  

    public void setProductName(String productName,String user) {

       //控制訪問許可權,只有建立訂單的人員才能夠修改

       if(user!=null && user.equals(this.getOrderUser())){

           super.setProductName(productName, user);

       }else{

           System.out.println("對不起"+user

+",您無權修改訂單中的產品名稱。");

       }

    }

    public void setOrderNum(int orderNum,String user) {

       //控制訪問許可權,只有建立訂單的人員才能夠修改

       if(user!=null && user.equals(this.getOrderUser())){

           super.setOrderNum(orderNum, user);

       }else{

           System.out.println("對不起"+user

+",您無權修改訂單中的訂購數量。");

       }

    }

    public void setOrderUser(String orderUser,String user) {

       //控制訪問許可權,只有建立訂單的人員才能夠修改

       if(user!=null && user.equals(this.getOrderUser())){

           super.setOrderUser(orderUser, user);

       }else{

           System.out.println("對不起"+user

+",您無權修改訂單中的訂購人。");

       }

    }

    public String toString(){

       return "productName="+this.getProductName()+",orderNum="

+this.getOrderNum()+",orderUser="+this.getOrderUser();

    }

}

(4)客戶端的變化不大,主要是不再直接面向OrderApi介面,而是使用Order物件了,另外建立代理的構造方法也發生了變化,示例程式碼如下:

public class Client {

    public static void main(String[] args) {

       //張三先登入系統建立了一個訂單

       Order order = new OrderProxy("設計模式",100,"張三");

       //李四想要來修改,那就會報錯

       order.setOrderNum(123, "李四");

       //輸出order

       System.out.println("李四修改後訂單記錄沒有變化:"+order);

       //張三修改就不會有問題

       order.setOrderNum(123, "張三");

       //再次輸出order

       System.out.println("張三修改後,訂單記錄:"+order);

    }

}

       去執行一下,測試看看,體會一下這種實現方式。

2:何時選用代理模式

建議在如下情況中,選用代理模式:

  • 需要為一個物件在不同的地址空間提供區域性代表的時候,可以使用遠端代理
  • 需要按照需要建立開銷很大的物件的時候,可以使用虛代理
  • 需要控制對原始物件的訪問的時候,可以使用保護代理
  • 需要在訪問物件的時候執行一些附加操作的時候,可以使用智慧指引代理

11.3.6  相關模式

l          代理模式和介面卡模式
    這兩個模式有一定的相似性,但也有差異。
    這兩個模式有相似性,它們都為另一個物件提供間接性的訪問,而且都是從自身以外的一個介面向這個物件轉發請求。
    但是從功能上,兩個模式是不一樣的。介面卡模式主要用來解決介面之間不匹配的問題,它通常是為所適配的物件提供一個不同的介面;而代理模式會實現和目標物件相同的介面。

l          代理模式和裝飾模式
    這兩個模式從實現上相似,但是功能上是不同的。
    裝飾模式的實現和保護代理的實現上是類似的,都是在轉調其它物件的前後執行一定的功能。但是它們的目的和功能都是不同的。
    裝飾模式的目的是為了讓你不生成子類就可以給物件新增職責,也就是為了動態的增加功能;而代理模式的主要目的是控制對物件的訪問。


---------------------------------------------------------------------------

研磨設計討論群【252780326】

---------------------------------------------------------------------------