天天看點

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及其實作是具體部分,通過聚集使得它們分離開來,進而能更加靈活地應對變化和擴充。

C++設計模式——橋接模式Bridge-Pattern

 采用橋接模式後加大了代碼的複雜度,但當需求發生變化時,任何的修改,添加将會變得非常容易,更容易維護。對于新增加的綠色、紫色,隻需要新增加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依賴于抽象的實作,這樣實作了依賴倒轉原則,而不管左邊的抽象如何變化,隻要實作方法不變,右邊的具體實作就不需要修改,而右邊的具體實作方法發生變化,隻要接口不變,左邊的抽象也不需要修改。

作者:王陸

下一篇: DFS之二