代理模式(Proxy)

定義

代理是一種結構型設計模式,讓你能夠提供對象的替代品或其佔位符。代理控制着對於原對象的訪問,並允許在將請求提交給對象前後進行一些處理。

【愚公系列】2022年05月 二十三種設計模式(十二)-代理模式(Proxy Pattern)_5月月更_愚公搬代碼_ide

前言

1. 問題

舉個例子:有這樣一個消耗大量系統資源的巨型對象, 你只是偶爾需要使用它,並非總是需要。

【愚公系列】2022年05月 二十三種設計模式(十二)-代理模式(Proxy Pattern)_5月月更_愚公搬代碼_#c++_02

你可以實現延遲初始化:在實際有需要時再創建該對象。對象的所有客户端都要執行延遲初始代碼。不幸的是,這很可能會帶來很多重複代碼。 在理想情況下,我們希望將代碼直接放入對象的類中,但這並非總是能實現:比如類可能是第三方封閉庫的一部分。

2. 解決方案

代理模式建議新建一個與原服務對象接口相同的代理類,然後更新應用以將代理對象傳遞給所有原始對象客户端。代理類接收到客户端請求後會創建實際的服務對象,並將所有工作委派給它。

代理將自己偽裝成數據庫對象,可在客户端或實際數據庫對象不知情的情況下處理延遲初始化和緩存查詢結果的工作。

【愚公系列】2022年05月 二十三種設計模式(十二)-代理模式(Proxy Pattern)_5月月更_愚公搬代碼_ide_03

這有什麼好處呢?如果需要在類的主要業務邏輯前後執行一些工作,你無需修改類就能完成這項工作。由於代理實現的接口與原類相同,因此你可將其傳遞給任何一個使用實際服務對象的客户端。

真實世界類比

【愚公系列】2022年05月 二十三種設計模式(十二)-代理模式(Proxy Pattern)_5月月更_愚公搬代碼_#後端_04


信用卡是銀行賬户的代理, 銀行賬户則是一大捆現金的代理。 它們都實現了同樣的接口, 均可用於進行支付。 消費者會非常滿意, 因為不必隨身攜帶大量現金; 商店老闆同樣會十分高興, 因為交易收入能以電子化的方式進入商店的銀行賬户中, 無需擔心存款時出現現金丟失或被搶劫的情況。

結構

【愚公系列】2022年05月 二十三種設計模式(十二)-代理模式(Proxy Pattern)_5月月更_愚公搬代碼_#後端_05

  1. 服務接口(Service Interface)聲明瞭服務接口。代理必須遵循該接口才能偽裝成服務對象。
  2. 服務(Service)類提供了一些實用的業務邏輯。
  3. 代理(Proxy)類包含一個指向服務對象的引用成員變量。代理完成其任務(例如延遲初始化、記錄日誌、訪問控制和緩存等)後會將請求傳遞給服務對象。通常情況下,代理會對其服務對象的整個生命週期進行管理。
  4. 客户端(Client) 能通過同一接口與服務或代理進行交互,所以你可在一切需要服務對象的代碼中使用代理。

適用場景

  • 延遲初始化(虛擬代理)。如果你有一個偶爾使用的重量級服務對象,一直保持該對象運行會消耗系統資源時,可使用代理模式。

你無需在程序啓動時就創建該對象,可將對象的初始化延遲到真正有需要的時候。

  • 訪問控制(保護代理)。如果你只希望特定客户端使用服務對象,這裏的對象可以是操作系統中非常重要的部分,而客户端則是各種已啓動的程序(包括惡意程序),此時可使用代理模式。

代理可僅在客户端憑據滿足要求時將請求傳遞給服務對象。

  • 本地執行遠程服務(遠程代理)。適用於服務對象位於遠程服務器上的情形。

在這種情形中,代理通過網絡傳遞客户端請求,負責處理所有與網絡相關的複雜細節。

  • 記錄日誌請求(日誌記錄代理)。適用於當你需要保存對於服務對象的請求歷史記錄時。代理可以在向服務傳遞請求前進行記錄。
  • 緩存請求結果(緩存代理)。適用於需要緩存客户請求結果並對緩存生命週期進行管理時,特別是當返回結果的體積非常大時。

代理可對重複請求所需的相同結果進行緩存,還可使用請求參數作為索引緩存的鍵值。

  • 智能引用。當一個對象被引用時提供一些額外的操作,比如將對象被調用的次數記錄下來等。可在沒有客户端使用某個重量級對象時立即銷燬該對象。

代理會將所有獲取了指向服務對象或其結果的客户端記錄在案。代理會時不時地遍歷各個客户端,檢查它們是否仍在運行。如果相應的客户端列表為空,代理就會銷燬該服務對象,釋放底層系統資源。代理還可以記錄客户端是否修改了服務對象。其他客户端還可以複用未修改的對象。

實現方式

  1. 如果沒有現成的服務接口,你就需要創建一個接口來實現代理和服務對象的可交換性。從服務類中抽取接口並非總是可行的,因為你需要對服務的所有客户端進行修改,讓它們使用接口。備選計劃是將代理作為服務類的子類,這樣代理就能繼承服務的所有接口了。
  2. 創建代理類,其中必須包含一個存儲指向服務的引用的成員變量。通常情況下,代理負責創建服務並對其整個生命週期進行管理。在一些特殊情況下,客户端會通過構造函數將服務傳遞給代理。
  3. 根據需求實現代理方法。在大部分情況下,代理在完成一些任務後應將工作委派給服務對象。
  4. 可以考慮新建一個構建方法來判斷客户端可獲取的是代理還是實際服務。 你可以在代理類中創建一個簡單的靜態方法,也可以創建一個完整的工廠方法。
  5. 可以考慮為服務對象實現延遲初始化。

