C++桥接模式如何分离抽象 实现独立变化的两个维度设计

桥接模式通过组合解耦抽象与实现。1.核心是将“做什么”和“怎么做”分离,避免类爆炸;2.结构包含抽象、精化抽象、实现者、具体实现者四个角色;3.适用于多维度变化场景如跨平台ui或图形绘制;4.c++++中需注意实现者生命周期管理;5.区别于策略模式(行为切换)和适配器模式(接口转换),侧重结构解耦。

C++桥接模式如何分离抽象 实现独立变化的两个维度设计

c++的桥接模式,说白了,就是把一个大问题拆成两个可以独立变化的小问题,让“做什么”(抽象)和“怎么做”(实现)这两条线能各走各的路,互不干扰。这就像一座桥,连接了两岸,但两岸上的风景怎么变,桥本身的功能和结构是相对独立的,它只负责连接。核心在于通过组合而非继承,将抽象层和实现层解耦,从而允许它们各自独立地扩展和演进。

C++桥接模式如何分离抽象 实现独立变化的两个维度设计

解决方案

我们在软件设计中,有时候会遇到这样的场景:你有一个概念,它有很多种变体,同时它又可以在多种不同的环境下运行或以多种方式实现。如果用传统的继承方式来处理,很快就会陷入一个“类爆炸”的泥潭。比如,你有一图形(圆形、方形、三角形),它们又要在不同的绘图API上绘制(OpenGL、DirectX、SVG)。如果直接用继承,你可能需要CircleOpenGL、CircleDirectX、SquareOpenGL、SquaredirectX……每增加一个图形或一个绘图API,类的数量就会呈乘法级增长,维护起来简直是噩梦。

C++桥接模式如何分离抽象 实现独立变化的两个维度设计

桥接模式提供了一个优雅的解决方案。它将抽象(比如Shape)和实现(比如DrawingAPI)分离开来。抽象层定义了高层接口,它内部持有一个指向实现层接口的指针或引用。当抽象层需要执行某个操作时,它就把这个操作委托给它所持有的实现对象。这样一来,抽象的具体实现(比如Circle)就不再关心它具体是在哪个绘图API上绘制的,它只知道通过一个DrawingAPI接口去调用绘制方法。而DrawingAPI的具体实现(比如OpenGLAPI)也只负责它自己的绘图逻辑,它不关心是哪个形状在调用它。

立即学习C++免费学习笔记(深入)”;

这种分离带来的好处是显而易见的:

C++桥接模式如何分离抽象 实现独立变化的两个维度设计

  1. 独立变化:你可以增加新的形状,而无需修改任何绘图API的实现;同样,你可以增加新的绘图API,而无需修改任何形状的实现。
  2. 减少耦合:抽象和实现之间的耦合度大大降低,它们通过一个接口松散地联系在一起。
  3. 运行时切换:你甚至可以在运行时切换抽象所使用的具体实现,比如在不同的绘图模式之间切换。

C++中桥接模式的实际应用场景有哪些?

说实话,C++里桥接模式的应用场景还挺多的,不只是图形绘制这么简单。我个人觉得,当你发现一个类或者一组类的继承体系,开始因为两个或更多个正交(也就是互相独立)的变化维度而变得臃肿不堪时,就应该考虑它了。

一个非常经典的例子就是跨平台的用户界面(UI)工具包。想象一下,你有一个Button抽象类,它在windows上可能对应一个Win32Button,在macos上对应一个CocoaButton,在linux上可能对应一个GTKButton。Button的“行为”(点击、禁用等)是一个维度,而它在不同操作系统上的“具体绘制和事件处理”(实现)是另一个维度。如果直接继承,你就会有WindowsButton、MacButton、LinuxButton,然后你再来个Windowscheckbox、MacCheckbox……这会很糟糕。

用桥接模式,你可以定义一个UIElement(抽象)和PlatformImplementor(实现)接口。Button、Checkbox等继承自UIElement,它们内部持有PlatformImplementor的引用。而Win32Implementor、Cocoaimplementor、GTKImplementor则实现PlatformImplementor接口。这样,你新增一个UI控件(比如Slider),只需要继承UIElement,而不需要关心平台细节;新增一个平台支持,只需要实现PlatformImplementor接口,而不需要修改所有UI控件的逻辑。

