天天看点

设计模式六大原则(3):里氏替换原则

定义:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 P 在所有的对象 o1 都代换成 o2 时,程序 P 的行为没有发生变化,那么类型 T2 是类型 T1 的子类型。这种描述不太好理解,里氏替换原则还有第二种定义:所有引用基类的地方必须能透明地使用其子类的对象。

场景:有一功能F1(会飞),由类A完成。现需要将功能F1进行扩展,扩展后的功能为F(会飞、会游泳),其中F由原有功能F1(会飞)与新功能F2(会游泳)组成。新功能F(会飞、会游泳)由类A的子类B(继承A)来完成,则子类B在完成新功能F2(会游泳)的同时,有可能会导致原有功能F1(会飞)发生故障。

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

我们知道,面向对象的语言的三大特点是继承、封装、多态,里氏替换原则就是依赖于继承、多态这两大特性。里氏替换原则简单来说就是,所有引用基类的地方必须能透明地使用其子类的对象。通俗点讲,子类可以扩展父类的功能,但不能改变父类原有的功能。说了那么多,其实最终总结就两个字:抽象。

下面通过上面的场景来举个例子,用来暴露不遵循里氏替换原则可能会出现的问题:

1.定义一个Animal类,此类有个功能fly(会飞):

/**
 * Created by LJW on 2018/8/23.
 */
public class Animal {

    public void fly(){

    }
}
           

2.定义一个Fish类,此类继承于Animal类,并扩展新的功能swim(会游泳):

/**
 * Created by LJW on 2018/8/23.
 */
public class Fish extends Animal {

    String name;
    public Fish(String name){
        this.name = name;
    }

    @Override
    public void fly() {
        Log.i(name, "远走高飞啦~-~");
    }

    public void swim(){
        Log.i(name, "遨游大海啦~-~");
    }
}
           

3.初始化Fish类,实现里面的功能:

Fish fish = new Fish("小黄鱼");
        fish.fly();
        fish.swim();
           

最终的打印结果为:

小黄鱼: 远走高飞啦~-~

小黄鱼: 遨游大海啦~-~

惊讶吧!原本只会游泳的小黄鱼,继承Animal类之后竟然会飞了!根本原因是,子类覆盖了父类的非抽象方法。这里,为了遵循里氏替换原则,我们可以修改Fish类中的代码,不让其覆盖fly()方法。

说明:这里我只是为了暴露出不遵循里氏替换原则可能会出现的问题,所以不要去纠结程序本身的调用!

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

里氏替换原则就为这类问题提供了指导原则,也就是建立抽象,通过抽象建立规范,具体的实现在运行时替换掉抽象,保证系统的高扩展性、灵活性。开闭原则和里氏替换原则往往是生死相依、不弃不离的,通过里氏替换来达到对扩展开放,对修改关闭的效果。然而,这两个原则都同时强调了一个OOP的重要特性——抽象,因此,在开发过程中运用抽象是走向代码优化的重要一步。

继续阅读