当前位置: 首页 > news >正文

精读C++设计模式20 —— 结构型设计模式:桥接模式

精读C++设计模式20 —— 结构型设计模式:桥接模式

​ 这是我们的第二个设计模式——桥接模式!桥接模式更加直白了,我们之前的适配器更倾向于对接口本身的桥接,这里说的是系统协作的桥接。笔者认为他跟适配器区别谈不上很大。但是还是要仔细说一说这个桥接模式,以及我们下面要引出的,笔者最最常用的pImpl法,他就属于桥接模式的一个响当当的代表

我们在桥接什么?

一句话:我们在桥接具体的抽象和具体的实现。或者说——让本来放在一个类中的实现和接口放在两个类中。举个例子,在过去,我们的类A的接口和实现会写在同一对.cpp/.h中,都隶属于A这个模块的代码。但是现在我们会放到两个类中。接口的类AInterface和因为不同情况而拥有不同实现的AImpl类。

桥接模式的精彩之处在于:将抽象(Abstraction)与实现(Implementor)分离,使两者可以独立变化。换句话说,把「接口/抽象层」和「实现层」分成两个维度,通过在抽象中持有对实现的引用来“桥接”它们。适合那些抽象和实现都可能独立扩展的场景。

这种设计模式被广泛的用在了跨平台库中

​ 太干了,直接甩出来一个例子:

// Implementor(实现接口)
struct DrawingAPI {virtual ~DrawingAPI() = default;// 这就是我们的接口virtual void drawCircle(double x, double y, double r) = 0;
};// ConcreteImplementorA / B
struct OpenGL_API : DrawingAPI { void drawCircle(...) override { /* OpenGL */ } };
struct DirectX_API : DrawingAPI { void drawCircle(...) override { /* DirectX */ } };// Abstraction
class Shape {
protected:std::unique_ptr<DrawingAPI> api_;
public:Shape(std::unique_ptr<DrawingAPI> api) : api_(std::move(api)) {}virtual ~Shape() = default;virtual void draw() = 0;
};// RefinedAbstraction
class Circle : public Shape {double x_, y_, r_;
public:Circle(double x,double y,double r,std::unique_ptr<DrawingAPI> api): Shape(std::move(api)), x_(x), y_(y), r_(r) {}void draw() override { api_->drawCircle(x_, y_, r_); }
};

​ 注意到了嘛?我们的Circle到底如何画的,被封在了DirectX_API或者是OpenGL_API,并且可以在不修改 Circle 的情况下新增新 DrawingAPI


PIMPL(Pointer to IMPLementation)

​ PIMPL法太出名了,我甚至想把今天的标题改成设计模式——PIMPL法详谈,但是这属于桥接模式,咱们就讲好了桥接模式,然后再仔细说一说pImpl法是如何工作和设计的。

​ pImpl法没啥稀奇的。当你觉得头文件太膨胀,将实现挪动一个C++编译单元隔离头文件的时候,你实际上就在不自觉的pImpl。但是没有太多,因为我们之间还是再用一个名称符号耦合。如果我们进一步弱化,将实现的细节不光挪到了一个文件中,而是挪到了一个承载了实现细节的IMPL类中,而我们的接口只用一个前置式的声明指针引用,就像这样一样:

class Impl;class Interface {
public:Interface() { ... }void work();
private:Impl*	impl;
};

​ 我们另开一个文件包含IMPL类的实现和声明,这样就完成了我们的工作。我们实际上以一个非常非常小的代价(一次指针解引用)换来了完全隔离的抽象。

​ 好像还是没啥感觉,那我们从头试试!

当我们不使用pIMPL法

直接在头里声明所有私有数据,编译依赖大,头变动导致大量重编译。

// widget.h
class Widget {
public:void doWork();
private:std::vector<int> data_;std::string name_;
};

​ 现在你在头文件中添加和减少一点成员,你很快气恼的发现,只要你动了哪怕一点,所有牵扯到的编译单元居然全部都要跟着变一次。如果这个是一个无敌巨大的项目,你一次改动就要重新编译成千上万个源文件!


最简单的 pImpl(裸指针 + 手动析构,缺 copy 支持)

把实现类前向声明,头文件只含指针,定义在 cpp。

// widget.h
class Widget {
public:Widget();~Widget();void doWork();
private:struct Impl; // forwardImpl* pImpl; // raw pointer
};// widget.cpp
struct Widget::Impl {std::vector<int> data;std::string name;void doWorkImpl() { /* ... */ }
};Widget::Widget() : pImpl(new Impl{/*init*/}) {}
Widget::~Widget() { delete pImpl; }
void Widget::doWork() { pImpl->doWorkImpl(); }

​ 最简单的IMPL法!完全可以随意的更改我们的实现了!因为所有的实现和非公开接口的变动,我们的Widget都完全不知道!我们可以随意的迭代我们的Widget::Impl实现!


std::unique_ptr 管理生命周期(推荐起点)

