Henry's Blog

設計模式 - 門面模式 (Facade Pattern)

Facade Pattern 中文又稱外觀模式或是門面模式,是一種將複雜的實作細節封裝,並對外提供簡單、方便和易懂的使用介面。

使用情境

假設今天有個需求:

  1. 透過打 API 取得 users 和 user posts 等資料
  2. 在打 API 時需要 log event,提供當出錯時 debug 用

一般來說,我們很有可能會這樣做:

import * as amplitude from '@amplitude/analytics-browser'

async function getUsers() {
  try {
    amplitude.logEvent('get_users_request')
    const res = await fetch('https://example.com/api/users', {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
      },
    })
    amplitude.logEvent('get_users_request_success')
    return res.json()
  } catch (error) {
    amplitude.logEvent('get_users_request_error', {
      error_code: error.code,
      error_message: error.message,
    })
  }
}

async function getUserPosts(id: string) {
  try {
    amplitude.logEvent('get_user_posts_request')
    const res = await fetch(`https://example.com/api/posts?id=${id}`, {
      method: 'GET',
      headers: {
        'Content-Type': 'application/json',
      },
    })
    amplitude.logEvent('get_user_posts_request_success')
    return res.json()
  } catch (error) {
    amplitude.logEvent('get_user_posts_request_error', {
      error_code: error.code,
      error_message: error.message,
    })
  }
}

為什麼可能會有問題

程式碼可以跑,看起來也沒有問題!不過有發現嗎?在每個 getXXX  function 裡都需要引用不同的服務/功能,加上如果今天需求改變,發現想要改用 axios 取代 fetch,或是換了 logging service,全部的 functions 都需要做調整。如果 codebase 有一定規模的話,將會是一個浩大的工程。

用 Facade Pattern 隱藏實作細節

這時候 Facade Pattern 就派上用場了,我們可以把打 API、log event 的功能另外封裝,並用一個統一的 getFetchWithLogging  作為對外的介面使用。

import * as amplitude from '@amplitude/analytics-browser'
import axios from 'axios'

async function getUsers() {
  return getFetchWithLogging(
    'https://example.com/api/users',
    {},
    'get_users_request'
  )
}

async function getUserPosts(userId: string) {
  return getFetchWithLogging(
    `https://example.com/api/posts`,
    { userId },
    'get_user_posts_request'
  )
}

async function getFetchWithLogging(
  url: string,
  params: Record<string, unknown> = {},
  eventName: string
) {
  try {
    amplitude.logEvent(eventName)
    const res = await axios.get(url, {
      params,
    })
    amplitude.logEvent(`${eventName}_success`)
    return res.data
  } catch (error) {
    amplitude.logEvent(`${eventName}_error`, {
      error_code: error.code,
      error_message: error.message,
    })
  }
}

使用 Facade Pattern 後,即便之後 axios API 有 break changes,或是我們想換成其他服務,只需要改動 getFetchWithLogging ,而不需要動到有使用 getFetchWithLogging  的地方。

後記

前端實務上,像是 UI components 也很適合用 Facade Pattern 封裝。公司可能會有固定使用的幾種 UI components,但基於各種考量,可能一開始會用 MUI 或是 Chakra UI 之類的 libraries;但後來慢慢發現選擇 library 的設計不一定符合公司需求,而有需要使用其他 library 或是自己實作,這時候如果一開始就先定義好 UI components 的 input/output,之後在轉換時就不需要在 codebase 做大幅度的改變,兒只需要改動 UI components 的實作細節就好。

雖然透過 facade 可以讓使用上更加容易和方便,不過 Facade Pattern 也是有像是可能會讓封裝的 facade 過於複雜、容易出錯、降低靈活性等隱憂,在使用上還是需要評估情境是否適合再使用。

References

#patterns