設計模式之代理模式(Proxy Pattern)

    這篇博客,我們要詳細講解的是代理模式(Proxy Pattern),將要講解的內容有:代理模式的定義,作用,
詳細設計分析等方面。

一、Pattern name

爲其他對象提供一種代理以控制對這個對象的訪問。 ——《設計模式 可複用面向對象軟件的基礎》

二、Problem

在日常的工作和程序設計中,你是否遇到過這樣的問題:

你製作了一個網頁,裏面有你對人生的思考和生活的感悟,而且,你爲了整個頁面的美觀而精心挑選製作了一些精美的圖片或者插圖等,整體看起來非常贊!但是,你卻發現由於使用了很多精美的圖片,在網絡不好時,甚至網絡狀態良好時,整個頁面加載起來也會比較慢,用戶體驗極差。而你又不願意捨棄精心挑選的圖片,爲此久久不能釋懷,找不到合適的解決方法。

你在製作網站時,發現需要對不同的用戶(遊客,註冊普通用戶,會員用戶,系統管理員,網站管理員等)進行不同的權限設置,而,如果把他們分別設計爲不同的功能類,你又發現不同的用戶之間有很多相似甚至相同的功能,整個系統看起來代碼重複現象嚴重,該如何巧妙的設計,既能體現不同用戶的不同權限,又能有效地減少代碼的重複現象呢?

如果你常常遇到上面描述的問題,而且百思不得其解,那麼,你應該和代理模式交個朋友,學會用代理模式解決上面的問題以及其他更多的問題。

三、Solution

首先,我們來看一下,代理模式的UML類圖結構:

這裏寫圖片描述

上圖是最基本、最簡單情況的代理模式,從圖中我們可以看出,Subject是表示一個抽象類或者接口,在這個接口中定義了客戶端(Client)需要調用的方法,其中RealSubject類是這個方法的真正實現類,而Proxy雖然也繼承了Subject抽象類或者實現了Subject接口,但是在Proxy中並不是真正的實現,而是生成一個對RealSubject對象的引用realSubject,再通過調用realSubject.Request()來實現Subject中要求的方法,即Request()方法。

在這裏,可以用一個形象的比喻:代理模式實際上告訴我們,Proxy實際上就是一個接活的,而真正幹活的是RealSubject,但是客戶(Client)不需要知道誰在真正幹活,他只需要通過Subject myProxy = new Proxy();創建一個代理(Proxy)的對象,然後調用myProxy.Request();方法來實現自己的要求,就可以了。至於代理(Proxy)是自己幹活還是找人幹活,Client並不關心,他只關心自己安排的任務是否完成了。

其實,代理模式變形非常多,設計處理也非常靈活,能夠通過適當變形解決很多問題,下面我們來嘗試用代理模式解決我們剛剛提出的那兩個問題。

1. 頁面加載問題:

我們首先來看一下,頁面加載問題解決方案的UML類圖結構:

這裏寫圖片描述

從圖中我們可以發現,其實整個頁面的加載控制程序就相當於(DocumentEditor),他並沒有直接new一個Image(圖像操作的真正實現類)的對象,而是通過Graphic接口創建一個代理對象(ImageProxy),而這個代理對象裏面存儲了該圖像最基本的信息如:圖像的名字(fileName),圖像的尺寸大小(extent),這樣當頁面加載時,需要通過頁面內元素大小調整佈局的時候,並不需要馬上創建一個Image的對象,而只需要創建一個代理對象(ImageProxy),並使用代理對象的extent屬性就可以完成界面的佈局工作。而當頁面已經移動(如下拉界面)到圖像位置時,再調用ImageProxy的Draw()方法,此時再由代理對象(ImageProxy)new一個Image對象,並調用Image對象的Draw()方法即可。

也就是說,只有在頁面的加載控制程序(DocumentEditor)激活(調用)圖像代理(ImageProxy)的Draw()操作以顯示這個圖像的時候,圖像代理(ImageProxy)纔會創建真正的圖像(Image),然後圖像代理(ImageProxy)將Draw()請求轉發給這個真正的圖像(Image)對象。

這樣可以有效地推遲Image對象的創建時間,如果Image對象的創建很耗時的話,會有效減少創建Image對象對整個頁面加載速度的影響,也就有效地解決了我們上面提到的加載頁面很慢的問題。

2. 網站權限控制問題

對於網站權限控制問題的UML類圖接口其實和上面的頁面加載問題的類似,我們就不畫UML類圖了,直接上代碼。

