設計模式之【代理模式】
設計原則是指導我們程式碼設計的一些經驗總結,也就是「心法」;面向對象就是我們的「武器」;設計模式就是「招式」。
以心法為基礎,以武器運用招式應對複雜的編程問題。
我:妹啊,怎麼我看你最近都很忙的樣子?不是換了個輕鬆點的工作嘛?
表妹:是啊,工作是輕鬆點了,但是上下班太遠了,想買輛二手車代步,所以我現在在做功課。
我:我看看…哇,還挺認真的,花了不少時間吧?
表妹:是啊,以前對這方面了解比較少,所以現在要做足功課才敢去買。
我:我有個同學是做這方面代理的,我讓他幫你搞定。
表妹:哇,這樣我就省事多啦~
你看,這不就是我們設計模式中的【代理模式】嘛?
為什麼要用代理模式?
-
中介隔離作用
在一些情況下,一個客戶類不想或者不能直接引用一個委託對象,而代理類對象可以在客戶類和委託對象之間起到中介作用,其特徵是代理類和委託類實現相同的介面。
-
不違背開閉原則的前提下,增加功能
代理類除了是客戶類和委託類的中介外,我們還可以通過代理類增加額外的功能來擴展委託類的功能,這樣做我們只需要修改代理類而不需要再修改委託類,符合程式碼設計的開閉原則。代理類主要負責為委託類預處理消息,過濾消息、把消息轉發給委託類,以及事後對返回結果的處理等。
代理類並不真正實現服務,而是通過調用委託類的相關方法,來提供特定的服務。真正的業務功能還是由委託類來實現,但是可以在業務功能執行的前後加入一些公關的服務。例如加入快取、日誌等功能,我們就可以使用代理類來完成,而沒必要修改已經封裝好的委託類。
按照代理創建的日期來進行分類的話,可以分為靜態代理和動態代理。
靜態代理:由程式設計師創建或特定工具自動生成源程式碼,再對其進行編譯。在編譯運行之前,代理類.class文件就已經被創建,代理類和委託類的關係在運行前就確定。
動態代理:動態代理類的源碼是在程式運行期間由JVM根據反射等機制動態的生成,所以不存在代理類的位元組碼文件。代理類和委託類的關係是在程式運行時確定的。
靜態代理是如何實現的?
代理實現的前提是,如何能夠獲取到委託類的調用?答案是有組合和繼承兩種方式。
組合:與委託類實現相同的介面,然後向代理類傳入委託類的引用從而調用到目標函數。
繼承:繼承委託類,重寫目標函數,然後通過super調用到目標函數。
我們先來看看用組合方式實現的。就舉表妹買二手車的例子來說。
第一步:創建買車介面:
1 public interface IBuyCar { 2 void buyCar() 3 }
第二步:表妹要買車,實現買車的介面
1 public class BiaoMeiBuyCar implements IBuyCar { 2 @Override 3 public void buyCar() { 4 System.out.println("交易二手車"); 5 } 6 }
第三步:找代理來幫表妹完成這件事
1 public class BiaoMeiBuyCarProxy implements IBuyCar { 2 private IBuyCar buyCar; 3 4 public BiaoMeiBuyCarProxy (final IBuyCar buyCar) { 5 this.buyCar = buyCar; 6 } 7 8 @Override 9 public void buyCar() { 10 // 代理除了幫忙交易外,還幫忙辦理前後手續 11 System.out.println("交易前手續辦理"); 12 buyCar.buyCar(); // 交易 13 System.out.println("交易後手續辦理"); 14 } 15 }
1 IBuyCar buyCar = new BiaoMeiBuyCarProxy (new BiaoMeiBuyCar());
因為委託類和代理類實現相同的介面,是基於介面而非實現編程,所以,將BiaoMeiBuyCar類對象替換為BiaoMeiBuyCarProxy類對象,不需要改動太多程式碼。
大家發現沒有,基於組合方式的靜態代理模式跟裝飾器模式很像。
是的,對於裝飾器模式來說,裝飾者和被裝飾者都實現一個介面;對代理模式來說,代理類和委託類也都實現同一個介面。不論我們使用哪一種模式,都可以很容易地在真實對象的方法前面或後面加上自定義的方法。
這裡我們先簡單看一下兩者的區別,另外一篇我們再仔細分析這兩種設計模式的區別哈。
代理模式注重的是對對象的某一功能的流程把控和輔助,它可以控制對象做某些事,重點是為了借用對象的功能完成某一流程,而非對象功能如何。
裝飾器模式注重的是對對象功能的擴展,不關心外界如何調用,只注重對對象功能加強,裝飾後還是對象本身。
但是,如果委託類並沒有定義介面,並且委託類程式碼並不是我們開發維護的(比如,它來自一個第三方的類庫),我們也沒辦法直接修改原始類,給它重新定義一個介面。在這種情況下,我們該如何實現靜態代理模式呢?
對於這種情況,我們一般採用繼承的方式。讓代理類繼承委託類,然後擴展附加功能。
1 public class BiaoMeiBuyCar { 2 public void buyCar() { 3 System.out.println("交易二手車"); 4 } 5 } 6 • 7 public class BiaoMeiBuyCarProxy extends BiaoMeiBuyCar { 8 public void buyCar() { 9 // 代理除了幫忙交易外,還替表妹辦理好前後手續 10 System.out.println("交易前手續辦理"); 11 super.buyCar(); // 交易 12 System.out.println("交易後手續辦理"); 13 } 14 } 15 • 16 public class Demo { 17 public static void main(String[] args) { 18 // 1、找代理對象 19 BiaoMeiBuyCar biaomeiBuyCarProxy = new BiaoMeiBuyCarProxy(); 20 // 2、代理負責幫表妹搞定一切 21 biaomeiBuyCarProxy .buyCar(); 22 } 23 }
你看,不管是基於組合方式還是基於繼承方式的靜態代理模式,一方面,都需要在代理類中,將原始類中的所有方法,都重新實現一遍,並且為每個方法都附加相似的程式碼邏輯。另一方面,如果要添加附加功能的類不止一個,我們就需要針對每個類都創建一個代理類。那必然會增加類的個數,增加了程式碼維護成本。而且,每個代理類中的程式碼都有點像模板式的「重複」程式碼,也增加了不必要的開發成本。
這時候,動態代理就派上用場了。
動態代理
在動態代理中,我們不再需要手動創建代理類,只需要編寫一個動態處理器就可以了。
JDK動態代理
真正的代理對象由JDK在運行時幫我們動態的創建。
第一步:創建買車介面:
1 public interface IBuyCar { 2 void buyCar() 3 }
第二步:表妹要買車,實現買車的介面
1 public class BiaoMeiBuyCar implements IBuyCar { 2 @Override 3 public void buyCar() { 4 System.out.println("交易二手車"); 5 } 6 }
第三步:實現動態處理器
1 import java.lang.reflect.InvocationHandler; 2 import java.lang.reflect.Method; 3 4 public class DynamicProxyHandler implements InvocationHandler { 5 private Object object; 6 7 public DynamicProxyHandler(final Object object) { 8 this.object = object; 9 } 10 11 @Override 12 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { 13 System.out.println("交易前手續辦理"); 14 Object result = method.invoke(object, args); 15 System.out.println("交易後手續辦理"); 16 return result; 17 } 18 }
1 public class Demo { 2 public static void main(String[] args) { 3 IBuyCar biaomeiBuyCar = new BiaoMeiBuyCar(); 4 IBuyCar buyCarProxy = (IBuyCar)Proxy.newProxyInstance(IBuyCar.class.getClassLoader(), new Class[]{IBuyCar.class}, new DynamicProxyHandler(biaomeiBuyCar)); 5 buyCarProxy.buyCar(); 6 } 7 }
但是,JDK實現動態代理需要實現類通過介面定義業務方法,對於沒有介面的類,就需要CGLib了。
CGLIB動態代理
CGLib採用了非常底層的位元組碼技術,其原理是通過位元組碼技術為一個類創建子類,並在子類中採用方法攔截的技術,攔截所有父類方法的調用,順勢織入橫切邏輯。
但因為採用的是繼承,所以不能對fianl修飾的類進行代理。
第一步:創建CGLib代理類。
1 package wei.proxy.impl; 2 3 import net.sf.cglib.proxy.Enhancer; 4 import net.sf.cglib.proxy.MethodInterceptor; 5 import net.sf.cglib.proxy.MethodProxy; 6 7 import java.lang.reflect.Method; 8 9 public class CglibProxy implements MethodInterceptor { 10 private Object target; 11 public Object getInstance(final Object target) { 12 this.target = target; 13 Enhancer enhancer = new Enhancer(); 14 enhancer.setSuperclass(this.target.getClass()); 15 enhancer.setCallback(this); 16 return enhancer.create(); 17 } 18 19 public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { 20 System.out.println("交易前手續辦理"); 21 Object result = methodProxy.invokeSuper(object, args); 22 System.out.println("交易後手續辦理"); 23 return result; 24 } 25 }
1 public class Demo { 2 public static void main(String[] args){ 3 IBuyCar biaomeiBuyCar = new BiaoMeiBuyCar(); 4 CglibProxy cglibProxy = new CglibProxy(); 5 BiaoMeiBuyCar buyCarCglibProxy = (BiaoMeiBuyCar) cglibProxy.getInstance(biaomeiBuyCar); 6 buyCarCglibProxy.buyCar(); 7 } 8 }
代理模式的優點
1、代理模式能將代理對象與真實被調用的目標對象隔離;
2、一定程度上降低了系統的耦合度,擴展性好;
3、可以起到保護目標對象的作用;
4、可以對目標對象的功能增強;
代理模式的缺點
1、代理模式會造成系統設計中類的數量增加;
2、在客戶端與目標對象之間增加一個代理對象,會造成請求處理速度變慢;
3、增加了系統的複雜度。
代理模式的應用場景
代理模式常用在業務系統中開發一些非功能性需求,比如:監控、統計、鑒權、限流、事務、冪等、日誌。
我們將這些附加功能與業務功能解耦,放到代理類統一處理,讓程式設計師只需要關注業務方面的開發。
除此之外,代理模式還可以用在RPC、快取等應用場景,
總結
代理模式其實就是在訪問對象時,引入一定程度的間接性,因為這種間接性,可以附加多種用途。
代理就是真實對象的代表。
參考資料
//www.cnblogs.com/yanggb/p/10952843.html
極客時間專欄《設計模式之美》
//www.cnblogs.com/daniels/p/8242592.html