設計模式六大原則(二):里氏替換原則


里氏替換原則的定義是:所有引用基類的地方必須能透明化地使用其子類的對象。

里氏替換原則針對的問題

有一個功能P1,由類A完成。現需要將功能P1進行擴展,擴展后的功能為P,其中P由原有功能P1與新功能P2組成。新功能P由類A的子類B來完成,則子類B在完成新功能P2的同時,有可能會導致原有功能P1發生故障。

里氏替換原則的解決方案

當使用繼承的時候,遵循里氏替換原則。類B繼承類A的時候,除了添加新的方法完成新增功能P2外,盡量不要重寫父類A的方法,也盡量不要重載父類A的方法。

繼承包含這樣一層含義:父類中范式已經實現好的方法(相對於抽象方法而言),實際上是在設定一系列的規范和契約,雖然它不強制要求所有的子類必須遵從這些契約,但是如果子類對這些非抽象方法任意修改,就會對整個繼承體系造成破壞。而里氏替換原則就是表達了這一層含義。

繼承作為面向對象的三大特征之一,在給程序設計帶來巨大便利的同時,也帶來了弊端。比如使用繼承會給程序帶來侵入性,程序的可移植性降低,增加了對象之間的耦合性。如果一個類被其他的類所繼承,則當這個類需要修改時,必須考慮到所有的子類,並且父類修改后,所有涉及到子類的功能都有可能會產生故障。

里氏替換原則的案例

關於里氏替換原則的例子,最有名的是【正方形不是長方形】。當然,生活中也有很多類似的例子,例如,企鵝、鴕鳥和幾維鳥從生物學的角度來划分,它們屬於鳥類;但是從類的繼承關系來看,由於它們不能繼承鳥類會飛的功能,所以它們不能被定義成鳥類的子類。如果要強行定義為鳥類的子類,則有一些行為是需要重寫鳥類的行為的,這樣就違背了里氏替換原則。

這里以【幾維鳥不是鳥】為例來說明里氏替換原則

代碼如下:

public class LspTest {
    public static void main(String[] args) {
        Bird bird1 = new Swallow();
        Bird bird2 = new BrownKiwi();
        bird1.setSpeed(120);
        bird2.setSpeed(120);
        System.out.println("如果飛行300公里:");
        try {
            System.out.println("燕子將飛行" + bird1.getFlyTime(300) + "小時。"); // 燕子將飛行2.5小時。
            System.out.println("幾維鳥將飛行" + bird2.getFlyTime(300) + "小時。"); // 幾維鳥將飛行Infinity小時。
        } catch (Exception err) {
            System.out.println("發生錯誤了!");
        }
    }
}

// 鳥類
class Bird {
    double flySpeed;

    public void setSpeed(double speed) {
        flySpeed = speed;
    }

    public double getFlyTime(double distance) {
        return (distance / flySpeed);
    }
}

// 燕子類
class Swallow extends Bird {
}

// 幾維鳥類
class BrownKiwi extends Bird {
    public void setSpeed(double speed) {
        flySpeed = 0;
    }
}

這里,因為幾維鳥類重寫了鳥類的setSpeed方法,導致了程序發生錯誤,違背了里氏替換原則。正確的做法應該是取消幾維鳥原來的繼承關系,定義鳥類和幾位鳥類的更一般的父類,比如動物類。他們都擁有奔跑的能力。幾維鳥的飛行速度雖然為0,但是奔跑速度不為0,可以計算出其奔跑300千米所要花費的時間。

修改后的代碼如下:

public class LspTest2 {
    public static void main(String[] args) {
        Animal bird_swallow = new Bird_Swallow();
        Animal notBird_brownKiwi = new NotBird_BrownKiwi();
        bird_swallow.setSpeed(120);
        notBird_brownKiwi.setSpeed(120);
        System.out.println("如果移動300公里:");
        try {
            System.out.println("燕子將花費" + bird_swallow.getFlyTime(300) + "小時。"); // 燕子將花費2.5小時。
            System.out.println("幾維鳥將花費" + notBird_brownKiwi.getFlyTime(300) + "小時。"); // 幾維鳥將花費2.5小時。
        } catch (Exception err) {
            System.out.println("發生錯誤了!");
        }
    }
}

// 動物類
class Animal {
    private double speed;

    public void setSpeed(double speed) {
        this.speed = speed;
    }

    public double getFlyTime(double distance) {
        return (distance / speed);
    }
}

// 燕子類
class Bird_Swallow extends Animal {
}

// 幾維鳥類
class NotBird_BrownKiwi extends Animal {

}

里氏替換原則的認識

里氏替換原則主要闡述了有關繼承的一些原則,也就是什么時候應該使用繼承,什么時候不應該使用繼承,以及其中蘊含的原理。里氏替換原則是繼承復用的基礎,它反映了基類與子類之間的關系,是對開閉原則的補充,是對實現抽象化的具體步驟的規范。

里氏替換原則的通俗理解就是:子類可以擴展父類的功能,但是不能改變父類原有的功能。也就是說,在子類繼承父類的時候,除了添加新的方法完成新增功能之外,盡量不要重寫父類的方法。

如果通過重寫父類的方法來完成新的功能,這樣寫起來雖然簡單,但是整個繼承體系的可復用性會比較差,特別是運用多態比較頻繁時,程序運行出錯的概率會非常大。

如果程序違背了里氏替換原則,則繼承的對象在基類出現的地方會出現運行錯誤。這時其修正方法是:取消原來的繼承關系,重新設計他們之間的關系。

它包含了以下4層含義:

1.子類可以實現父類的抽象方法,但是不能覆蓋父類的非抽象方法。

2.子類中可以增加自己特有的方法。

3.當子類的方法重載父類的方法時,方法的前置條件(即方法的形參)要比父類方法的輸入參數更加寬松。

 

"每天按部就班地做着自己的事,但是也會對你淡淡地想念。打開微信偶爾平常得聊幾句,問問最近你過得怎么樣,開心不開心,和你分享一些今天遇到的趣事,也不期待你能夠秒回。只是因為分享的對象是你,我就會覺得很滿足了。"


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM