1. 程式人生 > 實用技巧 >設計模式學習筆記(十五):代理模式

設計模式學習筆記(十五):代理模式

1 概述

1.1 引言

所謂代購,就是找人幫忙購買自己需要的商品,代購包括兩種型別,一種是在當地買不到商品,或者因為當地該商品價格較高,因此託人在其他地區或者國外購買,另一種型別是消費者對想要購買的商品訊息缺乏,只能委託中介或者中間商購買。

在軟體開發中,有時也需要提供與代購類似的功能,由於某些原因,客戶端不想或不能直接訪問物件,此時可通過一種叫代理的第三者來實現間接訪問,這種方案對應的設計模式稱為代理模式。

代理模式是一種應用很廣泛的結構型設計模式,而且變化很多。在代理模式中引入了一個新的代理物件,代理物件可以在客戶端物件和目標物件之間起到中介的作用,去掉客戶不能看到的內容或者增添客戶需要的額外服務。

1.2 定義

代理模式:給某一個物件提供一個代理,並由代理物件控制對原物件的引用。

代理模式是一種物件結構型模式。

1.3 結構圖

1.4 角色

  • Subject(抽象主題角色):聲明瞭真實主題和代理主題的共同介面,客戶端通常需要針對抽象主題角色程式設計
  • Proxy(代理主題角色):內部包含了對真實主題的引用,從而可以操作真實主題物件。代理主題角色中提供了一個與真實主題角色相同的介面,以便在任何時候都可以替代真實主題。代理角色還可以控制對真實主題的使用,在需要的時候建立或刪除真實主題物件,並對真實主題的使用加以約束。通常在代理主題角色中,客戶端呼叫之前或之後都需要執行特定操作,比如圖中的preRequest
    以及postRequest
  • RealSubject(真實主題角色):定義了代理角色所代表的真實物件,在真實主題角色中實現了真實的業務操作,客戶端可以通過代理角色間接呼叫真實主題角色中的操作

1.5 分類

代理模式根據目的以及實現方式可以分成很多類,常見的幾種如下:

  • 遠端代理:為一個位於不同的地址空間的物件提供一個本地的代理物件,這個不同的地址空間可以在同一臺主機中,也可以不在同一臺主機中。遠端代理又叫“大使”(Ambassador)
  • 虛擬代理:如果需要建立一個資源消耗較大的物件,先建立一個消耗相對較小的物件來表示,真實物件只在需要時才會被真正建立
  • 保護代理:控制對一個物件的訪問,可以給不同的使用者提供不同級別的使用許可權
  • 快取代理:為某一個目標操作的結果提供臨時的儲存空間,以便多個客戶端可以共享這些結果
  • 智慧引用代理:當一個物件被引用時,提供一些額外的操作,比如將物件被呼叫的次數記錄下來等

1.6 與裝飾模式的不同

代理模式和裝飾模式在實現時類似,主要區別如下:

  • 增加的職責範圍問題域不同:代理模式以及裝飾模式都能動態地增加職責,但是代理模式增加的是一些全新的職責,比如許可權控制,快取處理,智慧引用,遠端訪問等,這些職責與原有職責不屬於同一個問題域。對於裝飾模式,為具體構件類增加一些相關的職責,是原有職責的擴充套件,這些職責屬於同一個問題域
  • 目的不同:代理模式的目的是控制對物件的訪問,而裝飾模式是為物件動態增加功能

2 典型實現

2.1 步驟

  • 定義抽象主題角色:定義為抽象類/介面,宣告抽象業務方法
  • 定義真實主題角色:繼承/實現抽象主題角色,實現真實業務操作
  • 定義代理主題角色:繼承/實現抽象主題角色,將客戶端的請求轉發到真實主題角色進行呼叫,同時根據需要進行呼叫前/後的一些相關操作

2.2 抽象主題角色

這裡簡單實現為一個介面:

interface Subject
{
    void request();
}

2.3 真實主題角色

實現抽象主題介面,執行真正的業務操作:

class RealSubject implements Subject
{
    public void request()
    {
        System.out.println("真實主題角色方法");
    }
}

2.4 代理主題角色

同樣實現抽象主題介面,一般來說在呼叫真正的業務方法之前或之後會有相關操作:

class Proxy implements Subject
{
    private RealSubject subject = new RealSubject();
    public void pre()
    {
        System.out.println("代理前操作");
    }

