設(shè)計(jì)模式詳解之代理模式

代理模式是我們使用率比較高的一個(gè)模式耳高。它的定義是為其他對(duì)象提供一種代理以控制對(duì)這個(gè)對(duì)象的訪問(wèn)癞蚕。

如果只是從定義上來(lái)看,可能無(wú)法理解著蛙。為什么要用代理來(lái)對(duì)這個(gè)對(duì)象來(lái)訪問(wèn)删铃,我直接訪問(wèn)不行嗎?行踏堡,當(dāng)然行了猎唁。但是我們使用代理自然是有代理的優(yōu)勢(shì),我們舉個(gè)簡(jiǎn)單例子來(lái)說(shuō)明一下顷蟆。

有一個(gè)房東诫隅,他有一座房子要出售。但是房子買(mǎi)賣(mài)呢帐偎,我們知道逐纬,需要讓買(mǎi)房的人來(lái)看房,有意向之后還需要和顧客簽訂一系列的合同肮街,實(shí)在復(fù)雜风题。我們的房東比較懶,他不想做那么多事情,他只想單純賣(mài)房子沛硅。于是他找了一個(gè)中介眼刃,由中介來(lái)代替房東做這些事情。這個(gè)中介就可以說(shuō)是我們的代理摇肌。如果覺(jué)得還是覺(jué)得抽象的話擂红,下面我們會(huì)用實(shí)際開(kāi)發(fā)代碼來(lái)演示代理模式,深入理解围小。

代理模式的三個(gè)重要角色:

Subject抽象主題角色

  • 抽象主題類(lèi)可以是抽象類(lèi)也可以是接口昵骤,是一個(gè)最普通的業(yè)務(wù)類(lèi)型定義,無(wú)特殊要求肯适。

RealSubject具體主題角色

  • 被委托角色变秦,被代理角色。也就是我們的房東

Proxy代理主題角色

  • 也叫做委托類(lèi)框舔、代理類(lèi)蹦玫。它負(fù)責(zé)對(duì)真實(shí)角色的應(yīng)用,把所有抽象主題類(lèi)定義的方法限制委托給真實(shí)主題角色實(shí)現(xiàn)刘绣,并且在真實(shí)主題角色處理完畢前后做預(yù)處理和善后處理工作樱溉。(我們的中介)

關(guān)于我們的代理模式大致可以分成兩種,靜態(tài)代理模式和動(dòng)態(tài)代理模式纬凤。下面我們會(huì)用代碼來(lái)分別演示一下兩種代理的區(qū)別福贞。

靜態(tài)代理

靜態(tài)代理呢,就是我們的每一個(gè)具體主題角色都有自己專(zhuān)門(mén)的代理角色停士。我們用代碼來(lái)說(shuō)吧挖帘,我們開(kāi)發(fā)業(yè)務(wù)的時(shí)候,需要抽象service接口和具體實(shí)現(xiàn)向瓷。我們模擬一個(gè)增刪改查肠套。

package proxydemo;

public interface Service {
    
    //比如我們的業(yè)務(wù)有這四個(gè)方法
    
    void add();
    void delete();
    void update();
    void query();
    
}

然后就是我們?nèi)?shí)現(xiàn)這個(gè)接口

package proxydemo;

public class ServiceImpl implements Service {
    @Override
    public void add() {
        System.out.println("增加操作");
    }

    @Override
    public void delete() {
        System.out.println("刪除操作");
    }

    @Override
    public void update() {
        System.out.println("更新操作");
    }

    @Override
    public void query() {
        System.out.println("查詢操作");
    }
}

這個(gè)service接口就可以看作是我們的抽象主題角色,具體實(shí)現(xiàn)類(lèi)就是我們的具體主題角色猖任。我們可以在客戶端使用

package proxydemo;

public class Client {
    public static void main(String[] args) {
        ServiceImpl service = new ServiceImpl();
        service.add();
        service.delete();
        service.update();
        service.query();
    }
}

|----------控制臺(tái)輸出--------|
增加操作
刪除操作
更新操作
查詢操作

Process finished with exit code 0 

但是如果這個(gè)時(shí)候來(lái)了一個(gè)需求你稚,說(shuō)要在我們使用方法后,日志能夠記錄下來(lái)是進(jìn)行了什么操作朱躺。那我們?nèi)菀紫氲降木褪窃谖覀兊膶?shí)現(xiàn)類(lèi)去修改刁赖,在使用后方法后用日志記錄下來(lái)。但是我們?cè)陂_(kāi)發(fā)中最忌諱就是修改原有的代碼长搀,因?yàn)槲覀兪且祥_(kāi)閉原則的宇弛。所有我們就可以用代理模式來(lái)

