前言
关于桥接模式我有以下几个问题想问:
- 什么是桥接模式?
- 为什么需要桥接模式(也就是什么情况下使用桥接模式)?
- 桥接模式的优点是什么?
- 桥接模式的缺点是什么?
桥接模式与装饰模式很相似,然而,桥接模式主要是为了应对“多维度变化”。同时,桥接模式的使用场景存在一定的局限性。
模式定义
在《设计模式》一书中,对桥模式做了如下释义:
将抽象部分与实现部分分离,使它们都可以独立变化。
GoF这句话的意思是,桥模式能够让抽象部分与实现部分独立变化。这显然是一个解耦的过程,同时,需要两个维度的存在(抽象部分和实现部分)——这是因为,只有存在多个维度时,才有每个维度都可以独立变化的需求。
与继承比较
现在,假设我们需要做一个QQ这样的软件,它包括登陆,发送消息,画图与播放音乐这些功能。因此,抽象基类可以设计成如下样子:
class QQ {
public:
virtual void login()=0;
virtual void sendmsg()=0;
virtual void music()=0;
virtual void draw()=0;
virtual ~QQ(){};
};
众所周知,QQ能够安装在win上,也可以在苹果电脑上安装。基于不同设备,它们在播放音乐、画图功能上存在实现差别是理所当然的(例如不同浏览器对css的支持不尽相同)。所以,不同平台,不同实现:
class WinQQ: public QQ {
public:
virtual void music() {
// win播放音乐的操作
}
virtual void draw() {
// win画图的操作
}
};
class AppleQQ: public QQ {
virtual void music() {
// apple播放音乐的操作
}
virtual void draw() {
// apple画图的操作
}
};
依照现实情况,腾讯旗下的QQ其实有两种,一种是完整版的QQ(把它叫作QQPerfect吧),另一种是精简版的TIM。或许在登陆过程中,QQPerfect需要播放音乐而TIM为了足够简洁则不允许播放音乐;在发送消息上,QQPerfect能够发送用户自己画出来的图(draw()
产生的数据),TIM则不可以。两种版本的QQ都应该得到win和apple平台的支持。
class WinQQPerfect: WinQQ {
public:
virtual void login() {
WinQQ::music();
// 登陆逻辑
}
virtual void sendmsg() {
...
if (...) {
WinQQ::draw();
// 发送逻辑
}
}
};
class WinTim: WinQQ {
public:
virtual void login() {
// 登陆逻辑
}
virtual void sendmsg() {
// 发送逻辑
}
};
... // 省略AppleQQPerfect,AppleTim的代码
事实上,WinQQPerfect与AppleQQPerfect,以及WinTim与AppleTim,无非是平台不同,以至于继承的父类不同(前者继承WinQQ,后者继承AppleQQ),我们可以利用组合的方式合并这些类。此时需要对QQ这个抽象基类做些改动:
class QQ { // 改动后的QQ
protected:
QQ* qq;
public:
QQ(QQ* qq): qq(qq) {}
virtual void login()=0;
virtual void sendmsg()=0;
virtual void music()=0;
virtual void draw()=0;
virtual ~QQ(){};
};
class QQPerfect: public QQ { // 合并后的QQPerfect
public:
QQPerfect(QQ* qq): QQ(qq) {}
virtual void login() {
qq->music();
// 登陆逻辑
}
virtual void sendmsg() {
...
if (...) {
qq->draw();
// 发送逻辑
}
}
};
... // 省略TIM的代码
合并WinQQPerfect与AppleQQPerfect之后,麻烦也随之而来,这是因为如果现在实例化一个win版本的QQPerfect,需要这样:
WinQQ wqq = new WinQQ(); // 基类不可以被实例化
QQPerfect wqpft = new QQPerfect(wqq);
可是不行,因为WinQQ继承自抽象基类QQ,而QQ包含了登陆、发送消息、音乐、画图四个纯需函数,WinQQ仅仅重写了music()
和draw()
。也就是说,它还继承了父类的login()
和sendmsg()
两个纯需函数,WinQQ现在是一个抽象基类,不可以被实例化。
那我们在QQ类中实现登陆、发送消息两个功能可以吗?显然不行,因为完整版的QQPerfect与TIM的登陆细节不同。对WinQQ来说,亦是同样道理。
由此,陷入一种僵局状态...
桥接模式
回到最初的起点,整理一下思路。基类QQ有四个纯需函数:登陆,发送,音乐,画图。现在我们有两个需求,一个平台相关(Win、Apple),一个业务相关(QQPerfect、TIM)。画图音乐是平台相关,登陆发送是业务相关,平台与业务是两个维度,可我们将其设计在一个类中,看上去这不合理。
桥接模式认为,它们应该分手。原有的QQ于是一分为二,一个专注业务叫QQ,一个专注平台叫QQPlatform:
class QQ {
protected:
QQPlatform* platform;
public:
QQ(QQPlatform* plt): platform(plt);
virtual void login()=0;
virtual void sendmsg()=0;
};
class QQPlatform {
public:
virtual void music()=0;
virtual void draw()=0;
};
这样一来,平台基于QQPlatform:
class WinQQ: public QQPlatform {
public:
virtual void music() {
// win播放音乐的操作
}
virtual void draw() {
// win画图的操作
}
};
class AppleQQ: public QQPlatform {
virtual void music() {
// apple播放音乐的操作
}
virtual void draw() {
// apple画图的操作
}
};
业务基于QQ:
class QQPerfect: public QQ {
public:
QQPerfect(QQPlatform* plt): QQ(plt) {}
virtual void login() {
plt->music();
// 登陆逻辑
}
virtual void sendmsg() {
...
if (...) {
plt->draw();
// 发送逻辑
}
}
};
class Tim: public QQ {
public:
QQPerfect(QQPlatform* plt): QQ(plt) {}
virtual void login() {
// 登陆逻辑
}
virtual void sendmsg() {
// 发送逻辑
}
}
现在,平台与业务可以随意组合了:
// win上的完整版QQ
WinQQ* wqq = new WinQQ();
QQPerfect* wqpft = new QQPerfect(wqq);
// apple上的TIM
AppleQQ* aqq = new AppleQQ();
Tim* tim = new Time(aqq);
...
也许不论是完整的QQ还是TIM,其中的登陆逻辑与发送逻辑应该相同,区别在于login()
与sendmsg()
中的其他部分(放音乐或是发送画图数据)。虽说如此,但我仍没有直接在QQ中实现,这里把QQ作为一个抽象基类,是为了告诉子类们:“请按接口设计重写方法”。
关系梳理
继承关系:
桥接模式:
能够看到,当平台与业务很多时,桥模式可以有效控制子类的增长速度。同时,注意桥接模式与装饰模式的区别。
模式结构
桥接模式包含如下角色:
- Abstraction:抽象类
- RefinedAbstraction:扩充抽象类
- Implementor:实现类接口
- ConcreteImplementor:具体实现类
总结
桥接模式优点:
- 分离抽象接口与实现部分,保证多维度下每个维度能够独立变化;
- 桥接模式是对多继承方案的优化,提高代码复用性。
桥接模式缺点:
- 桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性。
感谢
- 参考李建忠老师的《C++模式设计》
- 参考《Graphic Design Patterns》中的桥接模式篇
还不快抢沙发