在模組化收納中,會希望符合規範的模組可以互相替換,想像一下我們有一個很多抽屜的櫃子,抽屜是設計成可替換的,如果今天櫃子的拼布抽屜髒了或壞了也可以改成木製或是塑膠製,這個概念就是里氏替換原則,而符合規範的概念則是契約式設計。
里氏替換原則 (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 的輸入或輸出格式進行重大變更時,可以建立一個新的版本在路徑中,使得新的客戶端可以選擇使用新版本,而不影響現有客戶端。
契約式設計原則有助於減少後續變更對現有功能的影響,同時確保用戶能夠方便地使用系統。