    public void request()
    {
        pre();
        subject.request();
        post();
    }

    public void post()
    {
        System.out.println("代理後操作");
    }
}

2.5 客戶端

客戶端針對抽象主題角色進行程式設計即可,如果不需要代理,則例項化真實主題角色,如果需要代理則例項化代理主題角色:

public static void main(String[] args) 
{
    Subject subject = new RealSubject();
    subject.request();
    System.out.println("\n使用代理:\n");
    subject = new Proxy();
    subject.request();
}

3 例項

一個已具有搜尋功能的系統,需要為搜尋新增身份認證以及日誌記錄功能,使用代理模式設計該系統。

設計如下:

  • 抽象主題角色:Searcher
  • 真實主題角色:RealSearcher
  • 代理主題角色:ProxySearcher

程式碼如下:

public class Test
{
    public static void main(String[] args) 
    {
        Searcher subject = new ProxySearcher();
        subject.search();
    }
}

interface Searcher
{
    void search();
}

class RealSearcher implements Searcher
{
    public void search()
    {
        System.out.println("搜尋");
    }
}

class ProxySearcher implements Searcher
{
    private RealSearcher subject = new RealSearcher();
    public void validate()
    {
        System.out.println("身份驗證");
    }

    public void search()
    {
        validate();
        subject.search();
        log();
    }

    public void log()
    {
        System.out.println("日誌記錄,查詢次數+1");
    }
}

進行搜尋之前,先驗證使用者,接著進行搜尋,搜尋完成後進行日誌記錄,這是保護代理以及智慧引用代理的例子。

4 動態與靜態代理

4.1 靜態代理

通常情況下,每一個代理類編譯之後都會生成一個位元組碼檔案,代理所實現的介面和所代理的方法都固定,這種代理稱為靜態代理。

靜態代理中,客戶端通過Proxy呼叫RealSubjectrequest方法,同時封裝其他方法(代理前/代理後操作),比如上面的查詢驗證以及日誌記錄功能。

靜態代理的優點是實現簡單,但是,代理類以及真實主題類都需要事先存在,代理類的介面以及代理方法都明確指定,但是如果需要:

  • 代理不同的真實主題類
  • 代理一個真實主題類的不同方法

需要增加新的代理類,這會導致系統中類的個數大大增加。

這是靜態代理最大的缺點,為了減少系統中類的個數,可以採用動態代理。

4.2 動態代理

動態代理可以讓系統根據實際需要動態建立代理類,同一個代理類可以代理多個不同的真實主題類,而且可以代理不同方法,在Java中實現動態代理需要Proxy類以及InvocationHandler介面。

4.2.1 Proxy

Proxy類提供了用於建立動態代理類和例項物件的方法,最常用的方法包括:

  • public static Class<?> getProxy(ClassLoader loader,Class<?> ... interfaces):該方法返回一個Class型別的代理類,在引數中需要提供類載入器並指定代理的介面陣列,這個陣列應該與真實主題類的介面列表一致
  • public staitc Object newProxyInstance(ClassLoader loader,Class<?> [] interfaces,InvocationHandler h):返回一個動態建立的代理類例項,第一個引數是類載入器,第二個引數表示代理類實現的介面列表,同理與真實主題的介面列表一致,第三個引數表示h所指派的呼叫處理程式類

4.2.2 InvocationHandler

InvocationHandler介面是代理程式類的實現介面,該介面作為代理例項的呼叫處理者的公共父類,每一個代理類的例項都可以提供一個相關的具體呼叫者(也就是實現了InvocationHandler的類),該介面中宣告以下方法:

  • public Object invoke(Object proxy,Method method,Object [] args):該方法用於處理對代理類例項的方法呼叫並返回相應結果,當一個代理例項中的業務方法被呼叫時自動呼叫該方法。第一個引數表示代理類的例項,第二個引數表示需要代理的方法,第三個引數表示方法的引數陣列

動態代理類需要在執行時指定所代理的真實主題類的介面,客戶端在呼叫動態代理物件的方法時,呼叫請求會自動轉發到InvocationHandlerinvoke方法,由invoke實現對請求的統一處理。

4.2.3 例項

為一個數據訪問Dao層增加方法呼叫日誌,記錄每一個方法被呼叫的時間和結果,使用動態代理模式進行設計。

