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)。
但在程式設計的行為上,它不是。
- 長方形的契約: 設定
width和height是獨立的。修改寬度不應影響高度 - 正方形的行為: 為了維持正方形特性,修改
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) 驅動派遣指令的建構:
| URI | Dispatch Format |
|---|---|
Acme.com | /pickupAddress/%s/pickupTime/%s/dest/%s |
*.* | /pickupAddress/%s/pickupTime/%s/destination/%s |
- 代價: 只因一家服務商不遵守介面契約,整個系統就必須多出一套「查表 + 格式化」機制
- 啟示: LSP 違反的代價不只是程式碼變醜,而是整體架構被迫增加額外機制來對抗不一致性
四、結論#
LSP 原本被視為繼承的使用指南,如今應被擴展至架構層級。
- 契約優於繼承: 只要「使用者依賴定義良好的介面,且期待實作可互換」,LSP 就適用——不論是 Java 介面、Ruby duck typing,或是 REST 服務
- 違反的成本: 一個看似微小的不可替換性,就足以讓系統被迫增加大量補丁機制
- 架構師守則: 在系統邊界上強制契約一致性,是維持架構整潔的基本功
LSP 不只是「子類別能否替換父類別」的語法問題。
它是介面契約可替換性的通用法則——任何違反,都會讓系統付出架構層級的代價。