C++設計模式之橋接模式(bridge)(結構型)

設想如果要繪製矩形、圓形、橢圓、正方形,我們至少需要4個形狀類,但是如果繪製的圖形需要具有不同的顏色,如紅色、綠色、藍色等,此時至少有如下兩種設計方案:

  • 第一種設計方案是爲每一種形狀都提供一套各種顏色的版本。

  • 第二種設計方案是根據實際需要對形狀和顏色進行組合。

假設我們需要對每種圖形需要10種顏色,使用方案一,那麼總共需要4種圖形*10種顏色=40個類。如果我們使用方案二需要4種圖形+10種顏色=14個類。爲什麼會出現這麼大的差別呢?仔細分析就會發現,畫圖的時候不僅對圖形有要求,而且還對顏色有要求,也就是說有兩個引起變化的點或者說有兩個變化的維度。方案一把圖形和顏色直接綁定在一起了,他們二者完全融合(耦合)在一起了,他們的屬性從生產出來就已經固化了(是靜態的),不能被改變了。 而方案二對圖形和顏色來說是毫無關係(解耦),二者互不相干,只有在使用的時候用戶決定用什麼顏色畫什麼圖形(動態設置),這個時候圖形和顏色才動態發生關係。

對於這種有兩個變化維度(即兩個變化的原因)的系統,採用方案二來進行設計系統中類的個數更少,且系統擴展更爲方便。設計方案二即是橋接模式的應用。橋接模式將繼承關係轉換爲關聯關係,從而降低了類與類之間的耦合,減少了代碼編寫量。

一、 模式定義

橋連模式:將抽象部分與實現部分分離,使它們都可以獨立的變化。它是一種結構性模式,又稱柄體(Handle and body)模式或者接口(Interface)模式。

二 、模式分析 

當一個抽象可能有多個實現時,通常用繼承來協調他們。抽象類的定義對該抽象的接口。而具體的子類則用不同的方式加以實現,但是此方法有時不夠靈活。繼承機制將抽象部分與他的視線部分固定在一起,使得難以對抽象部分和實現部分獨立地進行修改、擴充和充用。

理解橋接模式,重點需要理解如何將抽象化(Abstraction)與實現化(Implementation)脫耦,使得二者可以獨立地變化。
抽象化:抽象化就是忽略一些信息,把不同的實體當作同樣的實體對待。在面向對象中,將對象的共同性質抽取出來形成類的過程即爲抽象化的過程。
•實現化:針對抽象化給出的具體實現,就是實現化,抽象化與實現化是一對互逆的概念,實現化產生的對象比抽象化更具體,是對抽象化事物的進一步具體化的產物。
脫耦:脫耦就是將抽象化和實現化之間的耦合解脫開,或者說是將它們之間的強關聯改換成弱關聯,將兩個角色之間的繼承關係改爲關聯關係。橋接模式中的所謂脫耦,就是指在一個軟件系統的抽象化和實現化之間使用關聯關係(組合或者聚合關係)而不是繼承關係,從而使兩者可以相對獨立地變化,這就是橋接模式的用意。

 

三、 ULM圖

1、Abstraction(抽象類):

用於定義抽象類的接口,它一般是抽象類而不是接口,其中定義了一個Implementor(實現類接口)類型的對象並可以維護該對象,它與Implementor之間具有關聯關係,它既可以包含抽象業務方法,也可以包含具體業務方法。

2、RefinedAbstraction(擴充抽象類):

擴充由Abstraction定義的接口,通常情況下它不再是抽象類而是具體類,它實現了在Abstraction中聲明的抽象業務方法,在RefinedAbstraction中可以調用在Implementor中定義的業務方法。

3、Implementor(實現類接口):

定義實現類的接口,這個接口不一定要與Abstraction的接口完全一致,事實上這兩個接口可以完全不同,一般而言,Implementor接口僅提供基本操作,而Abstraction定義的接口可能會做更多更復雜的操作。Implementor接口對這些基本操作進行了聲明,而具體實現交給其子類。通過關聯關係,在Abstraction中不僅擁有自己的方法,還可以調用到Implementor中定義的方法,使用關聯關係來替代繼承關係。

4、ConcreteImplementor(具體實現類):

具體實現Implementor接口,在不同的ConcreteImplementor中提供基本操作的不同實現,在程序運行時,ConcreteImplementor對象將替換其父類對象,提供給抽象類具體的業務操作方法。

四 實例

4.1 模擬毛筆