另一个常见但可能不那么显眼的场景是,当你需要隐藏一个类的具体实现细节,只暴露一个稳定的接口给客户端时,也就是所谓的PIMPL(pointer to IMPLementation)惯用法。PIMPL本质上就是桥接模式的一种特例。你的公共类(抽象)只包含一个指向私有实现类的指针,所有实际的业务逻辑都在私有实现类中。这不仅可以减少头文件依赖,加快编译速度,还能在不破坏ABI兼容性的前提下修改内部实现。

如何在C++中构建桥接模式的各个组成部分?

在C++中实现桥接模式,主要涉及四个核心角色:抽象(Abstraction)、精化抽象(Refined Abstraction)、实现者(Implementor)和具体实现者(Concrete Implementor)。理解它们各自的职责以及如何用C++的特性来表达,是关键。

  1. 抽象(Abstraction): 这是客户端代码直接打交道的接口。它通常是一个抽象基类,定义了高层操作。它内部会持有一个指向Implementor接口的指针或引用。

    // 抽象基类 class DrawingAPI { // Implementor 接口 public:     virtual void drawCircle(double x, double y, double radius) = 0;     virtual ~DrawingAPI() = default; };  // 抽象 class Shape { protected:     DrawingAPI* drawingAPI_; // 持有实现者的引用 public:     Shape(DrawingAPI* api) : drawingAPI_(api) {}     virtual void draw() = 0; // 高层操作     virtual ~Shape() = default; };

    这里Shape是抽象,它依赖于DrawingAPI这个实现者接口。

  2. 精化抽象(Refined Abstraction): 这是Abstraction的具体子类,它实现了Abstraction定义的高层操作。这些操作通常会通过委托调用Implementor的方法。

    // 精化抽象:圆形 class Circle : public Shape { private:     double x_, y_, radius_; public:     Circle(double x, double y, double r, DrawingAPI* api)         : Shape(api), x_(x), y_(y), radius_(r) {}      void draw() override {         // 将具体绘制操作委托给 drawingAPI_         drawingAPI_->drawCircle(x_, y_, radius_);     } };

    Circle就是Shape的精化抽象,它通过其内部的drawingAPI_来完成绘制。

  3. 实现者(Implementor): 这是一个接口(在C++中通常是纯虚基类),定义了抽象层所需的所有基本操作。它不关心这些操作具体是如何被实现的,只提供一个规范。

    // DrawingAPI 就是 Implementor 接口 // class DrawingAPI { ... }; (已在上方定义)
  4. 具体实现者(Concrete Implementor): 这是Implementor接口的具体实现。每个具体实现者都提供了一套不同的方式来实现Implementor定义的操作。

    // 具体实现者:OpenGL 绘图API class OpenGLDrawingAPI : public DrawingAPI { public:     void drawCircle(double x, double y, double radius) override {         std::cout << "Drawing Circle with OpenGL at (" << x << "," << y << ") radius " << radius << std::endl;         // 实际的OpenGL调用...     } };  // 具体实现者:DirectX 绘图API class DirectXDrawingAPI : public DrawingAPI { public:     void drawCircle(double x, double y, double radius) override {         std::cout << "Drawing Circle with DirectX at (" << x << "," << y << ") radius " << radius << std::endl;         // 实际的DirectX调用...     } };

在实际使用时,客户端代码会创建具体的Implementor对象,然后将其传递给Abstraction的构造函数

