C++设计模式——桥接模式Bridge-Pattern

动机(Motivation)

  • 由于某些类型的固有的实现逻辑,使得它们具有两个变化的维度,乃至多个纬度的变化。
  • 如何应对这种“多维度的变化”?如何利用面向对象技术来使得类型可以轻松地沿着两个乃至多个方向变化,而不引入额外的复杂度?

模式定义

将抽象部分(业务功能)与实现部分(平台实现)分离,使它们都可以独立地变化。 ——《设计模式》GoF

模式举例

假如现在手头上有大、中、小3种型号的画笔,能够绘制红、黄、蓝3种不同颜色,如果使用蜡笔绘画,需要准备3*3=9支蜡笔,也就是说必须准备9个具体的蜡笔类。

 
//桥接模式对变换进行封装
#include <iostream>
using namespace std;

class BigRedPen
{
public:
    void draw()
    {
        cout<<"Draw with big red pen."<<endl;
    };
};
class BigYellowPen
{
public:
    void draw()
    {
        cout<<"Draw with big yellow pen."<<endl;
    };
};
class BigBluePen
{
public:
    void draw()
    {
        cout<<"Draw with big blue pen."<<endl;
    };
};
class MiddleRedPen
{
public:
    void draw()
    {
        cout<<"Draw with middle red pen."<<endl;
    };
};
class MiddleYellowPen
{
public:
    void draw()
    {
        cout<<"Draw with middle yellow pen."<<endl;
    };
};
class MiddleBluePen
{
public:
    void draw()
    {
        cout<<"Draw with middle blue pen."<<endl;
    };
};
class SmallRedPen
{
public:
    void draw()
    {
        cout<<"Draw with small red pen."<<endl;
    };
};
class SmallYellowPen
{
public:
    void draw()
    {
        cout<<"Draw with small yellow pen."<<endl;
    };
};
class SmallBluePen
{
public:
    void draw()
    {
        cout<<"Draw with small blue pen."<<endl;
    };
};
int main()
{
    BigRedPen brp;
    brp.draw();
    MiddleYellowPen myp;
    myp.draw();
    return 0;
}

头文件"pen.h"中定义了不同型号(大、中、小)不同颜色(红、黄、蓝)9个画笔类,如果希望颜色更丰富些,又增加了绿色和紫色,现在就需要改写头文件,再增加6个类:BigGreenPen、MiddleGreenPen、SmallGreenPen、BigPurplePen、MiddlePurplePen、SmallPurplePen。如果有更多型号和颜色,就要很多新的功能相似的类,这简直就是个灾难。


继承是一种常见扩展对象功能的手段,通常继承扩展的功能变化纬度都是一纬的。对于出现变化因素有多个,即有多个变化纬度的情况,用继承实现就会比较麻烦。画笔这个例子就有2个变化纬度:型号和颜色。
不管使用单继承还是多继承方式的设计并没有比上例更简单,反而需要更多的类,只不过是在逻辑关系上更清楚了些。但这样设计违背了类的单一职责原则,即引起一个类变化的原因只有一个,而这里有2个引起变化的原因,即笔的类型变化和笔的颜色变化,这会导致类的结构过于复杂,继承关系太多,不易于维护。最致命的一点是扩展性太差,和上例一样如果引入更多的变化因素,这个类的结构会迅速变得庞大,并且随着程序规模的加大,会越来越难以维护和扩展。

 
#include<iostream>
#define BIG 1
#define MIDDLE 2
#define SMALL 3
#define RED 1
#define YELLOW 2
#define BLUE 3
using namespace std ;
class Pen
{
private:
    int size;
    int color;
public:
    Pen()
    {
        size = 0;
        color = 0;
    }
    void select(int s, int c)
    {
        size = s;
        color = c;
    }
    void draw()
    {
        switch(size)
        {
        case BIG:
            cout<<"Draw with big pen."<<endl;
            break;
        case MIDDLE:
            cout<<"Draw with middle pen."<<endl;
            break;
        case SMALL:
            cout<<"Draw with small pen."<<endl;
            break;
        }
        switch(color)
        {
        case RED:
            cout<<"Pen's color is red."<<endl;
            break;
        case YELLOW:
            cout<<"Pen's color is yellow."<<endl;
            break;
        case BLUE:
            cout<<"Pen's color is blue."<<endl;
            break;
        }
    }
};

