代理模式

意图

为其他对象提供一种代理以控制对这个对象的访问

动机

GoF 中提到的例子形容的很好,我在稍微改动一下:

网页上一篇图文结合的文章,其中有许多特别大的图片 (创建的开销特别大), 当我网页打开这篇文章的时候,我需要保证网页的打开速度,不能等图片都创建完成之后在真正显示;还有一种情况,当用户没有阅读到图片的时候就关闭了网页,创建图片显得就很浪费

所以只有当真正阅读到图像的时候,图片才需要真正加载出来。但是打开网页的时候我们又需要创建一个图像的对象,上面介绍了一些情况下不直接实例化图像的原因,所以使用一个代理对象,代理真正的图像,并且代理中保存图像的尺寸信息,提供给网页图片大致的区域信息。与此同时,代理对象有跟原图像对象有一样的接口,只有当前端调用代理的 Draw 来显示图像的时候,代理才真正创建图像,并且将随后的请求转发给这个图像对象

应用场景

  • 远程代理:为一个对象在不同的地址空间提供局部代表,这个有点像 grpc,调用代理的方法,这个方法会被代理利用网络转发到远程执行,并且结果会通过网络返回给代理,在由代理将结果转给客户
  • 虚代理:根据需要创建开销很大的对象
  • 保护代理:控制对原始对象的访问,保护代理用于对象应该有不同的访问权限的时候
  • 智能指引:取代了简单的指针,它在访问对象时执行一些附加操作
    • 对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放它
    • 当第一次引用一个持久对象时,将它转入内存
    • 当访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它

简单点就是:代理模式实际上就是在访问对象的过程中引入了一定程度的间接性。相当于这时候的代理模式实际上引入了一个中间件,而中间件的存在,就可以编写出丰富的用途了

UML 图

uml

  • Subject: 定义 RealSubjectProxy 的共同接口,这样在任何使用 RealSubject 的地方都可以使用 Proxy
  • Proxy: 保存一个引用使得代理可以访问真正的对象 RealSubject
  • RealSubject: 定义被 Proxy 代理的实例

客户端操作 Proxy 时, Proxy 根据实际情况在适当的时候向 RealSubject 转发请求

来个例子

就用前文提到代理图片的例子

结合 UML 图,图片 Image 类还是代理图片的 ImageProxy 类有一个公共的父类,我这里将父类命名为 Graphic, 我指简单定义一个 Draw 绘图的接口

class Graphic {
public:
    virtual ~Graphic() {}
    virtual void Draw() = 0;
};

接下来是 Image 类, 根据用户提供的文件地址,创建一个 Image 对象, Draw 时,进行绘图

class Image :public Graphic
{
public:
    Image(std::string filepath) :m_filepath(filepath) {}
    ~Image() {}

    std::string getFilePath() { return m_filepath; }
    void Draw() { std::cout << "Draw Image" << std::endl; }
private:
    std::string m_filepath;
};

接下来是 ImageProxy, 持有一个 Image 对象,在合适的时候创建,合适的时候将消息转发给真正的 Image

class ImageProxy: public Graphic
{
public:
    ImageProxy(std::string filpath): m_image(nullptr), m_filepath(filpath) {}
    ~ImageProxy() { delete m_image; }

    std::string getFilePath() { return m_filepath; }
    void Draw() { GetImage()->Draw(); }
protected:
    Image* GetImage() {
        if (!m_image)
            m_image = new Image(m_filepath);
        return m_image;
    }
private:
    Image *m_image;
    std::string m_filepath;
};

客户端方面调用的接口

#include <iostream>
#include "image.h"

#define UNUSED(x) (void)x;
using namespace std;
int main(int argc, char *argv[])
{
    UNUSED(argc);
    UNUSED(argv);

    std::string filepath;
    ImagePtr ptr = ImagePtr(filepath);
    ptr->Draw();

    ImageProxy proxy = ImageProxy(filepath);
    proxy.Draw();

    return 0;
}

这样基本就完成了代理模式终代理的逻辑,但是如果有仔细看客户端方面调用部分的代码,会发现,我还用到一个 ImagePtr 对象

这个对象,主要是测试另一种非继承形式的代理,比如利用重载C++的存取运算符,比如仅仅只是需要创建开销很大的对象,可以尝试使用这种方法

class ImagePtr
{
public:
    ImagePtr(std::string filpath): m_image(nullptr), m_filepath(filpath) {}
    virtual ~ImagePtr() { delete m_image; }

    virtual Image* operator ->() { return LoadImage(); }
    virtual Image& operator *() { return *LoadImage(); }

private:
    Image * LoadImage() {
        if (!m_image)
            m_image = new Image(m_filepath);
        return m_image;
    }

private:
    Image *m_image;
    std::string m_filepath;
};

通过重载运算符,从而在使用 -> 或者 * 的时候,也就是引用对象的时候创建真正的对象

mainptr->Draw(); 像指针一样调用,但是实际上 ptr 是一个栈上的资源,有没有一种智能指针的感觉? 其实智能指针通过栈上的资源去代理管理堆上的生命周期,避免内存泄漏等问题,保证了资源的安全可靠。

当然这种方法相对于前一种实现有个很明显的问题,当我的图像需要在一个特定的时刻才会创建比如 Draw, 而不是引用这个图像就创建它,所以重载运算符并非对每一种代理来说都是好办法。

小结

如果看过之前总结的 适配器模式, 它们在形式上是有一定相似处的,客户端都是通过一个中间件访问真正的对象,适配器模式 通过 Adapter 来访问真正的 Adaptee代理模式 通过 Porxy 来访问真正的 RealSubject

但是结构上存在区别, Adapter 一般情况下继承自 Subject, 被适配的对象有不同于 Subject 的接口, 而代理模式中,ProxyRealSubject 都是继承自 Subject, 它们具有相同的接口

于此同时,它们的动机还是存在很大区别,适配器模式用来帮助无关的类协同工作,它通常在系统设计完成后才会被使用,而代理模式则是利用中间件的间接性,来实现更加灵活地功能。