vlambda博客
学习文章列表

C++设计模式——桥接模式(Bridge Pattern)

C++设计模式——桥接模式(Bridge Pattern)

目录

引言定义定义结构代码示例总结桥接模式和策略模式桥接模式和适配器模式适用场景优缺点参考资料

引言

假如有三个品牌的手机vivo,oppo和小米,如果手机手机壳一体生产,会是这样的: 

各产品定制生产

对应到相应的类中,将是1+3+6=10个有继承关系的类,如果这时再加一个华为手机,无疑是要多增加3个类,会带来类的急剧增长。


如果手机手机壳分开生产,然后按需搭配,就是这样的: 

C++设计模式——桥接模式(Bridge Pattern)
通过组合生产

对应到类设计中,只需要7个类,如果增加一类手机,只需增加一个类,增加一款手机壳,也只需增加一个类。


对应的各维度关系也可以通过下图观察: 

C++设计模式——桥接模式(Bridge Pattern)
维度拆分示意图


这种处理多维变化(手机和手机壳)的方式运用到软件设计中就是桥接模式。

定义

定义

Decouple an abstraction from its implementation so that the two can vary independently.        
解耦抽象和实现,使得两者可以独立的变化。

桥接模式即将抽象部分与它的实现部分分离开来,使他们都可以独立变化。

桥接模式将继承关系转化成关联关系,它降低了类与类之间的耦合度,减少了系统中类的数量,也减少了代码量。属于对象结构型模式。

桥接模式类似于多重继承方案,但是多重继承方案往往违背了类的单一职责原则,其复用性比较差。桥接模式是比多重继承更好的替代方案。

结构

桥接模式的通用UML类图如下:   

C++设计模式——桥接模式(Bridge Pattern)
桥接模式UML类图

从UML类图中,可以看到,桥接模式主要包含4种角色:


  • 抽象(Abstraction):该类持有一个对实现角色的引用以维护该对象,抽象角色中的方法需要实现角色来实现。抽象角色一般为抽象类(构造函数规定子类要传入一个实现角色)

  • 修正抽象(RefineAbstraction): Abstraction的具体实现。对Abstraction的方法进行完善和扩展

  • 实现(Implementor):定义实现维度的基本操作,提供给Abstraction使用。该类一般为接口或抽象类

  • 具体实现(ConcreteImplementor):Implementor的具体实现

观察UML类图中 Abstraction和Implementor连接的那条线(抽象层建立的抽象关联),就像一个桥,将两个变化维度连接起来,各个维度都可以独立的变化,这也是 桥接 的来源。       
可以明显看出,桥接模式使用组合代替了继承,将类之间的静态继承关系转换为动态的对象组合关系,使用组合而不用继承,会使系统更加灵活,并易于扩展,同时有效控制了系统中类的个数。

代码示例

以手机和手机APP的关系为例,一个手机可以运行不同的APP,一个APP可以不同的手机上运行。将A手机作为抽象,将APP作为实现,利用桥接模式代码示例如下:   
和之前一样,代码仍然采用智能指针.

#include <bits/stdc++.h>

//
//桥接模式
//关键代码:将实现独立出来,抽象类依赖现实类。
//以下示例中,将各类App、各类手机独立开来,实现各种App和各种手机的自由桥接/组合
//


//抽象App类,提供接口
class App {
public:
    virtual ~App() = default;

    virtual void run() 0;
};

//具体的App实现类
class GameApp : public App {
public:
    void run() override {
        std::cout << "GameApp Running" << std::endl;
    }
};

//具体的App实现类
class TranslateApp : public App {
public:
    void run() override {
        std::cout << "TranslateApp Running" << std::endl;
    }
};

//抽象手机类,提供接口
class MobilePhone {
public:
    virtual ~MobilePhone() = default;

    //构造函数不能定义为虚函数.有兴趣的可以思考一下why.
    MobilePhone(const std::shared_ptr<App> pApp) : m_pApp(pApp) {}

    virtual void appRun() 0;

protected:
    //由于继承类要使用该对象,因此不能定义为private类型
    std::shared_ptr<App> m_pApp;//持有一个实现的引用.类通过该对象实现app与手机的桥接
};

//具体的手机实现类
class XiaoMi : public MobilePhone {
public:
    XiaoMi(const std::shared_ptr<App> pApp) : MobilePhone(pApp) {}

    void appRun() override {
        std::cout << "XiaoMi: ";
        m_pApp->run();
    }
};

