桥接模式
如果你一直关注c++编译器(特别是GCC、Clang和MSVC)的最新进展,你可能已经注意到编译速度正在提高。特别是,编译器变得越来越增量化,因此编译器实际上只能重新构建已更改的定义,并重用其余的定义,而不是重新构建整个翻译单元。
我之所以提到c++编译,是因为过去开发人员一直在使用一个奇怪的技巧(又是这个短语!)来优化编译速度。当然,我说的是...
Pimpl编程技法
让我先解释一下在指向实现的指针(Pimpl(Pointer to implement)
)编程技法,。假设你决定创建一个Person
类来存储一个人的姓名并允许他们打印问候。与通常定义Person的成员不同,你继续这样定义类
struct Person
{
std::string name;
void greet();
Person();
~Person();
class PersonImpl;
PersonImpl* impl // good place for gsl::owner<T>
}
这太奇怪了。对于一个简单的类来说似乎有很多工作要做。让我们看看,我们有name
和greet()
函数,但为什么要费心使用构造函数和析构函数呢?这个类PersonImpl
是什么?
你现在看到的是Person
类,选择将其实现隐藏在另一个类(PersonImpl
)。需要注意的是,PersonImpl
这个类不是在头文件中定义的,而是驻留在.cpp文件(Person. cpp
, Person
和PersonImpl
耦合在一起)。它的定义很简单:
struct Person::PersonImpl
{
void greet(Person* p);
};
原始的Person
类向前声明PersonImpl
,并继续保留指向它的指针。在Person
的构造函数中初始化并在析构函数中销毁的正是这个指针; 如果智能指针能让你感觉更好,请随意使用。
Person::Person() :
impl(new PersonImpl)
{ }
Person::~Person( )
{
delete impl;
}
现在,我们要实现Person::greet()
,正如你可能已经猜到的,它只是将控制权传递给PersonImpl::greet()
void Person::greet()
{
impl->greet(this);
}
Person::PersonImpl::greet(Person* p)
{
printf("hello %s", p->name.c_str());
}
这就是Pimpl编程技法,唯一的问题是为什么?!? 为什么要这么费劲地委托greet()
并传递this
指针呢?这种方法有三个优点:
- 更大比例的类的实现被隐藏起来。如果
Person
类的实现需要提供许多私有/受保护成员,那么你将向客户端公开所有这些细节,即使客户端由于私有/受保护访问修饰符永远无法访问这些成员。使用Pimpl
编程技法,可以只提供公共接口。
- 修改隐藏
Impl
类的数据成员不会影响二进制兼容性。
- 头文件只需要包含声明所需的头文件,而不需要包含实现。例如,如果
Person
需要vector<string>
类型的私有成员,您将被迫在头文件Person.h
种#include
<vector>
和<string>
(这是传递性的,所以任何使用Person.h的人也会包括他们)。利用Pimpl编程技法,可以在.cpp
文件中#include
<vector>
和<string>
。
你将注意到,上述几点允许我们保留一个干净的、不变的头文件。这样做的一个减少编译时间,但对于我们来说, Pimpl
很好得揭示了桥接模式: 在我们的例子中,Pimpl
不透明的指针(不透明的相对透明的,也就是说,你不知道它背后是什么)作为一个桥梁, 将公共接口的成员与隐藏在.cpp
文件中的底层实现连接了起来。
桥接模式
Pimpl编程技法是桥梁设计模式的一个非常具体的说明,现在让我们来看看一些更普遍的东西。假设我们有两个对象类(在数学意义上):几何形状和可以在屏幕上绘制它们的渲染器。
就像我们对适配器模式的演示一样,我们假设渲染可以以矢量和栅格形式进行(尽管我们在这里不会编写任何实际的绘图代码),并且,就形状而言,我们将限制为圆形。
首先,我们给出基类Renderer
:
struct Renderer
{
virtual void render_circle(float x, float y, float radius) = 0;
};
我们可以很容易地构造矢量和栅格实现;下面我将使用一些代码模拟实际的呈现,以便向控制台编写内容
struct VectorRenderer : Renderer
{
void render_circle(float x, float y, float radius) override
{
cout << "Rasterizing circle of radius " << radius << endl;
}
};
struct RasterRenderer : Renderer
{
void render_circle(float x, float y, float radius) override
{
cout << "Drawing a vector circle of radius " << radius << endl;
}
};
基类Shape
持有渲染器的引用; 该形状将支持draw()
成员函数的自渲染,也将支持resize()
操作。
struct Shape
{
protected:
Renderer& renderer;
Shape(Renderer& renderer) : renderer { renderer } { }
public:
virtual void draw() = 0;
virtual void resize(float factor) = 0;
};
您会注意到Shape类引用了一个渲染器。这恰好是我们建造的桥梁。现在我们可以创建Shape
类的实现,提供额外的信息,比如圆心的位置和半径。
struct Circle : Shape
{
float x, y, radius;
void draw() override
{
render.render_circle(x, y, radius);
}
void resize(float factor) override
{
radius *= factor;
}
Circle(Renderer& renderer, float x, float y, float radius):
Shape{renderer},
x{x},
y{y},
radius{radius}
{}
};
}
好的,所以这个模式很快就写好了,当然,有趣的部分是在draw()
中:在这里我们使用桥梁连接圆(它有关于它的位置和大小的信息)和渲染过程。这里的桥就是一个Renderer
, 例如
RasterRenderer rr;
Circle raster_circle{ rr, 5, 5, 5 };
raster_circle.draw();
raster_circle.resize(2);
raster_circle.draw();
在前面的例子中,桥是RasterRenderer
: 你创建它的对象rr
,把rr
的一个引用传递给Circle
,然后调用draw()
将把RasterRenderer
作为桥,绘制圆圈。如果你需要微调圆,你可以调用resize()
调整它的大小,渲染仍然会工作得很好,因为渲染器不知道或关心Circle
。
总结
桥是一个相当简单的概念,作为一个连接器或胶水,连接两个部分在一起。抽象(接口)的使用允许组件在不真正了解具体实现的情况下相互交互。
也就是说,桥接模式的参与者确实需要知道彼此的存在。具体来说,一个Circle
需要一个对Renderer
引用,相反,渲染器知道如何具体地绘制圆(render_circle()
成员函数的名称)。这可以与中介模式形成对比,中介模式允许对象在不直接感知对方的情况下进行通信。