int main()
{
    Pen p;
    p.select(1,1);
    p.draw();
    p.select(2,2);
    p.draw();
    return 0;
}

如果又新增加了绿色和紫色,那就需要新增加2个宏GREEN、PURPLE:

#define GREEN 4
#define PURPLE 5

在switch中新加:

 case GREEN: cout<<"Pen's color is green."<<endl; break;
 case PURPLE: cout<<"Pen's color is purple."<<endl; break;

这样设计看起来简单清晰,但当需求发生变化后,都需要修改已有的类。这个类和涉及到这个类的所有直接或间接的代码都要重新做测试,对于一个大型系统来说工作量还是挺大的,而且还可能引入新bug,所以说这样设计不是一个明智的选择。

蜡笔型号和颜色是绑定的,如果用毛笔来绘画,情况就简单许多了,只需要3种型号的毛笔,外加3个颜料盒,用3+3=6个类就可以实现9支蜡笔的功能。毛笔和蜡笔的关键一点区别就在于毛笔的型号和颜色是能够分离的。蜡笔的型号和颜色是分不开的,所以必须使用色彩、大小各异的笔来进行绘画。毛笔能够将抽象与具体分离,使得二者可以独立地变化。

 
#include <iostream>
using namespace std;

class PenImpl
{
public:
    virtual void draw() = 0;
};

class IPen
{
public:
    virtual ~IPen() {};//析构函数 派生类delete时会被调用
    virtual void paint() = 0;
public:
    PenImpl *implementor;//指针
};
//型号
class BigPen : public IPen
{
public:
    ~BigPen()
    {
        delete implementor;
    };
    virtual void paint()
    {
        cout<<"Draw with big pen."<<endl;
        implementor->draw();
    };
};

class MiddlePen : public IPen
{
public:
    ~MiddlePen()
    {
        delete implementor;
    };
    virtual void paint()
    {
        cout<<"Draw with middle pen."<<endl;
        implementor->draw();
    };
};
class SmallPen : public IPen
{
public:
    ~SmallPen()
    {
        delete implementor;
    };
    virtual void paint()
    {
        cout<<"Draw with small pen."<<endl;
        implementor->draw();
    };
};


class Red : public PenImpl
{
public:
    virtual void draw()
    {
        cout<<"Pen's color is red............"<<endl;
    };
};

class Yellow : public PenImpl
{
public:
    virtual void draw()
    {
        cout<<"Pen's color is yellow."<<endl;
    };
};

class Blue : public PenImpl
{
public:
    virtual void draw()
    {
        cout<<"Pen's color is blue."<<endl;
    };
};


int main()
{
    IPen *bp = new BigPen;
    bp->implementor = new Red;
    bp->paint();
    IPen *mp = new MiddlePen;
    mp->implementor = new Yellow;
    mp->paint();
    delete bp;
    delete mp;
    return 0;
}

桥接模式是将继承改成了使用对象组合,从而把2个纬度分开,让每一个纬度单独去变化,然后通过对象组合的方式把2个纬度组合起来,这样便在很大程度上减少了实际实现类的个数。首先找出需求中变化:型号和颜色,然后使用抽象来封装变化。抽象类IPen把变化封装在它的“后面”,在抽象类IPen和PenImpl之间建立依赖关系(IPen里面包含一个PenImpl的指针,优先使用对象聚集,而不是继承)。IPen类及其实现就是抽象部分,而PenImpl及其实现是具体部分,通过聚集使得它们分离开来,从而能更加灵活地应对变化和扩展。

 

 采用桥接模式后加大了代码的复杂度,但当需求发生变化时,任何的修改,添加将会变得非常容易,更容易维护。对于新增加的绿色、紫色,只需要新增加2个从PenImpl派生的类Green、Purple,已有的类不需要做任何更改。
  

 class Green: public PenImpl  
 class Purple: public PenImpl

 