//具体的手机实现类
class HuaWei : public MobilePhone {
public:
    HuaWei(const std::shared_ptr<App> pApp) : MobilePhone(pApp) {}

    void appRun() override {
        std::cout << "HuaWei: ";
        m_pApp->run();
    }
};

int main() {
    std::shared_ptr<App> pGameApp = std::make_shared<GameApp>();//实现部分
    std::shared_ptr<MobilePhone> pXiaoMi = std::make_shared<XiaoMi>(pGameApp);//抽象部分
    pXiaoMi->appRun();

    std::shared_ptr<App> pTranslateApp = std::make_shared<TranslateApp>();//实现部分
    pXiaoMi = std::make_shared<XiaoMi>(pTranslateApp);//抽象部分
    pXiaoMi->appRun();

    std::cout << std::string(50'-') << std::endl;

    std::shared_ptr<MobilePhone> pHuaWei = std::make_shared<HuaWei>(pGameApp);
    pHuaWei->appRun();

    pHuaWei = std::make_shared<HuaWei>(pTranslateApp);
    pHuaWei->appRun();

    return 0;
    //运行结果:
    //XiaoMi: GameApp Running
    //XiaoMi: TranslateApp Running
    //--------------------------------------------------
    //HuaWei: GameApp Running
    //HuaWei: TranslateApp Running
}

思考以下问题:   
1.构造函数是否能够定义为虚函数,为什么?  
2.在MobilePhone的基类中,将App作为构造函数的参数传入进去来构造不同的MobilePhone;我们能否将App作为appRun()的参数,而不是作为构造函数的参数呢?

总结

桥接模式和策略模式

实际上所有模式可以只分为类模式和对象模式两种,类模式是用继承,而对象模式是用委托。Bridge模式和Strategy模式相似就是因为他们都将任务委托给了另外一个接口的具体实现,他们之间的区别在于:Bridge的目的是让底层实现和上层接口可以分别演化,从而提高移植性;而Strategy的目的是将复杂的算法封装起来,从而便于替换不同的算法。因此可以想象,一般情况下Bridge的实现几乎不会在运行时更改,而Strategy的算法则很有可能需要在运行时更换,这就导致在细节方面需要考虑的因素可能会很不相同。

桥接(Bridge)模式是结构型模式的一种,而策略(strategy)模式则属于行为模式。

举一个样例:           
策略模式:我要画圆。要实心圆,我能够用solidPen来配置。画虚线圆能够用dashedPen来配置。 
桥接模式:相同是画圆,我是在windows下来画实心圆。就用windowPen+solidPen来配置。在unix下画实心圆就用unixPen+solidPen来配置。假设要在windows下画虚线圆。就用windowsPen+dashedPen来配置,要在unix下画虚线圆,就用unixPen+dashedPen来配置。

画圆方法中,策略仅仅是考虑算法的替换,而桥接考虑的则是不同平台下需要调用不同的工具,接口仅仅是定义一个方法。而详细实现则由详细实现类完毕。

再次回顾一桥接模式的类图: 

策略模式通用类图

和桥接模式的类图进行对比,在桥接模式中,Abstraction通过聚合的方式引用Implementor;在策略模式中,Context也使用聚合的方式引用Startegy抽象接口。


从它们的结构图可知,在这两种模式中,都存在 一个对象使用聚合的方式引用另一个对象的抽象接口 的情况,而且该抽象接口的实现可以有多种并且可以替换。可以说两者在表象上都是调用者与被调用者之间的解耦,以及抽象接口与实现的分离。

那么两者的区别体现在什么地方呢?

1.首先,在形式上,两者还是有一定区别的,对比两幅结构图,我们可以发现,在桥接模式中不仅Implementor具有变化(ConcreateImplementior),而且Abstraction也可以发生变化(RefinedAbstraction),而且两者的变化是完全独立的,RefinedAbstraction与ConcreateImplementior之间松散耦合,它们仅仅通过Abstraction与Implementor之间的关系联系起来。而在策略模式中,并不考虑Context的变化,只有算法的可替代性。

2.其次在语意上,桥接模式强调Implementor接口仅提供基本操作,而Abstraction则基于这些基本操作定义更高层次的操作。而策略模式强调Strategy抽象接口的提供的是一种算法,一般是无状态、无数据的,而Context则简单调用这些算法完成其操作。