設計如下:

  • 抽象主題角色:AbstractUserDao
  • 真實主題角色:UserDao1+UserDao2
  • 請求處理角色:DAOLogHandler
  • 代理主題角色:無需手動定義,由Proxy.newInstance()生成

首先設計抽象主題角色:

interface AbstarctUserDao
{
    void findUserById(String id);
}

接著建立兩個具體類實現該介面:

class UserDao1 implements AbstarctUserDao
{
    public void findUserById(String id)
    {
        System.out.println("1號資料庫中查詢id" + 
            ("1".equals(id) ? "成功" : "失敗"));
    }
}


class UserDao2 implements AbstarctUserDao
{
    public void findUserById(String id)
    {
        System.out.println("2號資料庫中查詢id" + 
            ("2".equals(id) ? "成功" : "失敗"));
    }
}

接著定義請求處理角色:

class DAOLogHandler implements InvocationHandler
{
    private Object object;
    public DAOLogHandler(Object object)
    {
        this.object = object;
    }

    @Override
    public Object invoke(Object proxy,Method method,Object [] args) throws Throwable
    {
        beforeInvoke();
        Object result = method.invoke(object, args);
        postInvoke();
        return result;
    }

    private void beforeInvoke()
    {
        System.out.println("記錄時間");
    }

    private void postInvoke()
    {
        System.out.println("記錄結果");
    }
}

核心是實現了InvocationHandlerinvoke方法,該方法在呼叫抽象主題角色中的方法時自動轉發到該方法處理。

也就是說,假設抽象主題角色有A(),B(),C()三個方法,當呼叫A()時,將呼叫A()替換掉裡面的Object result = method.invoke(object.args),也就是實際上相當呼叫如下函式:

@Override
public Object invoke(Object proxy,Method method,Object [] args) throws Throwable
{
    beforeInvoke();
    Object result = A(args);
    postInvoke();
    return result;
}

當呼叫B()時,相當於呼叫以下函式:

@Override
public Object invoke(Object proxy,Method method,Object [] args) throws Throwable
{
    beforeInvoke();
    Object result = B(args);
    postInvoke();
    return result;
}

下面是測試客戶端的程式碼:

public static void main(String[] args) 
{
    AbstarctUserDao userDao1 = new UserDao1();
    AbstarctUserDao proxy = null;
    InvocationHandler handler = new DAOLogHandler(userDao1);
    proxy = AbstarctUserDao.class.cast(
        Proxy.newProxyInstance(AbstarctUserDao.class.getClassLoader(), new Class[]{AbstarctUserDao.class}, handler)
    );
    proxy.findUserById("2");

    AbstarctUserDao userDao2 = new UserDao2();
    handler = new DAOLogHandler(userDao2);
    proxy = AbstarctUserDao.class.cast(
        Proxy.newProxyInstance(AbstarctUserDao.class.getClassLoader(),new Class[]{AbstarctUserDao.class},handler)
    );
    proxy.findUserById("2");
}

輸出如下:

在測試程式碼中,代理主題角色由以下語句生成:

proxy = AbstarctUserDao.class.cast(
    Proxy.newProxyInstance(AbstarctUserDao.class.getClassLoader(), new Class[]{AbstarctUserDao.class}, handler)
);

其中cast()方法相當於是對強制型別轉換進行了包裝,轉換前進行了安全檢查。

Proxy.newInstance()中,第一個引數是抽象主題角色的類載入器,第二個引數表示抽象主題角色的所有方法都轉發請求到第三個引數中的invoke方法處理。第三個引數是自定義的InvocationHandler,通過構造方法注入抽象主題角色,目的是提供一個抽象主題角色的引用,呼叫代理方法時自動呼叫抽象主題角色的方法。

5 遠端代理

5.1 概述

遠端代理是一種常見的代理模式,使得客戶端程式可以訪問在遠端主機(或另一個JVM)上的物件,遠端代理可以將網路的細節隱藏起來,使得客戶端不必考慮網路的存在。客戶端完全可以認為被代理的遠端業務物件是本地的而不是遠端的,遠端代理物件承擔了大部分的網路通訊工作,並負責對遠端業務的方法呼叫。

遠端業務物件在本地主機中有一個代理物件,該代理物件負責對遠端業務物件的訪問和網路通訊,它對於客戶端而言是透明的。客戶端無須關心實現的具體業務是誰,只需要按照服務介面所定義的方式直接與本地主機中的代理物件互動即可。

