《C++20设计模式》---桥接模式学习笔记

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类型私有成员,则必须在Person.h头文件中同时包含(这是可传递的,所以任何使用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对象。

«abstract»
Shape
# renderer: Renderer&
#Shape(Renderer&)
+draw()
+resize(float)
«abstract»
Renderer
+render_circle(float)
+render_square(float)
Circle
- radius : float
+Circle()
+draw()
+resize(float)
VectorRenderer
+render_circle(float)
+render_square(float)
RasterRenderer
+render_circle(float)
+render_square(float)

7.3 总结

桥接模式的概念很简单,它通常作为连接器或黏合剂,将两个“不相关”的组件连接起来。抽象(接口)的使用允许组件之间在不了解具体实现的情况下彼此交互。
也就是说,桥接模式的参与者确实需要意识到彼此的存在。具体来说。Circle类需要引用Renderer; Renderer也需要知道如何绘制圆(即Renderer类的成员函数draw_circle的命名由来)。这与中介者模式形成了对比,中介者模式允许对象在毫不知晓对方的情况下进行通信。


7.4 代码

#include
#include

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;
}

你可能感兴趣的:(设计模式,c++20,设计模式,桥接模式)