桥接模式

意图

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

动机

还是使用 GoF 中提到的例子:

我有一个用户界面的 Window
因为需要跨平台,我需要分别基于 Window 这个基类实现基于 某个X Window SystemXWindow 和基于 Linux SystemLWindow
同时 Window 还有另外一个维度, 比如根据窗口类型来划分, IconWindow, TextWindow 等等
现在 IconWindow 很明显也需要实现跨平台的功能,如果按照之前继承的逻辑,现在的 UML 图应该是这样的

error_uml

现在就出现以下几个明显的问题

  • 每一种类型的窗口都需要定义两个类,分别针对不同的平台实现,并且一旦需要支持第三个平台时,所有已经实现的窗口类型又需要在添加一个新的类,很容易造成类数量的爆炸
  • 客户端在使用 IconWindow 时,不应该由客户端去决定实例化 XIconWindow 或者 LIconWindow,也就说客户端代码在创建窗口时就不应涉及到特定的平台

如果 IconWIndow 在实例化的时候自己决定是用 Linux系统 还是 windows系统 就好了?

结合上面的问题,将所有 Window 窗口类型都依赖的平台实现这一部分进行抽象,封装成一个新的类, 例如 WindowImp, 最后可以得到以下这样的新的 UML

new_uml

有点乱,简单解释一下:

Window 中引用一个 WindowImp 的对象,Window 对外提供接口,而实际上是通过 WindowImp 来实现,以函数 DrawRect() 画个矩形为例,它实际上就是画4条线 imp->DevDrawLine()
WindowImp 需要将基于系统方面所有可能用到的接口进行抽象,然后具体平台的子类分别实现即可

Window 提供给用户抽象的接口,WindowImp 提供可以实现 Window 抽象接口的基本接口,然后通过聚合的形式将 WindowWindowImp 连接在一起, 这种模式就是: 桥接模式

回头在看一下定义 将抽象部分与它的实现部分分离,使它们都可以独立地变化, Window 方面可以根据类型,独立的进行扩展,同样 WindowImp 如果需要添加新的支持系统,也是独立的变化,而扩展后,需要修改的代码只有 Window 实例化 WindowImp 部分的代码

这里其实可以在扩展一下: Window 如何实例化 WindowImp?
如果对之前总结的创建型有点印象,可以使用 单例+抽象工厂方法 来实现,抽象工厂方法 中提供一个返回 WindowImp 对象的方法,并且这个工厂处理所有与特定系统相关 (可能不仅仅是 WindowImp) 的对象。
为了简化代码以及工厂的唯一性,使用 单例 来获取这个工厂。
当然,为了更简单,写一个获取 WindowImp 的方法也行

使用场景

  • 你不希望在抽象和它的实现部分之间有一个固定的绑定关系。例如这种情况可能是因为,在程序运行时刻实现部分应可以被选择或者切换。
  • 类的抽象以及它的实现都应该可以通过生成子类的方法加以扩充。这时 Bridge模式 使你可以对不同的抽象接口和实现部分进行组合,并分别对它们进行扩充。
  • 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
  • 对一个抽象的实现部分的修改应对客户端不产生影响,即客户的代码不必重新编译
  • 你想对客户端完全隐藏抽象的实现部分

UML 图

uml

  • Abstraction: 定义抽象类的接口,维护一个指向 Implementor 类型对象的指针
  • RefinedAbstraction: 扩充由 Abstraction 定义的接口
  • Implementor: 定义实现类的接口,一般仅提供基本操作,而 Abstraction 则定义基于这些基本操作的较高层次的操作
  • ConcreteImplementor: 实现 Implementor 接口并定义它的具体实现

Abstractionclient 的请求转发给它的 Implementor 对象。

在仔细看一下这个图,桥接模式最大的特点是:

  • 抽象层 AbstractionImplementor 存在聚合关系
  • 对客户端提供的抽象接口,而这些抽象接口由 Implementor 这个抽象类来提供真正的实现,并且二者都可以通过生成子类的方法加以扩充,从而完成了抽象部分与它的实现部分分离,以及独立变化

来个例子

换个 《大话设计模式》 中的例子来进一步加深对桥接模式的理解

现在有2个手机品牌,每个手机品牌下有2个软件,通讯录和游戏,客户运行特定手机品牌下的特定的程序