package proxydemo;

//我們的代理角色,在不修改原來(lái)的代碼情況下源请,擴(kuò)展了日志的功能
public class ProxyServiceImpl implements Service{

    //使用組合模式代替繼承
    private ServiceImpl service;

    public void setService(ServiceImpl service) {
        this.service = service;
    }

    @Override
    public void add() {
        System.out.println("日志輸出:使用add方法");
        service.add();
    }

    @Override
    public void delete() {
        System.out.println("日志輸出:使用delete方法");
        service.delete();
    }

    @Override
    public void update() {
        System.out.println("日志輸出:使用update方法");
        service.update();
    }

    @Override
    public void query() {
        System.out.println("日志輸出:使用query方法");
        service.query();
    }
}

然后在我們的客戶端使用的時(shí)候枪芒,只需要給代理角色設(shè)置需要被代理的角色就行

package proxydemo;

public class Client {
    public static void main(String[] args) {
        //實(shí)例化我們?cè)瓉?lái)的功能
        ServiceImpl service = new ServiceImpl();
        //實(shí)例我們的代理角色
        ProxyServiceImpl proxyService = new ProxyServiceImpl();
        //設(shè)置需要被代理的角色
        proxyService.setService(service);
        //使用有日志功能的代理方法
        proxyService.add();
        proxyService.delete();
        proxyService.update();
        proxyService.query();
    }
}



|----------控制臺(tái)輸出--------|
日志輸出:使用add方法
增加操作
日志輸出:使用delete方法
刪除操作
日志輸出:使用update方法
更新操作
日志輸出:使用query方法
查詢操作

Process finished with exit code 0

靜態(tài)代理總結(jié)

好處:

  • 可以使真實(shí)角色的操作更加純粹彻况!不用去關(guān)注一些公共的業(yè)務(wù)
  • 一些非核心的功能交給了代理角色,實(shí)現(xiàn)了業(yè)務(wù)的分工
  • 公共業(yè)務(wù)擴(kuò)展的時(shí)候舅踪,方便集中管理

缺點(diǎn):

  • 上面也說(shuō)了纽甘,每一個(gè)真實(shí)角色就會(huì)產(chǎn)生一個(gè)代理角色,代碼量翻倍

動(dòng)態(tài)代理

動(dòng)態(tài)代理就是為了解決我們上面那個(gè)代碼量大的解決問(wèn)題抽碌,也是我們真正在開(kāi)發(fā)中會(huì)去使用的代理模式悍赢。我們的動(dòng)態(tài)代理有兩種方式去實(shí)現(xiàn),一個(gè)是JDK動(dòng)態(tài)代理(面向接口)货徙,一個(gè)是CGlib動(dòng)態(tài)代理(面向類(lèi))左权。我們這里主要是介紹概念,所以就用JDK動(dòng)態(tài)代理來(lái)實(shí)現(xiàn)我們上面的例子痴颊,另一種的使用可以看這里(動(dòng)態(tài)代理的兩種方式以及區(qū)別

(補(bǔ)充:現(xiàn)在也出了Javassit去實(shí)現(xiàn)動(dòng)態(tài)代理)

關(guān)于JDK的動(dòng)態(tài)代理赏迟,我們?cè)?code>java.lang.reflect包下,有這么一個(gè)接口

Interface InvocationHandler

InvocationHandler是由代理實(shí)例的調(diào)用處理程序實(shí)現(xiàn)的接口 蠢棱。

每個(gè)代理實(shí)例都有一個(gè)關(guān)聯(lián)的調(diào)用處理程序瀑梗。 當(dāng)在代理實(shí)例上調(diào)用方法時(shí),方法調(diào)用將被編碼并分派到其調(diào)用處理程序的invoke方法裳扯。

還有一個(gè)Proxy類(lèi),它提供了創(chuàng)建動(dòng)態(tài)代理類(lèi)和實(shí)例的靜態(tài)方法谤职,它也是由這些方法創(chuàng)建的所有動(dòng)態(tài)代理類(lèi)的超類(lèi)饰豺。

可以通過(guò)反射來(lái)創(chuàng)建代理

  Foo f = (Foo) Proxy.newProxyInstance(Foo.class.getClassLoader(),
                                          new Class<?>[] { Foo.class },
                                          handler); 

