Java設計模式之橋接模式(Bridge)

一、引入橋接模式

舉個例子,現在有M牌手機,手機裏安裝了通訊錄,這時又有一N牌手機,也安裝了通訊錄,如何實現。從面相對象的設計原則出發,設計類圖如下。

 

 如果此時,兩款手機又安裝了聊天軟件呢。

簡單寫下代碼,幫助理解。

手機品牌父類

/**
 * 手機品牌抽象父類
 */
public abstract class IPhoneBrand {
	public abstract void run();
}

手機品牌M和手機品牌N,這裏也可以用抽象類來進行實現

/**
 * 手機品牌 M
 */
public class PhoneBrandM extends IPhoneBrand{
	@Override
	public void run() {
		
	}
}

/**
 * 手機品牌 N
 */
public class PhoneBrandN extends IPhoneBrand{
	@Override
	public void run() {
		
	}
}

 手機品牌M和手機品牌N各自的子類

/**
 * 手機品牌 M 通訊錄
 */
public class MAddressList extends PhoneBrandM{
	@Override
	public void run() {
		System.out.println("運行手機品牌M通訊錄");
	}
}


/**
 * 手機品牌 M 聊天軟件
 */
public class MChat extends PhoneBrandM{
	@Override
	public void run() {
		System.out.println("運行手機品牌M聊天軟件");
	}
}

/**
 * 手機品牌 N 通訊錄
 */
public class NAddressList extends PhoneBrandN{
	@Override
	public void run() {
		System.out.println("運行手機品牌N通訊錄");
	}
}

/**
 * 手機品牌 N 聊天軟件
 */
public class NChat extends PhoneBrandN{
	@Override
	public void run() {
		System.out.println("運行手機品牌N聊天軟件");
	}
}

測試類

public class Test {

	public static void main(String[] args) {
		IPhoneBrand brand;
		
		brand = new MAddressList();
		brand.run();
		
		brand = new MChat();
		brand.run();
		
		brand = new NAddressList();
		brand.run();
		
		brand = new NChat();
		brand.run();
	}

}

 結果

如果兩款又要增加遊戲、音樂播放、輸入法呢?那就會再次增加手機品牌M和手機品牌N的子類。但如果這時要增加一款手機品牌S呢?這時變化可能就會比較大了,從代碼的角度來說,會增加許多子類。

換個角度來看。

這裏也寫一下代碼,和上面代碼比對看一下,幫助理解

 手機軟件父類

/**
 * 手機軟件抽象父類
 */
public abstract class IPhoneSoftware {
	public abstract void run();
}

通訊錄和聊天軟件

/**
 * 通訊錄
 */
public class AddressList extends IPhoneSoftware{
	@Override
	public void run() {
		
	}
}

/**
 * 聊天軟件
 */
public class Chat extends IPhoneSoftware{
	@Override
	public void run() {
		
	}
}

 各自子類

/**
 * 手機品牌 M 通訊錄
 */
public class MAddressList extends AddressList{
	@Override
	public void run() {
		System.out.println("運行手機品牌M通訊錄");
	}
}

/**
 * 手機品牌 N 通訊錄
 */
public class NAddressList extends AddressList{
	@Override
	public void run() {
		System.out.println("運行手機品牌N通訊錄");
	}
}

/**
 * 手機品牌 M 聊天軟件
 */
public class MChat extends Chat{
	@Override
	public void run() {
		System.out.println("運行手機品牌M聊天軟件");
	}
}

/**
 * 手機品牌 N 聊天軟件
 */
public class NChat extends Chat{
	@Override
	public void run() {
		System.out.println("運行手機品牌N聊天軟件");
	}
}

測試類

public class Test {

	public static void main(String[] args) {
		IPhoneSoftware brand;
		
		brand = new MAddressList();
		brand.run();
		
		brand = new NAddressList();
		brand.run();
		
		brand = new MChat();
		brand.run();
		
		brand = new NChat();
		brand.run();
	}

}

結果

 

此時,如果兩個品牌的手機要增加遊戲、音樂播放、輸入法軟件,又或是增加手機品牌S呢?似乎也很麻煩,並且違背了開放-封閉原則。那怎麼解決好呢。

這裏我們一直在用面向對象的原則在設計軟件。先有一個品牌,如果多個品牌,就抽象出來一個品牌抽象類,每個品牌就是它的品牌子類,而對於手機軟件,就讓它們繼承各自的手機品牌。這是從不同品牌的手機出發。

