今天读第十一种设计模式: 代理模式.
代理模式通常和装饰器模式一起对比出现, 装饰器模式一般适用于为类增添一些额外的功能, 而代理模式则是在尽量保持和原类一致的情况下(尽量保留一致的 API
), 为其他对象提供一种代理以控制对这个对象的访问.
不过Proxy
并不是真正的同质 API
,因为人们构建的不同种类的代理数量相当多并且服务完全不同目的。
代理模式一般有这样的核心结构:
既然是 Morden c++
那么我们肯定会用到智能指针, 好的, 这就是我们最熟悉的代理模式例子.
智能指针对普通指针做了封装, 同时增加了引用计数, 重写了部分运算符, 但是在使用时, 我们发现他和普通指针几乎一样.
#include
#include
using namespace std;
struct BankAccount {
int deposit_;
explicit BankAccount(const int deposit) : deposit_{deposit} {}
void deposit() const {
std::cout << "Your account have " << deposit_ << " yuan" << std::endl;
}
};
void test() {
const auto* ba = new BankAccount{1};
ba->deposit();
delete ba;
const auto ba2 = make_shared<BankAccount>(2);
ba2->deposit();
}
int main() {
test();
}
程序输出如下:
Your account have 1 yuan
Your account have 2 yuan
可见,无论ba
是普通指针还是智能指针,*ba
在这两种情况下可以获得底层对象。而且智能指针在某些地方可以用来替代普通指针(前者更安全)。
此外也有一些差异, 我们的 shared_ptr
有更多的功能而且采用引用计数可以自动回收.
在其他编程语言中,属性用于指示字段和该字段的一组getter/setter
方法。在 c++
中没有属性,但是如果我们想继续使用一个字段,同时给它特定的访问/修改(accessor/mutator
)行为,我们可以构建一个属性代理.
本质上讲属性代理是一个可以伪装成属性的类,所以我们这样定义:
template <typename T>
struct Property {
T value_;
explicit Property(const T initial_value) : value_{initial_value} {}
explicit operator T() const {
// 执行一些getter操作
return value_;
}
Property& operator=(const T& new_value) {
value_ = new_value;
// 执行一些setter操作
return *this;
}
};
现在从行为上来说, Property
显然伪装得很好, 我们可以像使用一个基本类型那样来使用它, 从本质上说我们的类Property
是 T
的替换, 不管它是什么。它的工作原理是简单地允许与T的转换,并允许两者都使用 value
字段.
struct Creature {
Property<int> strength{10};
Property<int> agility{5};
};
void test() {
Creature creature;
creature.agility = 20;
auto x = creature.strength;
}
一般来说我们在一个字段上的操作也能在属性代理类型的字段上工作.
某些情况下我们期望对象只在真正访问时才创造(类似单例的懒汉式)而不是立刻创造.
这种方法被称为延迟实例化. 如果需求已经明确那么可以提前准备. 如果不明确何时实例化对象, 那么我们可以构建一个接受现有对象并使其懒惰的代理, 也就是一个 虚拟代理
, 由于底层对象可能不存在, 所以我们访问这个虚拟代理而非底层对象.
我们用图片加载作为例子:
#include
#include
#include
#include
// 1. Subject接口
class Image {
public:
virtual ~Image() = default;
virtual void display() const = 0;
};
// 2. RealSubject:真实图片类
class RealImage final : public Image {
public:
explicit RealImage(std::string filename)
: filename_(std::move(filename)) {
loadFromDisk();
}
void display() const override {
std::cout << "Displaying image: " << filename_ << std::endl;
}
private:
void loadFromDisk() const {
std::cout << "Loading heavy image: " << filename_ << " (Costly operation)" << std::endl;
}
std::string filename_;
};
// 3. Proxy:控制图片的延迟加载
class ProxyImage final : public Image {
private:
std::string filename_;
mutable std::unique_ptr<RealImage> real_image_; // mutable允许在const方法中修改
public:
explicit ProxyImage(std::string filename)
: filename_(std::move(filename)) {}
void display() const override {
if (!real_image_) {
// 延迟初始化:仅在第一次调用display时加载真实图片
real_image_ = std::make_unique<RealImage>(filename_);
}
real_image_->display();
}
};
我们现在有了本文开头提到的三个类, 一个抽象的接口, 一个具体类, 一个代理类.
在我们的代理类中, 我们持有一个具体类的智能指针, 默认情况下他是一个 nullptr
, 只有到 display()
被调用时才真正初始化内部的 RealImage
对象, 这样就实现了我们想要的延迟初始化功能.
将输出:
Proxy created. Image not loaded yet.
Loading heavy image: high_res_photo.jpg (Costly operation)
Displaying image: high_res_photo.jpg
Displaying image: high_res_photo.jpg
最后是代理模式的另一个应用场景: 远程通信.
假设在 Bar
类型的对象上调用成员函数 foo()
。
典型假设是 Bar
与运行代码的机器分配在同一台机器上,并且我们希望与Bar::foo()
在同一进程中执行。
现在假设我们做出了一个设计决定,将 Bar
及其所有成员移到网络上的另一台机器上。但是我们仍然希望旧代码能够工作.
如果想和以前一样继续,这时候就需要一个通信代理 —— 一个代理“通过线路”的调用的组件,当然如果需要的话也会收集结果。
让我们实现一个简单的乒乓服务(ping-pong service)来说明这一点。首先,我们定义一个接口:
#include
#include
using namespace std;
struct Pingable {
virtual ~Pingable() = default;
virtual wstring ping(const wstring& message) = 0;
};
然后构建一个pingpong进程:
struct Pong final : Pingable {
wstring ping(const wstring& message) override { return message + L" pong"; }
};
好的, 现在我们 ping
一个 Pong
,它会将单词 “ pong”
附加到消息的末尾并返回该消息.
这里没有使用 ostringstream&
,而是在每次都时创建一个新字符串, 原因在于这个 API
很容易修改成为 Web
服务。
现在可以这样写测试用例:
void tryit(Pingable& pp) {
wcout << pp.ping(L"ping") << "\n";
}
void test() {
Pong pp;
for (int i = 0; i < 3; ++i) {
tryit(pp);
}
}
这样效果就是打印了3次“ping pong”.
然后的部分作者写了个用 .NET
框架和 REST SDK
的例子, 我也没用过, 直接贴原文了(用水平分割线隔开).
现在,假设你决定将 Pingable
服务重新定位到很远很远的 Web
服务器。也许你甚至决定使用其他平台,例如 ASP.NET
,而不是 C++
:
[Route("api/[controller]")]
public class PingPongController : Controller {
[HttpGet("{msg}")]
public string Get(string msg) { return msg + " pong"; }
} // achievement unlocked: use C# in a C++ book
通过此设置,我们将构建一个名为 RemotePong
的通信代理 这将用于代替 Pong
。微软的 REST SDK
在这里派上了用场。
struct RemotePong : Pingable {
wstring ping(const wstring& message) override {
wstring result;
http_client client(U("http://localhost:9149/"));
uri_builder builder(U("/api/pingpong/"));
builder.append(message);
pplx::task<wstring> task = client.request(methods::GET, builder.to_string())
.then([=](http_response r) {
return r.extract_string();
});
task.wait();
return task.get();
}
};
注1: Microsoft REST SDK 是一个用于处理 REST 服务的 C++ 库。它既是开源的又是跨平台的。你可以在 GitHub 上找到它:https:/ github.com/Microsoft/cpprestsdk.
如果你不习惯 REST SDK
,前面的内容可能看起来有点令人困惑;除了 REST
支持之外,SDK
还使用了并发运行时,这是一个 Microsoft
库,用于并发支持。实现此功能后,我们现在可以进行一个更改:
void test() {
RemotePong pp; // was Pong
for (int i = 0; i < 3; ++i) {
tryit(pp);
}
}
就是这样,你得到相同的输出,但实际的实现可以在地球另一端某个地方的 Docker
容器中的 Kestrel
上运行。
优点
缺点