c++20设计模式
- 第 7 章 桥接模式
- 7.1 Pimpl模式
- 7.2 桥接模式介绍
- 7.3 总结
- 7.4 代码
第 7 章 桥接模式
如果你一直关注C++编译器(特别GCC, Clang和MSVC)的最新进展,那么可能已经注意到编译速度提高了。特别是,编译的体量越来越大时。因为,编译器实际上只会重新编译改动的代码,并复用已编译好的未改动的部分,而不是重新构建整个编译单元。
之所以提起C++编译器,是因为过去开发者们一直在使用一个奇怪的技巧来加快编译速度。
7.1 Pimpl模式
首先,我们从技术角度来解释下Pimpl模式。假如我们要定义一个Person类。其中保存了人物的名字,并且提供了打印问候语的方法。除了像平时那样定义Person的成员,我们还可以像下面这样定义类:
struct Person {std::string name;void greet();Person();~Person();class PersonImpl;PersonImpl *impl;
};
看起来很简单的类尽然搞出如此多的工作!我们仔细看看:Person中定义了namec成员和greet()方法,但为什么要定义构造函数和西沟函数呢?PersonImpl类又是干什么的?
我们看到的这个Person类将其具体实现隐藏在另一个类中,即PersonImpl。需要格外注意的是,PersonImpl类并不是在头文件中定义的,而是驻留在 .cpp 文件(person.cpp,因此Person和PersonImpl放在一起)。它的定义非常简单:
struct Person::PersonImpl {void greet(Person *p);
};
类Person中对PersonImpl做了前向声明,并且保存了一个PersonImpl类型指针。我们在Person的构造函数中初始化这个指针,并且在析构函数中销毁它。如果我们习惯使用智能指针,也可以将其替换为智能指针。
Person::Person() : impl(new PersonImpl()) {}Person::~Person() {delete impl;
}
现在,我们要实现Person::greet()接口,正如你预料的那样,这个接口只是把控制权交给PersonImpl::greet():
void Person::greet() {impl->greet(this);
}void Person::PersonImpl::greet(Person *p) {printf("hello %s!\n", p->name.c_str());
}
简单来说,这就是Pimpl模式。因此,现在唯一的问题是:为什么?为什么要费尽周折实现greet()的代理并且传递this指针呢?这种方法有三个优点:
- 这种方法隐藏了类的大部分实现。如果Person类有许多私有 / 共有成员,即使由于private / protected访问限定符的存在,客户不能直接访问这些成员,但我们也会一系列丰富的API,由此暴露了Person类内部的某些成员。如果是Pimpl模式,那么只需要对外提供公共接口即可。
- 修改隐藏的Impl类的数据成员不会影响二进制文件的兼容性。
- 头文件只需包含声明所需的头文件,而不必包含实现所需的头文件。例如,如果Person.h头文件Person类有一个vector<string>类型私有成员,则必须在Person.h头文件中同时包含<vector>和<string>(这是可传递的,所以任何使用Person.h的文件都包含它们)。使用Pimpl模式,这可以在 .cpp 文件中完成。
Pimlpl莫斯可以使系统中的头文件更加整洁,并且不必频繁改动。不过,副作用是会影响编译速度。就本章介绍的内容而言,Pimpl模式是桥接模式的一种很好的体现:在刚才的示例中,Pimpl是一种不透明指针(即我们不知道它背后是什么),它起着桥梁的作用,将公共接口的成员与其隐藏在 .cpp文件中的底层实现结合起来。
7.2 桥接模式介绍
Pimpl模式是桥接(Bridge)模式的一种具体实现,现在我们看看关于桥接模式更加通用的做法。假设有两种(数学意义上的)对象:几何对象以及将几何对象绘制在屏幕上的渲染器对象。
如同我们在适配器模式中展示的那样,假设我们可以以向量和光栅形式进行渲染(尽管我们不会在这里编写任何实际的绘图代码),并且将几何对象的形状限制为圆形。
首先, 基类Render定义如下:
struct Render {virtual void render_circle(float x, float y, float radius) = 0;
};
我们可以轻松的构建向量渲染和光栅渲染的具体实现,下面将编写一些打印到控制台的代码来模拟实际渲染过程:
struct VectorRender: Render
{void render_circle(float x, float y, float radius) override {std::cout << "\nDrawing a vector circle of radius " << radius << "\n";}
};struct RasterRender:Render
{void render_circle(float x, float y, float radius) override {std::cout << "\nRasterizing circle of radius " << radius << "\n";}
};
几何对象的基类Shape可以保存一个渲染器的引用,我们在Shape中定义draw()个resize()两个成员函数用于支持渲染和调整尺寸的操作:
struct Shape
{
protected:Render& render;Shape(Render& render) : render(render) {}
public:virtual void draw() = 0;virtual void resize(float factor) = 0;
};
可以看到,Shape类含有一个Render类型的引用。这正是桥接模型的“桥”之所在。接下来,我们创建Shape类的一个具体实现,并添加诸如圆心位置和半径等更多信息。
struct Circle: Shape
{float x;float y;float radius;Circle(Render& render, float x, float y, float radius):Shape(render), x(x), y(y), radius(radius) {}void draw() override {render.render_circle(x, y, radius);}void resize(float factor) override {radius *= factor;}
};
桥接模式很快就展现在眼前!当然,最有趣的部分在于函数draw():这是链接Circle(包含位置和大小信息)和渲染过程的桥梁。这里的桥就是渲染器,例如:
void testBridgePattern(){RasterRender rr;Circle raster_cicle{rr, 5, 5, 5.5};raster_cicle.draw();raster_cicle.resize(2);raster_cicle.draw();}
在这段代码中,桥就是RasterRender:我们声明了一个RasterRenderer对象,并将其引用传递给Circle。此后,对函数draw()的调用将会以此RasterRenderer引用为桥梁来对Circle进行渲染。如果需要调整圆的大小,则可以调用resize(),渲染过程仍旧可以正常运行,因为渲染器并不知道也不关心其所渲染的Circle对象。
7.3 总结
桥接模式的概念很简单,它通常作为连接器或黏合剂,将两个“不相关”的组件连接起来。抽象(接口)的使用允许组件之间在不了解具体实现的情况下彼此交互。
也就是说,桥接模式的参与者确实需要意识到彼此的存在。具体来说。Circle类需要引用Renderer; Renderer也需要知道如何绘制圆(即Renderer类的成员函数draw_circle的命名由来)。这与中介者模式形成了对比,中介者模式允许对象在毫不知晓对方的情况下进行通信。
7.4 代码
#include<iostream>
#include<string>struct Person {std::string name;void greet();Person();~Person();class PersonImpl;PersonImpl *impl;
};struct Person::PersonImpl {void greet(Person *p);
};Person::Person() : impl(new PersonImpl()) {}Person::~Person() {delete impl;
}void Person::greet() {impl->greet(this);
}void Person::PersonImpl::greet(Person *p) {printf("hello %s!\n", p->name.c_str());
}void testPimpl() {Person p;p.name = "Sarry";p.greet();
}struct Render {virtual void render_circle(float x, float y, float radius) = 0;
};struct VectorRender: Render
{void render_circle(float x, float y, float radius) override {std::cout << "\nDrawing a vector circle of radius " << radius << "\n";}
};struct RasterRender:Render
{void render_circle(float x, float y, float radius) override {std::cout << "\nRasterizing circle of radius " << radius << "\n";}
};struct Shape
{
protected:Render& render;Shape(Render& render) : render(render) {}
public:virtual void draw() = 0;virtual void resize(float factor) = 0;
};struct Circle: Shape
{float x;float y;float radius;Circle(Render& render, float x, float y, float radius):Shape(render), x(x), y(y), radius(radius) {}void draw() override {render.render_circle(x, y, radius);}void resize(float factor) override {radius *= factor;}
};void testBridgePattern(){RasterRender rr;Circle raster_cicle{rr, 5, 5, 5.5};raster_cicle.draw();raster_cicle.resize(2);raster_cicle.draw();}int main()
{testPimpl();testBridgePattern();return 0;
}