Liskov Substitution Principle (LSP) Liskov 替換原則

ZHIH轉職人森
Coding Book Club
Published in
9 min readAug 27, 2022

來講講 LSP 替換原則

因為定義很重要真的很重要所以談論之前一定要先講定義啦,如下:

If S is a subtype of T, then objects of type T may be replaced with objects of type S without altering any of the desirable properties of the program (correctness, task performed, etc.)

程式碼中的類別T的物件 都可以被一個類別S的物件給取代 而且程式碼還運作正常 那你可以說類別S是類別T的subType

((這邊插入個人主觀想法:
接下來後面會提到的原則,都是為了能達到子類別可以完全替代主類別))

這有啥好處?( 乾可真是超重要!!)假設在code 主流程固定下,想要抽換某一個類別,但又不用大幅更動code商業邏輯,就可以保持原本的系統繼續運作,這不是很棒嗎!!!(不要以為自己的記憶很好,人腦是不可靠的QQ)

舉個例:

今天有一個父類別叫做Shape且有一個method 叫draw,繼承父類別的是Circle、Square

主流程不變的情況下,只要把同一個變數myShape把原先指向父類別的Shape換成子類別Circle or Square,這段code也不會因此出現error

LSP 存在與否差別

那遵守到底不遵守到有有什麼差呢?

•遵守:
實現開放封閉原則不用修改舊有的程式碼造成可能的error,又能維持原先功能正常運作,還能擴增功能

•違反:
若出現錯誤會不可預期,這其實也很好理解,如果替代的子類別無法取代父類別,舊有的功能會出現預期外的行為,此外隨著修改code也還有可能造成額外的error

繼承使用與否

上面看下來好像使用繼承Inheritance就可以搞定這個原則,但其實並不然,
why?

Inheritance是所有依賴關係裡面最強的 而你知道太過依賴總是沒啥好事

代表著如果沒處理好,子類會受到父類別給牽制,導致後續有些需異動,整包商業邏輯的code就需要大幅改動,那這不就喪失了LSV的好處了嗎?

話雖如此但有什麼可以做為判斷的呢

  • LSP => 7項指標
  • Polymorphism多態
  • Composition

接下來下面會依序來說明

LSV遵守原則

Signature Rule (3項)

signature:特別是對函式給出其輸入參數數量、型別與次序及輸出結果的型別。 許多編譯器產生的內部使用的函式名包含了其型別特徵

1. Method Argument Types:
the overridden subtype method argument types can be identical or wider than the supertype method argument types.

=> 子類別overrided method 的argument必須要相同或是更多

2. Return Types

The return type of the overridden subtype method can be narrower than the return type of the supertype method.

=>子類別overrided method回傳的 type 必須要相同或是更嚴謹

Ex: return NUMBER => return 正整數

3. Exceptions

The subtype method can throw fewer or narrower (but not any additional or broader) exceptions than the supertype method.
=> Exceptions 只能更嚴謹或相當於父類別

Methods Rule (2項)

1. Preconditions:
A subtype can weaken (but not strengthen) the precondition for a method it overrides

=>子類別overrided method 執行前的條件可以weaken 但不能strengthen

以上述舉例來說 num 這個數字只能在1~5之間不然就會炸exception,
但可以調整成1~10,至少在舊系統內輸入原先的num 的數值時,舊系統不會因此炸出exception依然能正常的運作。

2. Postconditions:
The subtype can strengthen (but not weaken) the postcondition for a method it overrides

=>子類別overrided method 執行後需要符合的條件可以strengthen 但不能 weaken

父類別
子類別

以上述舉例來說
原先 speed 這個數值在煞車時數值會減少
後來: 子類別使用後的時候,最低要求下煞車時一定要speed降低,但可以額外加上其他的條件如: charge 上升

Properties Rule(2項)

1. Class Invariants:

invariant定義:
A class invariant is an assertion concerning object properties that must be true for all valid states of the object

all subtype methods (inherited and new) must maintain or strengthen the supertype’s class invariants.

=> 子類別的所有方法內關於properties的調整,都必須要遵守維持或是加強這個properties 原先的設定

父類別
子類別

上述例子就是要 保持speed < limit 這個條件恆為真!!

2. History Constraint:

History Constraint定義:
Some assertion about how class property evolves over time

subclass methods (inherited or new) shouldn’t allow state changes that the base class didn’t allow.

=> 子類別的所有方法必須要遵守維持properties的設定不能改變

上述例子Mileage 只能一開始改設定之後不可隨時間修改 !!!

Polymorphism多態

  • 原由來自生物一個類別有多種形態
  • OOP中可以利用interface 或抽象類別來實現

這樣的好處也很好理解只要能產出符合interface 的類別,基本上就可以讓主流程的code不會掛掉

composition

  • 繼承(inheritance)VS 複合( composition )
  • 前者是「is a」,後者則是「has a」的關係

那兩者的差別其實也很好理解,以下面的例子來說,繼承PhysicsObject的CharacterInheritance如果想要撤換updatePhysics內部邏輯會被父類別綁死,而難以撤換,相較起composition 只需要引入PhysicsObject 類別,這個類別可以是自己本身PhysicsObject 也可以是繼承PhysicsObject的子類別(override掉父類別的method),或是乾脆使用抽象類別個別method實作,這樣子不但不用被父類別綁死也可以進行抽換不影響舊有的code

inheritance
composition

擴展闡述

所有對某個的介面的實現都可以視為對該介面的subtype

=> 無論介面背後如何實作,其最後的產出應該維持當時與客戶承諾的一致

這件事情很重要的 !! 剛進公司的時候完全沒這觀念,差點就出了一個大包XD
那為何這很重要呢,想想app 這個東西每一段時間就會發出新的version ,假設它們都有一條叫做A 的api,今天server 想要針對這條A api去做一些調整新增一些新內容,把format改得跟之前完全不一樣,那會發生什麼事?

答案是 完蛋了八比Q了,除了新vesion的app 可以運作,舊version app的用戶完全死光光,如果金流一秒幾百萬上下看你會不會被罵死。。。

所以說要改不是不行,最少最少要把A 這條api保證跟舊版本的承諾的回應的資料一樣,另外額外加一些新的資料無所謂,因為無論有加沒加都不會對舊版本的app造成crash的影響

結論

•繼承很可怕不要隨便亂用,要用不是不行,但有三項前提 LSP、多態、 composition缺一不可

•善用interface 和抽象類別

•擴展闡述: 無論介面背後的實現如何更動調整,行為都必須和當初對客戶的承諾一致

Reference

--

--