話不多說(shuō),我們用代碼了實(shí)現(xiàn)一下更容易明白這些抽象的解釋允蜈。

例子還是調(diào)用我們的上面的例子冤吨,只不過(guò)這個(gè)時(shí)候,我們的代理類(lèi)不需要去實(shí)現(xiàn)了饶套,我們實(shí)現(xiàn)InvocationHandler接口寫(xiě)調(diào)用程序就行了漩蟆。

package proxydemo;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyInvocationHandler implements InvocationHandler {

    //被代理的接口
    public Object target;

    public void setTarget(Object object) {
        this.target = object;
    }


    //通過(guò)反射生成得到代理類(lèi)
    public Object getProxy(){
        //三個(gè)參數(shù),當(dāng)前的類(lèi)加載器妓蛮,被代理的接口怠李,以及一個(gè)InvocationHandler,當(dāng)前即可
        return Proxy.newProxyInstance
                (this.getClass().getClassLoader(),
                        target.getClass().getInterfaces(),this);
    }

    //處理代理實(shí)例蛤克,返回結(jié)果
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //通過(guò)反射來(lái)調(diào)用方法
        Object result = method.invoke(target,args);
        return result;
    }
}

我們?cè)谖覀兊目蛻舳耸褂玫臅r(shí)候捺癞,需要去實(shí)例化一個(gè)InvocationHandler就行了。

package proxydemo;

public class Client {
    public static void main(String[] args) {
        //實(shí)例一個(gè)我們的需要被代理的角色
        ServiceImpl service = new ServiceImpl();
        //實(shí)例我們的代理調(diào)用處理
        ProxyInvocationHandler pih = new ProxyInvocationHandler();
        //設(shè)置被代理的角色
        pih.setTarget(service);
        //通過(guò)反射了來(lái)實(shí)例我們的代理類(lèi)
        Service proxyService = (Service) pih.getProxy();
        proxyService.add();
        proxyService.delete();
        proxyService.update();
        proxyService.query();
    }
}

|----------控制臺(tái)輸出--------|
增加操作
刪除操作
更新操作
查詢操作

Process finished with exit code 0

具體的調(diào)用過(guò)程就是

這個(gè)時(shí)候如果我們要增加一個(gè)日志輸出功能的話构挤,只需要在我們的ProxyInvocationHandler里面增加就行髓介。

package proxydemo;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class ProxyInvocationHandler implements InvocationHandler {

    //被代理的接口
    private Object target;

    //設(shè)計(jì)被代理的接口
    public void setTarget(Object object) {
        this.target = object;
    }

    //通過(guò)反射生成得到代理類(lèi)
    public Object getProxy(){
        //三個(gè)參數(shù),當(dāng)前的類(lèi)加載器筋现,被代理的接口唐础,以及一個(gè)InvocationHandler箱歧,當(dāng)前即可
        return Proxy.newProxyInstance
                (this.getClass().getClassLoader(),
                        target.getClass().getInterfaces(),this);
    }

    //處理代理實(shí)例,返回結(jié)果
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        //通過(guò)反射來(lái)調(diào)用方法
        log(method.getName());  //將方法名傳給我們的輸出方法
        Object result = method.invoke(target,args);
        return result;
    }
    
    //新增加的日志輸出功能
    public void log(String msg){
        System.out.println("日志輸出一膨,調(diào)用了"+msg+"方法");
    }
}

客戶端輸出:

日志輸出呀邢,調(diào)用了add方法
增加操作
日志輸出,調(diào)用了delete方法
刪除操作
日志輸出汞幢,調(diào)用了update方法
更新操作
日志輸出驼鹅,調(diào)用了query方法
查詢操作

Process finished with exit code 0

看到這里可能還是會(huì)有小伙伴不是很懂, 上面我們的靜態(tài)代理實(shí)現(xiàn)了一個(gè)代理類(lèi)森篷,這里不也是實(shí)現(xiàn)了一個(gè)調(diào)用處理嗎输钩?注意,這里我們的調(diào)用處理可是一個(gè)可以復(fù)用的仲智,只要你是實(shí)現(xiàn)了基于接口實(shí)現(xiàn)的業(yè)務(wù)就可以調(diào)用买乃。比如我們一個(gè)龐大的業(yè)務(wù)的層,有很多的servcie接口钓辆,都可以統(tǒng)一使用這個(gè)動(dòng)態(tài)代理模板剪验。我們不需要在實(shí)現(xiàn)階段去關(guān)心代理誰(shuí),在使用的時(shí)候才指定代理誰(shuí)G傲9ζ荨!