抽象類或者接口(Subject)代碼:

public interface Subject {
    //對網站進行的某項操作
    public void operation();
}

真正實現操作的實現類(RealSubject)代碼:

public class RealSubject implements Subject{
    public void operation() {
        //真正的實現類的操作
        System.out.println("這是真正的實現類的操作。");
    }
}

代理類(Proxy)代碼:

public class Proxy implements Subject{
    RealSubject realSubject = new RealSubject();
    //當然這裏也可以寫成Subject realSubject = new RealSubject();
    public void operation() {
        //調用真正實現操作的對象方法之前,可以做調用前操作處理。
        System.out.println("在這裏,你可以進行一些調用前操作處理。");        
        realSubject.operation();        
        //調用真正實現操作的對象方法之後,可以做調用後操作處理。
        System.out.println("在這裏,你可以進行一些調用後操作處理。");
    }
}

客戶端(Client)代碼:

public class Client {

    public static void main(String[] args) {
        Subject myProxy = new Proxy();
        myProxy.operation();
    }

}

上面幾個代碼,我想都很容易看懂,在這裏,主要解釋一下Proxy的代碼,從代碼中可以看到,在調用realSubject.operation();之前,你可以進行一下調用前的處理,比如檢查權限,檢查環境狀況等等,在調用之後你也可以進行相應的處理。當然了,如果你覺得根據用戶權限,有的用戶無法調用該方法,並在無權限用戶調用的時候返回提醒信息如:“請登錄後查看”等。你可以重新改造Proxy的operation方法,使用一個判斷語句,對用戶權限進行判斷,再根據判斷結果,選擇執不執行realSubject.operation()方法,如何執行該方法,是否要返回警告信息等。

到這裏呢,我們前面提到的兩個問題就都解決了,是不是已經慢慢發現代理模式的奇妙魅力!

四、Consequences

可以說通過增加一個代理,我們對這個對象的控制的靈活性大大增加,我們可以在對對象的訪問和操作方面增加我們需要的控制和保護等。

那麼我們說過代理模式非常靈活,變形非常多,應用的場景也非常多,那麼,它都有哪些變形以及都用在什麼場景呢?下面我們來做一下簡要的介紹:

(1)遠程代理(Remote Proxy) -可以隱藏一個對象存在於不同地址空間的事實。也使得客戶端可以訪問在遠程機器上的對象,遠程機器可能具有更好的計算性能與處理速度,可以快速響應並處理客戶端請求。

(2)虛擬代理(Virtual Proxy) – 允許內存開銷較大的對象在需要的時候創建。只有我們真正需要這個對象的時候才創建(其實這個和我們舉得那個頁面加載的問題相似)。

(3)寫入時複製代理(Copy-On-Write Proxy) – 用來控制對象的複製,方法是延遲對象的複製,直到客戶真的需要爲止。是虛擬代理的一個變體。

(4)保護代理(Protection (Access)Proxy) – 爲不同的客戶提供不同級別的目標對象訪問權限

(5)緩存代理(Cache Proxy) – 爲開銷大的運算結果提供暫時存儲,它允許多個客戶共享結果,以減少計算或網絡延遲。

(6)防火牆代理(Firewall Proxy) – 控制網絡資源的訪問,保護主題免於惡意客戶的侵害。

(7)同步代理(SynchronizationProxy) – 在多線程的情況下爲主題提供安全的訪問。

(8)智能引用代理(Smart ReferenceProxy) - 當一個對象被引用時,提供一些額外的操作,比如將對此對象調用的次數記錄下來等。

(9)複雜隱藏代理(Complexity HidingProxy) – 用來隱藏一個類的複雜集合的複雜度,並進行訪問控制。有時候也稱爲外觀代理(Façade Proxy),這不難理解。複雜隱藏代理和外觀模式是不一樣的,因爲代理控制訪問,而外觀模式是不一樣的,因爲代理控制訪問,而外觀模式只提供另一組接口。

在這裏我們詳細解釋一下copy-on-write的優化方式:

該優化與根據需要創建對象有關,拷貝一個龐大而複雜的對象是一種開銷很大的操作,如果這個拷貝根本沒有被修改,那麼這些開銷就沒有必要,用代理延遲這一拷貝過程,我們可以保證只有當這個對象被修改的時候纔對它進行拷貝。

