概述#

本章以一個名為 Lexi 的 WYSIWYG 文件編輯器為案例,展示如何在實際應用中運用設計模式。Lexi 的介面包含文件編輯區、下拉選單、捲軸和頁面圖示,使用者可自由混合文字與圖形。

作者從 Lexi 的設計中提煉出 七個設計問題,每個問題都導向一個或多個設計模式的解決方案。這種「問題驅動」的教學方式,讓讀者理解模式不是憑空出現的抽象概念,而是為了解決具體設計難題而產生的。

Figure 2.1: Lexi's user interface

七大設計問題一覽#

設計問題核心挑戰對應模式
文件結構統一處理簡單與複雜元素Composite
格式化可替換的排版演算法Strategy
UI 裝飾動態添加邊框、捲軸等Decorator
多種外觀風格跨平台 Widget 建立Abstract Factory
多種視窗系統封裝視窗系統實作差異Bridge
使用者操作統一的請求封裝與 Undo/RedoCommand
拼字檢查與斷字不修改 Glyph 類別就能增加分析功能Iterator + Visitor

文件結構 — Composite Pattern#

問題#

文件由字元、圖形、行、欄、頁等多層次元素組成。設計上需要:

  • 維護文件的物理結構(文字與圖形排列成行、欄、表格等)
  • 統一處理文字與圖形,不為任一方做特殊處理
  • 統一處理單一元素與群組元素,支援任意複雜的巢狀結構

解法:Recursive Composition#

透過 Recursive Composition(遞迴組合) 的技巧,將簡單圖形元素組合成行,行組合成欄,欄組合成頁,以此類推。每個元素(無論可見或不可見的結構元素)都對應一個物件。

Figure 2.2: Recursive composition of text and graphics

Figure 2.3: Object structure for recursive composition of text and graphics

Glyph 抽象類別#

定義 Glyph 作為所有文件結構物件的抽象基底類別,子類別包含基本圖形元素(Character、Image)和結構元素(Row、Column)。

Glyph 的三大職責:

  • 繪製自身Draw(Window) 操作
  • 回報佔用空間Bounds() 回傳包圍矩形;Intersects() 判斷點擊命中
  • 管理子元素InsertRemoveChildParent 操作

Figure 2.4: Partial Glyph class hierarchy

Composite Pattern 將遞迴組合的概念用物件導向的方式表達:讓葉節點和組合節點實作相同介面,客戶端無需區分處理。

格式化 — Strategy Pattern#

問題#

「表示」(representation)和「格式化」(formatting)是兩件事。有了文件結構後,還需要演算法來決定如何將 glyph 排列成行、欄。格式化演算法往往複雜且各有取捨:

  • 快速但品質一般的簡單演算法
  • 考慮「色彩」(文字與空白均勻分佈)的 TeX 演算法
  • 以空間換取速度的快取策略

解法:Compositor 與 Composition#