​ 我们是使用现代C++的,用 unique_ptr<Impl> 代替裸指针,自动析构,移动语义更自然。

// widget.h
class Widget {
public:Widget();~Widget();Widget(const Widget&);            // custom copyWidget& operator=(const Widget&); // custom copyWidget(Widget&&) noexcept = default;Widget& operator=(Widget&&) noexcept = default;void doWork();
private:struct Impl;std::unique_ptr<Impl> pImpl;
};// widget.cpp
struct Widget::Impl { /* same */ };Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;Widget::Widget(const Widget& other) : pImpl(std::make_unique<Impl>(*other.pImpl)) {}
Widget& Widget::operator=(const Widget& other) {if (this != &other) pImpl = std::make_unique<Impl>(*other.pImpl);return *this;
}

引入 clone()(将拷贝逻辑封装到 Impl)

将拷贝逻辑放到 Impl,更灵活,Impl 可以是抽象基类以支持多态 Impl。

struct Widget::Impl {virtual ~Impl() = default;virtual std::unique_ptr<Impl> clone() const = 0;virtual void doWorkImpl() = 0;
};struct ConcreteImpl : Impl {std::vector<int> data;std::unique_ptr<Impl> clone() const override { return std::make_unique<ConcreteImpl>(*this); }void doWorkImpl() override { /*...*/ }
};Widget::Widget(const Widget& other) : pImpl(other.pImpl ? other.pImpl->clone() : nullptr) {}
Widget& Widget::operator=(Widget other) { swap(*this, other); return *this; } // copy-and-swap

性能优化:避免不必要的拷贝 —— 支持移动与 noexcept

默认允许移动构造/移动赋值 noexcept,这样容器(如 vector)在扩容时可以用移动而不是拷贝:

Widget(Widget&&) noexcept = default;
Widget& operator=(Widget&&) noexcept = default;

并为拷贝赋值实现 copy-and-swap,以保证强异常安全语义:

friend void swap(Widget& a, Widget& b) noexcept {using std::swap;swap(a.pImpl, b.pImpl);
}
Widget& Widget::operator=(Widget other) { swap(*this, other); return *this; }
其他一些乱七八糟收集来的小建议
  1. 在头文件仅 forward declare struct Impl; 并持有 std::unique_ptr<Impl> pImpl;
  2. 在 cpp 中定义 Impl(不暴露在头)。
  3. Impl 提供 clone()(如果需要支持深拷贝)。
  4. 提供 copy ctor/assign 实现为深拷贝(使用 clone),并提供默认或 noexcept 的 move ctor/assign。使用 copy-and-swap 可保证异常安全。
  5. 对性能敏感且发生大量小对象分配时,考虑 SBO;但先用简单方案(unique_ptr + clone)做测量再优化。
  6. 在 API 设计上尽量把修改操作收窄(减少对 Impl 的频繁写操作),以减少开销影响。
  7. 文档标注:pImpl 会阻止部分函数内联(因为实现在 cpp),如需要强内联性能,考虑把该方法放在头部或使用 inline 或模板策略而非 pImpl。
  8. 对于公开类含虚函数的场景:pImpl 不会替代 vtable(虚函数表位于持有虚函数的对象本身),但可以把虚方法的实现委托给 Impl。如果希望把 vtable 也隔离出来,需要另行设计(比如桥接/策略模式)。

代码:一个较为完整的“最佳实践”范例

// widget.h
#include <memory>class Widget {
public:Widget();~Widget();Widget(const Widget&);            // deep copy by clone()Widget& operator=(const Widget&); // copy-and-swapWidget(Widget&&) noexcept = default;Widget& operator=(Widget&&) noexcept = default;void doWork();friend void inline swap(Widget& a, Widget& b) noexcept {std::swap;(a.pImpl, b.pImpl);}private:struct Impl;std::unique_ptr<Impl> pImpl;
};
// widget.cpp
#include "widget.h"
#include <vector>
#include <string>struct Widget::Impl {std::vector<int> data;std::string name;void doWorkImpl() { /* heavy work */ }std::unique_ptr<Impl> clone() const { return std::make_unique<Impl>(*this); }
};Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;Widget::Widget(const Widget& other) : pImpl(other.pImpl ? other.pImpl->clone() : nullptr) {}
Widget& Widget::operator=(const Widget& other) {Widget tmp(other);swap(*this, tmp);return *this;
}void Widget::doWork() { pImpl->doWorkImpl(); }

桥接(Bridge) vs 适配器(Adapter)——详细对比(含直观判断准则)

​ 说了这么多,我们回来,对比一下桥接(Bridge)和适配器(Adapter)。

​ 桥接模式,如你看到pIMPL法那样,它的核心工作更集中在:分离抽象与实现,让两边独立扩展。通常是“正面设计”——从一开始就按两个维度设计(例如:抽象类型×实现平台)。适合长期演化、多实现的系统。

