里氏替换原则

里氏替换原则,Liskov Substituion Principle,简称 LSP,一个软件实体如果使用的是一个父类的话,一定适用于其子类,而且它察觉不出父类和子类的区别。也就是说,在软件里面,把父类都替换成它的子类,程序的行为没有变化。简单来说,子类型必须能够替换掉它们的父类型。

使用动机

父类能够真正复用(继承),子类也能够在父类的基础上增加新的行为。

面向对象的编程思想中提供了继承和多态是我们可以很好的实现代码的复用性和可扩展性,但继承并非没有缺点,因为继承的本身就是具有侵入性的,如果使用不当就会大大增加代码的耦合性,而降低代码的灵活性,增加我们的维护成本,然而在实际使用过程中却往往会出现滥用继承的现象,而里式替换原则可以很好的帮助我们在继承关系中进行父子类的设计。

如何使用

  • 父类一般使用抽象类或接口。
  • 抽象类定义公共对象和状态;接口定义公共行为。
  • 子类通过继承父类和接口进行扩展。

使用原则

  • 子类方法的参数类型必须与父类相匹配或更抽象。
  • 子类的返回值类型必须与父类或其子类相匹配。
  • 子类方法的异常必须与父类能抛出的异常(或其子类)相匹配。
  • 子类不应该加强参数条件限制。
  • 子类不能修改父类的私有成员变量。

使用示例

以企鹅和鸟为例。

假设鸟是父类,有下蛋和飞翔两个方法。企鹅作为鸟如果继承了父类,就会出现问题,因为企鹅虽然有翅膀但不会飞。所以,这样设计父类和子类是不合理的,它违反了上面提到的原则,企鹅作为子类无法替换父类。

合理的做法是将飞翔的行为抽象为接口,父类鸟描述状态和公共方法(比如吃),然后会飞的子类再去实现飞翔接口,不会飞的就不用管了。

案例:几维鸟不是鸟

鸟通常都是会飞的, 比如燕子每小时120千米, 但是新西兰的几维鸟由于翅膀退化不会飞. 假如要设计一个实例,计算这两种鸟飞行 300 千米要花费的时间。显然,拿燕子来测试这段代码,结果正确,能计算出所需要的时间;但拿几维鸟来测试,结果会发生“除零异常”或是“无穷大”,明显不符合预期,其类图如图 1 所示。

/**
 * 鸟
 */
public class Bird {
    // 飞行的速度
    private double flySpeed;
 
    public void setFlySpeed(double flySpeed) {
        this.flySpeed = flySpeed;
    }
 
    public double getFlyTime(double distance) {
        return distance/flySpeed;
    }
}
 
/**
 * 燕子
 */
public class Swallow extends Bird{
}
 
/**
 * 几维鸟
 */
public class Kiwi extends Bird {
    @Override
    public void setFlySpeed(double flySpeed) {
        // 重写了父类的方法,导致违背了里氏替换原则
        flySpeed = 0;
    }
}
 
/**
  * 测试飞行耗费时间
  */
public class BirdTest {
    public static void main(String[] args) {
        Bird bird1 = new Swallow();
        Bird bird2 = new Kiwi();
        bird1.setFlySpeed(120);
        bird2.setFlySpeed(120);
        System.out.println("如果飞行300公里:");
        try {
            System.out.println("燕子花费" + bird1.getFlyTime(300) + "小时.");
            System.out.println("几维花费" + bird2.getFlyTime(300) + "小时。");
        } catch (Exception err) {
            System.out.println("发生错误了!");
        }
    }
}

程序运行错误的原因是:几维鸟类重写了鸟类的 setSpeed(double speed) 方法,这违背了里氏替换原则。正确的做法是:取消几维鸟原来的继承关系,定义鸟和几维鸟的更一般的父类,如动物类,它们都有奔跑的能力。几维鸟的飞行速度虽然为 0,但奔跑速度不为 0,可以计算出其奔跑 300 千米所要花费的时间。其类图如图 2 所示。

/**
 * 动物
 */
public class Animal {
    private double runSpeed;
 
    public double getRunTime(double distance) {
        return distance/runSpeed;
    }
 
    public void setRunSpeed(double runSpeed) {
        this.runSpeed = runSpeed;
    }
}
 
 
/**
 * 鸟
 */
public class Bird {
    // 飞行的速度
    private double flySpeed;
 
    public void setFlySpeed(double flySpeed) {
        this.flySpeed = flySpeed;
    }
 
    public double getFlyTime(double distance) {
        return distance/flySpeed;
    }
}
 
/**
 * 燕子
 */
public class Swallow extends Bird {
}
 
/**
 * 几维鸟
 */
public class Kiwi extends Animal {
    @Override
    public void setRunSpeed(double runSpeed) {
        // 继承更一般的父类
        super.setRunSpeed(runSpeed);
    }
}
 
/**
  * 测试飞行耗费时间
  */
public class BirdTest {
    public static void main(String[] args) {
        Bird bird1 = new Swallow();
        Animal bird2 = new Kiwi();
        bird1.setFlySpeed(120);
        bird2.setRunSpeed(110);
        System.out.println("如果飞行300公里:");
        try {
            System.out.println("燕子花费" + bird1.getFlyTime(300) + "小时.");
            System.out.println("几维鸟花费" + bird2.getRunTime(300) + "小时。");
        } catch (Exception err) {
            System.out.println("发生错误了!");
        }
    }
}

资源

  1. 里氏替换原则 (datawhalechina.github.io)
  2. 设计模式六大原则(二)----里式替换原则 - 腾讯云开发者社区-腾讯云 (tencent.com)