LSP 由 Barbara Liskov 於 1988 年提出,是定義「子型別(Subtype)」最嚴謹的規則。 在架構層次上,它不僅關乎繼承,更關乎介面與實作的契約遵守

LSP 定義:
「若 S 是 T 的子型別,則 T 的物件可以被 S 的物件替換,而不會改變程式原本應有的正確行為。」 (Subtypes must be substitutable for their base types.)

簡單來說:「如果它看來像鴨子,叫來像鴨子,但卻需要電池才能運作,那它就違反了 LSP。」 因為使用者預期的是一隻真鴨子,錯用結果會導致系統崩潰。

Figure 9.1: License and its derivatives conform to LSP

一、經典悖論:正方形是長方形嗎?#

在數學幾何上,正方形(Square)種長方形(Rectangle)。
但在程式設計的行為上,它不是

  • 長方形的契約: 設定 widthheight 是獨立的。修改寬度不應影響高度
  • 正方形的行為: 為了維持正方形特性,修改 width 必須同時改變 height

違反 LSP 的後果: 如果呼叫端(User)持有的是 Rectangle 參考,卻被傳入一個 Square 實例。
當 User 設定寬度時,會驚訝地發現高度也變了,導致計算面積錯誤。

為修復這 Bug,User 須在程式碼加入 if (type == Square) 的檢查——這就是架構被污染的開始。

Figure 9.2: The infamous square/rectangle problem

二、架構層次的 LSP:計程車調度案例#

LSP 不僅適用於物件導向的繼承,也適用於系統架構間的介面互動。
Uncle Bob 舉了一個「計程車調度服務」例子:

假設我們正開發一個透過 REST API 呼叫不同計程車行的系統。

  • 約定 URI: purplecab.com/driver/Bob/pickup/destination/123
  • Acme 計程車行的違規: 他們的工程師決定不遵守約定,把 destination 縮寫成 dest

1. 架構的污染#

為了支援這家違規的 Acme 公司,我們的系統核心邏輯被迫加入特殊判斷:

if (driver.getDispatchUri().startsWith("acme.com")) {
    dispatchUrl = ".../dest/...";         // 特例處理
} else {
    dispatchUrl = ".../destination/..."; // 標準處理
}
  • 具體名稱入侵: "acme" 這種具體字串出現在核心程式碼,意味著每新增一家違規業者都要改一次
  • 連鎖惡化: 若 Acme 併購 Purple Taxi,合併後仍維持雙品牌,我們是否又要加 if "purple"
  • 安全隱患: 以公司名稱做分支,容易產生難以察覺的錯誤與潛在安全漏洞

2. 架構師的補救:設定資料驅動#

合理的架構師不會容許上述寫法存在。正確做法是把特例從程式碼抽出,改由設定資料庫(Configuration Database) 驅動派遣指令的建構:

URIDispatch Format
Acme.com/pickupAddress/%s/pickupTime/%s/dest/%s
*.*/pickupAddress/%s/pickupTime/%s/destination/%s
  • 代價: 只因一家服務商不遵守介面契約,整個系統就必須多出一套「查表 + 格式化」機制
  • 啟示: LSP 違反的代價不只是程式碼變醜,而是整體架構被迫增加額外機制來對抗不一致性

四、結論#

LSP 原本被視為繼承的使用指南,如今應被擴展至架構層級

  • 契約優於繼承: 只要「使用者依賴定義良好的介面,且期待實作可互換」,LSP 就適用——不論是 Java 介面、Ruby duck typing,或是 REST 服務
  • 違反的成本: 一個看似微小的不可替換性,就足以讓系統被迫增加大量補丁機制
  • 架構師守則: 在系統邊界上強制契約一致性,是維持架構整潔的基本功

LSP 不只是「子類別能否替換父類別」的語法問題。
它是介面契約可替換性的通用法則——任何違反,都會讓系統付出架構層級的代價。