影分身之術 X 里氏替換原則 三分鐘斷捨離,讓每天都早點下班

me
林彥成
2023-10-06 | 3 min.
文章目錄
  1. 1. 里氏替換原則
  2. 2. 契約式設計

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

里氏替換原則

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 建立玩家物件,使用包含屬性的物件作為參數
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 的一部分)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定義一個基本玩家函式,接受包含玩家屬性的物件作為參數
function createPlayer(playerData) {
const { name, health, damage } = playerData;
return {
name,
health,
damage,

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 定義進階玩家函式,接受包含進階玩家屬性的物件作為參數
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,所以理論上要能替換基本玩家而不會對程式造成問題。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 通用的處理玩家資料函式,接受任何類型的玩家物件作為參數
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 的輸入或輸出格式進行重大變更時,可以建立一個新的版本在路徑中,使得新的客戶端可以選擇使用新版本,而不影響現有客戶端。

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


喜歡這篇文章,請幫忙拍拍手喔 🤣


share