《K的技術學習筆記》——良好OOP的設計原則:<SOLID Principles>(四)
![](https://assets.matters.news/embed/c71f8843-a696-4249-bf86-7273af63e470.png)
里氏替換原則(Liskov Substitution Principle)
sub-class必須能夠替換其super-class,並且不會出現問題。要做到這個效果,sub-class就最好不要覆寫super-class的已有行為,而是寫一個新的方法去擴展。
舉一個飛行比賽來說明。
現在有多種鳥類要參加這個比賽,並在所有參賽者飛到終點後公報結果。
鳥類(Bird)
![](https://assets.matters.news/embed/b6aba705-696e-4356-91aa-a90b7388cce7.png)
鳥類有飛行距離(airRange)和名字(name),而他們有著飛(fly)這一行為。一般鳥類的飛行速度是10。
飛行比賽(Air Race)
![](https://assets.matters.news/embed/32c516b1-bb5e-41b0-a724-b83230f20635.png)
飛行比賽有他的賽事長度(distance), 參賽鳥們(contestants)和每位參賽鳥的餘下賽事距離(remainingDistance),而在創造飛行比賽時就需要鳥類們來進行設置。而飛行比賽有著開始(start)這一行為。當開始比賽後,參賽鳥就會開始飛行,而所有鳥類飛到終點後,就會公報誰是第一名。
鷹(Eagle)
![](https://assets.matters.news/embed/3760b4bf-2124-4e51-82ca-9176fbff3976.png)
鷹和一般鳥類的飛行速度不同,所以創造了鷹類別再覆寫了他飛(fly)這一行為。
企鵝(Penguin)
![](https://assets.matters.news/embed/ba6a5050-1bbc-4061-badc-c32dacb9a193.png)
企鵝和一般鳥類不同,他並不會飛,所以創造了企鵝類別再覆寫了他飛(fly)這一行為。
設置比賽
![](https://assets.matters.news/embed/43959333-12c5-440b-ba71-d00dd4e72784.png)
我們先創造三隻參賽鳥,分別是鳥class的麻雀,鷹class的鷹和企鵝class的企鵝。然後,再創做一個鳥class的array,並把三種鳥都加進去。最後,用這個array來創造飛行比賽,並開始比賽。
這樣看來很正常,並且不覺得邏輯有問題,但是最終運行時出現了大問題。這個大問題就是比賽一直沒有結束,並且沒有公佈誰是第一名。
在這裡鷹class和企鵝class都是鳥class的sub-class,所以他們都可以代替鳥class被放進birds array裡。若果有遵守里氏替換原則的設計,我們即使不理解sub-class的所有資訊,這裡都不會出問題。這裡本來計設的前題是希望任何鳥類參加比賽都能飛,並且飛到終點。若果我們創造的三個參賽者都是用鳥class,這場比賽就會能結束,並公佈結果,但這裡用了企鵝class這個不會飛的鳥class的sub-class代替了。因此,這場比賽會就永遠不會結束。
跟據里氏替換原則,sub-class必須能夠替換其super-class,並且不會出現問題。這個例子還是違反了里氏替換原則。雖然鷹class替換鳥class,並沒有出現問題,但是企鵝class的替換就出現問題了。
當違反里氏替換原則時很大機會是在設計繼承時分類錯了。在我們現實生活中,企鵝和鷹都是鳥類。不同的是程式裡我們還要為class定義他們的行為,而sub-class一定能做到定義好的行為。在這裡我們定義鳥一定會飛,但現實是企鵝並不會飛。因此,我強行覆寫了企鵝的飛(fly)行為。而我們寫飛行比賽的大前題是所有參賽者都能飛。雖然放進比賽的鳥都有飛行(fly)這function被call,但企鵝當中的飛(fly)就被我覆寫為不會增加飛行距離。里氏替換原則希望本來預期所有鳥sub-class的飛(fly)function都是和鳥class一樣做飛行的動作,但企鵝class的飛(fly)function卻不做任何飛行的動作。這和我們預期有所出入,就導致不可預期的bug出現了。
這時就要審視我們自己設計class時的分類是不是出問題了。若果不是,就試試不要進行覆寫,而是進行擴展,例如:寫一個使用舊行為組成的新function。
現在用此例子修改為遵守里氏替換原則的樣子。
鳥類(Bird)<遵守里氏替換原則>
![](https://assets.matters.news/embed/f025a71a-b1bb-40dc-96f1-a0cf6dcb12b6.png)
現在鳥類只有名字(name),並且沒有定義任何行為。
速度(Speed)<遵守里氏替換原則>
![](https://assets.matters.news/embed/e38659de-4e30-42f5-85ca-cf2d0c00b698.png)
這個速度class會在之後的interface用到。他的功用是保儲一個不會少於0的數值。
能飛介面(Volant)<遵守里氏替換原則>
![](https://assets.matters.news/embed/140c3752-b6ca-419f-bab0-d962baba5402.png)
這個介面定義了飛行距離(airRange)和飛行速度(flySpeed),並且定義了飛(fly)這個行為。
能游泳介面(Swimming)<遵守里氏替換原則>
![](https://assets.matters.news/embed/a619b0bb-226b-46bd-a5e2-b0101b1b17fc.png)
這個介面定義了游泳距離(swimmingRange)和游泳速度(swimSpeed),並且定義了游泳(swim)這個行為。
能飛的鳥(VolantBird)<遵守里氏替換原則>
![](https://assets.matters.news/embed/b03924cd-2cde-4641-b1f1-1d3fe29fe06a.png)
這個就是繼承鳥class並且有飛行行為的新class。在這裡實作了飛行介面,會飛的鳥因為用了速度class,所以他的飛行速度不會是0。
能游泳的鳥(SwimmingBird)<遵守里氏替換原則>
![](https://assets.matters.news/embed/9892c473-b0f7-4ce2-adf0-4adbe242a97b.png)
這個就是繼承鳥class並且有游泳行為的新class。在這裡實作了游泳介面,會游泳的鳥因為用了速度class,所以他的游泳速度不會是0。
能飛和游泳的鳥(VolantAndSwimmingBird)<遵守里氏替換原則>
![](https://assets.matters.news/embed/24e7d334-a764-4862-9c13-25bb29a33c76.png)
這個就是繼承了能飛的鳥class並且有游泳行為的新class。這個寫法其實不太好,但為了方便講解里氏替換原則,就用了這個寫法。
鷹(Eagle)<遵守里氏替換原則>
![](https://assets.matters.news/embed/400bff29-7aac-4aed-8143-b9568971b78c.png)
鷹繼承了能飛的鳥class。這次只需覆寫他的飛行速度就可以了。
企鵝(Penguin)<遵守里氏替換原則>
![](https://assets.matters.news/embed/5522eedb-1564-4bdf-96b8-49be960b74ba.png)
企鵝繼承了能游泳的鳥class。這次只需覆寫他的游泳速度就可以了。
海鳥(Penguin)<遵守里氏替換原則>
![](https://assets.matters.news/embed/89d75edd-19f8-4464-9dab-80a46f9c4824.png)
企鵝繼承了能飛和游泳的鳥class。覆寫了他的飛行速度和游泳速度。
飛行比賽(Air Race)<遵守里氏替換原則>
![](https://assets.matters.news/embed/2403d98e-55a3-4120-832a-33d71c5b70d8.png)
這次飛行比賽只准許能飛的鳥參加。
設置比賽<遵守里氏替換原則>
![](https://assets.matters.news/embed/2ca605ef-d8f2-48e7-a2d0-bf9c1dad0c42.png)
這裡看見參加比賽的有麻雀,鷹,海鳥和企鵝。當中麻雀是能飛的鳥class,鷹和海鳥是能飛的鳥的sub-class,而企鵝是能游泳的鳥的sub-class。因此,企鵝是不能參加比賽,程式裡會出現紅底線表示程式寫錯了。在去掉出現紅底線哪行後,這次比賽最後能順利結速,並公佈第一名是鷹。
這次的寫法符合里氏替換原則,每一個sub-class裡繼承自super-class的行為都和我們預期一樣跟從super-class的行為一致性。這樣即使我們不知道sub-class的資訊,都能直接替換super-class,並且不會出問題。因為他們都是和super-class行為有著一致性不會作出我們預期以外的事,所以替換了沒有問題。
對於覆寫可能會違反里氏替換原則我會再講多一個例子。我現在會覆寫鷹class當中的行為。
鷹(Eagle)<違反里氏替換原則>
![](https://assets.matters.news/embed/9bdf2fa4-ca6f-4706-8432-e4be785a48d7.png)
現在鷹會進行特別飛行。特別飛行會向前飛,再之後向後飛。我們覆寫了鷹的飛(fly)行為,令他跟據isSpecialFly來決定是進行正常飛行,還是特別飛行。
這時不知道鷹class資料的人,並不知道要設置isSpecialFly去決定進行哪種飛行,再把鷹放進比賽,比賽再次不能順利完結。因為我們預期飛(fly)這一行為是只向前並不會向後,所以這樣覆寫來增加特別飛行是違反了里氏替換原則。
我們現在看看符合里氏替換原則的寫法。
鷹(Eagle)<遵守里氏替換原則>
![](https://assets.matters.news/embed/f9eb0786-da9f-402c-92d6-972667365b71.png)
我們今次不會覆寫鷹的飛(fly)行為,而是直接新增一個新行為------特別飛行(specialFly)。特別飛行(specialFly)的組成用到飛(fly)行為,這就是用了擴展的方法。
這次不知道鷹class資料的人,把鷹放進比賽中,比賽都能順利完結。因為擴展的行為並沒有改動到原來繼承自super-class的行為。這些行為都會如我們的預期。
總結
里氏替換原則可以確保在不知道sub-class資料時,替換了他的super-class,也不會出問題。這個原則也確保了使用多型(Polymorphism)時,不會出現不能預測的錯誤。Programmer就可以安心使用各種不同的設計模式(design pattern)。
想遵守里氏替換原則又想加新功能,最好不要覆寫,而是增加一個新function。若果還是要覆寫已有的行為,最好是寫下comment提醒自己這個行為的大前題是什麼,依據大前題來覆寫。
自己遵守是能保證不會出現不能預測的錯誤,所以計設新功能時不用去檢查哪些可能會替換super-class的哪些sub-class。可惜的是現實中你總要與人合作。🤦♂️因為你不知道對方會不會遵守,所以還是要去檢查哪些可能會替換super-class的哪些sub-class,再去設計新功能。或許留下comment,讓對方知道要遵守里氏替換原則才能用這新功能。別人不做,自己還是要好好遵守。這樣才能確保自己寫的code能保持質量。
喜欢我的作品吗?别忘了给予支持与赞赏,让我知道在创作的路上有你陪伴,一起延续这份热忱!
![](https://imagedelivery.net/kDRCweMmqLnTPNlbum-pYA/prod/avatar/23dcaa94-ad45-47a3-8255-4d2f5bc93a3e.jpeg/public)