一、代理模式概述
在 C++ 中,代理模式是一种结构型设计模式,它为其他对象提供一种代理,从而控制对这个对象的访问。代理模式的作用主要体现在以下几个方面:
- 职责清晰:真实的角色即实现实际的业务逻辑,不用关心其他非本职的事务,通过后期的代理来完成事务,使得编程简洁清晰。例如在视频网站的例子中,委托类FixBugVideoSite只负责实现视频网站的各种视频播放功能,而代理类FreeVideSiteProxy和vipVideSiteProxy则负责根据用户身份控制访问权限。
- 保护目标对象:代理对象可以在客户端和目标对象之间起到中介的作用,保护了目标对象。比如在访问真正的服务器需要通过代理服务器的例子中,代理服务器可以进行用户名密码校验,只有通过校验才允许访问真实服务器,从而保护了真实服务器。
- 高扩展性:具体主题角色可变,只要实现了接口函数,其代理类就无需修改,原样使用。例如项目中需要用到stl中的queue,可以创建一个代理类ProQueue,封装需要用到queue的push()和pop()函数,当queue发生变化时,只需要修改代理类中与queue的耦合部分即可。
代理模式通过引入代理对象,在不改变目标对象的情况下,实现了对目标对象的控制和扩展。这种模式在 C++ 编程中有着广泛的应用,可以提高程序的可维护性和可扩展性。
二、代理模式的结构与角色
(一)抽象主题角色
抽象主题角色可以是抽象类也可以是接口,它定义了真实主题和代理对象实现的业务方法,为 RealSubject 和 Proxy 提供了共用接口。在实际应用中,这个接口可以包含一系列的方法声明,这些方法将由具体主题角色和代理主题角色来实现。例如在一个网络访问的场景中,抽象主题角色可以定义一个request方法,用于发起网络请求。这个方法在具体主题角色(真实的网络访问对象)和代理主题角色(可能是一个网络访问代理,用于处理缓存、权限验证等额外功能)中都需要实现。
(二)具体主题角色
具体主题角色是业务逻辑的具体执行者,它定义了 Proxy 所代表的真实实体。以一个文件读取的例子来说,具体主题角色可能是一个FileReader类,它负责实际的文件读取操作。这个类实现了抽象主题角色中定义的方法,比如读取文件内容并返回。具体主题角色通常专注于实现特定的业务功能,而不关心其他非业务相关的事务,如权限验证、日志记录等。
(三)代理主题角色
代理主题角色保存一个引用使得代理可以访问实体,并提供与抽象主题相同的接口,代替客户端与真实主题交互。它可以在交互前后进行预处理和善后处理工作。例如在一个数据库访问的场景中,代理主题角色可以在客户端请求访问数据库之前进行权限验证,在访问之后进行日志记录。代理主题角色通过这种方式在不改变具体主题角色的情况下,为系统增加了额外的功能和控制。同时,代理主题角色可以根据具体的需求,选择是否将请求转发给具体主题角色,或者直接返回一个预设的结果。这种灵活性使得代理模式在很多场景下都非常有用,比如缓存代理、远程代理等。
三、代理模式的分类与应用场景
(一)远程代理
远程代理为一个对象在不同的地址空间提供局部代表,所有网络通讯操作交给代理去做,让客户可以忽略这些被代理的对象是不是远程的。例如,在分布式系统中,客户端可能需要访问位于远程服务器上的对象。通过远程代理,客户端可以像访问本地对象一样访问远程对象,而无需关心网络通信的细节。远程代理负责对请求及其参数进行编码,并向不同地址空间中的实体发送已编码的请求。调用代理的方法,会被代理利用网络转发到远程执行,并且结果会通过网络返回给代理,再由代理将结果转给客户。
(二)虚代理
虚代理用于开销很大,需要较长时间实例化的对象。虚代理可以缓存实体的附加信息,以便延迟对它的访问。直到我们真正需要一个对象的时候才创建它,比如当加载图片时,我们打开不同的相册,才会去显示所选相册的图片。当对象在创建前和创建中时,由虚拟代理来扮演对象的替身。对象创建后,代理就会将请求直接委托给对象。例如在文档编辑器中,有些图形对象的创建开销很大。但是打开文档必须很迅速,因此我们在打开文档时应避免一次性创建所有开销很大的对象。这里就可以运用代理模式,在打开文档时,并不打开图形对象,而是打开图形对象的代理以替代真实的图形。待到真正需要打开图形时,仍由代理负责打开。
(三)安全代理
安全代理控制真实对象的访问权限,用于对象有不同访问权限的时候。例如,有一个员工对象,保护代理可以允许普通员工调用对象的某些方法,管理员调用其他方法。安全代理添加信息,检查调用者是否具有实现一个请求所必须的访问权限。
(四)智能引用代理
智能引用代理取代了简单的指针,它在访问对象时执行一些附加操作,比如将对象被调用的次数记录下来等。例如,C++ 中的auto_ptr和smart_ptr就是智能引用的例子。auto_ptr类就是一个代理,客户只需操作auto_ptr的对象,而不需要与被代理的指针pointee打交道。auto_ptr的好处在于为动态分配的对象提供异常安全。因为它用一个对象存储需要被自动释放的资源,然后依靠对象的析构函数来释放资源。这样客户就不需要关注资源的释放,由auto_ptr对象自动完成。smart_ptr采用了引用计数的策略,当一个对象被引用时,引用计数器加 1,当对象的引用计数为 0 时,自动释放对象所占用的资源。
四、代理模式的代码实现与示例
(一)远程代理示例
以下是一个使用 C++ 实现远程代理的示例代码:
#include <iostream>
#include <string>
// 接口类
class Subject {
public:
virtual void request() = 0;
};
// 远程主题类
class RemoteSubject : public Subject {
public:
void request() override {
std::cout << "RemoteSubject: 处理远程请求" << std::endl;
}
};
// 代理类
class Proxy : public Subject {
private:
RemoteSubject* remoteSubject;
public:
Proxy() : remoteSubject(nullptr) {}
void request() override {
if (remoteSubject == nullptr) {
// 创建远程对象
remoteSubject = new RemoteSubject();
}
// 在调用远程主题前进行网络连接等操作
std::cout << "Proxy: 进行网络连接" << std::endl;
// 调用远程主题
remoteSubject->request();
// 在调用远程主题后进行网络关闭等操作
std::cout << "Proxy: 关闭网络连接" << std::endl;
}
};
int main() {
Proxy proxy;
proxy.request();
return 0;
}
在这个示例中,Subject是一个接口类,RemoteSubject是远程的处理类,Proxy是代理类。当调用Proxy的request方法时,它会在调用远程主题前后进行网络连接和关闭操作,实现了远程代理的功能。
(二)虚代理示例
#include <iostream>
#include <string>
class RealObject {
public:
void performAction() {
std::cout << "RealObject: Performing expensive action." << std::endl;
}
};
class VirtualProxy {
private:
RealObject* realObject = nullptr;
public:
void performAction() {
if (realObject == nullptr) {
realObject = new RealObject();
}
realObject->performAction();
}
};
int main() {
VirtualProxy proxy;
// 在真正需要时才会实例化真实对象
proxy.performAction();
return 0;
}
在这个例子中,VirtualProxy是虚代理类,它在需要的时候才创建RealObject,避免了在不必要的时候进行昂贵的对象实例化操作。
(三)安全代理示例
#include <iostream>
#include <string>
class SecuredObject {
public:
void performActionForAdmin() {
std::cout << "SecuredObject: Admin action performed." << std::endl;
}
void performActionForUser() {
std::cout << "SecuredObject: User action performed." << std::endl;
}
};
class SecurityProxy {
private:
SecuredObject* securedObject;
std::string userRole;
public:
SecurityProxy(std::string role) : userRole(role), securedObject(nullptr) {}
void performAction() {
if (securedObject == nullptr) {
securedObject = new SecuredObject();
}
if (userRole == "admin") {
securedObject->performActionForAdmin();
} else {
securedObject->performActionForUser();
}
}
};
int main() {
SecurityProxy adminProxy("admin");
adminProxy.performAction();
SecurityProxy userProxy("user");
userProxy.performAction();
return 0;
}
这个示例展示了安全代理的实现,根据用户的角色来控制对真实对象的访问权限。
(四)智能引用代理示例
#include <iostream>
class Resource {
public:
void useResource() {
std::cout << "Resource: Resource being used." << std::endl;
}
};
class SmartProxy {
private:
Resource* resource;
int referenceCount;
public:
SmartProxy() : resource(nullptr), referenceCount(0) {}
~SmartProxy() {
if (resource!= nullptr && referenceCount == 0) {
delete resource;
}
}
void useResource() {
if (resource == nullptr) {
resource = new Resource();
}
referenceCount++;
resource->useResource();
}
void releaseResource() {
referenceCount--;
if (resource!= nullptr && referenceCount == 0) {
delete resource;
resource = nullptr;
}
}
};
int main() {
SmartProxy proxy;
proxy.useResource();
proxy.useResource();
proxy.releaseResource();
proxy.releaseResource();
return 0;
}
这里的SmartProxy实现了智能引用代理,通过引用计数来管理资源的生命周期。
五、代理模式的优缺点
(一)优点
- 职责清晰:真实对象专注于自身业务逻辑,将非本职内容交给代理完成,使得编程简洁清晰。例如在游戏代理的场景中,真实游戏类只负责游戏的加载和退出等核心业务,而代理类则负责处理充值等额外功能,两者职责明确,互不干扰。
- 高扩展性:具体主题角色可变,只要实现了接口函数,其代理类就无需修改,原样使用。例如在网络编程中,如果需要更换远程服务器的实现,只需要修改真实主题角色的实现,而代理类可以保持不变,继续提供对新的真实主题角色的代理功能。
- 保护目标对象:代理对象可以在客户端和目标对象之间起到中介的作用,保护了目标对象。例如在安全代理的例子中,代理可以控制对真实对象的访问权限,防止未经授权的访问,从而保护了真实对象的安全。
- 提高性能:虚拟代理可以减少系统资源的消耗。例如在加载大型图片或复杂图形对象时,虚拟代理可以先显示一个占位符,等到真正需要显示图片或图形对象时再进行加载,这样可以减少系统在启动时的资源消耗,提高系统的启动速度。
(二)缺点
- 请求速度降低:由于在客户端和真实对象中加入了代理,一定程度上会降低整个系统流程的运行效率。例如在远程代理的例子中,每次访问远程对象都需要通过代理进行网络通信,这会增加请求的响应时间,降低系统的性能。
- 增加系统复杂度:代理的职责往往较冗杂,需要实现额外的功能,如权限验证、日志记录等。同时,为每个原对象创建一个对应的代理对象也会增加系统的复杂度。例如在安全代理的例子中,代理需要实现不同用户角色的权限验证逻辑,这增加了代码的复杂性。
- 可能引入额外的抽象层问题:这种模式引入了另一个抽象层,这有时可能是一个问题。如果真实主题被某些客户端直接访问,并且其中一些客户端可能访问代理类,这可能会导致不同的行为。例如在智能引用代理的例子中,如果客户端直接访问真实对象,而不是通过代理访问,那么引用计数的功能就无法正常工作,可能会导致资源泄漏等问题。
总的来说,代理模式在带来诸多优点的同时,也存在一些缺点。在实际应用中,需要根据具体的需求和场景,权衡代理模式的优缺点,选择是否使用代理模式。如果对性能要求较高,或者系统的复杂度已经很高,那么可能需要谨慎考虑使用代理模式。但如果需要实现对目标对象的访问控制、保护目标对象或者提高系统的可扩展性,那么代理模式可能是一个不错的选择。
六、代理模式与其他模式的区别
(一)代理模式与适配器模式
- 接口不同:代理模式提供的接口和原来的接口是一样的,代理模式的作用是不把实现直接暴露给 client,而是通过代理这个层,并且代理能够做一些处理。例如在网络访问的场景中,代理对象可以在客户端请求访问网络资源之前进行权限验证等操作,接口与真实的网络访问对象一致。而适配器模式是因为新旧接口不一致导致出现了客户端无法得到满足的问题,但是旧的接口不能被完全重构,而我们想使用实现了这个接口的服务,就应该把新接口转换成旧接口。比如在 Java 中早期的枚举接口是Enumeration而后定义的枚举接口是Iterator,有很多旧的类实现了enumeration接口暴露出了一些服务,但是这些服务我们现在想通过传入Iterator接口而不是Enumeration接口来调用,这时就需要一个适配器。
- 目的不同:代理模式的目的是控制对一个对象的访问,在真正的业务处理前后进行一些处理,如进行权限验证、日志记录等。而适配器模式的目的是将一个类的接口转接成用户所期待的,使因接口不兼容而不能在一起工作的类能够工作在一起。
- 实现方式不同:代理模式通常是在代理类中保存一个对真实主题对象的引用,在代理类的方法中调用真实主题对象的方法,并在调用前后进行一些额外的处理。例如在安全代理的例子中,安全代理类保存对被保护对象的引用,根据用户角色在调用被保护对象的方法前进行权限验证。适配器模式则是通过创建一个新的类,将新接口转换为旧接口,使得客户端能够使用旧的接口调用新的服务。例如在 Java 枚举接口转换的例子中,创建一个适配器类,将Iterator接口转换为Enumeration接口,使得实现了旧接口的服务能够被新的客户端使用。
(二)代理模式与装饰模式
- 接口相同但目的不同:代理模式和装饰模式都实现同一个接口。代理模式关注于控制对对象的访问,例如在访问真正的服务器需要通过代理服务器的例子中,代理服务器可以进行用户名密码校验,只有通过校验才允许访问真实服务器,从而保护了真实服务器。而装饰模式关注于在一个对象上动态的添加方法,以对已有的业务逻辑进一步的封装,使其增加额外的功能。比如在 Java 中的 IO 流就使用了装饰者模式,用户在使用的时候,可以任意组装,达到自己想要的效果。
- 实现方式略有不同:代理模式通常在代理类中创建一个对象的实例,并在调用真实对象的方法前后进行一些控制操作。例如在远程代理的例子中,代理类在调用远程对象的方法前进行网络连接等操作,调用后进行网络关闭等操作。装饰模式通常将原始对象作为一个参数传给装饰者的构造器,通过层层嵌套的方式对对象进行装饰。例如在三明治的例子中,最里面先创建一个香肠对象,然后在香肠的外面依次包裹奶油、蔬菜,最后用面包夹住,每一层装饰都是通过构造器传入被装饰的对象来实现的。
综上所述,代理模式与适配器模式、装饰模式在接口、目的和实现方式上都存在不同,在实际应用中需要根据具体的需求选择合适的设计模式。