1. 定义
1988年,Barbara Liskov在描述如何定义子类型时写下了这样一段话:
这里需要的是一种可替换性:如果对于每个类型是S的对象o1都存在一个类型为T的对象o2,能使操作T类型的程序P在用o2替换o1时行为保持不变,我们就可以将S称为T的子类型。
反过来讲:假设某个函数f(),它的参数是指向某个基类B的指针或者引用;与此同时,存在B的某个派生类D,如果把D的对象作为B类型传递给f(),就会导致f()出现错误的行为。那么此时D就违反了LSP;因为用D的对象替换B的对象后,f()的行为发生了变化。
2. 举例
假设我们有一个License类。该类中有一个名为calcFee()的方法,该方法将由Billing应用程序来调用。而License类有两个“子类型”:PersonalLicense与BusinessLicense,这两个类会用不同的算法来计算授权费用。
上述设计是符合LSP原则的,因为Billing应用程序的行为并不依赖于其使用的任何一个衍生类。也就是说,这两个衍生类的对象都是可以用来替换License类对象的。
3. 反例
正方形/长方形问题
- 长方形Rectangle类有两个属性:length、width,并用共有的get和set进行访问;
- 正方形Square是一种特殊的长方形,特殊在于length和width相等;
- 为了保证Square的length和width相等,提供getSide()和setSide()方法;
//Square 主要方法
//保证正方形两边相等
public void setSide(int side){
super.setLength(side);
super.setWidth(side);
}
public void setLength(int length){
setSide(length);
}
public void setWidth(int width){
setSide(width);
}
- 对于方法getAreaAfterResize()返回resize后的矩形面积,如果传入参数从Rectangle替换成Square会得到完全不同的结果,因此Square不能替换Rectangle,也就是这里不满足LSP原则。
//返回resize后的矩形面积
public static int getAreaAfterResize(Rectangle r){
r.setWidth(5);
r.setLength(2);
return r.area();
}
static main(){
//使用Rectangle初始化得到的面积是10 2*5
Rectangle r = new Rectangle();
int area= resize(r)
//使用Square初始对象得到的面积是4 2*2
Rectangle r2=new Square();
area = resize(r)
}
4. 最佳实践
为了避免子类型针对基类型的行为添加附加的约束(即违背LSP),基类型中应该只提供尽量少的必需的行为,而且不针对这些行为进行任何实现。此时,那些基类型往往就是抽象类(行为没有任何实现),甚至是接口。
由此,由LSP可以引申出一条新的规则:即只要有可能,不要从具体类继承,而应该由抽象类继承或由接口实现。
上面长方形和正方形可以改为:
将setLength和setWidth移出原有的基类后,重新构造一个基类型四边形(Quadrangle),该类型仅提供剩下的两个行为getLength和getWidth。由于该类型中没有定义任何数据成员,也无法提供任何实现,因此可以直接将四边形定义为一个更抽象的接口,而矩形(Rectangle)和正方形(Square)分别作为两个子类型存在。
5. 反思
- 评价设计质量好坏:
评价一个设计模型的质量,并不是孤立地看待设计模型本身的好坏,而应该从使用该模型的客户程序来衡量,根据客户的需求做出合理的假设来进行评价。
当然,设计者很难考虑到类客户的一切使用情况,而且过度的假设也会带来不必要的复杂性“臭味”。因此,对于设计人员来说,只考虑那些明显违反LSP的情况,直到出现相关的脆弱性“臭味”时,才做进一步的处理。 - is a 关系:
泛化代表的是一种“is a”的关系;而正方形和矩形之间就是“is a”的关系,即“正方形也是矩形”,但问题出在哪里呢?
对于普通的用户而言,正方形的确也是矩形,它们的形状类似,计算周长、面积等算法相同。然而,对于resize程序而言,正方形就不是矩形了 - 契约式设计(Design by Contract, DbC):
模型的质量、“is a”关系等都需要从使用者的角度去做合理假设。那么,到底哪些算合理假设呢?客户的要求到底如何来体现呢?
契约主要分为两类:一类是为类定义不变式(invariants),对于该类的所有对象,不变式一直为真;另一类是为类的方法声明前置条件(preconditions)和后置条件(postconditions),只有前置条件为真时,该方法才可以执行,而方法执行完成后,必须保证后置条件为真。
再回到长方形和正方形的例子,对于长方形的setLength和setWidth而言,其存在相应的后置条件:在修改长方形的长度时,长度变成新的长度,而宽度应保持不变。
派生类的前置条件和后置条件规则是“在重新声明派生类中的方法时,只能使用相等或者更弱的前置条件来替换原始的前置条件,只能使用相等或者更强的后置条件来替换原始的后置条件。”派生类的前置条件和后置条件规则是“在重新声明派生类中的方法时,只能使用相等或者更弱的前置条件来替换原始的前置条件,只能使用相等或者更强的后置条件来替换原始的后置条件。”