3.桥接模式中不仅定义Implementor的接口而且定义Abstraction的接口,Abstraction的接口不仅仅是为了与Implementor通信而存在的,这也反映了结构型模式的特点:通过继承、聚合的方式组合类和对象以形成更大的结构。在策略模式中,Startegy和Context的接口都是两者之间的协作接口,并不涉及到其它的功能接口,所以它是行为模式的一种。行为模式的主要特点就是处理的是对象之间的通信方式,往往是通过引入中介者对象将通信双方解耦,在这里实际上就是将Context与实际的算法提供者解耦。

所以相对策略模式,桥接模式要表达的内容要更多,结构也更加复杂。桥接模式表达的主要意义其实是接口隔离的原则,即把本质上并不内聚的两种体系区别开来,使得它们可以松散的组合,而策略在解耦上还仅仅是某一个算法的层次,没有到体系这一层次。从结构图中可以看到,策略的结构是包容在桥接结构中的,桥接中必然存在着策略模式,Abstraction与Implementor之间就可以认为是策略模式,但是桥接模式一般Implementor将提供一系列的成体系的操作,而且Implementor是具有状态和数据的静态结构。而且桥接模式Abstraction也可以独立变化。

桥接模式和适配器模式

桥接模式和适配器模式用于设计的不同阶段,桥接模式用于系统的初步设计,对于存在两个独立变化维度的类可以将其分为抽象化和实现化两个角色,使它们可以分别进行变化;而在初步设计完成之后,当发现系统与已有类无法协同工作时,可以采用适配器模式。但有时候在设计初期也需要考虑适配器模式,特别是那些涉及到大量第三方应用接口的情况。

适用场景

1.一个系统/类存在多个独立变化的维度,且每个维度都有可能变化时,那么可以通过桥接模式将这些维度分离出来,让它们单独变化,减少它们之间的耦合。   
2.对于那些不希望使用继承或因为多层次继承导致系统类的个数急剧增加的系统,桥接模式尤为适用。             
3.“抽象部分”和“实现部分”可以以继承的方式独立扩展而互不影响,在程序运行时可以动态将一个抽象化子类的对象和一个实现化子类的对象进行组合,即系统需要对抽象化角色和实现化角色进行动态耦合。

优缺点

  • 优点      
    1.抽象和实现分离。分离抽象接口及其实现部分。桥接模式是为了解决继承的缺点而提出的设计模式。多层继承方案违背了“单一职责原则”,复用性较差,且类的个数非常多,桥接模式是比多层继承方案更好的解决方法,它通过使用“对象间的组合关系”解耦了抽象和实现之间的绑定关系,极大减少了子类的个数。       
    2.优秀的扩展能力。桥接模式提高了系统的可扩充性,在两个变化维度中任意扩展一个维度,都不需要修改原有系统。符合开闭原则。     
    3.实现细节对客户透明。可以对用户隐藏实现细节,它已经由抽象层通过聚合关系完成了封装。

  • 缺点    
    1.增加系统的理解与设计难度。由于关联关系建立在抽象层,要求开发者一开始就针对抽象进行设计与编程。     
    2.桥接模式要求正确识别出系统中两个独立变化的维度,因此其使用范围具有一定的局限性,如何正确识别两个独立维度也需要一定的经验积累。

桥接模式的核心在于解耦抽象和实现。   
此处的抽象并不是 抽象类接口 这种高层概念,实现也不是 继承接口实现抽象 与实现其实指的是两种变化的维度。其中,抽象包含实现,因此,一个抽象类的变化可能涉及到多种维度的变化导致的。

在使用桥接模式时,首先应该识别出一个类所具有的两个独立变化的维度,将它们设计为两个独立的继承等级结构,为两个维度都提供抽象层,并建立抽象耦合。

桥接模式的重点我认为可以这样理解:通过分离进行组合

参考资料

1.设计模式读书笔记-----桥接模式 https://www.cnblogs.com/chenssy/p/3317866.html    
2.简说设计模式——桥接模式 https://www.cnblogs.com/adamjwh/p/9033548.html      
3.桥接模式https://www.jianshu.com/p/7d8a2aae0041      
4.设计模式--桥接模式https://www.jianshu.com/p/69a8661cd6bf  
5.桥接模式和策略模式的区别 https://blog.csdn.net/youthon/article/details/7653389  
6.从桥接模式与策略模式谈起http://www.blogjava.net/wangle/archive/2007/04/25/113545.html      
7.设计模式之桥梁模式和策略模式的差别 https://www.cnblogs.com/zhchoutai/p/7233632.html


图注:幼儿园的学霸