iT邦幫忙

2023 iThome 鐵人賽

DAY 21
0

在模組化收納中,會希望符合規範的模組可以互相替換,想像一下我們有一個很多抽屜的櫃子,抽屜是設計成可替換的,如果今天櫃子的拼布抽屜髒了或壞了也可以改成木製或是塑膠製,這個概念就是里氏替換原則,而符合規範的概念則是契約式設計。

里氏替換原則

里氏替換原則 (Liskov Substitution principle) 是對子類型的特別定義

衍生類別 (子類) 物件可以在程式中代替其基礎類別 (超類) 物件

舉個常見的手機多種登入方式來說,對於身份驗證和登入方式的設計,通常建議提供多個選項,各種登入方式(如 FaceID、圖形、PIN 碼、指紋等)可以互相替代,以滿足不同使用者的需求和偏好。

讓我們複習一下之前提到的情境描述,假設我們正在開發一個角色扮演遊戲 (RPG) 的程式,其中有不同類型的玩家,包括基本玩家和進階玩家,每個玩家類型都有自己的檢查條件,重構的目標是希望確保程式碼易於擴充,以應對未來可能新增的玩家類型。

// 建立玩家物件,使用包含屬性的物件作為參數
const player1 = createPlayer({
  name: "玩家1",
  health: 100,
  damage: 10,
});
const player2 = createAdvancedPlayer({
  name: "玩家2",
  health: 150,
  damage: 15,
  agility: 20,
});

// 處理玩家資料
processPlayerData(player1);
// 輸出:玩家1 符合基本條件
processPlayerData(player2);
// 輸出:玩家2 符合基本條件
// 輸出:玩家2 符合進階條件

在這個例子中會有一個基礎的 Player 物件該有的規範,首先我們定義了一個基本的玩家函式 createPlayer,它接受包含玩家屬性的物件作為參數,這個函式包含擁有通用檢查函式的玩家物件,該函式確保了基本玩家類別遵循了 Player 介面 (LSP 的一部分)。

// 定義一個基本玩家函式,接受包含玩家屬性的物件作為參數
function createPlayer(playerData) {
  const { name, health, damage } = playerData;
  return {
    name,
    health,
    damage,

    // 通用的檢查函式,每個玩家都可以使用
    checkPlayerCondition() {
      return this.health > 10 && (this.name === "foo" || this.damage < 5);
    },
  };
}

接著,我們定義了進階玩家函式 createAdvancedPlayer,擴充了基本玩家,同樣接受包含進階玩家屬性的物件作為參數。

// 定義進階玩家函式,接受包含進階玩家屬性的物件作為參數
function createAdvancedPlayer(advancedPlayerData) {
  const { name, health, damage, agility } = advancedPlayerData;
  const player = createPlayer({ name, health, damage });
  return {
    ...player,
    agility,

    // 進階玩家特有的檢查函式
    checkAdvancedPlayerCondition() {
      return this.name !== "bar" || this.agility > 20;
    },
  };
}

這確保了進階玩家類別同樣遵循了 Player 介面,擁有基本 Player 物件該有的規範和通用檢查函式 checkPlayerCondition,所以理論上要能替換基本玩家而不會對程式造成問題。

// 通用的處理玩家資料函式,接受任何類型的玩家物件作為參數
function processPlayerData(player) {
  if (player.checkPlayerCondition()) {
    // 做一些基本玩家的操作
    console.log(`${player.name} 符合基本條件`);
  }

  if (
    player?.checkAdvancedPlayerCondition &&
    player?.checkAdvancedPlayerCondition()
  ) {
    // 做一些進階玩家的操作
    console.log(`${player.name} 符合進階條件`);
  }

  // 非常長的函式內容...
  if (statusCode === 20100) {
    // ...
  }

  if (statusCode === 20101) {
    // ...
  }
}

契約式設計

契約式設計 (Design by Contract) 是一個非常重要的軟體設計原則,它強調一旦確立了契約或介面,就應該堅守下去,不輕易變更。

這個原則有助於確保系統的穩定性和可預測性,舉個常見的 API 版本號碼當例子,小編的第一個工作就需要提供 API 服務各種版本的 App,當需要對 API 做出變更或加入新功能時一定要注意相容舊版,這時候增加版本號碼是一個常見的做法,以確保現有的客戶端不會受到破壞。

  • /api/v1/user-info
  • /api/v2/user-info

在 API 設計中,一旦確立了一個特定的輸入和輸出格式,建議堅持不輕易變更它,因為這會影響到使用該 API 的客戶端應用程式。

每當對 API 的輸入或輸出格式進行重大變更時,可以建立一個新的版本在路徑中,使得新的客戶端可以選擇使用新版本,而不影響現有客戶端。

契約式設計原則有助於減少後續變更對現有功能的影響,同時確保用戶能夠方便地使用系統。


上一篇
把自己角色扮演好
下一篇
離職後的君子之交淡如水 X 最小知識原則
系列文
前端三分鐘 X 每天三分鐘的斷捨離,讓每一天都可以早點下班30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言