設計原則之里氏替換原則(LSP)

簡介

里氏替換原則是在作繼承設計時須要遵循的原則,不遵循了 LSP 的繼承類會帶來意想不到的問題。html

定義

里氏替換原則(Liskov Substitution Principle) 是由 Barbara Liskov 在 1987 年提出來的,Liskov 是她的姓,國內翻譯成 里氏。java

原則聲明:若是類型 S 是類型 T 的子類型,那麼 T 類型的對象能夠替換成 S 類型的對象,而不會影響程序的行爲。ide

LSP 對語言增長了新的簽名約束(協變與逆變能夠看這篇文章Java中的逆變與協變):ui

  • Contravariance of method arguments in the subtype.
  • Covariance of return types in the subtype.
  • No new exceptions should be thrown by methods of the subtype, except where those exceptions are themselves subtypes of exceptions thrown by the methods of the supertype.

從契約角度來看,里氏替換原則有4層含義:this

  1. 方法的前置條件要求不能更嚴格(能夠更寬鬆)
  2. 方法的後置條件不能更寬鬆(能夠更嚴格)
  3. 子類要保持父類約定的不變性
  4. 歷史約束。類屬性只能經過方法來修改,因爲子類會引入父類中不存在的方法,方法的引入可能會致使原來在父類中不可修改的屬性在子類中能夠修改了,歷史約束禁止這種行爲。

思考

繼承描述的是 is-a 關係,開閉原則要求咱們使用繼承增長功能,LSP 原則是指導咱們如何繼承。編碼

在之前寫的一篇里氏替換原則 的文章裏,我提到過:.net

每一個類都會有public方法,有些類會實現interface,供其餘類使用,自身就處在一個服務的位置上。
每一個public方法都是自身所作出的一個承諾,只要你按照要求調用,就會提供正確的服務。
子類在繼承後,當然是得到了超類的帶來的‘財富’,更重要的是要遵照超類作出的承諾,
破壞了這個承諾其實是沒有資格繼承超類的。

若是破壞了繼承原則,那麼開閉原則也就沒法使用。子類不按照契約設定編碼,那就是在給使用者挖坑。翻譯

實踐

需求要求設計一個鳥的繼承體系,以下是咱們設計的抽象基類:設計

public abstract class Bird {
    private String name;
    public void setName(String name){
        this.name = name;
    }
    public void fly() {
        System.out.println(name + " fly");
    }
}

大部分鳥在這個基類中都工做的很好,可是有一天來了一隻企鵝,企鵝是不會飛的,所以咱們重寫 fly 方法code

public class Penguin {
    @Override
    public void fly() {
        throw new RuntimeException();
    }
}

因爲企鵝不會飛,在 fly 方法裏直接拋出了異常。

注意,這裏已經違反了 LSP 原則,在基類中並無異常拋出,使用方正常使用,而在 Penguin 類中 fly 方法拋出了異常,違反了基類遵照的契約。

要解決這個問題,咱們須要應用接口分離原則來拆分 Bird 類,由 Penguin 來看, fly 功能並非 Bird 承擔的職責,應該將其單獨放到一個接口中,會飛的鳥自行實現。若是像上面那樣,大部分鳥都有一個默認的飛行實現,則咱們能夠作一個默認的飛行實現類,使用組合的方式放到會飛的鳥中。

public abstract class Bird {
    private String name;
    public void setName(String name){
        this.name = name;
    }
}


public interface Flyable {
    public void fly();
}

總結

里氏替換原則是繼承須要遵循的原則,有時咱們可能在無心中就已經違反了原則要求,一是由於咱們沒有意識到,二是咱們設計的接口、抽象基類有問題。遇到違反 LSP 原則的繼承,有兩招來解決:1. 修改實現,2。 更改設計。

相關文章
相關標籤/搜索