在模組化收納中,會希望符合規範的模組可以互相替換,想像一下我們有一個很多抽屜的櫃子,抽屜是設計成可替換的,如果今天櫃子的拼布抽屜髒了或壞了也可以改成木製或是塑膠製,這個概念就是里氏替換原則,而符合規範的概念則是契約式設計。
里氏替換原則
里氏替換原則 (Liskov Substitution principle) 是對子類型的特別定義
衍生類別 (子類) 物件可以在程式中代替其基礎類別 (超類) 物件
舉個常見的手機多種登入方式來說,對於身份驗證和登入方式的設計,通常建議提供多個選項,各種登入方式(如 FaceID、圖形、PIN 碼、指紋等)可以互相替代,以滿足不同使用者的需求和偏好。
讓我們複習一下之前提到的情境描述,假設我們正在開發一個角色扮演遊戲 (RPG) 的程式,其中有不同類型的玩家,包括基本玩家和進階玩家,每個玩家類型都有自己的檢查條件,重構的目標是希望確保程式碼易於擴充,以應對未來可能新增的玩家類型。
1 | // 建立玩家物件,使用包含屬性的物件作為參數 |
在這個例子中會有一個基礎的 Player
物件該有的規範,首先我們定義了一個基本的玩家函式 createPlayer,它接受包含玩家屬性的物件作為參數,這個函式包含擁有通用檢查函式的玩家物件,該函式確保了基本玩家類別遵循了 Player 介面 (LSP 的一部分)。
1 | // 定義一個基本玩家函式,接受包含玩家屬性的物件作為參數 |
接著,我們定義了進階玩家函式 createAdvancedPlayer,擴充了基本玩家,同樣接受包含進階玩家屬性的物件作為參數。
1 | // 定義進階玩家函式,接受包含進階玩家屬性的物件作為參數 |
這確保了進階玩家類別同樣遵循了 Player 介面,擁有基本 Player
物件該有的規範和通用檢查函式 checkPlayerCondition
,所以理論上要能替換基本玩家而不會對程式造成問題。
1 | // 通用的處理玩家資料函式,接受任何類型的玩家物件作為參數 |
契約式設計
契約式設計 (Design by Contract) 是一個非常重要的軟體設計原則,它強調一旦確立了契約或介面,就應該堅守下去,不輕易變更。
這個原則有助於確保系統的穩定性和可預測性,舉個常見的 API 版本號碼當例子,小編的第一個工作就需要提供 API 服務各種版本的 App,當需要對 API 做出變更或加入新功能時一定要注意相容舊版,這時候增加版本號碼是一個常見的做法,以確保現有的客戶端不會受到破壞。
/api/v1/user-info
/api/v2/user-info
在 API 設計中,一旦確立了一個特定的輸入和輸出格式,建議堅持不輕易變更它,因為這會影響到使用該 API 的客戶端應用程式。
每當對 API 的輸入或輸出格式進行重大變更時,可以建立一個新的版本在路徑中,使得新的客戶端可以選擇使用新版本,而不影響現有客戶端。
契約式設計原則有助於減少後續變更對現有功能的影響,同時確保用戶能夠方便地使用系統。
喜歡這篇文章,請幫忙拍拍手喔 🤣