要点总结

  • Bridge模式使用“对象间的组合关系”解耦了抽象和实现之间固有的绑定关系,使得抽象和实现可以沿着各自的维度来变化。所谓抽象和实现沿着各自纬度的变化,即“子类化”它们。
  • Bridge模式有时候类似于多继承方案,但是多继承方案往往违背单一职责原则(即一个类只有一个变化的原因),复用性比较差。Bridge模式是比多继承方案更好的解决方法。
  • Bridge模式的应用一般在“两个非常强的变化维度”,有时一个类也有多于两个的变化维度,这时可以使用Bridge的扩展模式。
合成/聚合复用原则(CARP):尽量使用合成/聚合,尽量不用使用类继承(这是一种强耦合)。优先使用对象的合成/聚合有助于保持每个类被封装,并被集中在单个任务上,这样类和类继承层次会保持比较小的规模,并且不大可能增长为不可控制的庞然大物。

结构(Structure)

 

 抽象基类及接口:
1、Abstraction::Operation():定义要实现的操作接口
2、AbstractionImplement::Operation():实现抽象类Abstaction所定义操作的接口,由其具体派生类ConcreteImplemenA、ConcreteImplemenA或者其他派生类实现。
3、在Abstraction::Operation()中根据不同的指针多态调用AbstractionImplement::Operation()函数。

 

基本代码

#include <iostream>
using namespace std;

class Implementor {
public:
    virtual void Operation() = 0;
    virtual ~Implementor(){}
};

class ConcreteImplementorA : public Implementor{
public:
    void Operation() {
        cout << "ConcreteImplementorA" << endl;
    }
};

class ConcreteImplementorB : public Implementor{
public:
    void Operation() {
        cout << "ConcreteImplementorB" << endl;
    }
};

class Abstraction {
protected:
    Implementor* implementor;
public:
    void setImplementor(Implementor* im) {
        implementor = im;
    }
    virtual void Operation() {
        implementor->Operation();
    }
    virtual ~Abstraction(){}
};

class RefinedAbstraction : public Abstraction{
public:
    void Operation() {
        implementor->Operation();
    }
};

int main() {
    Abstraction* r = new RefinedAbstraction();
    ConcreteImplementorA* ca = new ConcreteImplementorA();
    ConcreteImplementorB* cb = new ConcreteImplementorB();
    r->setImplementor(ca);
    r->Operation();
    r->setImplementor(cb);
    r->Operation();

    delete ca;
    delete cb;
    delete r;
    return 0;
}

Bridge用于将表示和实现解耦,两者可以独立的变化。在Abstraction类中维护一个AbstractionImplement类指针,需要采用不同的实现方式的时候只需要传入不同的AbstractionImplement派生类就可以了。

Bridge的实现方式其实和Builde十分的相近,可以这么说:本质上是一样的,只是封装的东西不一样罢了。

两者的实现都有如下的共同点:
抽象出来一个基类,这个基类里面定义了共有的一些行为,形成接口函数(对接口编程而不是对实现编程),这个接口函数在Buildier中是BuildePart函数在Bridge中是Operation函数;
其次,聚合一个基类的指针,如Builder模式中Director类聚合了一个Builder基类的指针,而Brige模式中Abstraction类聚合了一个AbstractionImplement基类的指针(优先采用聚合而不是继承);

而在使用的时候,都把对这个类的使用封装在一个函数中,在Bridge中是封装在Director::Construct函数中,因为装配不同部分的过程是一致的,而在Bridge模式中则是封装在Abstraction::Operation函数中,在这个函数中调用对应的AbstractionImplement::Operation函数。就两个模式而言,Builder封装了不同的生成组成部分的方式,而Bridge封装了不同的实现方式。
 
桥接模式就将实现与抽象分离开来,使得RefinedAbstraction依赖于抽象的实现,这样实现了依赖倒转原则,而不管左边的抽象如何变化,只要实现方法不变,右边的具体实现就不需要修改,而右边的具体实现方法发生变化,只要接口不变,左边的抽象也不需要修改。

 
posted @ 2020-03-31 08:15  王陸  阅读(1130)  评论(0编辑  收藏  举报