意图
为其他对象提供一种代理以控制对这个对象的访问
动机
GoF
中提到的例子形容的很好,我在稍微改动一下:
网页上一篇图文结合的文章,其中有许多特别大的图片 (创建的开销特别大), 当我网页打开这篇文章的时候,我需要保证网页的打开速度,不能等图片都创建完成之后在真正显示;还有一种情况,当用户没有阅读到图片的时候就关闭了网页,创建图片显得就很浪费
所以只有当真正阅读到图像的时候,图片才需要真正加载出来。但是打开网页的时候我们又需要创建一个图像的对象,上面介绍了一些情况下不直接实例化图像的原因,所以使用一个代理对象,代理真正的图像,并且代理中保存图像的尺寸信息,提供给网页图片大致的区域信息。与此同时,代理对象有跟原图像对象有一样的接口,只有当前端调用代理的 Draw 来显示图像的时候,代理才真正创建图像,并且将随后的请求转发给这个图像对象
应用场景
- 远程代理:为一个对象在不同的地址空间提供局部代表,这个有点像
grpc
,调用代理的方法,这个方法会被代理利用网络转发到远程执行,并且结果会通过网络返回给代理,在由代理将结果转给客户 - 虚代理:根据需要创建开销很大的对象
- 保护代理:控制对原始对象的访问,保护代理用于对象应该有不同的访问权限的时候
- 智能指引:取代了简单的指针,它在访问对象时执行一些附加操作
- 对指向实际对象的引用计数,这样当该对象没有引用时,可以自动释放它
- 当第一次引用一个持久对象时,将它转入内存
- 当访问一个实际对象前,检查是否已经锁定了它,以确保其他对象不能改变它
简单点就是:代理模式实际上就是在访问对象的过程中引入了一定程度的间接性。相当于这时候的代理模式实际上引入了一个中间件,而中间件的存在,就可以编写出丰富的用途了
UML 图
Subject
: 定义RealSubject
和Proxy
的共同接口,这样在任何使用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;
};
通过重载运算符,从而在使用 ->
或者 *
的时候,也就是引用对象的时候创建真正的对象
在 main
中 ptr->Draw();
像指针一样调用,但是实际上 ptr
是一个栈上的资源,有没有一种智能指针的感觉? 其实智能指针通过栈上的资源去代理管理堆上的生命周期,避免内存泄漏等问题,保证了资源的安全可靠。
当然这种方法相对于前一种实现有个很明显的问题,当我的图像需要在一个特定的时刻才会创建比如 Draw
, 而不是引用这个图像就创建它,所以重载运算符并非对每一种代理来说都是好办法。
小结
如果看过之前总结的 适配器模式
, 它们在形式上是有一定相似处的,客户端都是通过一个中间件访问真正的对象,适配器模式
通过 Adapter
来访问真正的 Adaptee
, 代理模式
通过 Porxy
来访问真正的 RealSubject
但是结构上存在区别, Adapter
一般情况下继承自 Subject
, 被适配的对象有不同于 Subject
的接口, 而代理模式中,Proxy
和 RealSubject
都是继承自 Subject
, 它们具有相同的接口
于此同时,它们的动机还是存在很大区别,适配器模式用来帮助无关的类协同工作,它通常在系统设计完成后才会被使用,而代理模式则是利用中间件的间接性,来实现更加灵活地功能。