本章主軸#

橋接模式(Bridge)是本書到目前為止最複雜、也是最強大的模式。本章不只介紹 Bridge,還示範如何從問題中「推導」出這個模式——這個推導過程比結果更值得學。

GoF 對 Bridge 的意圖描述#

把抽象(abstraction)與其實作(implementation)解耦,使兩者可以獨立變化。

這段話容易讓人困惑。關鍵釐清:「實作」指的是抽象類別與其衍生類別所使用的物件——不是抽象類別的衍生類別本身(後者叫具象類別)。

推導範例:要畫多種形狀,又要支援多套繪圖程式#

初始需求#

  • 必須支援 DP1、DP2 兩個繪圖程式
  • 介面不一致:
    • DP1:draw_a_line(x1,y1,x2,y2) / draw_a_circle(x,y,r)
    • DP2:drawline(x1,x2,y1,y2) / drawcircle(x,y,r)
  • 起初只有 Rectangle,client 不應在意實際使用哪一個 DP

嘗試一:純繼承#

  • Rectangle 為抽象類別,V1RectangleV2Rectangle 各自實作 drawLine
  • 後來追加 Circle,再多一層 Shape 抽象,RectangleCircle 都繼承自它
  • 每種形狀都得衍生兩個版本(V1、V2)

Figure 10-3: 直觀做法——兩種 Shape × 兩種繪圖程式造成的類別爆炸

Figure 10-7: 替代繼承結構——先依繪圖程式分支,仍無法消除重複

問題浮現:類別爆炸#

  • 形狀 × 繪圖程式 = 類別數
  • 加一個新繪圖程式 → 多 N 個類別
  • 加一個新形狀 → 多 M 個類別
  • 還伴隨重複程式碼、低凝聚、緊密耦合

這正是「過度依賴繼承」的典型症狀。Alexander 的話就是:抽象(形狀)與實作(繪圖程式)被綁死了,無法獨立變化。

在不知道解法前就能識別問題#

學習設計模式時的常見錯誤:盯著「解法」找適用之處。

更有效的做法是研究模式背後的問題——這樣才知道「何時、為什麼」要用它。

「我有一個抽象,它有不同實作;我希望這兩個維度能獨立變動。」——一旦你能描述問題,即使還不會實作 Bridge,也已經知道它就是合適的解法。

用基本策略推導 Bridge#

依循兩條老原則:

  • 找出變動點並封裝
  • 偏好聚合勝於繼承

步驟一:分別封裝兩個變動軸#

  • 把形狀的變化(Rectangle、Circle、…)封進 Shape
  • 把繪圖程式的變化(V1Drawing、V2Drawing、…)封進 Drawing

Figure 10-9: 找出變動點——抽象的 Shape 與 Drawing

Figure 10-10: 表達變化——Shape 衍生 Rectangle/Circle,Drawing 衍生 V1Drawing/V2Drawing

步驟二:決定誰使用誰#

  • Drawing 不該知道 Shape 的細節(會違反「物件對自己負責」與封裝)
  • 應由 Shape 使用 Drawing

完成設計#

  • Shape 持有 Drawing 參考,呼叫 drawLinedrawCircle
  • V1Drawing 內呼叫 DP1,V2Drawing 內呼叫 DP2
  • 任何時刻只有三個物件:Client、某個具體 Shape、某個具體 Drawing

Figure 10-11: 把兩個類別樹串起來——Shape 透過聚合使用 Drawing

Figure 10-12: 擴充設計——加入 drawLine、drawCircle 與 DP1/DP2 連結

Figure 10-13: 抽象與實作徹底分離——左為 Shape Abstraction,右為 Drawing Implementation

Figure 10-14: 任何時刻其實只有三個物件:Shape、Drawing、DP

abstract class Shape {
    protected Drawing myDrawing;
    abstract public void draw();

    Shape(Drawing drawing) { myDrawing = drawing; }

    protected void drawLine(double x1, double y1, double x2, double y2) {
        myDrawing.drawLine(x1, y1, x2, y2);
    }
    protected void drawCircle(double x, double y, double r) {
        myDrawing.drawCircle(x, y, r);
    }
}

「One rule, one place」——Shape 內保留 drawLinedrawCircle 方法,避免每個 Rectangle / Circle 子類都直接呼叫 Drawing。將來其他形狀也能重用,且改動只在一處。

Bridge 的關鍵特性#

欄位內容
Intent解耦一群實作與使用它們的物件
Problem抽象的衍生類別需使用多種實作,又不能讓類別數爆炸
Solution為實作定義介面,由抽象類別的衍生類別透過該介面使用
ParticipantsAbstraction 定義對外的介面;Implementor 定義實作介面;Concrete 雙方互不知對方實際型別
Consequences提升擴充性;client 不知實作細節;類別數隨「形狀數 + 實作數」線性增長
Implementation把實作封進抽象類別或介面,Abstraction 持有其參考

Figure 10-15: Bridge 模式的通用結構——Abstraction 持有 Implementor,雙方互不知具體型別

實務筆記#

印表機驅動是經典案例#

但 Bridge 真正威力在於提醒你:當你看到 X 永遠搭配 S、Y 永遠搭配 T 時,可以考慮把 S、T 抽象成共同實作介面,讓 X、Y 都能任選其一。

常與 Adapter 搭配使用#

兩個(或更多)模式緊密整合時,稱為複合模式(compound design pattern)——不再是「composite」是為避免和 Composite 模式混淆。

V1、V2 是現成系統,介面非作者所定,因此需要 Adapter 把它們適配成 Drawing 的介面。

Bridge 不總是完美#

  • 加新實作很乾淨——只新增一個 ConcreteImplementor
  • 加新抽象有時會逼你修改實作介面(例如要新增橢圓 →Drawing 要加新方法)
  • 但即使要改,影響範圍仍局限、有明確流程

設計模式不是給你完美解法,而是凝聚多年集體經驗的「比你獨自想出來更好」的解法。

從重構(refactoring)的角度看 Bridge#

  • 不要為了未來「可能」的多實作就先做 Bridge
  • 透過遵守「One rule, one place」,將來真的需要 Bridge 時也能輕鬆重構

Bridge 中的物件導向原則#

原則在 Bridge 裡的展現
物件對自己負責各種 Shape 自行呼叫 draw
抽象類別Shape、Drawing 都是純概念佔位符
透過抽象類別封裝Client 看不到具體 Shape;Shape 看不到具體 Drawing
One rule, one placedrawLine、drawCircle 集中在 Shape
可測試性N×M 種組合的測試,被縮減成 N+M 個獨立測試

本章要記住的事#

  • Bridge 的本質:抽象與實作分屬兩條獨立繼承樹,透過聚合連結
  • 它解決多軸變動下的類別爆炸問題
  • 你可以在「會不會實作」之前,就先「決定要用 Bridge」
  • 常與 Adapter 搭配;最終的解法可能是複合模式
  • 模式的價值在於提升思考層次,幫你看見原本被細節遮蔽的可能