橋接模式(Bridge)
橋接模式(Bridge),將抽象部分與它的實現部分分離,使它們都可以獨立地變化。
兩個抽象類別之間,就像一座橋,有一個聚合線。
什麼叫做抽象與實現分離?
什麼叫做抽象與實現分離,這並不是說,讓抽象類別與其衍生類別分離,因為這沒有任何意義。
實現指是抽象類別和它的衍生類別用來實現自己的物件。
比如,手機可以按照品牌來分類,也可以按照功能來分類:
(這樣的繼承結構,如果不斷地增加新品牌或新功能,類別會越來越多,面對新的需求時,改動過大並影響其他類別的不合理情況。)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
手機品牌(按照品牌來分類)
手機品牌M
手機品牌M遊戲
手機品牌M通訊錄
手機品牌S
手機品牌S遊戲
手機品牌S通訊錄
手機軟體(按照功能來分類)
遊戲
手機品牌M遊戲
手機品牌S遊戲
通訊錄
手機品牌M通訊錄
手機品牌S通訊錄
由於實現的方式有很多種,橋接模式的核心意圖就是把這些實現獨立出來,讓它們各自地變化。這就使得每種實現的變化不會影響其他實現,從而達到應對變化的目的。
1
2
3
4
5
6
手機品牌
品牌N
品牌S
手機軟體
通訊錄
遊戲
結構圖
1
2
3
4
5
6
7
8
9
10
11
抽象 Abstraction
+ Operation()
RefinedAbstraction 被提煉的抽象
+ Operation()
實現 Implementor
+ OperationImp()
ConcreteImplementorA 具體實現A
+ OperationImp()
ConcreteImplementorB 具體實現B
+ OperationImp()
基本程式碼
實現 Implementor
1
2
3
abstract class Implementor {
public abstract void Operation();
}
衍生類別 ConcreteImplementorA/ConcreteImplementorB
1
2
3
4
5
6
7
8
9
10
11
12
13
//具體實現A
class ConcreteImplementorA: Implementor {
public override void Operation() {
Console.WriteLine("具體實現A的方法執行");
}
}
//具體實現B
class ConcreteImplementorB: Implementor {
public override void Operation() {
Console.WriteLine("具體實現B的方法執行");
}
}
Abstraction
1
2
3
4
5
6
7
8
9
10
class Abstraction {
protected Implementor implementor;
public void SetImplementor(Implementor implementor) {
this.implementor = implementor;
}
public virtual Operation() {
implementor.Operation();
}
}
RefinedAbstraction 被提煉的抽象
1
2
3
4
5
class RefinedAbstraction: Abstraction {
public override Operation() {
implementor.Operation();
}
}
用戶端
1
2
3
4
5
6
Abstraction ab = new RefinedAbstraction();
ab.SetImplementor(new ConcreteImplementorA());
ab.Operation();
ab.SetImplementor(new ConcreteImplementorB());
ab.Operation();
何時用?
在發現我們需要多角度去分類實現物件,而只用繼承會造成大量的類別增加,不能滿足「開放-封閉原則」時,就應該要考慮用「橋接模式」了。
緊耦合
很多情況用繼承會帶來麻煩。比如,物件的繼承關係是在編譯時就定義好了,所以無法在執行時改變從父類別繼承的實現。子類別的實現與父類別有非常緊密的依賴關係,以至於父類別實現中的任何變化必然會導致子類別發生變化。當你需要複用子類別時,如果繼承下來的實現不適合解決新的問題,則父類別必須重寫或被其他更適合的類別替換。這種依賴關係限制了靈活性,也限制了複用性。
在物件導向設計中,我們還有一個很重要的設計原則,那就是「合成/聚合複用原則」。即優先使用物件合成/聚合,而不是類別繼承。
盲目使用繼承會造成麻煩,而其本質原因是:繼承是一種強耦合的結果。父類別變,子類別就必須要變。
所以在用繼承時,一定要是在is-a的關係時再考慮使用,而不是任何時候都去使用。
合成/聚合複用原則(CARP)
合成/聚合複用原則(CARP),儘量使用合成/聚合,儘量不要使用類別繼承。
- 合成(
Composition,也有翻譯成組合):表示一種「弱」的「擁有」關係,表現的是A物件可以包含B物件,但B物件不是A物件的一部分。 - 聚合(
Aggregation):是一種「強」的「擁有」關係,表現了嚴格的部分和整體的關係,部分和整體的生命周期一樣。
比如說,大雁有兩個翅膀,翅膀與大雁是部分和整體的關係,並且牠們的生命周期是相同的,於是大雁和翅膀就是「合成關係」。
而大雁是群居動物,所以每隻大雁都是屬於一個雁群,一個雁群可以有多隻大雁,所以大雁和雁群是「聚合關係」。
1
2
翅膀 → 大雁 → 雁群
合成 聚合
好處
合成/聚合複用原則(CARP)的好處是,優先使用物件的合成/聚合將有助於你維持每個類別被封裝,並集中在單個任務上。這樣類別和類別繼承層次會保持較小規模,並且不太可能增長為不可控制的龐然大物。
比如,手機和PC、手機和軟體(手機品牌和手機軟體),需要學會用物件的職責,而不是結構來考慮問題。
緊耦合的程式(橋接模式(Bridge))
結構
1
2
3
4
5
6
7
手機品牌
手機品牌N
手機品牌M
手機軟體
通訊錄
遊戲
音樂播放
程式
手機軟體-抽象類別、遊戲/通訊錄具體類別
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//手機軟體-抽象類別
abstract class HandsetSoft {
public abstract void Run();
}
//遊戲
class HandsetGame: HandsetSoft {
public override void Run() {
Console.WriteLine("執行手機遊戲");
}
}
//通訊錄
class HandsetAddressList: HandsetSoft {
public override void Run() {
Console.WriteLine("執行手機通訊錄");
}
}
手機品牌-抽象類別、品牌N/品牌M具體類別
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//手機品牌-抽象類別
abstract class HandsetBrand {
//宣告一個受保護的手機軟體變數
protected HandsetSoft soft;
//設定手機軟體
//品牌需要關注軟體,所以可在機器中安裝軟體(設定手機軟體),以備執行
public void SetHandsetSoft(HandsetSoft soft) {
this.soft = soft;
}
//執行
public abstract void Run();
}
//手機品牌N
class HandsetBrandN: HandsetBrand {
public override void Run() {
soft.Run();
}
}
//手機品牌M
class HandsetBrandM: HandsetBrand {
public override void Run() {
soft.Run();
}
}
用戶端程式碼
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void Main()
{
//手機品牌N
HandsetBrand phone = new HandsetBrandN();
phone.SetHandsetSoft(new HandsetGame()); //設定遊戲
phone.Run();//執行
phone.SetHandsetSoft(new HandsetAddressList());//設定通訊錄
phone.Run();//執行
//手機品牌M
phone = new HandsetBrandM();
phone.SetHandsetSoft(new HandsetGame());//設定遊戲
phone.Run();//執行
phone.SetHandsetSoft(new HandsetAddressList());//設定通訊錄
phone.Run();//執行
}
新增需求
如果現在要增加功能或手機品牌,那麼只要增加這個類別就行了,不會影響其他任何類別。類別的個數增加也只是一個,不會影響其他類別的改動。
- MP3音樂播放功能
- 手機品牌S
1
2
3
4
5
6
7
8
9
10
11
12
13
//MP3音樂播放功能
class HandsetMP3: HandsetSoft {
public override void Run() {
Console.WriteLine("執行手機MP3音樂播放");
}
}
//手機品牌S
class HandsetBrandS: HandsetBrand {
public override void Run() {
soft.Run();
}
}
這顯然也符合「開放-封閉原則」,這樣的設計不會修改原來的程式碼,而只是擴展類別就行了。