描述的有点简单,先回想一下 UML 图, 2个抽象层 手机品牌手机软件, 手机软件 属于 手机品牌,但是它也可以不依赖 手机品牌 独立存在,所以他们是聚合关系,并且 手机品牌手机软件 二者都需要独立变化 软件提供一个 run() 启动的接口, 手机品牌 启动软件 runsoftware()

uml

结合图,先实现手机软件 HandsetSoft

#include <iostream>
class HandsetSoft
{
public:
    virtual ~HandsetSoft() {}
    virtual void run() = 0;
};
// 游戏
class HandsetGame: public HandsetSoft
{
public:
    void run() { std::cout << "run HandsetGame" << std::endl; }
};
// 通讯录
class HandsetAddressList: public HandsetSoft
{
public:
    void run() { std::cout << "run HandsetAddressList" << std::endl; }
};

接下来是手机品牌

#include "handsetsoft.h"
#define SAFE_DELETE(p) { if(p) { delete p; p=nullptr;}}

class HandsetBrand
{
public:
    HandsetBrand() : m_software(nullptr) {}
    virtual ~HandsetBrand() { SAFE_DELETE(m_software);}

    void installSoftware(HandsetSoft *software){ SAFE_DELETE(m_software); m_software = software; }
    virtual void runsoftware() = 0;

protected:
    HandsetSoft *m_software;
};
// A 品牌
class HandsetBrandA : public HandsetBrand
{
public:
    void runsoftware()  { std::cout << "HandsetBrandA: ";if(m_software) m_software->run(); }
};
// B 品牌
class HandsetBrandB : public HandsetBrand
{
public:
    void runsoftware()  { std::cout << "HandsetBrandB: ";if(m_software) m_software->run(); }
};

最后调用部分的代码如下:

#include "handsetbrand.h"

int main()
{
    HandsetBrandA brandA;
    brandA.installSoftware(new HandsetGame);
    brandA.runsoftware();

    brandA.installSoftware(new HandsetAddressList);
    brandA.runsoftware();

    HandsetBrandB brandB;
    brandB.installSoftware(new HandsetGame);
    brandB.runsoftware();

    brandB.installSoftware(new HandsetAddressList);
    brandB.runsoftware();

    return 0;
}

运行结果:

HandsetBrandA: run HandsetGame
HandsetBrandA: run HandsetAddressList
HandsetBrandB: run HandsetGame
HandsetBrandB: run HandsetAddressList

小结

桥接模式可能是我目前接触到最难理解的,主要难点在于抽象层需要进行聚合关联,且二者都可以独立变化

而且在看 GoF《大话设计模式》 时,我感觉二者对这个模式的侧重点是有点出入的

GoF 强调的是抽象部分和实现部分分离,抽象部分提供的抽象接口是实现提供的接口较高层次的呈现,举个例子:
我实现一下 绘图类绘图类 提供画线的接口,但是 windows 下画线的实现肯定和 Linux 下画线的实现时不同的,也就出现了2个子类,而且可以随着需要支持系统的增加就需要实现更多子类,然后为了方便客户端实际的使用,不可能直接使用 绘图类 来绘制各种图形,所以我在这个 绘图类 的基础上再次封装,实现一个 图形类,这个 图形类 提供给客户端比如画矩形的接口,当然 图形类 可以扩展出画三角形等等其他子类, 图形类 可以独立变化。图形类 提供了抽象的接口,通过调用 绘图类 来实现
就像 《Head First 设计模式》 中提到的用途时,特意指出了适合使用在需要跨越多个平台的图形和窗口系统上。

《大话设计模式》 给我的感觉更强调的是合成/聚合,优先使用对象的合成/聚合将有助于你保持每个类被封装,并被集中在单个任务上,这样类和类继承层次会保持较小规模,并且不太可能增长为不可控制的庞然大物
换句话说本来是 m*n 的数量的类,现在被封装成 mn 个数量,组合的形式大大降低的类的数量
实现系统可能有多角度分类,每一种分类都有可能变化,那么就把这种多角度分离出来让它们独立变化,减少它们之间的耦合

整个看下来,在我的理解里应该是,客户端的依赖的抽象类又依赖于另一个抽象类,这2个抽象类都可以独立变化,并且它们之间是存在一些内在联系的,不管是上层的 图形类 依赖于底层的 绘图类, 还是 软件 归属于 手机,而这样设计目的是希望现在设计的比较灵活,比较松耦合,以后扩展的时候可以少送掉命。