使用表格驅動方法和狀態模式管理前端狀態
2021年8月28日4 分鐘閱讀

使用表格驅動方法和狀態模式管理前端狀態

這篇文章是通過 AI 翻譯生成,可能有不準確之處。

封面照片由 @shawnangggUnsplash 提供

本文的目的是分享一個在元件中管理狀態分支邏輯的模式。

在前端開發中,一個常見的情況是根據不同的狀態切換不同的 UI 或行為。例如:

  • 活動頁面根據活動狀態、登入狀態等顯示不同的 UI
  • 產品元件根據產品狀態、評論狀態、違規狀態等以不同的方式呈現

如果我們不小心處理,就會在程式碼中產生複雜的邏輯。這會使程式碼變得更難維護且容易出錯。

為了緩解這個問題,我們可以使用以下 2 種方法:

  1. 表格驅動方法%20to%20figure%20it%20out.)
  2. 狀態模式

表格驅動方法

這是來自《Code Complete》一書的程式設計方法,用以更簡潔的方式管理 if else 分支邏輯(有時候)。

讓我們看一個例子:

// If Else
if (a === 'foo') {
  if (b === 'abc') {
    action1()
  } else if (b === 'xyz') {
    action2()
  }
} else if (a === 'bar') {
  action3()
}

// Table driven
const action = {
  foo: {
    abc: action1,
    xyz: action2,
  },
  bar: {
    abc: action3,
    xyz: action3,
  },
}[a][b]
action()

雖然第二種方法有重複的 action3 宣告,但在這種情況下仍然更直接。但請記住,如果你只有少數幾個動作但有很多狀態,表格驅動方法可能會相當冗餘和冗長。我們需要找到何時使用哪種方法的平衡點。

這裡有一個創建這種表格驅動 JS 程式碼的簡單工具,你可能想看看。歡迎對這個工具提供任何意見 :)

https://playground.kenneth.pro/easy-state

狀態模式

據我理解,GOF 狀態模式的主要思想是:

首先根據給定狀態選擇一組預定義的方法和變數,並提供相同的介面,使其依賴者可以使用該介面而不需要知道細節。

如果我們使用狀態模式,實作會是:

  • 一開始就預定義所有需要切換的變數方法,並根據給定狀態選擇集合。

讓我們看一個 Typescript 的實際例子,結合我們剛才談到的表格驅動方法。

在這個例子中,CampaignComponent 根據不同的 LoginStatusCampaignStatusUserCampaignStatus 顯示不同的 UI:

// Define Status Type
enum LoginStatus {
  NOT_LOGIN,
  LOGIN,
}

enum CampaignStatus {
  NOT_STARTED,
  STARTED,
  ENDED,
}

enum UserCampaignStatus {
  NOT_ATTENDED,
  ATTENDED,
}

// Define Component Props Type
interface Props {
  banner: string
  submit: () => void
  // ...
}

// Define Concrete Props
const NotStarted: Props = { banner: 'Campaign Not stated' /*...*/ }
const NotLogin: Props = { banner: 'Please login' /*...*/ }
const Eligible: Props = { banner: 'Welcome!!' /*...*/ }
const Attended: Props = { banner: 'Attended before' /*...*/ }
const Ended: Props = { banner: 'Campaign is ended' /*...*/ }

// Define States Mapping
const States = {
  [LoginStatus.NOT_LOGIN]: {
    [CampaignStatus.NOT_STARTED]: {
      [UserCampaignStatus.NOT_ATTENDED]: NotStarted,
      [UserCampaignStatus.ATTENDED]: NotStarted,
    },
    [CampaignStatus.STARTED]: {
      [UserCampaignStatus.NOT_ATTENDED]: NotLogin,
      [UserCampaignStatus.ATTENDED]: NotLogin,
    },
    [CampaignStatus.ENDED]: {
      [UserCampaignStatus.NOT_ATTENDED]: CampaignEnded,
      [UserCampaignStatus.ATTENDED]: CampaignEnded,
    },
  },
  [LoginStatus.LOGIN]: {
    [CampaignStatus.NOT_STARTED]: {
      [UserCampaignStatus.NOT_ATTENDED]: NotStarted,
      [UserCampaignStatus.ATTENDED]: NotStarted,
    },
    [CampaignStatus.STARTED]: {
      [UserCampaignStatus.NOT_ATTENDED]: Eligible,
      [UserCampaignStatus.ATTENDED]: Attended,
    },
    [CampaignStatus.ENDED]: {
      [UserCampaignStatus.NOT_ATTENDED]: CampaignEnded,
      [UserCampaignStatus.ATTENDED]: CampaignEnded,
    },
  },
}

在這個例子中,雖然一些狀態分支是冗餘和冗長的,但它將業務邏輯與你的 UI 元件分離。這在某種程度上更可測試和可維護。

以 React 為例,我們可以在我們的自定義 hooksredux containers 中準備我們的 props。

此外,你可以將 States 物件包裝在一個函數中,以使其對於給定的業務需求更直接或更簡潔,像這樣:

const getState = (
  loginStatus: LoginStatus,
  campaignStatus: CampaignStatus,
  userCampaignStatus: UserCampaignStatus
) => {
  if (campaignStatus === CampaignStatus.NOT_STARTED) return NotStarted
  if (campaignStatus === CampaignStatus.ENDED) return CampaignEnded

  const States = {
    [LoginStatus.NOT_LOGIN]: {
      [CampaignStatus.STARTED]: {
        [UserCampaignStatus.NOT_ATTENDED]: NotLogin,
        [UserCampaignStatus.ATTENDED]: NotLogin,
      },
    },
    [LoginStatus.LOGIN]: {
      [CampaignStatus.STARTED]: {
        [UserCampaignStatus.NOT_ATTENDED]: Eligible,
        [UserCampaignStatus.ATTENDED]: Attended,
      },
    },
  }
  return States[loginStatus][campaignStatus][userCampaignStatus]
}

只要程式碼是可理解的、可測試的、一致的並且能正確運作,我們使用哪種範式並不重要。這就是每個人都能使用的好程式碼。

希望這篇文章或多或少對你有幫助,開發愉快!