iT邦幫忙

2026 iThome 鐵人賽

DAY 17
0
自我挑戰組

一杯咖啡的設計課:30 天 Design Pattern 的自我修煉系列 第 17

Day 17|Facade(外觀模式) 如何透過 Facade 打造收銀的單一窗口

  • 分享至 

  • xImage
  •  

昨天用 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 為多個複雜的子系統提供一個高層級且統一的簡易入口(單一窗口),讓呼叫端不需要去搞懂底層的運作細節

核心角色

  1. Facade(外觀角色 / 門面)
    職責:知道哪些子系統負責處理請求,將客戶端的呼叫「委派」給適當的子系統物件群
  2. Subsystems(子系統角色群)
    職責:處理 Facade 分派下來的實際工作。子系統各自獨立,完全不知道 Facade 的存在,也不持有 Facade 的參考
  3. Client(客戶端 / 呼叫端)
    職責:透過呼叫 Facade 來簡化操作,不需要與複雜的子系統打交道(但若有特殊需求,仍保留直接呼叫子系統的彈性)

結構與觀念

  1. 子系統各自獨立:每個子系統該做、怎麼做的事,不受影響,Facade 不會動到內部邏輯
  2. 統一入口:Facade 提供一個簡化過的高層方法,內部負責呼叫哪些子系統、用什麼順序呼叫
  3. 不鎖死存取:呼叫端大部分時候走 Facade 就好,但真的有需要,還是可以直接繞過 Facade,找底層子系統

舉個生活化的例子,週末我最愛窩在客廳追劇

但家裡設備一堆,想看場電影,我得要做什麼事?

https://ithelp.ithome.com.tw/upload/images/20260915/20183470ievzscKNEj.jpg

  1. 走去牆邊把客廳燈關掉
  2. 拿喇叭遙控器把喇叭打開
  3. 拿電視遙控器把電視打開

看完要關掉,還要把這三個動作倒過來再做一次,漏關一個,喇叭可能就通宵耗電

// 三個各自獨立的子系統,各自有各自的開關方法,互不相干
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();

LightSoundBarTv 對這三個子系統來說,內部邏輯沒有被修改,Facade 只是站在它們前面,替呼叫端記住「這個情境該按哪些鍵、按什麼順序」

例子很簡單,也明白 Facade 為封裝用的介面,將系統和類別等雜亂或是困難的函數式封裝起來,成為一個容易使用和理解的介面

回到柴咖啡

柴咖啡結帳的當下,其實要做好幾件事:算這筆訂單多少錢、扣款(順便處理發票、報表)、通知老闆有新訂單進來

這幾件事柴咖啡的系統裡,已經各自有專門的類別在處理:

  • OrderCalculator:專門算金額
  • IPaymentProcessor:專門處理扣款,扣款的同時,發票跟報表也會一併處理掉
  • LineNotifier:專門發通知給老闆

門市結帳的 Controller,如果直接把這三個子系統一個個接起來

依序呼叫 OrderCalculatorIPaymentProcessorLineNotifier

結帳到底要照什麼順序做哪些事,就變成 Controller 自己得記住的細節

之後小黑想在扣款前加一道防呆檢查,或是調整通知的時機,都得直接動到這個處理請求的 Controller

Facade 上場

阿柴學到 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 流程會像這樣

https://ithelp.ithome.com.tw/upload/images/20260915/20183470zVnIPF5TZi.png

Facade 的認知

不鎖死底層子系統OrderCalculatorIPaymentProcessorLineNotifier 都還是 public,後台想單獨補算一筆金額、單獨重發一次通知,還是可以直接繞過 CheckoutFacade 找它們,Facade 只是多開一條比較好走的路,不是唯一的路

跟 SRP 會不會互相矛盾? 阿柴一開始也有點疑惑,先前才學過 SRP 要把職責拆開,今天 Facade 卻好像把東西「合」了回去,這兩個原則會不會打架

其實不會,OrderCalculatorIPaymentProcessorLineNotifier 各自的職責完全沒變

一個算錢、一個管扣款流程、一個發通知

CheckoutFacade 自己也只有一個職責: 協調這三者的呼叫順序,負責「按對順序把三個電話打出去」

換句話說,SRP 管的是「每一個子系統自己該不該做超過一件事」

Facade 管的是「呼叫端該不該知道有這麼多子系統要協調」

今天學到的知識比較好理解,明天來聊聊 Proxy 代理模式上場

延伸閱讀資料

Refactoring Guru - Facade

用 JavaScript 玩轉設計模式 - 你一定用過但可能不知道的 Facade Pattern(外觀模式)

C# Facade Design Pattern


上一篇
Day 16|Adapter (轉接器模式) 用萬國轉接頭收服規格打架
下一篇
Day 18|Proxy (代理模式) 天啊!是替身攻擊!
系列文
一杯咖啡的設計課:30 天 Design Pattern 的自我修煉26
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言