// 客户端代码 // #include <iostream> // #include <memory> // For std::unique_ptr  // ... (类定义) ...  int main() {     // 创建具体的实现者     std::unique_ptr<DrawingAPI> openglAPI = std::make_unique<OpenGLDrawingAPI>();     std::unique_ptr<DrawingAPI> directxAPI = std::make_unique<DirectXDrawingAPI>();      // 创建抽象,并传入不同的实现者     std::unique_ptr<Shape> circleOpenGL = std::make_unique<Circle>(1.0, 2.0, 3.0, openglAPI.get());     std::unique_ptr<Shape> circleDirectX = std::make_unique<Circle>(5.0, 6.0, 7.0, directxAPI.get());      circleOpenGL->draw();  // 输出: Drawing Circle with OpenGL...     circleDirectX->draw(); // 输出: Drawing Circle with DirectX...      // 运行时切换实现(如果抽象允许)     // 比如,你可以在一个工厂方法中根据配置返回不同的 DrawingAPI 实例     // 或者 Shape 内部提供一个 setDrawingAPI 方法     // 但通常来说,Bridge模式的连接是在对象创建时建立的。      return 0; }

需要注意的是,在C++中处理Implementor的生命周期是个关键点。上面例子中我用了原始指针,但实际项目中,为了避免内存泄漏和管理复杂性,通常会使用智能指针(如std::unique_ptr或std::shared_ptr)来管理DrawingAPI对象的生命周期。如果一个DrawingAPI实例会被多个Shape对象共享,那么std::shared_ptr会是更好的选择。

桥接模式与策略模式、适配器模式有何异同?

这几个设计模式确实在结构上有些相似之处,都涉及到了“委托”或者“封装”,但它们解决的问题和侧重点是不同的。理解它们的异同,能帮助我们更准确地选择合适的模式。

  1. 与策略模式(Strategy Pattern)的异同

    • 相同点:两者都使用了对象组合(Composition)而非继承,并且都通过委托来执行行为。它们都将算法或行为封装在独立的类中。
    • 不同点
      • 目的:策略模式的目的是封装一组可互换的算法,让客户端可以在运行时选择不同的算法。它关注的是“行为”的变化。桥接模式的目的是将抽象和实现分离,让它们可以独立地变化和扩展。它关注的是“结构”和“维度”的分离。
      • 维度:策略模式通常处理一个维度上的变化(不同的算法)。桥接模式处理的是两个或多个正交维度上的变化(抽象的种类和实现的种类)。
      • 层次:策略模式通常在同一抽象层次上提供不同的行为实现。桥接模式则是在不同层次(抽象层和实现层)之间建立桥梁。
    • 举例
      • 策略:计算税费有多种方法(普通税、增值税、消费税),你可以把这些方法封装成不同的策略,然后根据需要切换。
      • 桥接:绘制圆形,可以在OpenGL上画,也可以在DirectX上画。圆形是抽象,OpenGL/DirectX是实现。
  2. 与适配器模式(Adapter Pattern)的异同

    • 相同点:两者都涉及到一个类“使用”另一个类,并且都可能涉及到接口的转换。
    • 不同点
      • 目的:适配器模式的目的是让一个不兼容的接口变得兼容,通常是为了让两个本来无法协同工作的接口能够一起工作。它解决的是“接口不匹配”的问题。桥接模式的目的是分离抽象和实现,允许它们独立演化。它解决的是“多维度变化导致类爆炸”的问题。
      • 方向:适配器模式通常是单向的,将一个现有接口转换为目标接口。桥接模式则是双向的,它在抽象和实现之间建立一个稳定的连接点,两者都可以独立扩展。
      • 设计时机:适配器模式通常是在系统已经存在,需要集成现有不兼容组件时使用(事后弥补)。桥接模式更多是在系统设计之初,预见到多维度变化时使用(事前规划)。
    • 举例
      • 适配器:你有一个老旧的LegacyLogger接口,但你的新系统只认识NewLogger接口,你需要一个LegacyLoggerAdapter来把LegacyLogger包装成NewLogger。
      • 桥接:同上文的图形绘制或跨平台UI。

总的来说,桥接模式是当你预见到系统将会有两个或更多个独立变化的维度时,用来解耦和避免类爆炸的利器。它让你的设计更加灵活,更易于维护和扩展。当然,引入桥接模式会增加一些类的数量和间接性,所以并非所有简单场景都适用,它更适合那些复杂度较高、变化频繁的系统。

© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享