將格式化演算法封裝在 Compositor 物件中,與文件結構解耦:

  • Composition — 一種特殊的 Glyph,持有一個 Compositor 實例
  • Compositor — 抽象類別,子類別實作不同的排版演算法(如 SimpleCompositorTeXCompositor

Figure 2.5: Composition and Compositor class relationships

運作流程:Composition 在需要格式化時呼叫 Compositor 的 Compose 操作,Compositor 遍歷 Composition 的子元素,插入 Row、Column 等結構性 glyph。

Figure 2.6: Object structure reflecting compositor-directed linebreaking

Strategy Pattern 的核心:將演算法封裝在物件中,讓策略(Compositor)與上下文(Composition)之間的介面夠通用,就能在不修改任何一方的前提下替換演算法,甚至可以在 執行時期動態切換

UI 裝飾 — Decorator Pattern#

問題#

文件編輯區需要邊框和捲軸等裝飾。這些裝飾可能隨時增減,若用繼承實作會造成類別爆炸(BorderedComposition、ScrollableComposition、BorderedScrollableComposition…)。

解法:Transparent Enclosure#

透明封裝(Transparent Enclosure) 結合兩個概念:

  • 單一子元素組合 — 裝飾物件包裹一個被裝飾的元件
  • 相容介面 — 裝飾物件與被裝飾物件實作相同介面,客戶端無法區分

定義 MonoGlyph 抽象類別,預設將所有請求轉發給內含的元件。子類別如 BorderScroller 覆寫特定操作,在轉發前後加入自己的行為(例如 Border::Draw 先讓元件繪製自身,再畫邊框)。

Figure 2.7: MonoGlyph class relationships

裝飾可以自由組合與排列。例如先用 Scroller 包裹 Composition,再用 Border 包裹 Scroller,順序不同會產生不同的視覺效果。

Figure 2.8: Embellished object structure

Decorator Pattern 將「裝飾」這個概念一般化:任何為物件添加職責的行為都算裝飾。相比繼承,它在執行時期動態組合,避免子類別膨脹。

多種外觀風格 — Abstract Factory Pattern#

問題#

Lexi 需要支援多種 Look-and-Feel 標準(Motif、Presentation Manager、Mac 等)。直接在程式碼中 new MotifScrollBar() 會造成:

  • 無法在執行時期切換風格
  • 移植時需逐一修改所有建構呼叫
  • 遺漏一處就可能在 Mac 應用裡出現 Motif 選單

解法:抽象化物件建立過程#

定義 GUIFactory 抽象類別,提供 CreateScrollBarCreateButton 等操作。每個具體子類別(MotifFactory、PMFactory、MacFactory)回傳對應風格的 Widget。

Figure 2.9: GUIFactory class hierarchy

Figure 2.10: Abstract product classes and concrete subclasses

客戶端透過 guiFactory 變數取得工廠實例,程式碼中完全不出現特定平台的類別名稱。工廠可在啟動時根據設定初始化,甚至可以重新初始化來動態切換整個 Widget 家族

Abstract Factory Pattern 的重點在於「產品家族」:一次替換所有相關物件(按鈕、捲軸、選單等),確保它們風格一致。

多種視窗系統 — Bridge Pattern#

問題#

不同平台有不同的視窗系統(X Window、Presentation Manager、Macintosh),API 互不相容。與 Look-and-Feel 不同,這裡無法自行實作視窗系統,必須橋接到既有的平台 API。

解法:分離抽象與實作#

建立兩個獨立的類別階層:

  • Window 階層 — 面向應用程式開發者的邏輯抽象(ApplicationWindow、IconWindow、DialogWindow)
  • WindowImp 階層 — 封裝特定視窗系統的實作(XWindowImp、PMWindowImp)

Window 持有一個 WindowImp 的參照。當 Window 的操作被呼叫時,它將請求委託給 WindowImp,由後者轉換為平台特定的 API 呼叫。例如 DrawRect 在 X Window 下呼叫 XDrawRectangle,在 PM 下則使用多段路徑 API。

Window 與 WindowImp 的配對可透過 Abstract Factory 完成:WindowSystemFactory 負責建立正確的 WindowImp 子類別實例。

Bridge Pattern 讓兩個類別階層各自獨立演化。Window 階層可以新增子類別而不影響 WindowImp,反之亦然。關鍵在於:Window 的介面面向使用者需求,WindowImp 的介面面向系統能力,兩者的設計驅動力不同。

使用者操作 — Command Pattern#

問題#

使用者操作(建立文件、儲存、剪貼、字型變更等)散佈在各個物件中,且可能從不同介面觸發(選單、按鈕、快捷鍵)。需要:

  • 統一機制存取分散的功能
  • 不將操作綁定到特定 UI 元件
  • 支援任意層級的 Undo/Redo

解法:將請求封裝為物件#

定義 Command 抽象類別,提供 Execute 操作。每個子類別封裝一種請求(FontCommand、PasteCommand、SaveCommand 等)。MenuItem 持有一個 Command 物件,使用者點擊時呼叫 Execute

Figure 2.11: Partial Command class hierarchy

Figure 2.12: MenuItem-Command relationship

Undo/Redo 機制#

  • 在 Command 介面加入 Unexecute — 反轉 Execute 的效果
  • 加入 Reversible — 回傳布林值,判斷該命令是否可復原(例如無效的字型變更不需要 Undo)
  • 維護 Command History(命令歷史) — 一個命令列表加上「現在」指標,向左 Undo、向右 Redo,支援任意層級

Command Pattern 將請求參數化為物件,讓同一個操作可以從不同入口觸發,並自然地支援 Undo/Redo、命令佇列、命令日誌等進階功能。

拼字檢查與斷字 — Iterator + Visitor Pattern#

問題#

拼字檢查和斷字需要遍歷文件結構中分散的文字資訊,且未來可能新增搜尋、字數統計、文法檢查等分析功能。有兩個子問題:

  1. 如何存取散佈在不同資料結構中的資訊
  2. 如何執行分析而不必每次都修改 Glyph 類別

解法一:Iterator 封裝存取與遍歷#

將遍歷邏輯從 Glyph 中抽離,封裝在 Iterator 物件中:

  • ArrayIteratorListIterator — 對應不同底層資料結構
  • PreorderIteratorPostorderIterator — 對應不同遍歷順序

每個 Glyph 提供 CreateIterator 操作,回傳適合自身資料結構的 Iterator。新增遍歷方式只需增加 Iterator 子類別,不必修改 Glyph。

Figure 2.13: Iterator class and subclasses

解法二:Visitor 封裝分析操作#

遍歷與分析應該分離,因為不同分析通常使用相同的遍歷方式。定義 Visitor 抽象類別,為每種 Glyph 子類別提供 Visit... 操作。Glyph 提供 Accept(Visitor&) 操作,內部呼叫 Visitor 上對應的方法(雙重分派)。

具體 Visitor 範例:

  • SpellingCheckingVisitor — 累積字元組成單字,遇到非字母字元時檢查拼字
  • HyphenationVisitor — 累積單字後計算斷字點,在適當位置插入 Discretionary glyph(行尾顯示連字號,行中則隱藏)

Visitor Pattern 適合類別結構穩定但分析操作頻繁新增的場景。新增分析只需增加 Visitor 子類別,不需修改任何 Glyph 類別。但反過來,若新增 Glyph 子類別,則所有 Visitor 都需更新。

本章總結#

Lexi 案例展示了八種設計模式如何協同運作,解決一個真實應用中的各種設計問題:

模式解決的問題核心手法
Composite文件的階層結構表示統一葉節點與組合節點介面
Strategy可替換的格式化演算法將演算法封裝在物件中
Decorator動態添加 UI 裝飾透明封裝 + 相容介面
Abstract Factory跨平台 Widget 建立抽象化物件建立過程
Bridge跨視窗系統支援分離抽象與實作階層
Command統一操作介面與 Undo將請求封裝為物件
Iterator統一遍歷不同資料結構封裝存取與遍歷機制
Visitor開放式分析功能擴充雙重分派 + 分離操作與結構

這些模式並非文件編輯器特有,任何非平凡的應用程式都可能用到其中多種。反覆出現的設計原則是:封裝會變化的概念(encapsulate the concept that varies)