【LSP】里氏替换原则

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. 反例

正方形/长方形问题
在这里插入图片描述

  1. 长方形Rectangle类有两个属性:length、width,并用共有的get和set进行访问;
  2. 正方形Square是一种特殊的长方形,特殊在于length和width相等;
  3. 为了保证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);
}
  1. 对于方法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. 反思

  1. 评价设计质量好坏:
      评价一个设计模型的质量,并不是孤立地看待设计模型本身的好坏,而应该从使用该模型的客户程序来衡量,根据客户的需求做出合理的假设来进行评价。
      当然,设计者很难考虑到类客户的一切使用情况,而且过度的假设也会带来不必要的复杂性“臭味”。因此,对于设计人员来说,只考虑那些明显违反LSP的情况,直到出现相关的脆弱性“臭味”时,才做进一步的处理。
  2. is a 关系:
      泛化代表的是一种“is a”的关系;而正方形和矩形之间就是“is a”的关系,即“正方形也是矩形”,但问题出在哪里呢?
      对于普通的用户而言,正方形的确也是矩形,它们的形状类似,计算周长、面积等算法相同。然而,对于resize程序而言,正方形就不是矩形了
  3. 契约式设计(Design by Contract, DbC):
      模型的质量、“is a”关系等都需要从使用者的角度去做合理假设。那么,到底哪些算合理假设呢?客户的要求到底如何来体现呢?
      契约主要分为两类:一类是为类定义不变式(invariants),对于该类的所有对象,不变式一直为真;另一类是为类的方法声明前置条件(preconditions)和后置条件(postconditions),只有前置条件为真时,该方法才可以执行,而方法执行完成后,必须保证后置条件为真。
      再回到长方形和正方形的例子,对于长方形的setLength和setWidth而言,其存在相应的后置条件:在修改长方形的长度时,长度变成新的长度,而宽度应保持不变。
    派生类的前置条件和后置条件规则是“在重新声明派生类中的方法时,只能使用相等或者更弱的前置条件来替换原始的前置条件,只能使用相等或者更强的后置条件来替换原始的后置条件。”派生类的前置条件和后置条件规则是“在重新声明派生类中的方法时,只能使用相等或者更弱的前置条件来替换原始的前置条件,只能使用相等或者更强的后置条件来替换原始的后置条件。”
  • 15
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值