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依賴於抽象的實現,這樣實現了依賴倒轉原則,而不管左邊的抽象如何變化,只要實現方法不變,右邊的具體實現就不需要修改,而右邊的具體實現方法發生變化,只要接口不變,左邊的抽象也不需要修改。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM