Bridge桥接模式

设计模式 2019-02-24 4802 字 1333 浏览 点赞

前言

关于桥接模式我有以下几个问题想问:

  • 什么是桥接模式?
  • 为什么需要桥接模式(也就是什么情况下使用桥接模式)?
  • 桥接模式的优点是什么?
  • 桥接模式的缺点是什么?

桥接模式与装饰模式很相似,然而,桥接模式主要是为了应对“多维度变化”。同时,桥接模式的使用场景存在一定的局限性。

模式定义

在《设计模式》一书中,对桥模式做了如下释义:

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

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》中的桥接模式


本文由 Guan 创作,采用 知识共享署名 3.0,可自由转载、引用,但需署名作者且注明文章出处。

还不快抢沙发

添加新评论