谈谈RAII惯用法-代理对象RAII(上)
转发自公众号文章:谈谈RAII惯用法-代理对象RAII(上)
本文介绍一种特殊的RAII对象,同时它也是一个代理对象,能够代理某个对象,并在对对象的访问操作时进行控制。
下面看一个动物类的类层次体系:
class animal {
public:
virtual ~animal() = default;
virtual void run() = 0;
};
class dog : public animal {
public:
virtual void run() override {
puts("car is running");
}
};
class rabbit : public animal {
public:
virtual void run() override {
puts("rabbit is running");
}
};
假设要对animal的run()成员函数在调用时进行一些log输出,比如在调用它的前后位置的地方打印时间信息。因为dog和rabbit都是animal的派生类,它们都重写了run(),可以在基类中通过多态机制调用派生类的run()函数,并在调用前后添加相应的功能。
class animal {
void before() {
// 输出时间的代码;
}
void after() {
// 输出时间的代码;
}
public:
virtual ~animal() = default;
virtual void run() = 0;
void log_run() {
before();
run();
after();
}
};
显然只要是animal类的派生类,例如dog和rabbit,都可以通过调用log_run()来输出日志,这是设计模式中模板方法模式的典型应用。
假设我们有一个汽车类,它也有一个相同的run()成员函数:
class car {
public:
void run() {
puts("car is running");
}
};
如果也需要在调用它的成员函数run()的前后位置处,进行功能相同的log输出,因为car不是animal类的派生类,显然无法按照上面介绍的模板方法模式来实现。
通常情况下,可以使用相同的设计方案,并编写重复的代码来实现:让car是一个基类的派生类,比如基类是一个交通工具类-vehicle类,这样也像animal那样,在vehicle类中来实现模板方法,显然这是面向对象设计的典型思维模式。
显然,尽管dog类和car类都有相同的run()成员函数,但是因为它们没有共同的基类,不属于同一个类层次体系,需要为它们分别设计不同的类层次体系结构。
这种OOP方案虽然常见,但是并不灵活,需要在设计阶段就预先定义好各个类的层次体系,我们知道类之间的继承关系是静态的,在编程时是无法动态扩展的,不同于组合关系。
我们不妨按照OOP的思想来分析一下,这个需求实际上是如何为一组类的方法扩展相同的功能,如果这组类属于同一个基类的派生类,可以按照基类、派生类的继承路线进行扩展,即上面说的模板方法模式:在基类animal中实现了before()和after(),它们用于控制调用dog和rabbit的run()函数,从而为它们的run()扩展了相同的功能。
那么问题来了,如果任意一个类中都提供了void run()成员函数,如何使用最简单的编程方式为它们扩展相同的功能呢?
其实,除了使用模板方法模式实现外,还有一种实现方式是使用代理模式,它使用的是组合机制,比使用继承的模板方法模式更为灵活。
代理模式是为其它对象提供一种代理以控制对这个对象的访问。
在某些情况下,一个对象不直接使用另一个对象,而是通过一个代理对象在客户端和目标对象之间起到中介的作用,这个代理对象提供了和目标对象完全一样的功能接口,客户端访问这个代理对象就像是在访问目标对象一样,同时代理对象根据需要在访问目标对象时进行了控制。
具体实现是:目标类和代理类都实现了某个接口,同时目标类又是代理类中的数据成员,在代理类实现接口方法时,要调用目标类的接口,同时在调用前后进行控制。
可见,代理模式不是通过继承的方式扩展功能,而是使用组合的方式,在代理对象内部引用目标对象,从而在调用目标对象的方法时,进行控制,只要是有相同的接口,在编程时就能通过引用的方式来组合。
无论对于Dog还是Car,它们都有相同的run()成员函数,按照Java语言的说法,尽管它们不是同一个类层次体系中的类,但是可以让它们实现一个相同的接口interface,这是它们共同的特点:
interface Runnable {
void run();
}
class Dog implements Runnable {
void run() {...}
}
class Car implements Runnable {
void run() {...}
}
因此,在Java语言中,尽管Car和Dog都没有相同的基类,但因为它们都实现了相同的接口Runnable,可以使用代理模式来为它们的调用实现日志输出:
class Proxy implements Runnable {
private Runnable subject;
public Proxy(Runnable runnable) {
this.subject = runnable;
}
@Override
public void run() {
before();
subject.run();
after();
}
}
}
它们都实现了Runnable接口,Car和Dog都是被代理的角色,它们都可以被Proxy代理角色引用,并且在Proxy调用它们的run()时可以添加控制功能。虽然Dog和Car不属于同一个类层次体系的对象,但是在Proxy看来,它们都可以通过Runnable接口来调用它们的run()方法。
下面是这种模式的应用场景:
void test(Runnable runnable) {
Proxy proxy = new Proxy(runnable);
proxy.run();
}
void client() {
Dog dog = new Dog();
test(dog);
Car car = new Car();
test(car);
}
下面看一下两种实现模式的类结构关系图。
模板方法模式的animal和dog、rabbit类结构图:
显然,这里走的从基类到派生类的路线,是一种沿着类层次的“纵向”方向(如图中的红色虚线)进行的功能扩展。因为在纵向方向,派生类可以继承上层基类的功能,基类扩展了功能,派生类也能继承下来。其实,OOP的封装、继承、多态机制就是沿着“基类-派生类”这条“纵向”路线进行设计的。
代理模式的Proxy和Dog、Car类结构图:
显然Dog、Car这些类,和Proxy类的关系是组合引用关系,它们都实现了Runnable接口,Proxy类的功能是控制作用,虽然Proxy类控制、扩展了Dog和Car这些类的run()方法的执行过程,但是它们在类结构中只是简单的平行关系(横向),不同于继承关系中的基类和派生类,它们有层次关系(纵向)。
Dog和Car属于不同的类,在概念上它们之间没有直接的关联关系,和Proxy也没有直接的关联关系,但是如果从水平方向上看待这些类,如同在这些对象上面横向切开了一个切面(如果把图中的红色虚线,看作是一个横向“切面”的话),在这个切面中有相同的run()函数,可以使用相同的机制来统一控制它,所以有一个专门的术语:面向切面编程-Aspect Oriented Programming,简称AOP。
如果OOP是在类层次“纵向”方向的面向继承和多态的编程技术,而AOP就是没有层次关系的不同类在“横向”方向的面向切面函数的技术,它是OOP的补充。
其实,我们从Proxy类实现就能看出,实现一个已有类的代理类非常简单,而且实现的代码也已经套路化了。因此,Java语言为了方便程序员开发,减低开发时的编写代码的工作量,提供了动态代理机制,可以由Java程序在运行时,为一个interface的实现类动态的生成代理类,显然这就为开发AOP带来了方便。
但是C++语言是没有动态反射机制的,也就无法像Java语言那样能够为一个接口类提供动态代理的实现机制。那么在C++中能否也能像Java那样,可以方便的实现AOP吗?
我们知道,C++有模板技术,只要对象类型中的成员函数符合模板参数类型的调用要求,就可以在模板中调用它,不管对象是什么类型。下面看一下C++的一个实现方案。
先定义两个函数:
#include <format> // 使用C++20编译
void before() {
auto now = std::chrono::system_clock::now();
std::cout << std::format("开始时间: {:%Y-%m-%d %H:%M:%S}", now) << std::endl;;
}
void after() {
auto now = std::chrono::system_clock::now();
std::cout << std::format("结束时间: {:%Y-%m-%d %H:%M:%S}", now) << std::endl;
}
再使用模板定义一个AOP的实现,只要一个类型有run()成员函数,就能在调用run()的前后位置处使用before和after:
template<typename T>
void test_aop(T t) {
before();
t.run();
after();
}
显然可以用于dog和car类型:
int main() {
test_aop(dog{});
test_aop(car{});
}
虽然能满足要求,但是程序存在异常不安全的风险,假如在before()执行时申请了内存或者互斥锁等资源,但是在中间某个环节发生了异常,最后没有调用after(),导致资源没有释放。
因此在这里,可以把RAII的概念延伸一下,在构造函数中获取资源,也可以认为是一个“开始”的操作,例如分配内存、获得锁、打开文件、建立连接、开始记录等操作,而析构函数中释放资源,也可以认为是一个“结束”的操作,例如释放内存、释放锁、关闭文件、断开连接、结束记录的等操作,也就是在构造函数和析构函数中完成一对相对应的操作。
显然,既然有“始”,就得有“终”,为了防止发生只有“始”,因为程序异常导致而没有发生“终”的错误,可以使用RAII惯用法进行处理。在这里可以把输出开始的时间看作是“始“,而把输出结束的时间看作是”终”,那么使用RAII机制来实现也就是不二之选了。
封装一个RAII类用于AOP操作,它的构造函数实现before的逻辑,而析构函数实现after的逻辑:
#include <format> // 使用C++20编译
struct Aop {
Aop() {
auto now = std::chrono::system_clock::now();
std::cout << std::format("开始时间: {:%Y-%m-%d %H:%M:%S}", now) << std::endl;;
}
~Aop() {
auto now = std::chrono::system_clock::now();
std::cout << std::format("结束时间: {:%Y-%m-%d %H:%M:%S}", now) << std::endl;
}
};
那么,使用AOP的方案就修改为:
template<typename T>
void test_aop(T t) {
Aop aop;
t.run();
}
运行前面dog和car的测试代码,运行结果显然达到了目的:
开始时间: 2026-03-15 02:31:43.721583338
dog is running
结束时间: 2026-03-15 02:31:43.721649916
开始时间: 2026-03-15 02:31:43.721659769
car is running
结束时间: 2026-03-15 02:31:43.721667174
不过,使用时仍有不便之处,还可以继续改进,下一篇继续讨论。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)