現需要提供大中小3種型號的畫筆,能夠繪製5種不同顏色,如果使用蠟筆,我們需要準備3*5=15支蠟筆,也就是說必須準備15個具體的蠟筆類。而如果使用毛筆的話,只需要3種型號的毛筆,外加5個顏料盒,用3+5=8個類就可以實現15支蠟筆的功能。

實際上,蠟筆和毛筆的關鍵一個區別就在於筆和顏色是否能夠分離。即將抽象化(Abstraction)與實現化(Implementation)脫耦,使得二者可以獨立地變化"。關鍵就在於能否脫耦。蠟筆的顏色和蠟筆本身是分不開的,所以就造成必須使用15支色彩、大小各異的蠟筆來繪製圖畫。而毛筆與顏料能夠很好的脫耦,各自獨立變化,便簡化了操作。在這裏,抽象層面的概念是:"毛筆用顏料作畫",而在實現時,毛筆有大中小三號,顏料有紅綠藍黑白等5種,於是便可出現3×5種組合。每個參與者(毛筆與顏料)都可以在自己的自由度上隨意轉換。蠟筆由於無法將筆與顏色分離,造成筆與顏色兩個自由度無法單獨變化,使得只有創建15種對象才能完成任務。

Bridge模式將繼承關係轉換爲組合關係,從而降低了系統間的耦合,減少了代碼編寫量。

ULM如下圖:
 

代碼如下:

#include <iostream>
#include <string>

//實現類接口Color(顏色類)
class Color 
{
public:
	virtual void bepaint(const std::string&, const std::string&) = 0;
};

//具體實現類Red
class Red : public Color {
public:
	void bepaint(const std::string& penType, const std::string& name) 
	{
		std::cout << penType + "紅色的" + name + "." << std::endl;
	}
};

//具體實現類Green
class Green : public Color {
public:
	void bepaint(const std::string& penType, const std::string& name) 
	{
		std::cout << penType + "綠色的" + name + "." << std::endl;
	}
};

//抽象類Pen
class Pen {
public:
	virtual void draw(const std::string& name) = 0;
	void setColor(Color* color) 
	{
		this->color = color;
	}
protected:
	Color* color;
};

//擴充抽象類BigPen
class BigPen : public Pen {
public:
	void draw(const std::string& name) {
		std::string penType = "大號毛筆繪製";
		this->color->bepaint(penType, name);
	}
};

//擴充抽象類SmallPen
class SmallPen : public Pen {
public:
	void draw(const std::string& name) {
		std::string penType = "小號毛筆繪製";
		this->color->bepaint(penType, name);
	}
};

//客戶端測試類
int main(void) {
	Color* color;
	Pen* pen;

	//這裏假裝用反射獲取 
	color = (Color*) new Green();
	pen = (Pen*) new SmallPen();

	pen->setColor(color);
	pen->draw("聖誕樹");

	delete color;
	delete pen;

	return 0;
}

 結果如下:

4.2 安裝操作系統

考慮裝操作系統,有多種配置的計算機,同樣也有多款操作系統。如何運用橋接模式呢?可以將操作系統和計算機分別抽象出來,讓它們各自發展,減少它們的耦合度。當然了,兩者之間有標準的接口。這樣設計,不論是對於計算機,還是操作系統都是非常有利的。下面給出這種設計的UML圖,其實就是橋接模式的UML圖。

#include <iostream>

//操作系統
class OS
{
public:
	virtual void InstallOS_Imp() = 0;
};
class WindowOS : public OS
{
public:
	void InstallOS_Imp() 
	{ 
		std::cout << "安裝Window操作系統" << std::endl; 
	}
};
class LinuxOS : public OS
{
public:
	void InstallOS_Imp() 
	{ 
		std::cout << "安裝Linux操作系統" << std::endl; 
	}
};
class MacOS : public OS
{
public:
	void InstallOS_Imp() 
	{ 
		std::cout << "安裝Mac操作系統" << std::endl; 
	}
};
//計算機
class Computer
{
public:
	virtual void InstallOS(OS* os) = 0;
};

class AppleComputer : public Computer
{
public:
	void InstallOS(OS* os) 
	{ 
		std::cout << "在蘋果電腦上";
		os->InstallOS_Imp(); 
	}
};
class HPComputer : public Computer
{
public:
	void InstallOS(OS* os) 
	{ 
		std::cout << "在惠普電腦上";
		os->InstallOS_Imp(); 
	}
};

int main()
{
	OS* os1 = new WindowOS();
	OS* os2 = new MacOS();
	
	Computer* computer1 = new AppleComputer();
	computer1->InstallOS(os1);
	computer1->InstallOS(os2);

	OS* os3 = new LinuxOS();
	Computer* computer2 = new HPComputer();
	computer2->InstallOS(os3);

	delete os1;
	delete os2;
	delete os3;
    
	return 0;
}