在Java中可以通過RMI(Remote Method Invocation,遠端方法呼叫)機制來實現遠端代理,它能夠實現一個JVM中的物件呼叫另一個JVM中的物件,下面看一個簡單的例子。

5.2 RMI簡例

這個簡單的例子有以下四個類:

  • 介面:Hello
  • 介面實現類:HelloImpl
  • 服務端:HelloServer
  • 客戶端:HelloClient

程式碼如下:

interface Hello extends Remote
{
    String sayHello(String name) throws RemoteException;
}

一個簡單的sayHello方法,注意裡面的方法需要宣告為丟擲RemoteException

接著是介面實現類:

public class HelloImpl extends UnicastRemoteObject implements Hello{
    public HelloImpl() throws RemoteException
    {
        super();
    }
    public String sayHello(String name) throws RemoteException
    {
        System.out.println("Hello");
        return "Hello"+name;
    }
}

實現sayHello方法。

接下來是服務端:

public class HelloServer {
    public static void main(String[] args) {
        try {
            Hello hello = new HelloImpl();
            LocateRegistry.createRegistry(8888);
            System.setProperty("java.rmi.server.hostname", "127.0.0.1");
            Naming.bind("rmi://localhost:8888/hello", hello);
            System.out.println("遠端繫結物件成功");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

服務端中首先註冊了一個本地埠8888,接著設定系統屬性rmi服務的主機名為本地地址,也就是127.0.0.1,如果是部署在伺服器上修改對應ip即可。下一步是通過Naming的靜態方法bind繫結該URL到RMI伺服器上,並命名為hello。其中rmi:(RMI協議)可以省略。

最後是客戶端:

public class HelloClient {
    public static void main(String[] args) {
        try
        {
            Hello hello = Hello.class.cast(
                Naming.lookup("rmi://127.0.0.1:8888/hello")
            );
            System.out.println(hello.sayHello("111"));
        }
        catch(Exception e)
        {
            e.printStackTrace();
        }
    }
}

客戶端通過Naminglookup查詢引數URL中對應的遠端服務物件hello,找到後返回並強制轉換為Hello,接著即可呼叫遠端物件的方法sayHello

首先執行服務端:

接著啟動客戶端:

可以看到來自服務端的結果。

再檢視服務端:

可以看到這是呼叫了sayHello的結果。

6 虛擬代理

6.1 概述

對於一直佔用系統資源較多或者載入時間較長的物件,可以給這些物件提供一個虛擬代理。在真實物件建立成功之前虛擬代理扮
演真實物件的替身,當真實物件建立之後,虛擬代理將使用者的請求轉發給真實物件。

6.2 適用情況

以下兩種情況可以考慮使用虛擬代理:

  • 由於物件本身複雜性或者網路等原因導致一個物件需要較長的載入時間,此時可以用一個載入時間相對較短的代理物件來代表真實物件
  • 當一個物件的載入十分消耗系統資源的時候,也非常適合使用虛擬代理。虛擬代理可以讓那些佔用大量記憶體或處理起來非常複雜的物件推遲到使用到它們的時候才建立,而在此之前用一個相對來說佔用資源較少的代理物件來代表真實物件

6.3 優缺點

  • 優點:由於應用程式啟動時由於不需要建立和裝載所有的物件,因此加速了應用程式的啟動
  • 缺點:不能保證特定的應用程式物件被建立,在訪問這個物件的任何地方都需要提前進行判空操作

6.4 簡例

有一批人找老闆談事情,談事情之前需要先通過老闆的助手進行預約,預約這件事只需要助手完成,真正執行預約列表裡面的任務時才需要老闆出現,使用虛擬代理模式進行設計。

設計如下:

  • 抽象主題角色:Approvable
  • 真實主題角色:Boss
  • 代理主題角色:Assistant

程式碼如下:

//抽象主題角色
interface Approvable
{
    void approve();
}

下一步定義真實主題角色Boss

class Boss implements Approvable
{
    private List<String> orders = new LinkedList<>();
    
    static
    {
        System.out.println("\n老闆來處理了\n");
    }

    public Boss(List<String> orders)
    {
        this.orders = orders;
    }

    public void addOrder(String order)
    {
        orders.add(order);
    }

    @Override
    public void approve()
    {
        while(orders.size() > 0)
        {
            System.out.println("老闆處理了<"+orders.remove(0)+">");
        }
    }
}

使用List儲存待處理的事件,approve表示處理所有的事件。

代理主題角色如下:

class Assistant implements Approvable
{
    private List<String> orders = new LinkedList<>();
    private volatile Boss boss;

    public void addOrder(String order)
    {
        if(boss != null)
        {
            System.out.println("老闆將<"+order+">新增到預約列表");
            boss.addOrder(order);
        }
        else
        {
            System.out.println("助手將<"+order+">新增到預約列表");
            orders.add(order);
        }
    }

    @Override
    public void approve()
    {
        if(boss == null)
        {
            synchronized(this)
            {
                if(boss == null)
                {
                    boss = new Boss(orders);
                }
            }
        }
        boss.approve();       
    }
}

在新增事件(addOrder)函式中,首先判斷boss是否為null,如果為null表示還沒建立老闆物件,這時讓助手新增到預約列表中去,如果不為null表示已經存在老闆物件,直接交由老闆加入預約列表。

對於approve方法,首先判斷boss是否為null,不為null表示老闆能直接處理所有事件。為null表示老闆物件還沒有建立,新建一個Boss並將待處理的事件作為引數注入boss中。

測試類:

public static void main(String[] args) 
{
    Assistant assistant = new Assistant();
    assistant.addOrder("找老闆面試");
    assistant.addOrder("找老闆借錢");
    assistant.addOrder("找老闆聊天");
    assistant.approve();

    assistant.addOrder("找老闆吃飯");
    assistant.addOrder("找老闆喝酒");
    assistant.approve();
}

輸出如下:

7 快取代理

快取代理為某一個目標操作的結果提供臨時的儲存空間,以便多個客戶端可以共享這些結果,在這裡使用快取代理模式模擬YouTube對使用整合的第三方庫下載進行快取。

設計如下:

  • 模擬第三方庫:ThirdPartyYouTubeLib+ThirdPartyYouTubeClass
  • 模擬視訊檔案:Video
  • 模擬快取代理:YouTubeCacheProxy
  • 模擬下載器:YouTubeDownloader

首先是第三方類庫,通常情況下是沒有原始碼實現的,其中ThirdPartyYouTubeLib是一個介面,並且ThirdPartyYouTubeClass以及YouTubeCacheProxy實現了它,也就是說:

  • ThirdPartyYouTubeLib是抽象主題角色
  • ThirdPartyYouTubeClass是真實主題角色
  • YouTubeCacheProxy是代理主題角色

7.1 抽象主題角色

首先定義抽象主題角色:

interface ThirdPartyYouTubeLib
{
    HashMap<String,Video> popularVideos();
    Video getVideo(String videoId);
}

一個是獲取熱門視訊的方法,一個是根據id獲取具體視訊的方法。

7.2 真實主題角色

class ThirdPartyYouTubeClass implements ThirdPartyYouTubeLib
{
    private static final String URL = "https://www.youtube.com";
    @Override
    public HashMap<String,Video> popularVideos()
    {
        connectToServer(URL);
        return getRandomVideos();
    }

    @Override
    public Video getVideo(String id)
    {
        connectToServer(URL+id);
        return getSomeVideo(id);
    }

    private int random(int min,int max)
    {
        return min+(int)(Math.random()*((max-min)+1));
    }

    private void experienceNetworkLatency()
    {
        int randomLatency = random(5, 10);
        for(int i=0;i<randomLatency;++i)
        {
            try
            {
                Thread.sleep(100);
            } 
            catch(InterruptedException e)
            {
                e.printStackTrace();
            }
        }
    }

    private void connectToServer(String url)
    {
        System.out.println("連線到 " + url + " ...");
        experienceNetworkLatency();
        System.out.println("連線成功!\n");
    }

    private HashMap<String,Video> getRandomVideos()
    {
        System.out.println("正在下載熱門視訊");
        experienceNetworkLatency();
        HashMap<String,Video> map = new HashMap<>();
        map.put("1111111",new Video("1111","1111.mp4"));
        map.put("2222222",new Video("2222","2222.avi"));
        map.put("3333333",new Video("3333","3333.mov"));
        map.put("4444444",new Video("4444","4444.mkv"));
        System.out.println("下載完成!\n");
        return map;
    }

    private Video getSomeVideo(String id)
    {
        System.out.println("正在下載id為"+id+"的視訊");
        experienceNetworkLatency();
        System.out.println("下載完成!\n");
        return new Video(id,"title");
    }
}

獲取熱門視訊或者某一個視訊時,進行了一個模擬連線到伺服器的操作,首先輸出提示連線到xxx,接著模擬了網路延遲,最後提示下載完成並返回相應的視訊。

7.3 代理主題角色

class YouTubeCacheProxy implements ThirdPartyYouTubeLib
{
    private ThirdPartyYouTubeLib youtubeService = new ThirdPartyYouTubeClass();

    private HashMap<String,Video> cachePopular = new HashMap<>();
    private HashMap<String,Video> cacheAll = new HashMap<>();

    @Override
    public HashMap<String,Video> popularVideos()
    {
        if(cachePopular.isEmpty())
        {
            cachePopular = youtubeService.popularVideos();
        }
        else
        {
            System.out.println("從快取檢索中熱門視訊");            
        }
        return cachePopular;
    }

    @Override
    public Video getVideo(String id)
    {
        Video video = cacheAll.get(id);
        if(video == null)
        {
            video = youtubeService.getVideo(id);
            cacheAll.put(id,video);
        }
        else
        {
            System.out.println("從快取中檢索id為"+id+"的視訊");
        }
        return video;
    }

    public void reset()
    {
        cachePopular.clear();
        cacheAll.clear();
    }
}

這裡的快取代理角色其實就是在呼叫真實主題角色的獲取視訊方法之前,首先判斷是否存在快取,存在的話直接從快取中獲取,不存在的話首先呼叫獲取視訊方法並存儲在快取中,下次獲取時從快取中獲取。

7.4 其他

class Video
{
    private String id;
    private String title;
    private String data;
    public Video(String id,String title)
    {
        this.id = id;
        this.title = title;
    }
    //getter+setter...
}

class YouTubeDownloader
{
    private ThirdPartyYouTubeLib api;

    public YouTubeDownloader(ThirdPartyYouTubeLib api)
    {
        this.api = api;
    }

    public boolean useCacheProxy()
    {
        return api instanceof YouTubeCacheProxy;
    }

    public void renderVideoPage(String id)
    {
        Video video = api.getVideo(id);
        System.out.println("\n-------------------------------------------");
        System.out.println("ID:"+video.getId());
        System.out.println("標題:"+video.getTitle());
        System.out.println("資料:"+video.getData());
        System.out.println("\n-------------------------------------------");
    }

    public void renderPopularVideos()
    {
        HashMap<String,Video> list = api.popularVideos();
        System.out.println("\n-------------------------------------------");
        System.out.println("熱門視訊");
        list.forEach((k,v)->System.out.println("ID:"+v.getId()+"\t標題:"+v.getTitle()));
        System.out.println("\n-------------------------------------------");
    }
}

7.5 測試

public class Test
{
    public static void main(String[] args) 
    {
        YouTubeDownloader naiveDownloader = new YouTubeDownloader(new ThirdPartyYouTubeClass());
        YouTubeDownloader smartDownloader = new YouTubeDownloader(new YouTubeCacheProxy());

        long navie = test(naiveDownloader);
        long smart = test(smartDownloader);
        System.out.println("快取代理節約的時間:"+(navie-smart)+"ms");
    }

    private static long test(YouTubeDownloader downloader)
    {
        long startTime = System.currentTimeMillis();
        downloader.renderPopularVideos();
        downloader.renderVideoPage("1111");
        downloader.renderPopularVideos();
        downloader.renderVideoPage("2222");
        downloader.renderVideoPage("3333");
        downloader.renderVideoPage("4444");
        long estimatedTime = System.currentTimeMillis() - startTime;
        System.out.println(downloader.useCacheProxy() ? "使用快取執行時間:" : "不使用快取執行時間:");
        System.out.println(estimatedTime+"ms\n");
        return estimatedTime;
    }
}

模擬了兩個下載器,一個使用原生下載,一個使用快取代理下載,輸出如下:

連線到 https://www.youtube.com ...
連線成功!

正在下載熱門視訊
下載完成!


-------------------------------------------
熱門視訊
ID:4444 標題:4444.mkv
ID:2222 標題:2222.avi
ID:3333 標題:3333.mov
ID:1111 標題:1111.mp4

-------------------------------------------
連線到 https://www.youtube.com1111 ...
連線成功!

正在下載id為1111的視訊
下載完成!


-------------------------------------------
ID:1111
標題:title
資料:null

-------------------------------------------
連線到 https://www.youtube.com ...
連線成功!

正在下載熱門視訊
下載完成!


-------------------------------------------
熱門視訊
ID:4444 標題:4444.mkv
ID:2222 標題:2222.avi
ID:3333 標題:3333.mov
ID:1111 標題:1111.mp4

-------------------------------------------
連線到 https://www.youtube.com2222 ...     
連線成功!

正在下載id為2222的視訊
下載完成!


-------------------------------------------
ID:2222
標題:title
資料:null

-------------------------------------------
連線到 https://www.youtube.com3333 ...     
連線成功!

正在下載id為3333的視訊
下載完成!


-------------------------------------------
ID:3333
標題:title
資料:null

-------------------------------------------
連線到 https://www.youtube.com4444 ...     
連線成功!

正在下載id為4444的視訊
下載完成!


-------------------------------------------
ID:4444
標題:title
資料:null

-------------------------------------------
不使用快取執行時間:
9312ms

連線到 https://www.youtube.com ...
連線成功!

正在下載熱門視訊
下載完成!


-------------------------------------------
熱門視訊
ID:4444 標題:4444.mkv
ID:2222 標題:2222.avi
ID:3333 標題:3333.mov
ID:1111 標題:1111.mp4

-------------------------------------------
連線到 https://www.youtube.com1111 ...
連線成功!

正在下載id為1111的視訊
下載完成!


-------------------------------------------
ID:1111
標題:title
資料:null

-------------------------------------------
從快取檢索中熱門視訊

-------------------------------------------
熱門視訊
ID:4444 標題:4444.mkv
ID:2222 標題:2222.avi
ID:3333 標題:3333.mov
ID:1111 標題:1111.mp4

-------------------------------------------
連線到 https://www.youtube.com2222 ...
連線成功!

正在下載id為2222的視訊
下載完成!


-------------------------------------------
ID:2222
標題:title
資料:null

-------------------------------------------
連線到 https://www.youtube.com3333 ...
連線成功!

正在下載id為3333的視訊
下載完成!


-------------------------------------------
ID:3333
標題:title
資料:null

-------------------------------------------
連線到 https://www.youtube.com4444 ...
連線成功!

正在下載id為4444的視訊
下載完成!


-------------------------------------------
ID:4444
標題:title
資料:null

-------------------------------------------
使用快取執行時間:
7611ms

快取代理節約的時間:1701ms

可以看到快取代理是能節省時間的,除了第一次獲取視訊外,隨後的獲取視訊都是從快取中直接提取。

8 主要優點

  • 降低耦合度:代理模式能夠協調呼叫者以及被呼叫者,一定程度上降低了系統的耦合度
  • 靈活可擴充套件:客戶端可以針對抽象主題角色進行程式設計,增加和更換代理類無須修改原始碼,符合OCP,系統具有較好的靈活性和可擴充套件性
  • 提高整體效率(遠端代理):遠端代理為位於兩個不同的地址空間物件的訪問提供了一種實現機制,可以將一些消耗資源較多的物件和操作移至效能更好的計算機上,提高系統整體執行效率
  • 節約開銷(虛擬代理):虛擬代理通過一個消耗資源較少的物件來代表一個消耗資源較多的物件,可以在一定程度上節省系統的執行開銷
  • 控制權限(保護代理):保護代理可以控制一個物件的訪問許可權,為不同使用者提供不同級別的使用許可權

9 主要缺點

  • 速度變慢:由於在客戶端以及真實主題之間增加了代理物件,因此可能會造成處理速度變慢,比如保護代理
  • 實現複雜:實現代理模式需要額外的操作,有些代理模式其實很複雜,比如遠端代理

10 適用場景

  • 客戶端需要訪問遠端主機中的物件,使用遠端代理
  • 需要一個消耗資源較少的物件來代表資源較多的物件時,使用虛擬代理
  • 需要控制訪問許可權,使用保護代理
  • 需要為一個頻繁訪問的操作結果提供臨時儲存空間,使用快取代理
  • 需要為一個物件的訪問(引用)提供額外的操作時,使用智慧引用代理

11 總結

如果覺得文章好看,歡迎點贊。

同時歡迎關注微信公眾號:氷泠之路。