代理模式也是用于强化对象的功能,但其目标是在提供某些内部功能的同时准确(或尽可能地)保留正在使用的API。
代理模式不是一种同质的模式,因为人们构建的不同类型的代理相当多,并且服务于不同的目的。本章将介绍一些不同的代理对象。
1. 智能指针
智能指针是代理模式最简单而且最直接的展示:智能指针是一个包装类,其中封装了原始指针,同时维护着一个引用计数,并重载了部分运算符。但总体来说,智能指针提供了原始指针所具有的接口,基本上在原始指针出现的位置,都可以使用智能指针。
当然二者之间也有差别,最明显地差异在于,对于智能指针,不必再调用delete。
2. 属性代理
在其他编程语言中,术语“属性”表示底层成员与该成员的getter/setter
方法的组合。
C++中没有内置属性的支持;最常见的方法是创建一对与该属性成员名称类似的get/set方法。
然而,这意味着如果要操作x.foo
,我们必须分别调用x.get_foo()
和x.set_foo(value)
。
但是,如果我们想继续使用属性成员的访问语法(即x.foo
),同时为其提供特定的访问器/修改器,那么就可以构建一个属性代理。
本质上,属性代理是一个可以根据使用语义伪装成普通成员的类。可以这样定义:
template <typename T>
struct Property
{
T value_;
Property(const T init_value)
{
*this = init_value;
}
operator T() //隐式类型转换,将Property隐式转换为T类型
{
// perform some getter action
return value_;
}
T operator =(T new_value) //赋值运算符重载
{
//perform some setter here
return value_ = new_value;
}
};
本质上,类Property<T>
是底层T
类型的替代品,不管这个类型是什么。它仅仅是允许与T
相互转换,让二者都在幕后使用value
成员。
3. 虚拟代理
如果试图对nullptr
或未初始化指针解引用,那么就是在自找麻烦。
但是在某些情况下,我们只希望在访问对象时再构造该对象,而不希望过早地为它分配内存,因此在实际使用它之前将其保持为nullptr
或类似未初始化的状态。
这种方法称为惰性实例化(lazy instantiation)或惰性加载(lazy loading)。
如果确切地知道哪些地方需要这种延迟行为,则可以提前计划并为它们制定特别的规定。
但如果不知道,则可以构建一个代理,让该代理接受现有对象并使其成为惰性对象。
我们称之为虚拟代理,因为底层对象可能根本不存在,所以我们不是在访问具体的对象,而是在访问虚拟的对象。
以Image接口为例进行说明:一个典型的Image
接口如下:
struct Image
{
virtual void draw() = 0;
};
类Bitmap
实现了Image
接口,它的eager模式的实现将在构建时就从文件加载图像,即使该图像实际上并不需要任何东西:
struct Bitmap : Image
{
std::string filename_;
Bitmap(const std::string& filename) : filename_{filename}
{
std::cout << "Loading image from " << filename << std::endl;
//image gets loaded here
}
void draw() override
{
std::cout << "Drawing image " << filename_ << std::endl;
}
};
上面的实现并不是我们想要的,我们需要的是在调用draw()
方法时才加载图像。现在我们将它变为惰性(Lazy)模式,但要假设它是固定不变的且不可修改(或者说是不可继承的):
struct LazyBitmap : Image
{
private:
Bitmap* bmp{nullptr};
std::string filename_;
public:
LazyBitmap(const std::string& filename) : filename_{filename} {}
~LazyBitmap() {
delete bmp; //允许对空指针进行delete操作
// bmp = nullptr; //关于指针是否在delete之后置空,存在争议。
}
void draw() override
{
if(!bmp) {
bmp = new Bitmap(filename_);
}
bmp->draw();
}
};
如上代码所示,这个LazyBitmap
的构造函数要轻量得多,它所做的只是存储要从中加载图像的文件名,仅此而已——图像不会立即加载。所有神奇的事情都发生在draw()
中。
4. 通信代理
允许在改变了对象的物理位置(例如移动到了云上)的情况下使用同样的API;
具体介绍略。
5.值代理
值代理是某个值的代理,通常封装原语类型,并根据其用途提供增强的功能。
例子:我们考虑需要将一些值传递到一个函数中。该函数既可以接受具体的固定值,也可以在运行时从预定义数据集合中选择随机值。
一种方法是修改这个函数并引入几个重载函数,不过我们要修改函数的参数类型。
另一种方法是使用值代理:我们引入一个辅助类,这个类只有一个纯虚函数,负责执行隐式类型转换:
template <typename T>
struct Value
{
//该函数负责执行隐式类型转换
virtual operator T() const = 0;
};
基于该辅助类,引入一个代表常量值的类:
template <typename T>
struct Const : Value<T>
{
const T v_;
Const() :v_{} {}
Const(T v) : v_{v} {}
operator T() const override
{
return v_;
}
};
类似地,引入一个从一组不同的值中以相同的概率随机选择一个值的类:
template <typename T>
struct OneOf : Value<T>
{
std::vector<T> values_;
OneOf() : values_{{T{}}} {} //这是什么语法??
OneOf(std::initializer_list<T> val) : values_{val} {}
operator T() const override
{
return values_[rand() % values_.size()];
}
};
然后,就可以在应用程序中使用这些类型了。
6. 总结
与装饰器模式不同,代理不会尝试通过添加新成员来扩展对象的功能————它所做的只是强化现有成员的潜在行为。代理主要作为一种替代品,并且有许多不同的类型:
- 属性代理:是底层成员的替身,可以在分配或访问期间替换成员并执行其他操作。
- 虚拟代理:为底层成员提供虚拟访问的接口,并且实现了诸如惰性加载的功能。
- 通信代理:允许在改变了对象的物理位置的情况下使用同样的API。
- 值代理:可以替换单个(标量)值,并为其赋予额外的功能。
除此之外,还有很多其他代理,我们自己构建的代理很可能不属于预先存在的类别,不过它们会在我们的应用程序中执行具体而便捷的操作。