結果如下:

 4.3 Java虛擬機

(1) Java語言通過Java虛擬機實現了平臺的無關性。

2) 一個 Java桌面軟件總是帶有所在操作系統的視感(LookAndFeel),如果一個Java軟件是在Unix系統上開發的,那麼開發人員看到的是Motif用戶界面的視感;在Windows上面使用這個系統的用戶看到的是Windows用戶界面的視感;而一個在Macintosh上面使用的用戶看到的則是Macintosh用戶界面的視感,Java語言是通過所謂的Peer架構做到這一點的。Java爲AWT中的每一個GUI構件都提供了一個Peer構件,在AWT中的Peer架構就使用了橋接模式。

    (3) JDBC驅動程序也是橋接模式的應用之一。使用JDBC驅動程序的應用系統就是抽象角色,而所使用的數據庫是實現角色。一個JDBC驅動程序可以動態地將一個特定類型的數據庫與一個Java應用程序綁定在一起,從而實現抽象角色與實現角色的動態耦合。

另外我們在window上使用的畫圖,visio等都是很好的例子。

五、橋接模式的優點

  1. 分離抽象接口及其實現部分。橋接模式使用“對象間的關聯關係”解耦了抽象和實現之間固有的綁定關係,使得抽象和實現可以沿着各自的維度來變化。所謂抽象和實現沿着各自維度的變化,也就是說抽象和實現不再在同一個繼承層次結構中,而是“子類化”它們,使它們各自都具有自己的子類,以便各自子類的組合,從而獲得多維度組合對象。
  2. 在很多情況下,橋接模式可以取代多層繼承方案,多層繼承方案違背了“單一職責原則(SRP)”,複用性較差,且類的個數非常多,橋接模式是比多層繼承方案更好的解決方法,它極大減少了類的個數。
  3. 橋接模式提高了系統的可擴展性,在兩個變化維度中任意擴展一個維度,都不需要修改原有系統,符合“開閉原則(OCP)”。

六、橋接模式的缺點

  1. 橋接模式的使用會增加系統的理解與設計難度,由於關聯關係建立在抽象層,要求開發者一開始就就要針對抽象層進行設計與編程。
  2. 橋接模式要求正確識別出系統中兩個獨立變化的維度,因此其使用範圍具有一定的侷限性,如何正確識別兩個獨立維度也需要一定的經驗積累。

七、橋接模式的使用場景

  1. 如果一個系統需要在抽象化和具體化之間增加更多的靈活性,避免在兩個層次之間建立靜態的繼承關係,通過橋接模式可以使它們在抽象層建立一個關聯關係。
  2. “抽象部分”和“實現部分”可以以繼承的方式獨立擴展而互不影響,在程序運行時可以動態將一個抽象化子類的對象和一個實現化子類的對象進行組合,即系統需要對抽象化角色和實現化角色進行動態耦合。
  3. 一個類存在兩個(或多個)獨立變化的維度,且這兩個(或多個)維度都需要獨立進行擴展。
  4. 對於那些不希望使用繼承或因爲多層繼承導致系統類的個數急劇增加的系統,使用橋接模式。

八、模式擴展

8.1 抽象工廠(Abstract Factory 模式可以用來創建和配置一個特定的Bridge模式。

8.2 適配器模式與橋接模式的聯用

    • 橋接模式和適配器模式用於設計的不同階段,橋接模式用於系統的初步設計,對於存在兩個獨立變化維度的類可以將其分爲抽象化和實現化兩個角色,使它們可以分別進行變化;而在初步設計完成之後,當發現系統與已有類無法協同工作時,可以採用適配器模式。但有時候在設計初期也需要考慮適配器模式,特別是那些涉及到大量第三方應用接口的情況。

8.3  橋接模式與裝飾的區別:

這兩個模式在一定程度上都是爲了減少子類的數目,避免出現複雜的繼承關係,但是它們解決的方法卻各有不同.

裝飾模式: 裝飾模式把子類中比基類中多出來的部分放到單獨的類裏面,以適應新功能增加的需要,當我們把描述新功能的類封裝到基類的對象裏面時,就得到了所需要的子類對象,這些描述新功能的類通過組合可以實現很多的功能組合 .

橋接模式:橋接模式則把原來的基類的實現化細節抽象出來,在構造到一個實現化的結構中,然後再把原來的基類改造成一個抽象化的等級結構,這樣就可以實現系統在多個維度上的獨立變化 。

發佈了243 篇原創文章 · 獲贊 37 · 訪問量 25萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章