登录  | 立即注册

游客您好!登录后享受更多精彩

查看: 114|回复: 0

C++设计模式07.桥接模式

[复制链接]

68

主题

2

回帖

91

积分

网站编辑

积分
91
发表于 2024-12-24 19:52:04 | 显示全部楼层 |阅读模式

桥接模式

如果你一直关注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>
}

这太奇怪了。对于一个简单的类来说似乎有很多工作要做。让我们看看,我们有namegreet()函数,但为什么要费心使用构造函数和析构函数呢?这个类PersonImpl是什么?

你现在看到的是Person类,选择将其实现隐藏在另一个类(PersonImpl)。需要注意的是,PersonImpl这个类不是在头文件中定义的,而是驻留在.cpp文件(Person. cpp, PersonPersonImpl耦合在一起)。它的定义很简单:

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()成员函数的名称)。这可以与中介模式形成对比,中介模式允许对象在不直接感知对方的情况下进行通信。

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

QQ|Archiver|手机版|小黑屋|断点社区 |网站地图

GMT+8, 2025-1-19 02:40 , Processed in 0.070742 second(s), 28 queries .

Powered by XiunoBBS

Copyright © 2001-2025, 断点社区.

快速回复 返回顶部 返回列表