優點

  • 你可以在客户端毫無察覺的情況下控制服務對象。
  • 如果客户端對服務對象的生命週期沒有特殊要求,你可以對生命週期進行管理。
  • 即使服務對象還未準備好或不存在,代理也可以正常工作。
  • 開閉原則。你可以在不對服務或客户端做出修改的情況下創 建新代理。

缺點

  • 代碼可能會變得複雜,因為需要新建許多類。
  • 服務響應可能會延遲。

與其他模式的關係

  • 適配器能為被封裝對象提供不同的接口,代理能為對象提供相同的接口,裝飾則能為對象提供加強的接口。
  • 外觀與代理的相似之處在於它們都緩存了一個複雜實體並自行對其進行初始化。代理與其服務對象遵循同一接口,使得自己和服務對象可以互換,在這一點上它與外觀不同。
  • 裝飾和代理有着相似的結構,但是其意圖卻非常不同。這兩個模式的構建都基於組合原則,也就是説一個對象應該將部分工作委派給另一個對象。兩者之間的不同之處在於代理通常自行管理其服務對象的生命週期,而裝飾的生成則總是由客户端進行控制。

實例

本例演示如何使用代理模式在第三方視頻程序庫中添加延遲初始化和緩存。

程序庫提供了視頻下載類。但是該類的效率非常低。如果客户端程序多次請求同一視頻,程序庫會反覆下載該視頻,而不會將首次下載的文件緩存下來複用。

代理類實現和原下載器相同的接口,並將所有工作委派給原下載器。不過,代理類會保存所有的文件下載記錄,如果程序多次請求同一文件,它會返回緩存的文件。

【愚公系列】2022年05月 二十三種設計模式(十二)-代理模式(Proxy Pattern)_5月月更_愚公搬代碼_#後端_06

ServiceInterface.h:

#ifndef SERVICE_INTERFACE_H_
#define SERVICE_INTERFACE_H_

#include <string>

// 遠程服務接口
class ThirdPartyTVLib {
 public:
    virtual std::string listVideos() = 0;
    virtual std::string getVideoInfo(int id) = 0;
};

#endif  // SERVICE_INTERFACE_H_

Service.h:

#ifndef SERVICE_H_
#define SERVICE_H_

#include <string>
#include "ServiceInterface.h"

// 視頻下載類
// 該類的方法可以向遠程視頻後端服務請求信息, 請求速度取決於用户和服務器的網絡狀況
// 如果同時發送大量請求, 即使所請求的信息一模一樣, 程序的速度依然會變慢
class ThirdPartyTVClass : public ThirdPartyTVLib {
 public:
    std::string listVideos() override {
        // 向遠程視頻後端服務發送一個API請求獲取視頻信息, 這裏忽略實現
        return "video list";
    }

    std::string getVideoInfo(int id) override {
        // 向遠程視頻後端服務發送一個API請求獲取某個視頻的元數據, 這裏忽略實現
        return "video info";
    }
};

#endif  //  SERVICE_H_

Proxy.h:

#ifndef PROXY_H_
#define PROXY_H_

#include <string>
#include "ServiceInterface.h"

// 為了節省網絡帶寬, 我們可以將請求緩存下來並保存一段時間
// 當代理類接受到真實請求後才會將其委派給服務對象
class CachedTVClass : public ThirdPartyTVLib {
 public:
    explicit CachedTVClass(ThirdPartyTVLib* service) : service_(service), need_reset_(false), list_cache_(""), video_cache_("") {}
    void reset() {
        need_reset_ = true;
    }

    std::string listVideos() override {
        if (list_cache_ == "" || need_reset_) {
            list_cache_ = service_->listVideos();
        }
        return list_cache_;
    }

    std::string getVideoInfo(int id) override {
        if (video_cache_ == "" || need_reset_) {
            video_cache_ = service_->getVideoInfo(id);
        }
        return video_cache_;
    }

 private:
    ThirdPartyTVLib* service_;
    std::string list_cache_;
    std::string video_cache_;
    bool need_reset_;
};

#endif  // PROXY_H_

Client.h:

#ifndef CLIENT_H_
#define CLIENT_H_

#include <string>
#include <cstdio>
#include "Service.h"

// 之前直接與服務對象交互的 GUI 類不需要改變, 前提是它僅通過接口與服務對象交互。
// 我們可以安全地傳遞一個代理對象來代替真實服務對象, 因為它們都實現了相同的接口。
class TVManager {
 public:
    explicit TVManager(ThirdPartyTVLib* s) : service_(s) {}
    void renderVideoPage(int id) {
        std::string video_info = service_->getVideoInfo(id);
        // 渲染視頻頁面, 這裏忽略實現
        printf("渲染視頻頁面: %s\n", video_info.c_str());
        return;
    }
    void renderListPanel() {
        std::string videos = service_->listVideos();
        // 渲染視頻縮略圖列表, 這裏忽略實現
        printf("渲染視頻縮略圖列表: %s\n", videos.c_str());
        return;
    }

 private:
    ThirdPartyTVLib* service_;
};

#endif  // CLIENT_H_

main.cpp:

#include "Client.h"
#include "Service.h"
#include "Proxy.h"

int main() {
    ThirdPartyTVClass* aTVService = new ThirdPartyTVClass();
    CachedTVClass* aTVProxy = new CachedTVClass(aTVService);
    TVManager* manager = new TVManager(aTVProxy);

    manager->renderVideoPage(1);
    manager->renderListPanel();

    delete aTVService;
    delete aTVProxy;
    delete manager;
}

編譯運行:

$g++ -g main.cpp -std=c++11 -o proxy
$./proxy 
渲染視頻頁面: video info
渲染視頻縮略圖列表: video list