總結(jié)

關(guān)于代理我們也了解似嗤,它主要的優(yōu)點(diǎn)也很明顯

  • 職責(zé)清晰啸臀。真實(shí)的角色就是實(shí)現(xiàn)實(shí)際的業(yè)務(wù)邏輯,不用關(guān)心其他非本職責(zé)的事務(wù)烁落,通過(guò)后期的代理完成一件事務(wù)乘粒,附帶的結(jié)果就是編程簡(jiǎn)潔清晰。
  • 高擴(kuò)展性伤塌。具體主題角色是隨時(shí)都會(huì)發(fā)生變化的灯萍,只要它實(shí)現(xiàn)了接口,甭管它如何變化每聪,都逃不脫如來(lái)佛的手掌(接口)旦棉,那我們的代理類(lèi)完全就可以在不做任何修改的情況下使用。
  • 智能化药薯。都不用自己實(shí)現(xiàn)代理他爸,只需要在使用的時(shí)候去指定就行了。

使用場(chǎng)景

關(guān)于代理模式應(yīng)用的場(chǎng)景果善,一個(gè)比較典型的動(dòng)態(tài)代理就是Spring AOP了诊笤,我們用AOP來(lái)面向切面編程。在我們完成核心業(yè)務(wù)之后巾陕,然后可以橫向擴(kuò)展我們的周邊業(yè)務(wù)讨跟,比如日志輸出纪他,監(jiān)控等。具體的可以去了解AOP晾匠,了解之后對(duì)其他的場(chǎng)景茶袒,下次一見(jiàn)到就能看出來(lái)這用到了代理模式了。

參考資料

設(shè)計(jì)模式之禪(第二版)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末凉馆,一起剝皮案震驚了整個(gè)濱河市薪寓,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌澜共,老刑警劉巖向叉,帶你破解...
    沈念sama閱讀 216,324評(píng)論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異嗦董,居然都是意外死亡母谎,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,356評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)京革,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)奇唤,“玉大人,你說(shuō)我怎么就攤上這事匹摇∫龋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,328評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵廊勃,是天一觀的道長(zhǎng)冗栗。 經(jīng)常有香客問(wèn)我,道長(zhǎng)供搀,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,147評(píng)論 1 292
  • 正文 為了忘掉前任钠至,我火速辦了婚禮葛虐,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘棉钧。我一直安慰自己屿脐,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,160評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布宪卿。 她就那樣靜靜地躺著的诵,像睡著了一般。 火紅的嫁衣襯著肌膚如雪佑钾。 梳的紋絲不亂的頭發(fā)上西疤,一...
    開(kāi)封第一講書(shū)人閱讀 51,115評(píng)論 1 296
  • 那天,我揣著相機(jī)與錄音休溶,去河邊找鬼代赁。 笑死扰她,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的芭碍。 我是一名探鬼主播徒役,決...
    沈念sama閱讀 40,025評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼窖壕!你這毒婦竟也來(lái)了忧勿?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,867評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤瞻讽,失蹤者是張志新(化名)和其女友劉穎鸳吸,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體卸夕,經(jīng)...
    沈念sama閱讀 45,307評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡层释,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,528評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了快集。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片贡羔。...
    茶點(diǎn)故事閱讀 39,688評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖个初,靈堂內(nèi)的尸體忽然破棺而出乖寒,到底是詐尸還是另有隱情,我是刑警寧澤院溺,帶...
    沈念sama閱讀 35,409評(píng)論 5 343
  • 正文 年R本政府宣布楣嘁,位于F島的核電站,受9級(jí)特大地震影響珍逸,放射性物質(zhì)發(fā)生泄漏逐虚。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,001評(píng)論 3 325
  • 文/蒙蒙 一谆膳、第九天 我趴在偏房一處隱蔽的房頂上張望叭爱。 院中可真熱鬧,春花似錦漱病、人聲如沸买雾。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,657評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)漓穿。三九已至,卻和暖如春注盈,著一層夾襖步出監(jiān)牢的瞬間晃危,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,811評(píng)論 1 268
  • 我被黑心中介騙來(lái)泰國(guó)打工老客, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留山害,地道東北人纠俭。 一個(gè)月前我還...
    沈念sama閱讀 47,685評(píng)論 2 368
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像浪慌,于是被迫代替她去往敵國(guó)和親冤荆。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,573評(píng)論 2 353