意图
将抽象部分与它的实现部分分离,使它们都可以独立地变化。
动机
还是使用 GoF
中提到的例子:
我有一个用户界面的 Window
类
因为需要跨平台,我需要分别基于 Window
这个基类实现基于 某个X Window System
的 XWindow
和基于 Linux System
的 LWindow
同时 Window
还有另外一个维度, 比如根据窗口类型来划分, IconWindow
, TextWindow
等等
现在 IconWindow
很明显也需要实现跨平台的功能,如果按照之前继承的逻辑,现在的 UML 图应该是这样的
现在就出现以下几个明显的问题
- 每一种类型的窗口都需要定义两个类,分别针对不同的平台实现,并且一旦需要支持第三个平台时,所有已经实现的窗口类型又需要在添加一个新的类,很容易造成类数量的爆炸
- 客户端在使用
IconWindow
时,不应该由客户端去决定实例化XIconWindow
或者LIconWindow
,也就说客户端代码在创建窗口时就不应涉及到特定的平台
如果 IconWIndow
在实例化的时候自己决定是用 Linux系统
还是 windows系统
就好了?
结合上面的问题,将所有 Window
窗口类型都依赖的平台实现这一部分进行抽象,封装成一个新的类, 例如 WindowImp
, 最后可以得到以下这样的新的 UML
图
有点乱,简单解释一下:
Window
中引用一个 WindowImp
的对象,Window
对外提供接口,而实际上是通过 WindowImp
来实现,以函数 DrawRect()
画个矩形为例,它实际上就是画4条线 imp->DevDrawLine()
WindowImp
需要将基于系统方面所有可能用到的接口进行抽象,然后具体平台的子类分别实现即可
Window
提供给用户抽象的接口,WindowImp
提供可以实现 Window
抽象接口的基本接口,然后通过聚合的形式将 Window
和 WindowImp
连接在一起, 这种模式就是: 桥接模式
回头在看一下定义 将抽象部分与它的实现部分分离,使它们都可以独立地变化, Window
方面可以根据类型,独立的进行扩展,同样 WindowImp
如果需要添加新的支持系统,也是独立的变化,而扩展后,需要修改的代码只有 Window
实例化 WindowImp
部分的代码
这里其实可以在扩展一下:
Window
如何实例化WindowImp
?
如果对之前总结的创建型有点印象,可以使用单例
+抽象工厂方法
来实现,抽象工厂方法
中提供一个返回WindowImp
对象的方法,并且这个工厂处理所有与特定系统相关 (可能不仅仅是WindowImp
) 的对象。
为了简化代码以及工厂的唯一性,使用单例
来获取这个工厂。
当然,为了更简单,写一个获取WindowImp
的方法也行
使用场景
- 你不希望在抽象和它的实现部分之间有一个固定的绑定关系。例如这种情况可能是因为,在程序运行时刻实现部分应可以被选择或者切换。
- 类的抽象以及它的实现都应该可以通过生成子类的方法加以扩充。这时
Bridge模式
使你可以对不同的抽象接口和实现部分进行组合,并分别对它们进行扩充。 - 一个类存在两个独立变化的维度,且这两个维度都需要进行扩展。
- 对一个抽象的实现部分的修改应对客户端不产生影响,即客户的代码不必重新编译
- 你想对客户端完全隐藏抽象的实现部分
UML 图
Abstraction
: 定义抽象类的接口,维护一个指向Implementor
类型对象的指针RefinedAbstraction
: 扩充由Abstraction
定义的接口Implementor
: 定义实现类的接口,一般仅提供基本操作,而Abstraction
则定义基于这些基本操作的较高层次的操作ConcreteImplementor
: 实现Implementor
接口并定义它的具体实现
Abstraction
将 client
的请求转发给它的 Implementor
对象。
在仔细看一下这个图,桥接模式最大的特点是:
- 抽象层
Abstraction
和Implementor
存在聚合关系 - 对客户端提供的抽象接口,而这些抽象接口由
Implementor
这个抽象类来提供真正的实现,并且二者都可以通过生成子类的方法加以扩充,从而完成了抽象部分与它的实现部分分离,以及独立变化
来个例子
换个 《大话设计模式》 中的例子来进一步加深对桥接模式的理解
现在有2个手机品牌,每个手机品牌下有2个软件,通讯录和游戏,客户运行特定手机品牌下的特定的程序
描述的有点简单,先回想一下 UML
图, 2个抽象层 手机品牌
和 手机软件
, 手机软件
属于 手机品牌
,但是它也可以不依赖 手机品牌
独立存在,所以他们是聚合关系,并且 手机品牌
和 手机软件
二者都需要独立变化 软件提供一个 run()
启动的接口, 手机品牌
启动软件 runsoftware()
结合图,先实现手机软件 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
的数量的类,现在被封装成 m
和 n
个数量,组合的形式大大降低的类的数量
实现系统可能有多角度分类,每一种分类都有可能变化,那么就把这种多角度分离出来让它们独立变化,减少它们之间的耦合
整个看下来,在我的理解里应该是,客户端的依赖的抽象类又依赖于另一个抽象类,这2个抽象类都可以独立变化,并且它们之间是存在一些内在联系的,不管是上层的 图形类
依赖于底层的 绘图类
, 还是 软件
归属于 手机
,而这样设计目的是希望现在设计的比较灵活,比较松耦合,以后扩展的时候可以少送掉命。