在實現copy-on-write時,必須對實體進行引用計數,拷貝代理僅會增加引用計數,只有當用戶請求修改該實體時,代理纔會真正的拷貝它,在這種情況下,代理還必須減少實體的引用計數,當引用的數目爲0時,這個實體將被刪除。

五、常見疑問解答及其他

疑問1:在Proxy中創建RealSubject的對象時,是使用RealSubject realSubject = new RealSubject();還是採用Subject realSubject = new RealSubject();?其實,也就是說,Proxy是應該持有一個Subject對象還是應該持有一個RealSubject對象?

我在學習這個模式的時候,也有過這樣的疑問,看到好多代碼,有的是採用第一種方式,有的是採用第二種方式。那麼究竟採用哪種方式比較好呢?其實要視情況確定的。

情況一:如果在Subject(抽象類或接口)中聲明的所有方法和RealSubject對外提供的所有方法相同,那麼原則上兩種聲明方式都可以。但是考慮到面向接口編程原則,而不是面向實現編程,聲明爲Subject realSubject;即持有一個Subject對象,會更好一些。

情況二:如果想要實現一個代理類(Proxy)可以同時代理多個實現類(RealSubject)而不需要修改Proxy的話,那麼,Proxy採用Subject realSubject;即持有一個Subject對象,會好很多。而且,我們需要在Proxy的構造函數中指定其所指代的具體是哪個實現類(RealSubject)。這樣Client在創建代理類對象的時候,需要傳入一個他通過代理想要操作的對象。如:

代理類(Proxy)部分代碼:

public class Proxy implements Subject{
    private Subject realSubject;
    public Proxy(Subject realSubject){
            this.realSubject = realSubject;
    }
}

客戶端(Client)部分代碼:

Subject realSubject1 = new RealSubject1();
Subject realSubject2 = new RealSubject2();
Subject proxy1 = new Proxy(realSubject1);
Subject proxy2 = new Proxy(realSubject2);

看到這裏,有的人可能又有疑問,既然Client已經獲得了具體實現類(RealSubject)的對象,那麼Client爲什麼不直接調用具體實現類中提供的方法呢?而且,將具體實現類的對象輕易地給Client,豈不是允許Client對具體實現類進行一些非法操作?

其實,你們想的對,這樣做確實會出現這樣的弊端,但是,我們是可以通過進一步設計消除這個弊端的。我們可以在RealSubject的外面再包一層代理,或者直接在RealSubject中添加一層判斷處理,用於判斷調用方法的來源是否是Proxy,如果不是Proxy就拒絕訪問。也就是,使RealSubject只接受來自Proxy的方法調用。這樣問題就解決了,但是貌似有點複雜了。

情況三:如果RealSubject對外提供的方法多於Subject中定義的方法,或者RealSubject中定義的方法和Subject中定義的方法不同,而此時Proxy的主要作用可能是隱藏起RealSubject的複雜操作或者Proxy只有調用RealSubject中存在而Subject中不存在的方法才能實現功能時,此時,聲明爲RealSubject realSubject = new RealSubject();即,持有一個RealSubject對象,可能會更合適。常見的應用有:包裝數據庫操作,提供數據庫代理等。

疑問2:如何實現一個代理類(Proxy)不變,然後可以給這個代理分配不同的被代理對象(RealSubject),即不同的實現類。

這個問題的解答呢,請看疑問1中的情況二的說明。

疑問3:如何實現一個實現類(RealSubject),對應不同的代理類(Proxy),即實現同一對象,不同代理方式或代理功能等?

至於這個問題,要實現的不同代理方式或代理功能差異不是很大,完全可以考慮在原有代理類中添加控制代碼,當然了,如果差異很大,或者爲了更好地踐行OCP原則,可以添加一個對實現類負責的代理類,或者添加一個對原有代理類負責的新代理類,然後在新添加的代理類中,進行相應的修改和設計。

六、總結

到這裏呢,我們的代理模式的講解,算是告一段落了。正如我前面所說,其實代理類涉及範圍很廣,絕不僅限於我們上面討論的這些,除此之外還有很多方面需要學習與運用。

特別要強調一個有待進一步研究的方面,那就是關於動態代理模式的思考與分析,由於篇幅與時間問題,對於動態代理模式的分析,我們將在以後的設計模式系列博客中繼續學習,希望大家關注。同樣,如果有任何疑問或者好的建議,歡迎你留言,我們一起研究,一起進步。

發佈了46 篇原創文章 · 獲贊 113 · 訪問量 15萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章