研磨設計模式 之 代理模式(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】
---------------------------------------------------------------------------