昨天用 Adapter 把外送平台的資料格式接進柴咖啡的系統,今天我們繼續留在結構型 Pattern
來聊聊 Facade 外觀模式
原文的定義,一樣來自 Design Patterns: Elements of Reusable Object-Oriented Software
Provide a unified interface to a set of interfaces in a subsystem. Facade defines a higher-level interface that makes the subsystem easier to use.
用中文理解可以是
為子系統中的一組介面,提供一個統一的高層介面,讓這個子系統更容易被使用
我的理解是:
Facade 為多個複雜的子系統提供一個高層級且統一的簡易入口(單一窗口),讓呼叫端不需要去搞懂底層的運作細節
核心角色
結構與觀念
舉個生活化的例子,週末我最愛窩在客廳追劇
但家裡設備一堆,想看場電影,我得要做什麼事?

看完要關掉,還要把這三個動作倒過來再做一次,漏關一個,喇叭可能就通宵耗電
// 三個各自獨立的子系統,各自有各自的開關方法,互不相干
public class Light
{
public void Off() => Console.WriteLine("客廳燈關閉");
public void On() => Console.WriteLine("客廳燈打開");
}
public class SoundBar
{
public void On() => Console.WriteLine("喇叭開啟");
public void Off() => Console.WriteLine("喇叭關閉");
}
public class Tv
{
public void On() => Console.WriteLine("電視開啟");
public void Off() => Console.WriteLine("電視關閉");
}
沒有 Facade 之前,呼叫端要自己記得三支遙控器、記得開跟關的順序
var light = new Light();
var soundBar = new SoundBar();
var tv = new Tv();
// 看電影前,三個動作一個都不能漏
light.Off();
soundBar.On();
tv.On();
// 看完之後,又要倒過來按一輪
tv.Off();
soundBar.Off();
light.On();
我們用 Facade 把這串協調邏輯收進來,呼叫端只剩兩個方法可以叫(開啟/關閉看電影模式)
public class HomeTheaterFacade
{
private readonly Light _light;
private readonly SoundBar _soundBar;
private readonly Tv _tv;
public HomeTheaterFacade(Light light, SoundBar soundBar, Tv tv)
{
_light = light;
_soundBar = soundBar;
_tv = tv;
}
public void WatchMovie()
{
_light.Off();
_soundBar.On();
_tv.On();
}
public void EndMovie()
{
_tv.Off();
_soundBar.Off();
_light.On();
}
}
var homeTheater = new HomeTheaterFacade(new Light(), new SoundBar(), new Tv());
homeTheater.WatchMovie(); // 將子功能的功能統一由 WatchMovie() 處理
homeTheater.EndMovie();
Light、SoundBar、Tv 對這三個子系統來說,內部邏輯沒有被修改,Facade 只是站在它們前面,替呼叫端記住「這個情境該按哪些鍵、按什麼順序」
例子很簡單,也明白 Facade 為封裝用的介面,將系統和類別等雜亂或是困難的函數式封裝起來,成為一個容易使用和理解的介面
柴咖啡結帳的當下,其實要做好幾件事:算這筆訂單多少錢、扣款(順便處理發票、報表)、通知老闆有新訂單進來
這幾件事柴咖啡的系統裡,已經各自有專門的類別在處理:
OrderCalculator:專門算金額IPaymentProcessor:專門處理扣款,扣款的同時,發票跟報表也會一併處理掉LineNotifier:專門發通知給老闆門市結帳的 Controller,如果直接把這三個子系統一個個接起來
依序呼叫 OrderCalculator、IPaymentProcessor、LineNotifier
結帳到底要照什麼順序做哪些事,就變成 Controller 自己得記住的細節
之後小黑想在扣款前加一道防呆檢查,或是調整通知的時機,都得直接動到這個處理請求的 Controller
阿柴學到 Facade 之後,把這段協調邏輯收進一個新的 CheckoutFacade
public class CheckoutFacade
{
private readonly OrderCalculator _calculator;
private readonly IPaymentProcessor _paymentProcessor;
private readonly LineNotifier _notifier;
public CheckoutFacade(OrderCalculator calculator, IPaymentProcessor paymentProcessor, LineNotifier notifier)
{
_calculator = calculator;
_paymentProcessor = paymentProcessor;
_notifier = notifier;
}
public async Task<PaymentResult> Checkout(Order order)
{
var total = _calculator.Calculate(order);
var result = _paymentProcessor.Charge(order.OrderId, (int)total);
await _notifier.Notify(total);
return result;
}
}
往後門市的 Controller,改成只認識這一個入口
public class CheckoutController
{
private readonly CheckoutFacade _checkoutFacade;
public CheckoutController(CheckoutFacade checkoutFacade)
{
_checkoutFacade = checkoutFacade;
}
public Task<PaymentResult> Checkout(Order order) => _checkoutFacade.Checkout(order);
}
小黑要加的「扣款前先檢查訂單」防呆,阿柴只要改 CheckoutFacade.Checkout() 這一個地方CheckoutController 不需要動,它只負責接請求、把工作丟給 CheckoutFacade,不用知道裡面實際協調了哪些子系統
最近使用 mermaid 畫流程圖,效果我蠻喜歡的,依上述柴咖啡的例子,配上 Facade pattern 流程會像這樣

不鎖死底層子系統:OrderCalculator、IPaymentProcessor、LineNotifier 都還是 public,後台想單獨補算一筆金額、單獨重發一次通知,還是可以直接繞過 CheckoutFacade 找它們,Facade 只是多開一條比較好走的路,不是唯一的路
跟 SRP 會不會互相矛盾? 阿柴一開始也有點疑惑,先前才學過 SRP 要把職責拆開,今天 Facade 卻好像把東西「合」了回去,這兩個原則會不會打架
其實不會,OrderCalculator、IPaymentProcessor、LineNotifier 各自的職責完全沒變
一個算錢、一個管扣款流程、一個發通知
CheckoutFacade 自己也只有一個職責: 協調這三者的呼叫順序,負責「按對順序把三個電話打出去」
換句話說,SRP 管的是「每一個子系統自己該不該做超過一件事」
Facade 管的是「呼叫端該不該知道有這麼多子系統要協調」
今天學到的知識比較好理解,明天來聊聊 Proxy 代理模式上場
用 JavaScript 玩轉設計模式 - 你一定用過但可能不知道的 Facade Pattern(外觀模式)