设计模式之桥接模式:如何实现抽象协议与不同实现的绑定?
在上一講中,我們學習了第一種結構型模式——適配器模式,它是在不改變目標類代碼的情況下,通過引入適配器類來給目標類擴展功能。適配器模式在維護開發中經常會使用到,比如,常用在一些無法直接修改原有功能的舊系統里,開發一些新的擴展功能。
今天,我們繼續學習另外一種結構型模式——橋接模式。橋接模式的原理非常簡單,但是使用起來會有一定的難度,所以相對于適配器模式來說,在理解橋接模式時,我們學習的重點要能跳出局部,多從整體結構上去思考。
話不多說,讓我們正式開始今天的學習吧!
模式原理分析
橋接模式的定義是:將抽象部分與它的實現部分分離,使它們都可以獨立地變化。
不過,這里的抽象常常容易被理解為抽象類,并將實現理解為繼承后的“派生類”,但是這樣理解存在局限性,因為 GoF 的本意是想表達“從對象與對象間的關系去看,做抽象實體與抽象行為的分離”,所以使用抽象實體和抽象行為來描述更為準確。
我們來看看橋接模式的 UML 描述,如下圖所示:
從該圖中,我們可以看到橋接模式主要包含了以下四個關鍵角色。
-
抽象實體:定義的一種抽象分類。比如,電腦中的 CPU、內存、攝像頭、顯示屏等。
-
具體實體:繼承抽象實體的子類實體。比如,Intel i7 CPU、三星內存、徠卡攝像頭、京東方顯示屏幕等。
-
抽象行為:定義抽象實體中具備的多種行為。比如,CPU 邏輯運算、內存讀寫存儲、攝像頭拍照、屏幕顯示圖像等。
-
具體行為:實現抽象行為的具體算法。比如,Intel 使用 X64 架構實現 CPU 邏輯運算,Mac M1 芯片使用 ARM 架構實現 CPU 邏輯運算,等等。
在我看來,橋接模式原理的核心是抽象與抽象之間的分離,這樣分離的好處就在于,具體的實現類依賴抽象而不是依賴具體,滿足 DIP 原則,很好地完成了對象結構間的解耦。換句話說,抽象的分離間接完成了具體類與具體類之間的解耦,它們之間使用抽象來進行組合或聚合,而不再使用繼承。
下面我們再來看看橋接模式對應 UML 圖的代碼實現,具體如下:
public abstract class AbstractEntity {//行為對象protected AbstractBehavior myBehavior;//實體與行為的關聯public AbstractEntity(AbstractBehavior aBehavior) {myBehavior = aBehavior;}//子類需要實現的方法public abstract void request();}public class DetailEntityA extends AbstractEntity {public DetailEntityA(AbstractBehavior aBehavior) {super(aBehavior);}@Overridepublic void request() {super.myBehavior.operation1();}}public class DetailEntityB extends AbstractEntity {public DetailEntityB(AbstractBehavior aBehavior) {super(aBehavior);}@Overridepublic void request() {super.myBehavior.operation2();}}public abstract class AbstractBehavior {public abstract void operation1();public abstract void operation2();}public class DetailBehaviorA extends AbstractBehavior{@Overridepublic void operation1() {System.out.println("op-1 from DetailBehaviorA");}@Overridepublic void operation2() {System.out.println("op-2 from DetailBehaviorA");}}public class DetailBehaviorB extends AbstractBehavior {@Overridepublic void operation1() {System.out.println("op-1 from DetailBehaviorB");}@Overridepublic void operation2() {System.out.println("op-2 from DetailBehaviorB");}}從上面的代碼實現你會很容易發現,橋接模式封裝了如下變化:
-
實體變化;
-
行為變化;
-
兩種變化之間的關系;
-
變化引起的變化。
橋接模式封裝變化的本質上是對事物進行分類(實體),并對實體中的功能性(行為)再劃分的一種解決方案。比如,電子產品可以被分為手機、電腦等,其中手機隱藏了手機一類相關的變化;同樣,手機和電腦都具備使用 App 軟件的功能,它們各自隱藏了如何使用 App 的具體方式。在面向對象軟件開發中,我們通常是使用接口或抽象類來作為抽象實體和具體實體,使用具體對象實例和實現接口的對象作為抽象行為和具體行為。
所以說,橋接模式的本質是通過對一個對象進行實體與行為的分離,來將需要使用多層繼承的場景轉換為使用組合或聚合的方式,進而解耦對象間的強耦合關系,達到對象與對象之間的動態綁定的效果,提升代碼結構的擴展性。
使用場景分析
一般來講,橋接模式的常用場景有如下幾種。
-
需要提供平臺獨立性的應用程序時。 比如,不同數據庫的 JDBC 驅動程序、硬盤驅動程序等。
-
需要在某種統一協議下增加更多組件時。 比如,在支付場景中,我們期望支持微信、支付寶、各大銀行的支付組件等。這里的統一協議是收款、支付、扣款,而組件就是微信、支付寶等。
-
基于消息驅動的場景。 雖然消息的行為比較統一,主要包括發送、接收、處理和回執,但其實具體客戶端的實現通常卻各不相同,比如,手機短信、郵件消息、QQ 消息、微信消息等。
-
拆分復雜的類對象時。 當一個類中包含大量對象和方法時,既不方便閱讀,也不方便修改。
-
希望從多個獨立維度上擴展時。 比如,系統功能性和非功能性角度,業務或技術角度等。
-
需要在運行時切換不同實現方法時。 比如,通過門面模式調用外部 RPC 服務。
接下來,我們通過一個不同操作系統下的文件上傳例子來快速理解橋接模式的使用場景。
我們首先創建一個抽象實體類 FileUploader,它包含了兩個抽象行為:上傳(upload)和檢查(check)。
public interface FileUploader {Object upload(String path);boolean check(Object object);}然后,我們再建立一個具體實體類 FileUploaderImpl,其中包含了抽象行為類 FileUploadExcutor(文件上傳執行器),實現了抽象行為 upload 和 check。
public class FileUploaderImpl implements FileUploader {private FileUploadExcutor excutor = null;public FileUploaderImpl(FileUploadExcutor excutor) {this.excutor = excutor;}@Overridepublic Object upload(String path) {return excutor.uploadFile(path);}@Overridepublic boolean check(Object object) {return excutor.checkFile(object);}}public interface FileUploadExcutor {Object uploadFile(String path);boolean checkFile(Object object);}接下來,在 Linux 平臺上實現文件上傳執行器 LinuxFileUpLoadExcutor,在 Windows 上實現文件上傳執行器 WindowsFileUpLoadExcutor,具體代碼如下所示:
public class LinuxFileUpLoadExcutor implements FileUploadExcutor {@Overridepublic Object uploadFile(String path) {return null;}@Overridepublic boolean checkFile(Object object) {return false;}}public class WindowsFileUpLoadExcutor implements FileUploadExcutor {@Overridepublic Object uploadFile(String path) {return null;}@Overridepublic boolean checkFile(Object object) {return false;}}從上面的代碼中,你會發現:通過將文件上傳執行器和文件上傳行為進行分離,就能實現實體和行為的靈活演化。比如,當你想要實現一個新的上傳到云存儲的文件上傳執行器時,你可以先新建一個叫 OSSFileUploaderImpl 的具體實現類,然后建立對應的云存儲文件執行器,接著再分別實現華為云、阿里云、騰訊云等各種不同云存儲的文件上傳執行器。如果你還想要在執行器里加入新的行為,比如刪除,這時平臺上的執行器并不需要調用“刪除”這個接口,這樣就做到了實體和行為的解耦,極大地提升了代碼的擴展性。
細心的你可能已經發現了,當我們做了實體和行為的分離后,我們還可以結合更多的模式來擴展橋接模式。比如,這里我簡單擴展了一下橋接模式的 UML 圖:
在實現抽象行為時,我們可以使用上一講介紹的適配器模式來擴展功能,也可以使用后面會學到的門面模式來擴展更多外部的服務功能。
總體來說,橋接模式的使用場景非常靈活,側重于實體和行為的分離,然后再基于這兩個維度進行獨立的演化。
為什么要使用橋接模式?
分析完橋接模式的原理和使用場景后,我們再來說說使用橋接模式的原因,主要有以下三個。
第一個,為了靈活擴展代碼結構。 上面使用了適配器模式和門面模式的橋接模式就是一個很好的思考方向,與通過硬編碼直接調用 API 的形式相比,“通過模式來擴展”會更容易控制代碼行數和邏輯結構。而從我多年的工作經驗來看,在很多大規模的代碼系統中,有結構的代碼的可維護性會更好。因為是人來維護代碼的,而人的特性是天生對結構型的東西更“敏感”,并且靈活的結構在后期進行代碼重構時也能更好地替換與修改。
第二個,為了更好地解決跨平臺兼容性問題。 橋接模式之所以能很好地解決跨平臺的兼容性問題,就是因為橋接模式通過抽象層次上結構的分離,讓相關的分類能夠聚合到各自相關的層次邏輯中,而不同的平臺對于同一個 API 在具體的代碼實現上是不同的,這樣反而符合不同操作系統按照各自維度演化的特性。
第三個,為了在運行時組合不同的組件。 無論是框架還是外部服務,我們都需要基于一個統一的協議進行協同工作,但是通過靜態的繼承方法很難做到在程序運行時進行方法或組件的動態更換。而使用橋接模式和門面模式就可以很方便地進行替換,比如,在上面文件上傳執行器的案例中,我們可以使用一個統一的 API 網關調用不同的云服務來完成文件上傳。
收益什么?損失什么?
使用橋接模式主要有以下四個大的優點。
-
分離實體與行為,可以提升各自維度的演化效率。 比如,訂單中的會員信息可以理解為抽象實體,普通會員和 plus 會員就是不同的具體實體;會員中的積分累計就是抽象行為,不同會員按照各自的積分計算軌跡進行計算就是具體行為的體現。那么,會員可以再繼續增加更多會員類別,而積分計算規則也可以不斷更新。
-
符合開閉原則,提升代碼復用性。 每一個維度的類都以組合或聚合關系進行合作,新增類或修改類都在各自類內部進行,不影響其他類。
-
用組合關系替代了多重繼承,提升了代碼結構的演化靈活性。我們都知道多繼承違背了單一職責原則,雖然關聯性更強,但復用性很差。組合關系的優勢就在于可以在任意階段進行升級與替換,并且可以按需進行組合與撤銷,這對于需求快速變化的開發場景而言很適用,能夠極大地提升代碼結構的靈活性。
-
符合表達原則,提升代碼的可理解性。由于橋接模式從抽象層次就進行了分離,不同的類別會按照各自的特點進行演化,所以不管是在結構上還是代碼內在含義上,都更聚焦,這樣在閱讀代碼時也就能更容易理解。
同樣,橋接模式也有一些缺點。
-
增加了維護成本。 橋接模式因為需要做很多實體和行為的分離,所以會間接地要增加不少代碼行數。再加上使用組合和聚合關系不像繼承關系那樣容易找到對象之間的調用關系,稍不注意就會影響到其他對象,這樣大大增加了代碼修改維護的成本。
-
導致性能下降。 組合或聚合關系在面向對象編程中使用的是委托的實現方式,簡單理解就是調用的對象變多了,自然也就影響到了程序的性能。
-
增加設計難度。 橋接模式更重視聚合而非繼承關系,那么就需要建立更多的抽象層,要求開發者針對抽象層進行設計與編程。我們都知道,找到正確的抽象層有時是一件相當困難的事情,雖然現在有很多優秀的設計能夠作為借鑒,但在一些新的領域里依然會有一定的設計難度。
總結
橋接模式可以說是 DIP 原則的具體實踐。在軟件開發中,一個對象可以從實體和行為兩個角度來進行分離,其實就是將依賴從一個大而全的對象變換到依賴兩個可以獨立變化的維度,控制也就發生了反轉。
橋接模式因為重視組合和聚合,從而有效避免了多重繼承帶來的問題。也就是說,通過抽象實體與抽象行為的關聯,將靜態的繼承關系轉換為了動態的組合關系,從而使得系統結構更加靈活。
在實際開發中,你應該將橋接模式和更多的模式結合起來使用,將不同模式或服務作為某一個獨立的維度來進行演化。另外,多在實踐中尋找可以做實體和行為分離的場景,并嘗試使用橋接模式來解決,這才是學習橋接模式最好的辦法。
課后思考
在使用橋接模式新增加實體時,你認為能夠復用現有抽象行為的可能性有多大?如果不能,那又會帶來哪些問題呢?
歡迎留言分享,我會第一時間給你回復。
在下一講,我會接著與你分享“組合模式:如何用樹形結構處理對象之間的復雜關系?”這個話題,記得按時來聽課!
總結
以上是生活随笔為你收集整理的设计模式之桥接模式:如何实现抽象协议与不同实现的绑定?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 记在2019,winter is com
- 下一篇: asp.net ajax控件工具集 Au