而從不同的手機軟件出發,由於有了多個手機軟件,就抽象出來一個手機軟件抽象類,每種軟件就是它的手機軟件子類,而對於不同品牌的手機,就讓它們繼承各自需要的手機軟件。

雖然說繼承是一種很好用的方法,但有的時候也不是特別適用。而且,繼承也有一個很大的缺點,就是如果父類方法改變,子類必須改變。這其實某種程度上限制了類的複用性。

這裏引用《大話設計模式》中的一段話進一步說明一下。

對象的繼承關係是在編譯時就定義好了,所以無法在運行時改變從父類繼承的實現。子類的實現與它的父類有非常緊密的依賴關係,以至於父類實現中的任何變化必然會導致子類發生變化。當你需要複用子類時,如果繼承下來的實現不適用解決新的問題,則父類必須重寫或被其他更適合的類替換。這種依賴關係限制了靈活性並最終限制了複用性。

其實,在面向對象設計中,還有一個很重要的設計原則,就是合成/聚合複用原則。即優先使用對象合成/聚合,而不是類繼承。

合成/聚合複用原則,儘量使用合成/聚合,儘量不要使用類繼承。

聚合表示一種弱的“擁有”關係,體現的是A對象可以包含B對象,但B對象不是A對象的一部分。

合成表示一種強的“擁有”關係,體現了嚴格的部分和整體的關係,部分和整體的生命週期一樣。

舉個例子說明一下,大雁有兩個翅膀,翅膀和大雁是部分與整體的關係,且聲明週期一樣,所以大雁和翅膀是合成關係。大雁是羣居動物,每隻大雁都屬於一個雁羣,雁羣包含許多隻大雁,所以雁羣和大雁是聚合關係。

從合成/聚合的角度再去考慮上面的手機和軟件的關係,這時我們發現,手機和軟件其實就是一種聚合關係,如果將上面的兩種分類方式(手機品牌抽象類和手機軟件抽象類)合成一種的話是不是更簡便呢。先畫個類圖看看。

現在再來看一下,如果此時增加遊戲、音樂播放、輸入法等軟件,只需要增加手機軟件抽象類的子類即可。如果增加手機品牌S,只需要增加手機品牌抽象類的子類即可。這樣就符合了開放-封閉原則。

來看下代碼

手機品牌和手機軟件抽象父類

/**
 * 手機品牌
 */
public abstract class PhoneBrand {
	protected Software software;
	//設置手機軟件
	public void setPhoneBrand(Software software) {
		this.software = software;
	}
	
	public abstract void run();
}

/**
 * 手機軟件
 */
public abstract class Software {
	public abstract void run();
}

手機品牌的子類

/**
 * 手機品牌 M
 */
public class PhoneBrandM extends PhoneBrand{
	@Override
	public void run() {
		System.out.print("手機品牌M");
		software.run();
	}
}

/**
 * 手機品牌 N
 */
public class PhoneBrandN extends PhoneBrand{
	@Override
	public void run() {
		System.out.print("手機品牌N");
		software.run();
	}
}

手機軟件的子類

/**
 * 通訊錄
 */
public class AddressList extends Software{
	@Override
	public void run() {
		System.out.println("運行通訊錄軟件");
	}
}

/**
 * 聊天軟件
 */
public class Chat extends Software{
	@Override
	public void run() {
		System.out.println("運行聊天軟件");
	}
}

測試類

public class Test {

	public static void main(String[] args) {
		//手機品牌 M
		PhoneBrand brandM = new PhoneBrandM();
		
		brandM.setPhoneBrand(new AddressList());
		brandM.run();
		
		brandM.setPhoneBrand(new Chat());
		brandM.run();
		
		//手機品牌 N
		PhoneBrand brandN = new PhoneBrandN();
		
		brandN.setPhoneBrand(new AddressList());
		brandN.run();
		
		brandN.setPhoneBrand(new Chat());
		brandN.run();
	}

}

結果

二、橋接模式

1. 定義

橋接模式,將抽象部分與它的實現部分分離,使它們都可以獨立地變化。(引自《大話設計模式》)

2. 解釋

什麼叫“將抽象部分與它的實現部分分離”?這裏的實現指的是抽象類和它的派生類用來實現自己的對象。

就上面的例子來說,就是讓“手機”既可以按照品牌來分類,也可以按照功能來分類。

