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