里氏替换原则(LSP)的定义
如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有的对象o1都替换成o2时,程序P的行为没有变化,那么类型T2是类型T1的子类型。
也就是说,所有引用基类的地方必须能透明地使用其子类的对象。
1、子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
2、子类中可以增加自己特有的方法。具体体现:
子类必须实现父类所有非私有的属性和方法,或子类的所有非私有属性和方法必须在父类中声明。即,子类可以有自己的“个性”,这也就是说,里氏代换原则可以正着用,不能反着用(在项目中,采用里氏替换原则时,尽量避免子类的“个性”,一旦子类有“个性”,这个子类和父类之间的关系就很难调和了)。根据里氏代换原则,为了保证系统的扩展性,在程序中通常使用父类来进行定义,如果一个方法只存在子类中,在父类中不提供相应的声明,则无法在以父类定义的对象中使用该方法。尽量把父类设计为抽象类或者接口。让子类继承父类或实现父接口,并实现在父类中声明的方法,运行时,子类实例替换父类实例,我们可以很方便地扩展系统的功能,同时无须修改原有子类的代码,增加新的功能可以通过增加一个新的子类来实现。
里氏替换原则的要求
子类可以实现父类的抽象方法,当不能覆盖子类的非抽象方法。
子类中可以增加自己特有的方法。
当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
例子
我们写了一个针对所有车型的抽象类,限制了速度的上限。并且定义了一个方法,刹车。对于该方法,要求前置条件输入必须为0,后置条件要求速度必须对比之前降低。
abstract class Vehicle {
int speed, limit;
//@ invariant speed < limit;
//@ requires speed != 0;
//@ ensures speed < \old(speed)
void brake();
}
接着我们实现一个子类汽车继承该抽象类
class Car extends Vehicle {
int fuel;
boolean engineOn;
//@ invariant speed < limit;
//@ invariant fuel >= 0;
//@ requires fuel > 0 && !engineOn;
//@ ensures engineOn;
void start() { … }
void accelerate() { … }
//@ requires speed != 0;
//@ ensures speed < \old(speed)
void brake() { … }
}
可以看出,在汽车这个子类中,它的不变性相对于父类增强了。前置条件和后置条件的强度并没有改变,这符合LSP原则的定义。
接下来,再看下面的例子
还是以车为例
class Car extends Vehicle {
int fuel;
boolean engineOn;
//@ invariant speed < limit;
//@ invariant fuel >= 0;
//@ requires fuel > 0 && !engineOn;
//@ ensures engineOn;
void start() { … }
void accelerate() { … }
//@ requires speed != 0;
//@ ensures speed < \old(speed)
void brake() { … }
}
class Hybrid extends Car {
int charge;
//@ invariant charge >= 0;
//@ requires (charge > 0
|| fuel > 0) && !engineOn;
//@ ensures engineOn;
void start() { … }
void accelerate() { … }
//@ requires speed != 0;
//@ ensures speed < \old(speed)
//@ ensures charge > \old(charge)
void brake() { … }
}
在这个例子中,子类混合动力车继承自汽车类,可以看到子类重写了引擎方法。在父类中要求前置条件必须保证有原料。但是在子类中放宽了条件,只需要保证有燃料或者有电能即可。这符合LSP原则。同时可以发现,对于刹车方法后置条件增强了。也符合LSP原则。
为什么要符合LSP
在项目中,采用LSP原则,可以有效的避免子类的“个性”,一旦子类有“个性”,子类和父类之间的关系就很难调和,采用LSP原则可以提高代码的健壮性、复用性、降低耦合性以及便于扩充功能。