
結構型模式的最后一個,是橋接模式(Bridge)。它可能是七個結構型里最抽象、也最容易被講得云里霧里的一個,但只要抓住它要解決的那個具體問題,就會豁然開朗。這個問題是:當一個東西同時在兩個(或多個)獨立的維度上變化時,如果用繼承去表達,類的數量會維度相乘式爆炸。我們在裝飾器那篇見過一次類爆炸(2? 組合),但那是同一維度的多個可選增強的疊加。橋接面對的是另一種類爆炸,更常見也更隱蔽——兩個正交維度的交叉。舉個我們訂單場景里的例子:訂單有不同類型(普通訂單、團購訂單、預售訂單),發通知又有不同渠道(短信、App 推送、郵件)。這是兩個完全獨立的維度:訂單類型的增減,和通知渠道的增減,毫不相干??扇绻阌美^承硬來,就會寫出普通訂單短信通知、普通訂單App通知、團購訂單郵件通知……3 種類型 × 3 種渠道 9 個類,再加一種類型或渠道,就是 12 個、16 個,呈乘法增長。橋接模式的核心思想,就是把這兩個糾纏在一起的維度拆開,讓它們各自獨立地變化,再用一座橋把它們連起來。GoF 給它的正式定義是一句很經典但初看費解的話:“將抽象部分與它的實現部分分離,使它們都可以獨立地變化?!?這一篇我們就把這句話徹底翻譯成人話。這篇文章按這條線索展開:先復現兩個維度用繼承導致的乘法類爆炸;再引出橋接如何把維度拆開、用組合搭一座橋;然后講清那句抽象與實現分離到底在說什么(這是全篇最關鍵、也最容易誤解的地方);接著看它的現實身影;最后辨析它與相近模式,給出適用邊界,并為結構型模式畫上句號。貫穿例是訂單類型 × 通知渠道。目錄兩個維度用繼承:乘法式類爆炸橋接模式:把維度拆開,搭一座橋抽象與實現分離到底在說什么現實身影:JDBC 與日志框架橋接 vs 相近模式,以及什么時候用結構型模式,到此收官一、兩個維度用繼承:乘法式類爆炸先把問題擺清楚。我們要做給訂單發通知這件事,它牽涉兩個維度:維度 A:訂單類型—— 普通訂單、團購訂單、預售訂單(不同類型通知的文案、邏輯不同);維度 B:通知渠道—— 短信、App 推送、郵件(不同渠道的發送方式不同)。如果用繼承來表達某類訂單通過某渠道發通知,你會不自覺地寫成這樣:abstractclassOrderNotify{abstractvoidnotifyUser();}// 為類型 × 渠道的每一種組合,都建一個類classNormalOrderSmsNotifyextendsOrderNotify{...}// 普通訂單 短信classNormalOrderAppNotifyextendsOrderNotify{...}// 普通訂單 AppclassNormalOrderEmailNotifyextendsOrderNotify{...}// 普通訂單 郵件classGroupOrderSmsNotifyextendsOrderNotify{...}// 團購訂單 短信classGroupOrderAppNotifyextendsOrderNotify{...}// 團購訂單 App// ... 3 類型 × 3 渠道 9 個類災難在于乘法增長:現在是 3×39 個類。產品說再加一個預售的訂單類型,你要加 3 個類(配齊三個渠道);運營說接入一個企業微信渠道,你要加 3 個類(配齊三種訂單)。每增加一個維度上的一項,類的數量就按另一個維度的規模成倍增加。而且這些類里全是重復——NormalOrderSmsNotify和GroupOrderSmsNotify里,發短信的代碼幾乎一樣,只是訂單文案不同。問題的根源是:繼承只有一條軸,卻被迫同時表達兩個維度。你把訂單類型和通知渠道這兩件本來毫不相干的事,硬塞進了同一條繼承鏈,于是它們被死死綁在一起,任何一個變,都牽動另一個。正確的直覺應該是:這倆維度本來就該分開。訂單類型該有自己的一套類,通知渠道該有自己的一套類,然后在運行時把某個類型和某個渠道自由組合起來——3 個類型類 3 個渠道類 6 個類,就能覆蓋 9 種組合;以后加一個類型是 1,加一個渠道也是 1,變回了加法。怎么做到?用組合,搭一座橋。二、橋接模式:把維度拆開,搭一座橋橋接的做法:把兩個維度拆成兩套獨立的繼承體系,然后讓其中一個體系(抽象)持有另一個體系(實現)的引用——這個持有的組合關系,就是那座橋。先把通知渠道這個維度獨立出來,做成一套體系(橋接里管它叫實現部分):// 維度 B:通知渠道 —— 獨立的一套體系publicinterfaceNotifyChannel{voidsend(Stringmessage);}classSmsChannelimplementsNotifyChannel{publicvoidsend(Stringm){/* 發短信 */}}classAppChannelimplementsNotifyChannel{publicvoidsend(Stringm){/* App 推送 */}}classEmailChannelimplementsNotifyChannel{publicvoidsend(Stringm){/* 發郵件 */}}再把訂單類型這個維度也獨立出來(橋接里管它叫抽象部分),關鍵是——它持有一個NotifyChannel引用,這就是橋:// 維度 A:訂單類型 —— 另一套體系,持有渠道引用(這就是橋)publicabstractclassOrderNotify{protectedfinalNotifyChannelchannel;// ← 橋:持有另一個維度publicOrderNotify(NotifyChannelchannel){this.channelchannel;}publicabstractvoidnotifyUser();}classNormalOrderNotifyextendsOrderNotify{publicNormalOrderNotify(NotifyChannelchannel){super(channel);}publicvoidnotifyUser(){channel.send(您的訂單已創建);// 委托給渠道去發,自己只管文案}}classGroupOrderNotifyextendsOrderNotify{publicGroupOrderNotify(NotifyChannelchannel){super(channel);}publicvoidnotifyUser(){channel.send(拼團成功,訂單已生成);// 團購的文案不同,但發送委托給渠道}}現在,某類訂單 某渠道的組合,是在運行時自由拼出來的:// 普通訂單 短信OrderNotifyn1newNormalOrderNotify(newSmsChannel());n1.notifyUser();// 團購訂單 郵件 —— 隨意組合,不用新建類OrderNotifyn2newGroupOrderNotify(newEmailChannel());n2.notifyUser();對比第一節,升級點非常清晰:類的數量從 3×39 個,降到了 336 個;從乘法變回了加法。加一個訂單類型?加一個OrderNotify子類(1)。加一個通知渠道?加一個NotifyChannel實現(1)。兩個維度各自獨立地變化,誰也不牽連誰——這正是 GoF 定義里都可以獨立地變化的含義。用一張圖看這座橋最直觀:圖里最該記住的,就是中間那條連接兩套體系的橋(OrderNotify持有NotifyChannel)。左右兩欄可以各自向下無限擴展,而它們的組合數是左 × 右,類的數量卻只是左 右。橋接的全部威力,就濃縮在這座用組合搭起來的橋上——它又一次是組合優于繼承的勝利。三、抽象與實現分離到底在說什么現在來啃 GoF 那句定義:“將抽象部分與它的實現部分分離?!?這句話之所以讓無數人困惑,是因為這里的抽象和實現,不是我們平時說的抽象類/接口和實現類的意思。必須把它翻譯清楚,否則你會一直誤解橋接。在橋接的語境里:“抽象”(Abstraction)指的是主體維度、高層的那套邏輯—— 在我們的例子里就是OrderNotify(訂單類型)。它是面向用戶、定義要做什么的那一側。“實現”(Implementor)指的是被主體維度調用的、底層的那套能力—— 就是NotifyChannel(通知渠道)。它提供具體怎么做的基礎操作,供上層調用。所以抽象與實現分離,翻譯成人話就是:把高層邏輯維度和底層能力維度拆成兩套獨立的體系,讓高層通過組合去調用底層,而不是用繼承把兩者焊死。OrderNotify(高層)通過持有的channel(橋)去調用NotifyChannel(底層)的能力,兩者解耦,各自能獨立擴展和替換。這里有個關鍵點,也是橋接和策略模式(下一篇)容易混的地方:橋接強調的是兩側都會獨立演化的、穩定的維度劃分——它是一種架構層面的結構設計,你在設計之初就預見到這里有兩個正交維度會各自增長,于是提前用橋把它們分開。它不是臨時替換一個算法,而是讓兩個類體系長期地、各自地生長。理解了抽象主體維度、實現被調用的能力維度,橋接就不再神秘了——它就是把’本該分開的兩個維度’用組合連起來,如此而已。四、現實身影:JDBC 與日志框架橋接最經典的工業級案例,是JDBC。想想 JDBC 的結構:抽象側:java.sql包里的Connection、Statement、DriverManager這套 API,是面向應用開發者的、穩定的高層抽象——你寫的代碼只調這套接口;實現側:各家數據庫廠商提供的JDBC 驅動(MySQL 的、Oracle 的、PostgreSQL 的),是底層實現——它們各自實現了 JDBC 定義的接口。你的應用代碼(抽象側)通過DriverManager持有一個具體的Driver(實現側),這就是那座橋。于是兩側獨立變化:JDK 升級 JDBC API(抽象側演進)不影響驅動;數據庫廠商更新驅動(實現側演進)不影響你的應用代碼;你想從 MySQL 換成 Oracle,只需換個驅動(換實現),應用代碼一行不改。這正是橋接抽象與實現各自獨立變化的完美體現,也是為什么 JDBC 能讓一套代碼適配幾十種數據庫。另一個例子是日志框架的門面 實現:SLF4J(抽象側,統一 API) Logback/Log4j2(實現側,具體日志實現)。你的代碼面向 SLF4J,底層實現可隨意替換。有意思的是,SLF4J 我們在外觀那篇也提過。這說明一個真實的框架,往往同時用到多個模式:SLF4J 對使用者是外觀(簡化日志的使用),對API 與實現的解耦則體現了橋接的思想。模式不是互斥的標簽,而是從不同角度描述同一個設計的多個側面——不必糾結它到底是哪個模式,理解它解決了什么問題更重要。五、橋接 vs 相近模式,以及什么時候用橋接容易和幾個模式混,辨析一下:橋接 vs 策略(下一篇):兩者代碼結構很像(都是持有一個接口引用、委托調用)。區別在意圖和粒度:策略是一個算法有多種可替換的實現,關注的是運行時替換單一行為,通常只有一個變化維度(策略本身);橋接是兩個維度都會獨立、長期地擴展,關注的是架構層面的維度分離,強調的是兩側各自成體系地生長。一句話:策略換的是一個算法,橋接分的是兩條演化軸。橋接 vs 適配器:適配器是事后補救——兩個已經存在、接口不兼容的東西,加個轉接頭讓它們能協作;橋接是事前設計——在設計之初就主動把兩個維度分開,讓它們從一開始就能獨立生長。適配器解決已經不兼容了怎么辦,橋接解決怎么設計才不會耦合。適合用橋接的信號:一個類存在兩個或多個獨立變化的維度(正交),且每個維度都可能擴展;你不希望這些維度之間用繼承產生乘法式的類爆炸;你想在運行時靈活組合不同維度的實現。不必用的信號:只有一個變化維度——那可能用策略、或者別的模式就夠了,橋接是為多維度準備的;兩個維度中有一個其實是穩定的、幾乎不變的——那分離的收益不大,可能是過度設計。判斷的核心還是那句話:先確認真的存在兩個都會獨立增長的維度,橋接才有價值。很多時候你以為有兩個維度,其實一個維度是固定的,那就別為想象中的第二維度提前搭橋——這又是過度設計的陷阱。六、結構型模式,到此收官第 13 篇結束,七個結構型模式全部講完了。它們關心類和對象如何組合連接,我們用一張總表收尾,把整個家族刻進腦子:模式一句話核心意圖關鍵手法代理 Proxy給對象一個替身控制訪問同接口包裝 織入裝飾器 Decorator洋蔥層層包裹動態增強功能同接口 組合 遞歸適配器 Adapter接口轉接頭轉換接口轉換 委托組合 Composite單個和一組一樣統一處理樹形容器持有抽象組件外觀 Facade給子系統蓋門面簡化使用中間層收攏編排享元 Flyweight提取共享省內存扛住海量對象內外狀態分離 緩存池橋接 Bridge分離兩個維度讓維度獨立變化抽象持有實現(搭橋)回望這七個模式,一條主線貫穿始終:它們幾乎全都在用組合代替繼承。代理、裝飾、適配、橋接靠組合持有對象,組合靠組合搭樹,外觀靠組合收攏子系統,享元靠組合(引用)共享。第一篇講的合成復用原則(組合優于繼承),在整個結構型家族里被反復驗證——當你想改變、擴展、連接對象的行為時,組合幾乎總比繼承更靈活。這也是結構型模式給我們最深的一課。小結。橋接模式專治兩個獨立維度用繼承導致的乘法類爆炸。它把兩個維度拆成兩套獨立體系,讓抽象側(主體維度/高層邏輯)通過組合持有實現側(被調用的能力維度)的引用——這個組合關系就是橋,從而讓兩個維度各自獨立地變化,把類的數量從乘法變回加法。GoF 那句抽象與實現分離的真正含義,是把高層邏輯維度和底層能力維度解耦,JDBC 的 API 與驅動、SLF4J 與日志實現都是它的經典身影。至此,結構型七模式全部收官,而它們共同的底色,就是組合優于繼承。從下一篇開始,我們進入設計模式里數量最多、也最精彩的一類——行為型模式,它們關心對象之間如何協作、如何分配職責。第一站是消滅if-else的利器:策略模式。