07
07

设计模式五大原则(2):里氏替换原则

在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。


里氏替换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一。 任何基类可以出现的地方,子类一定可以出现。 LSP是继承复用的基石,只有当衍生类可以替换掉基类,软件单位的功能不受到影响时,基类才能真正被复用,而衍生类也能够在基类的基础上增加新的行为,这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的。

定义1:子类可以扩展父类的功能,但不能改变父类原有的功能。

定义2:一个基类对象替换成它的子类对象,程序将不会产生任何错误和异常,反过来则不成立,如果一个软件实体使用的是一个子类对象的话,那么它不一定能够使用基类对象。

定义3:所有使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

问题由来:有一功能P1,由类A完成。现需要将功能P1进行扩展,扩展后的功能为P,其中P由原有功能P1与新功能P2组成。新功能P由类A的子类B来完成,则子类B在完成新功能P2的同时,有可能会导致原有功能P1发生故障。

解决方案:当使用继承时,遵循里氏替换原则。类B继承类A时,除添加新的方法完成新增功能P2外,尽量不要重写父类A的方法,也尽量不要重载父类A的方法。

继承包含这样一层含义:父类中凡是已经实现好的方法(相对于抽象方法而言),实际上是在设定一系列的规范和契约,虽然它不强制要求所有的子类必须遵从这些契约,但是如果子类对这些非抽象方法任意修改,就会对整个继承体系造成破坏。而里氏替换原则就是表达了这一层含义。

继承作为面向对象三大特性之一,在给程序设计带来巨大便利的同时,也带来了弊端。比如使用继承会给程序带来侵入性,程序的可移植性降低,增加了对象间的耦合性,如果一个类被其他的类所继承,则当这个类需要修改时,必须考虑到所有的子类,并且父类修改后,所有涉及到子类的功能都有可能会产生故障。

举例说明继承的风险:

public class Bird {
    public void eat(){
        System.out.println("它吃虫子");
    }
    public void fly(){
        System.out.println("它会飞");
    }
}

public class Start {
    public static void main(String[] args) {
        Bird bird = new Bird();
        bird.eat();
        bird.fly();
    }
}

运行结果:

它吃虫子
它会飞

后来我们有了一个新的对象--家禽,家禽也属于鸟,家禽能扩展出公鸡,母鸡,锦毛鸡

public class Poultry extends Bird{
    public void eat(){
        System.out.println("它吃谷子");
    }
    public void fly(){
        System.out.println("它不能飞,但能滑翔,它能在地面蹦跶");
    }
}

public class Cock extends Poultry{  }

public class Hen extends Poultry{  }

public class Chicken extends Poultry{  }

public class Start {
    public static void main(String[] args) {
        Poultry cock = new Cock();
        cock.eat();
        cock.fly();

        Poultry hen = new Hen();
        hen.eat();
        hen.fly();

        Poultry chicken = new Chicken();
        chicken.eat();
        chicken.fly();
    }
}

运行结果:

它吃谷子
它不能飞,但能滑翔,它能在地面蹦跶
它吃谷子
它不能飞,但能滑翔,它能在地面蹦跶
它吃谷子
它不能飞,但能滑翔,它能在地面蹦跶

很显然这个扩展的家禽动作的代码是成功的,因为公鸡、母鸡、锦毛鸡能是家禽同时都是鸟,但是确忽略了鸽子也是家禽的一种

public class Dove extends Poultry{ }

public class Start {
    public static void main(String[] args) {
        Bird bird = new Dove ();
        bird.eat();
        bird.fly();
    }
}

运行结果:

它吃谷子
它不能飞,但能滑翔,它能在地面蹦跶

然而鸽子并不是不能飞的家禽,我们认为Bird类里面已经作出了飞和吃虫子的处理,只要获得Bird类对象就能让鸽子飞和吃虫子,但实际上继承关系是这样的


我们在Poultry中重写了Bird的eat和fly这两个方法,造成了所有的家禽都不会飞和只吃谷子,在本例中,引用基类Bird完成的功能,换成子类Dove之后,发生了异常。很显然,当Bird或者Poultry出现的时候,子类Dove并不能被调用,这样就违背了里氏替换原则。

在实际编程中,我们常常会通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性会比较差,特别是运用多态比较频繁时,程序运行出错的几率非常大。

里氏替换原则可以简单的理解为:子类可以扩展父类的功能,但不能修改父类原有的功能。它包含以下4层含义:

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。

  • 子类中可以增加自己特有的方法。

  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。

  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

假如我不遵循里氏替换原则那么你的代码出现问题的几率将会大大增加

 


蜗牛学院,只为成就更好的你!

你!敢不敢!用你三个月的时间,换你不一样的未来!

赶快关注蜗牛学院官方微信,了解更多信息吧!

20181009_153045_341.jpg



版权所有,转载本站文章请注明出处:蜗牛学苑, https://www.woniuxy.cn/article/8