cv.cm

編碼 · 設計模式

外觀模式詳解與範例

閱讀約 6 分鐘

簡答

外觀是一種結構型設計模式,它為複雜子系統中的一組介面,提供一個簡單且較高層次的介面。用戶端程式碼不必按照正確順序去呼叫庫存、付款、運送和電子郵件服務,只要呼叫像 shop.placeOrder() 這樣的一個方法,由外觀來協調各個子系統類別。子系統仍然存在,需要時也可以直接使用;外觀只是讓常見的情況變得簡單。

意圖

為子系統中的一組介面提供統一的介面。外觀定義一個較高層次的介面,讓子系統更容易使用。它是《設計模式》(Gang of Four,簡稱 GoF)一書中七種結構型模式之一。

它解決的問題

想像一間網路商店。下一筆訂單需要好幾個服務:檢查並保留庫存、向信用卡收款、付款失敗時釋放庫存、建立出貨單,以及寄出確認電子郵件。每個服務都有自己的 API 和規則。

如果每個呼叫端(例如網頁結帳、行動 API 和管理工具)都自己執行這些步驟,同一串流程就會複製到許多地方。每一份複製都可能弄錯順序或忘了回復,而且每個呼叫端都依賴五個服務。當其中一個服務改變,所有呼叫端都得跟著改。外觀把流程集中在同一個地方,並給呼叫端一個方法。

結構與參與者

參與者職責範例中的對應
外觀(Facade)知道哪些子系統類別處理哪種請求,並依正確的順序委派給它們。OrderFacade
子系統類別負責真正的工作。它們不知道外觀的存在。Inventory、Payments、Shipping、Mailer
用戶端在常見的工作中使用外觀;遇到特殊情況時仍可直接使用子系統類別。結帳程式碼

TypeScript 實作

// Subsystem classes, each with its own API.
class Inventory {
  private stock = new Map([["book", 2]]);
  reserve(sku: string): boolean {
    const n = this.stock.get(sku) ?? 0;
    if (n === 0) return false;
    this.stock.set(sku, n - 1);
    return true;
  }
  release(sku: string): void { this.stock.set(sku, (this.stock.get(sku) ?? 0) + 1); }
}
class Payments {
  charge(card: string, cents: number): boolean { return card.startsWith("4") && cents > 0; }
}
class Shipping {
  createShipment(sku: string, address: string): string { return `SHIP-${sku}-${address.length}`; }
}
class Mailer {
  send(to: string, text: string): void { console.log(`mail to ${to}: ${text}`); }
}

接著,外觀會在單一方法的背後,依正確的順序執行整個工作流程:

// Facade: one method that runs the whole workflow in the right order.
class OrderFacade {
  constructor(private inventory = new Inventory(), private payments = new Payments(),
              private shipping = new Shipping(), private mailer = new Mailer()) {}

  placeOrder(sku: string, card: string, cents: number, email: string, address: string): string {
    if (!this.inventory.reserve(sku)) return "out of stock";
    if (!this.payments.charge(card, cents)) {
      this.inventory.release(sku); // roll back the reservation
      return "payment declined";
    }
    const shipmentId = this.shipping.createShipment(sku, address);
    this.mailer.send(email, `Order confirmed, shipment ${shipmentId}`);
    return "ok";
  }
}

const shop = new OrderFacade();
console.log(shop.placeOrder("book", "4111", 1999, "[email protected]", "1 Main St"));
// mail to [email protected]: Order confirmed, shipment SHIP-book-9
// ok
console.log(shop.placeOrder("book", "5500", 1999, "[email protected]", "2 Side St")); // payment declined

呼叫端現在只依賴一個類別、一個方法。回復的規則也只存在一處。子系統類別仍然可以使用,所以只需要釋放庫存的管理工具,可以直接呼叫 Inventory。

何時使用、何時不該用

在以下情況使用外觀:

  • 子系統很複雜,而大多數用戶端只需要它功能中簡單、常見的一部分。
  • 你想減少用戶端程式碼與眾多子系統類別之間的依賴數量。
  • 你想把系統組織成多個層次,並讓外觀成為每一層的入口。
  • 你包裝了一個龐大的第三方函式庫,並希望其餘程式碼只使用其中需要的少數幾個操作。

在以下情況應避免使用或加以限制:

  • 子系統本來就很簡單。一個一對一轉發每個呼叫的外觀,只是多加一層,卻什麼也沒隱藏。
  • 外觀開始收集來自系統各處毫不相干的方法和業務規則。這就是「上帝物件」問題:一個類別知道太多、做得太多。請把它拆成幾個各自專注的外觀。

實際應用

  • Node.js:fs.readFile() 在一次呼叫中完成開啟檔案、讀到結尾並關閉。較低階的 fs.open()、fs.read() 和 fs.close() 仍然可用。
  • jQuery 能流行起來,很大程度上是因為它在 DOM 和當年各瀏覽器的差異之前,放了一套簡短且一致的 API。
  • GoF 一書的範例是編譯器:一個 Compiler 類別,把掃描器、剖析器、程式碼產生器等其他子系統類別,對只想編譯程式碼的程式隱藏起來。
  • 後端應用程式中的服務層與 API 閘道,對許多內部服務扮演著外觀的角色。

相關模式

模式與外觀的差異
轉接器讓一個現有的介面符合另一個現有的介面。外觀則是為許多類別定義一個新的、較簡單的介面。
中介者同樣會集中協調,但子系統物件認識中介者,並透過它來溝通。使用外觀時,子系統類別並不知道外觀的存在。
抽象工廠可以搭配外觀,以不依賴平台的方式建立子系統物件。
單例外觀通常只有一個實例,因為子系統通常只需要一個入口。
代理與某一個物件具有相同的介面,並控制對它的存取。外觀則是在許多物件之上,提供一個不同的、較簡單的介面。

常見問題

外觀和轉接器有什麼差別?

轉接器把一個現有的介面轉換成用戶端已經期待的另一個介面。外觀則是創造一個新的、較簡單的介面,讓整個子系統更容易使用。

外觀會完全隱藏子系統嗎?

不會。外觀讓常見的工作變得簡單,但需要更多控制的用戶端仍然可以直接使用子系統類別。

一個系統可以有多個外觀嗎?

可以。大型系統常常為每個子系統或每個使用情境各設一個外觀,讓每個外觀保持精簡而專注。

服務層就是外觀嗎?

通常是的。一個在單次呼叫後面協調儲存庫、外部 API 與通知的服務方法,對呼叫它的程式碼而言,就扮演著外觀的角色。

全部編碼教學