​ 适配器更加像是一种补救了,他会把把一个已有接口转换成另一个期望接口,主要用于兼容性或重用。通常是“补救/集成”——把现成类“粘合”到新的接口上——啊哈,这不是补救史山嘛!

总结

我们在解决什么问题

桥接(Bridge)要解决的问题:把“抽象”(接口/上层逻辑)与“实现”(底层细节/平台/策略)分离,让两者能独立演进,避免在单一类层次里把所有组合穷尽(类爆炸)。常见场景:图形库(shape × drawing backend)、跨平台 API、不同策略组合等。

pImpl(作为桥接的代表)要解决的问题:头文件暴露实现细节导致的巨量编译依赖和 ABI 不稳定。目标是把实现移动到编译单元或 Impl 类里,减少头文件变化带来的连锁重编译,同时隐藏第三方/重依赖,提供更好的封装和二进制兼容性。


我们怎么解决的(方法与演进步骤 / 核心模式)
  • 抽象层(Abstraction)持有一个实现接口(Implementor)的指针/引用。
  • 抽象层定义高层 API;实现接口定义底层能力;具体实现(ConcreteImplementor)实现底层细节。
  • 抽象与实现各自独立扩展,运行时可组合(例如 Circle + OpenGL_APISquare + DirectX_API)。

方案对比和收获(优缺点、何时用、实践建议)
  • 裸指针 + 手写析构
    • 优:实现简单、隐藏实现。
    • 缺:内存管理繁琐,拷贝/异常容易出问题。
  • unique_ptr<Impl> + 自定义拷贝(clone)
    • 优:安全、移动高效、拷贝语义明确(深拷贝),推荐默认选项。
    • 缺:每次深拷贝可能有分配开销;阻止部分函数内联。
  • shared_ptr<Impl>(共享) / COW
    • 优:拷贝廉价;读多写少场景有效(COW 可节省复制)。
    • 缺:引用计数开销、多线程下复杂、写时语义增加复杂度。
  • SBO / placement-new
    • 优:减少小对象频繁堆分配开销、提升性能。
    • 缺:实现复杂,调试与维护成本高。只在有测量证据时使用。
干嘛pImpl?
  • 降低编译耦合:头变动不会触发大量重编译,适合库界面/ABI 稳定需求。
  • 隐藏第三方依赖:头文件不用包含 heavy headers(vector/string 等),只在 cpp 引入。
  • ABI 稳定:Impl 可随意演化而不破坏使用方二进制。
  • 缺点/权衡:额外一层指针间接与(可能)堆分配;失去部分内联优化;实现更复杂(拷贝/异常/并发需考虑)。
Bridge vs Adapter
  • 意图不同
    • Bridge:从设计一开始就想把抽象和实现分成两个独立维度,面向长期演化。
    • Adapter:事后为兼容或复用而把现有类包一层,转换接口——更多是补救或集成。
  • 实践判别
    • 如果你是为了“允许抽象与实现独立扩展”用 Bridge。
    • 如果你是为了“把已有类适配到新接口”用 Adapter。
  • 外观相似:两者都可能包含委托指针,但关键看设计时机与意图
http://www.dtcms.com/a/419327.html

相关文章:

  • AI+传统工作流:Photoshop/Excel的智能插件开发指南
  • 冀州网站制作沈阳网站建设索王道下拉
  • java设计模式:抽象工厂模式 + 建造者模式
  • ps做 网站教程服装花型图案设计网站
  • 指令集、立即数和伪指令
  • 危机领导力:突发事件中的决策与沟通策略
  • Unity学习之垃圾回收GC
  • 五次样条速度规划方法介绍
  • 找人做网站被骗怎么办wordpress 评论 姓名
  • 如何建立公司企业网站社区网站建设方案ppt
  • 解密C++多态:一篇文章掌握精髓
  • Git 进阶指南:深入掌握 git log 查看提交历史
  • C++ 引用协程
  • 淄博企业网站设计公司网页无法打开怎么办
  • 添加测试设备到苹果开发者平台
  • 填坑:VC++ 采用OpenSSL 3.0接口方式生成RSA密钥
  • 郑州做网站的网站再就业技能培训班
  • Vscode 连接服务时候一直出现setting ssh Host server
  • 全面解析数据库审批平台:主流工具对比与选型指南
  • 【Docker项目实战】使用Docker部署IT运维管理平台CAT
  • spring事务传播级别的实操案例2
  • 泰州专一做淘宝网站如何用html做网站头像
  • 电子商务网站设计与实现个人网站做捐赠发布违法吗
  • Java滑动窗口算法题目练习
  • 介绍一下HTTP和WebSocket的头部信息
  • Linux系统学习之---库的理解和加载(毛坯初版...)
  • 南山模板网站建设公司怎么看网站的外链
  • 企业网站策划大纲模板文山住房和城乡建设局网站
  • Linux 基础IO与系统IO
  • 【IEDA】已解决:IDEA中jdk的版本切换