精读《设计模式 - Bridge 桥接模式》

2022-03-14 18:17:51 浏览数 (3)

Bridge(桥接模式)

Bridge(桥接模式)属于结构型模式,是一种解决继承后灵活拓展的方案。

意图:将抽象部分与它的实现部分分离,使它们可以独立地变化。

桥接模式比较难理解,我会一步步还原该设计模式的思考,让你体会这个设计模式是如何一步一步被提炼出来的。

举例子

如果看不懂上面的意图介绍,没有关系,设计模式需要在日常工作里用起来,结合例子可以加深你的理解,下面我准备了三个例子,让你体会什么场景下会用到这种设计模式。

汽车生产线改造为新能源生产线

汽油车与新能源汽车的生产流程有很大相似之处,那么汽油车生产线能否快速改造为新能源汽车生产线呢?

如果汽油车生产线没有将内部实现解耦,只把生产汽油车的各部分独立了出来,对新能源车生产线是没什么用处的,但如果汽油车生产线提供了更底层的能力,比如加装轮胎,加装方向盘,那么这些步骤是可以同时被汽油车与新能源车所共享的。

在设计汽油车生产线时,就将生产过程与汽油车解耦,使其可以快速运用到新能源汽车的生产,这就是桥接模式的一种运用。

窗口(Window)类的派生

假设存在一个 Window 窗口类,其底层实现在不同操作系统是不一样的,假设对于操作系统 A 与 B,分别有 AWindow 与 BWindow 继承自 Window,现在要做一个新功能 ManageWindow(管理器窗口),就要针对操作系统 A 与 B 分别生成 AManageWindow 与 BManageWindow,这样显然不容易拓展。

无论我们新增支持 C 操作系统,还是新增支持一个 IconWindow,类的数量都会成倍提升,因为我们所做的 AMangeWindow 与 BMangeWindow 同时存在两个即以上的独立维度,这使得增加维度时,代码变得很冗余。

适配多个搭建平台的物料

做前端搭建平台时,经常出现一些物料(组件)因为固化了某个搭建平台的 API,因此无法迁移到另一个搭建平台,如果要迁移,就需要为不同的平台写不同的组件,而这些组件中大部分 UI 逻辑都是一样的,这使得产生大量代码冗余,如果再兼容一个新搭建平台,或者为已有的 10 个搭建平台再创建一个新组件,工作量都是写一个组件的好几倍。

意图解释

意图:将抽象部分与它的实现部分分离,使它们可以独立地变化。

“抽象” 部分与 “实现” 部分分离,这句话看起来很像接口与实现。确实,如果 “抽象” 指的是 接口(Interface),而 “实现” 指的是 类(Class) 的话,这就是简简单单的 class MyWindow implements Window 类实现过程而已。

但后半句话 “使它们可以独立地变化” 会让你难以和前半句联系起来,如果说 “抽象” 不变,“实现” 可以随意改变还好理解,但反过来就难以解释了。

其实桥接模式中,抽象指的是一种接口(Abstraction),实现指的也是一种接口(Implementor),其中 Implementor 并不是直接实现了 Abstraction 定义的接口,而是提供更底层的方法,使 Abstraction 可以基于它们封装出自己的接口实现。

这样一来,Abstraction 的接口可以随意变化,毕竟调用的是 Implementor 提供函数的组合,只要 Implementor 提供的功能全面,Implementor 可以不变;相应的,Implementor 的实现也可以随意变化,只要提供的底层函数不变,就不影响 Abstraction 对其的使用。

上面举的三个例子都是这样,我们应该把汽油车生产线的标准与通用汽车生产线标准分离、将具体功能窗口与适配不同操作系统的基础 GUI 能力隔离、将组件功能与平台功能隔离,只有做到了抽象部分与实现部分的隔离,才可以通过组合满足更多场景。

结构图

  • Abstraction:定义抽象类的接口。
  • RefinedAbstraction:扩充 Abstraction。
  • Implementor:定义实现类的接口,该接口可以与 Abstraction 接口不一致。
  • ConcreteImplementor:实现 Implementor 接口并定义它的具体实现。

抽象部分就是 Abstraction,实现部分就是 Implementor,在这个结构图中,它们是分离的,可以各自独立变化的,桥接模式,就是指 imp 这个桥,通过 Implementor 实现 Abstraction 接口,就算是桥接上了,这种组合的桥接相比普通的类实现更灵活,更具有拓展性。

代码例子

对于完全版桥接模式,Implementor 可以有多套实现,Abstraction 不需关心具体用的是哪一种实现,而是通过抽象工厂方式封装。下面举一个简单版的例子。

下面例子使用 typescript 编写。

代码语言:javascript复制
class Window {
  private windowImp: WindowImp

  public drawBox() {
    // 通过画线生成 box
    this.windowImp.drawLine(0, 1)
    this.windowImp.drawLine(1, 1)
    this.windowImp.drawLine(1, 0)
    this.windowImp.drawLine(0, 0)
  }
}

// 拓展 window 就非常容易
class SuperWindow extends Window {
  public drawIcon {
    // 通过自定义画线
    this.windowImp.drawLine(0, 5)
    this.windowImp.drawLine(3, 9)
  }
}

桥接模式的精髓,通过上面的例子可以这么理解:

Window 的能力是 drawBox,那继承 Window 容易拓展 drawIcon 吗?默认是不行的,因为 Window 并没有提供这个能力。经分析可以看出,划线是一种基础能力,不应该与 Window 代码耦合,因此我们将基础能力放到 windowImp 中,这样 drawIcon 也可以利用其基础能力画线了。

弊端

不要过度抽象,桥接模式是为了让类的职责更单一,维护更便捷,但如果只是个小型项目,桥接模式会增加架构设计的复杂度,而且不正确的模块拆分,把本来关联的逻辑强制解耦,在未来会导致更大的问题。

另外桥接模式也有简单与复杂模式之分,只有一种实现的场景就不要用抽象工厂做过度封装了。

总结

桥接模式让我们重新审视类的设计是否合理,把类中不相关,或者说相互独立的维度抽出去,由桥接模式做桥接的方式使用,这样会使每个类功能更内聚,代码量更少更清晰,组合能力更强大,更容易做拓展。

下图做了一个简单的解释:

版权声明:自由转载-非商用-非衍生-保持署名(创意共享 3.0 许可证)

1 人点赞