ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

【设计模式系列 (七) 】桥接模式

【设计模式系列 (七) 】桥接模式 ⭐️在这个怀疑的年代我们依然需要信仰。个人主页 YYYing.⭐️设计模式系列专栏设计模式系列系列上期内容【设计模式系列 (六) 】适配器模式系列下期内容暂无目录1. 概述2. 结构3. 仓库实现数据持久化bridge/3.1 场景3.2 Implementor实现类接口3.3 ConcreteImplementor两个具体实现3.4 Abstraction持有实现层抽象的那一层桥3.5 Client配置驱动的客户端3.6 关于 AbstractionImpl3.7 桥接 vs 多层继承类个数的账4. 优缺点与适用环境5. 面试专题5.1 开场题说说桥接模式吧5.2 必问桥接 vs 适配器5.3 高频桥接 vs 策略5.4 高频桥接能不能完全取代多继承5.5 追问清单结语一辆车可以按车型分轿车、SUV、卡车也可以按发动机分汽油、电动、混动。如果一条路走到黑用继承你要写 3 × 3 9 个类而实际上车型和发动机是两个互不相干的变化方向——轿车能装汽油机SUV 也能装电动组合本该是自由的。桥接模式解决的就是这件事把两个独立变化的维度分开用组合代替继承。本文代码取自 design-pattern-cpp 仓库的design-pattern/bridge/使用 C11。为阅读方便片段省略了头文件保护宏。1. 概述WikipediaThe bridge pattern is a design pattern used in software engineering that is meant todecouple an abstraction from its implementation so that the two can vary independently.桥接模式是软件工程中使用的一种设计模式旨在 “将抽象与其实现分离以便两者可以独立变化”三个关键词① 两个独立的维度这是使用桥接模式的前提。只有一个变化方向时用策略模式就够只有当类同时沿着两条轴变化、且两条轴互不依赖时桥接才有意义。识别哪两个维度是独立的靠经验这也是桥接模式最大的门槛。② 别名柄体模式Handle And Body抽象部分是柄实现部分是体——你握着柄体可以随便换。③ 用关联取代多层继承多层继承是静态的编译期绑定类爆炸桥接是动态的运行期换实现。它比多层继承方案更符合单一职责和开闭原则。桥接模式属于结构型模式且通常在开发前期设计——这一点常和适配器对比见 6.2。2. 结构角色职责Abstraction抽象类定义抽象接口持有一个 Implementor 对象二者是关联关系。既可以有抽象业务方法也可以有具体业务方法AbstractionImpl扩充抽象类继承 Abstraction实现其中的抽象业务方法在方法里调用 Implementor 的业务方法。通常是具体类Implementor实现类接口定义实现类的接口。一般只提供基本操作与 Abstraction 的接口可以完全不同ConcreteImplementor具体实现类实现 Implementor 接口提供基本操作的具体实现运行时替换父类对象关键点Abstraction 里持有的成员类型是实现层的抽象所以抽象层和实现层可以各自扩展、互不修改。3. 仓库实现数据持久化bridge/3.1 场景同一个保存数据的业务可以存到数据库也可以存到文件系统。这里两个维度是抽象维度业务侧怎么发起持久化Persistence将来可以扩展出带缓存、带事务的变体实现维度数据落到哪里DataBase/FileSystem3.2 Implementor实现类接口namespace bp { class PersistenceImplementor { public: virtual void saveData() 0; virtual void updateData() 0; }; }只声明基本操作不掺业务语义。3.3 ConcreteImplementor两个具体实现class DataBase : public PersistenceImplementor { DECLARE_CLASS(bp::DataBase); public: void saveData() override; void updateData() override; }; ​ void bp::DataBase::saveData() { std::cout 成功保存到数据库 std::endl; } void bp::DataBase::updateData() { std::cout 成功更新到数据库 std::endl; } class FileSystem : public PersistenceImplementor { DECLARE_CLASS(bp::FileSystem); public: void saveData() override; void updateData() override; }; ​ void bp::FileSystem::saveData() { std::cout 成功保存到文件中 std::endl; } void bp::FileSystem::updateData() { std::cout 成功保存到文件中 std::endl; }3.4 Abstraction持有实现层抽象的那一层桥namespace bp { class PersistenceImplementor; // ★ 前向声明头文件不依赖实现层 class Persistence { private: PersistenceImplementor* impl; // ★ 桥成员类型是实现类接口 public: Persistence(PersistenceImplementor* impl); void saveData(); void updateData(); }; } bp::Persistence::Persistence(PersistenceImplementor* impl) { this-impl impl; } ​ void bp::Persistence::saveData() { impl-saveData(); } void bp::Persistence::updateData() { impl-updateData(); }Persistence自己不做任何实际工作只是把请求转发给被持有的实现对象。这一层就是柄DataBase/FileSystem是体。3.5 Client配置驱动的客户端int main() { // 创建持久化实现对象 CREATE_PROPERTIES(prop, conf); std::string cls prop.getProperty(brp); // conf.properties: brpbp::DataBase GET_INSTANCE_BY_NAME(PersistenceImplementor*, impl, cls); ​ // 创建持久化对象把实现注入抽象 Persistence persistence(impl); ​ // 完成持久化操作 persistence.saveData(); persistence.updateData(); ​ delete impl; // 如上所述所有权在客户端 return 0; }换实现只改配置brpbp::DataBase # 存数据库 # brpbp::FileSystem # 存文件DECLARE_CLASS/IMPLEMENT_CLASS/GET_INSTANCE_BY_NAME是工厂篇讲过的自注册反射宏作用是按字符串类名创建对象。3.6 关于 AbstractionImpl上图中有个AbstractionImpl角色但例子把它省了——Persistence直接就把两个方法透传下去。真实项目里扩展的那一层才是桥接的价值所在比如class TransactionalPersistence : public Persistence // 扩展抽象维度 { public: void saveData() { beginTransaction(); Persistence::saveData(); // 还是走那把桥 commit(); } };TransactionalPersistence×{DataBase, FileSystem}两种新组合只需要1 个新类——这就是 3.7 要说的效果。3.7 桥接 vs 多层继承类个数的账设想形状圆、方形、三角× 颜色红、蓝两个维度方案类个数新增一种颜色新增一种形状多层继承m × n 3 × 2 63 个类2 个类桥接m n 3 2 51 个类1 个类规模一大差距是爆炸性的10 × 10 时继承要 100 个类桥接只要 20 个。而且继承方案里每个类都同时承担了形状和颜色两份职责违背单一职责。一句话记忆继承是 m × n桥接是 m n。4. 优缺点与适用环境主要优点分离抽象接口及其实现部分用对象间的关联关系解耦了抽象与实现固有的绑定关系两者可沿各自维度独立变化取代多层继承方案极大减少子类个数m n m × n符合单一职责可扩展性高任意扩展一个维度都不需要修改原有系统符合开闭原则抽象层的实现细节对客户端透明抽象层里可以随时替换实现。主要缺点增加了理解与设计难度关联关系建立在抽象层要求一开始就针对抽象层设计编程要求正确识别两个独立变化的维度识别错了模式就白用了使用范围有局限性。适用环境想拆分或重组一个具有多重功能的庞杂类例如能与多个数据库服务器交互的类希望在几个独立维度上扩展一个类需要在运行时切换不同实现方法。5. 面试专题5.1 开场题说说桥接模式吧按概念 → 角色 → 场景 → 扩展的顺序答概念桥接模式将抽象部分与实现部分分离使二者可以独立变化。它用对象间的关联关系取代了传统的多层继承别名柄体模式Handle And Body属于结构型模式。角色Abstraction 抽象类持有 Implementor 接口对象RefinedAbstraction 扩充抽象接口在业务方法里转发给 ImplementorConcreteImplementor 提供基本操作的具体实现。适用场景类存在两个独立变化的维度典型如形状 × 颜色想拆分一个多重功能的庞杂类需要在运行时切换实现。可扩展接上和适配器的区别必问见 5.2。如果面试官追问为什么不直接继承答 3.7 的 m × n 与 m n。5.2 必问桥接 vs 适配器两个模式结构上都是持有一个对象、转发调用但意图和时机完全不同桥接 Bridge适配器 Adapter意图分离两个可独立变化的维度让它们各自扩展转换接口让不兼容的类能协作时机开发前期设计是架构决定已有程序中的事后补救维度两个维度同时存在且独立变化只有一个接口不对口的问题关系的性质抽象与实现是对称的、长期共存Target 与 Adaptee 是主从的Adaptee 是被迫兼容的一方有没有兼容的意味没有本来就是自己设计的有是它存在的唯一理由一句话记忆桥接是预先分家适配器是事后接线。追问变体适配器能不能改造成桥接可以——当适配器越来越多时说明接口设计本身没分好层此时把 Target 层和 Adaptee 层重新设计成两个独立维度就是向桥接的演进。5.3 高频桥接 vs 策略两者都是持有接口、转发调用区别在于维度的数量策略只有一个维度算法Context 是稳定的变化的是内部算法的选择本质是替换桥接有两个维度抽象层和实现层都会各自扩展本质是二维正交分解。换个说法策略模式里 Abstraction 通常不参与继承扩展桥接模式里的 Abstraction 一定有自己的继承树RefinedAbstraction。结构相近看有没有两条继承线就能分清。5.4 高频桥接能不能完全取代多继承能解决的问题两个独立维度上的组合爆炸m × n → m n。不能解决的问题真正的is-a多继承——一个类确实需要同时具备两个父类的身份和行为如既是文件又是流。这种语义上的多继承桥接替代不了。另外 C 的多继承容易带来菱形继承和this指针调整的问题用组合桥接反而更安全。5.5 追问清单追问答法桥接模式属于哪一类结构型模式别名柄体模式Handle And Body桥接模式体现哪些设计原则单一职责两个维度各管一摊、开闭原则扩展维度不改原码、组合复用原则关联取代继承、里氏代换、依赖倒转两侧都依赖抽象桥接模式的桥指的是什么Abstraction 里持有的那个Implementor 类型的成员也就是抽象与实现之间的关联关系Abstraction 和 Implementor 的接口必须一致吗不必事实上可以完全不同。Implementor 只提供基本操作Abstraction 可能做更复杂的组合操作运行时能换实现吗可以这是桥接相对继承的核心优势。但要注意换之前先处理旧实现的所有权避免泄漏或双重释放谁负责创建 Implementor通常是工厂/反射本仓库用配置 自注册反射再注入 Abstraction。桥接本身不管创建只管使用——这是它和工厂模式经常配合出现的原因桥接模式的缺点提高了理解与设计难度且必须先正确识别两个独立维度识别不出来就不该用判断要不要用桥接的标准数一下这个类是不是同时沿着两条轴变化是 → 桥接只有一条 → 策略够了框架里哪里见过桥接JDBC 驱动、JDK 的 AWT/Swing Peer、C 的 iostreamstream 与 streambuf 分离、游戏引擎里渲染 API × 平台结语我是YYYing后面还有更精彩的内容希望各位能多多关注支持一下主包。无限进步我们下次再见
返回列表