按品牌來分類的結構圖:

按功能來分類的結構圖:

但其實無論是按品牌分類還是按功能分類,它只是“手機”分類的一種維度,我們可以將這兩種維度分裂開來,各自建立繼承結構,這樣也更符合“單一職責原則”,更靈活,易於擴展,即,手機可以按照品牌分類是一種維度,然後抽象父類、子類實現;手機可以按照功能分類是另一種維度,然後抽象父類、子類實現。再將兩個維度中的抽象父類建立一個關聯關係,就像一座連接彼此的橋一樣,橋接模式的名字就由此而來。再來說說這兩個維度,我們將其中一個與“手機”業務關係更爲密切的維度稱之爲抽象類層次結構(抽象部分),另一維度就是實現類層次結構(實現部分)。

 這個時候如果增加手機品牌S或者說增加手機遊戲軟件都只是增加了抽象部分和實現部分的子類即可,不需要改動太多,很好的實現了“開放-封閉原則”。

再舉個例子說明一下按維度分類的實現,比如說“上衣”可以按照型號進行分類,可以按照款式進行分類。型號從業務角度來說和“上衣”更爲親密,所以這裏按型號分類這一維度作爲“抽象部分”。

 好了,就醬紫。

3. 基本結構圖

Abstraction:抽象類。這裏通常使用抽象類而非接口,用於定義抽象類的接口,其中維護了Implementor對象,還會包含一些具體業務方法的實現(比如上面PhoneBrand類的setPhoneBrand()方法)。

RefinedAbstraction:被提煉的抽象類。實現了Abstractor的方法,在方法中調用了Implementor定義的業務方法。

Implementor:實現類。這裏使用抽象類和接口均可,在這個類中通常只是對方法進行聲明,具體實現交給子類,這一點和Abstractor不同,Abstractor中可能會有一些更復雜的操作。

ConcreteImplementorA和ConcreteImplementorB:Implementor的派生類。實現Implementor中聲明的方法。

4. 基本代碼

Implementor

public abstract class Implementor {
	public abstract void operation();
}

ConcreteImplementorA和ConcreteImplementorB

public class ConcreteImplementorA extends Implementor{
	@Override
	public void operation() {
		System.out.println("具體實現A的方法執行");
	}
}

public class ConcreteImplementorB extends Implementor{
	@Override
	public void operation() {
		System.out.println("具體實現B的方法執行");
	}
}

Abstraction

public abstract class Abstractor {
	protected Implementor implementor;
	
	//設置Implementor
	public void setImplementor(Implementor implementor) {
		this.implementor = implementor;
	}
	
	public abstract void operation();
}

RefinedAbstraction

public class RefinedAbstractor extends Abstractor{
	@Override
	public void operation() {
		implementor.operation();
	}
}

測試類

public class Test {

	public static void main(String[] args) {
		Abstractor a = new RefinedAbstractor();
		
		a.setImplementor(new ConcreteImplementorA());
		a.operation();
		
		a.setImplementor(new ConcreteImplementorB());
		a.operation();
	}

}

結果

三、總結

橋接模式,“將抽象部分和實現部分分離”,如果不好理解,可以這樣理解:將一個系統(或一個對象)從多維度分類,把多維度各自獨立出來,獨立變化,取代多層級繼承層次結構,減少它們之間的耦合。

1. 適用場景

  • 系統存在多層繼承結構。
  • 系統可以從多個維度進行分類,並且每個維度可以獨立變化和擴展。

2. 優點

  • 解耦。分離了“抽象類”和“實現類”,解耦了抽象和實現的固定關係,讓它們可以在各自的維度裏變化而不互相影響。
  • 可擴展性強。這種多維度實現形式,使得系統的擴展性更強了,只需要擴展各自維度(增加子類實現)即可,而不需要修改原系統代碼,符合“開放-封閉原則”。
  • 複用性強。分離多維度替代多層繼承,減少子類個數,複用性更強,相較於多層繼承,更符合“單一職責原則”。
  • 客戶不需要知道具體的細節,對客戶不透明。

3. 缺點

對系統進行多維度的分離比較有難度,需要對整個系統業務有一定的理解和經驗。

 

 

寫在最後,

本文主要是小貓看了《大話設計模式》的一些記錄筆記,再加之自己的一些理解整理出此文,方便以後查閱,僅供參考。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章