里氏替换原则_软件构造心得:里氏替换原则(LSP)保持行为一致的动机与目的...

14179a11996ecb7be361cf00da2fd26a.png

我们逐渐对于软件构造有了更深的要求。
设计良好的代码需要做到可以不通过修改而扩展,新的功能通过添加新的代码来实现,而不需要更改已有的可工作的代码。
抽象(Abstraction)和多态(Polymorphism)是实现这一原则的主要机制,而继承(Inheritance)则是实现抽象和多态的主要方法。而提高这一质量指标,打下构建可维护性和可重用性代码的基础,我们首先需要知道一个重要的原则。
本文参考了一些资料和课内内容,将着重解释,1)什么是里氏替换原则 2)里氏替换原则保持行为一致的动机与目的
什么是里氏替换原则
里氏替换原则(Liskov Substitution Principle),由Barbara Liskov 在 1988 年提出。

0e68400593165c4035ba683c536b6600.png


一句话概括:就是基类指针可以在任何不知道衍生类的条件下使用衍生类的对象。
Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.
详细说明:就是要确保在父类使用的时候,(无论这时候的父类实际上是哪一种衍生类),表现都是一样的。也就是说,使用者在书写代码使用该类时无需关注与考虑有什么衍生类,以及本次是什么衍生类。
What is wanted here is something like the following substitution property: If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.
违背LSP思想的一个生动的例子
在运行时查看其所属于的子类型,我们在之前的学习中就曾经遇见过这种风格的代码。其显然破坏了多项原则,其中由于我们需要了解其中的ShapeShapeShape的子类的信息,所以其违背了LSP原则
void DrawShape(const Shape& s) { if (typeid(s) == typeid(Square)) DrawSquare(static_cast<Square&>(s)); else if (typeid(s) == typeid(Circle)) DrawCircle(static_cast<Circle&>(s)); }
一个从矩形与正方形的继承引发的LSP的动机思考
我们来看下面这个面向对象设计中违背LSP原则经典的例子
public class Rectangle { private double _width; private double _height; public void SetWidth(double w) { _width = w; } public void SetHeight(double w) { _height = w; } public double GetWidth() { return _width; } public double GetHeight() { return _height; } }
这个程序正常工作,没有问题。我们不妨设想,它被成功地部署到了各个位置,发起作用。然后,假如某一天,我们需要一个正方形类型squaresquaresquare类来处理正方形。
通常来讲,继承关系是一种is-a的关系。换句话来讲,如果一种新的对象与一种已有对象满足 is-a 的关系,那么新的对象的类应该是从已有对象的类继承来的。
而这里面由于很明显正方形是一个长方形,所以其显而易见可以从长方形衍生。
然而,当我们什么也不考虑时,我们可能就会直接写出如下代码:
public class Square extends Rectangle { //Require width == height }
进而,我们在使用rectangle的set方法时,如果输入的其实是square类型变量,那么该方法就会直接破坏square的RI。
那么我们单纯的避免这种情况,在square中复写一下的set方法呢?
public class Square extends Rectangle { public void SetWidth(double w) { base.SetWidth(w); base.SetHeight(w); } public void SetHeight(double w) { base.SetWidth(w); base.SetHeight(w); } }
现在,只要设置 Square 对象的 Width,那么它的 Height 也会相应跟着变化。而当设置其Height 时,Width 也同样会改变。这样做之后,Square 仍然可以保持其RI,Square 对象仍然是一个看起来很合理的数学中的正方形,看起来很完美。那么现在我们拥有了两个类,无论对于square做什么,它都保持着对于正方形的定义。对于retangle也是会保持着对于长方形的定义。那么现在我们可以放心的使用继承出来的square了吗?
并没有…
设想存在下面的情况
void Test1(Rectangle r) { r.SetWidth(5); r.SetHeight(4); Assert.AreEqual(r.GetWidth() * r.GetHeight(), 20); }
我们可以理解这段代码的期待。显然,这个函数做了一个合理的假设,唯独在我们书写了square,并且square传递进来之后,才会出现并不符合的问题。因此我们还是不能放心的使用继承的square。
总结来看,因为我们要从使用者的合理假设的角度来分析,所以所有的衍生类必须符合使用者所期待的基类的行为。
那不禁想问,我们已经思考过is-a了啊,那为什么既然正方形是长方形,那么到底哪里有问题呢?
所以哪错了?
实际上,到底什么是is-a,怎么算做一个is-a。其实,我们的behavioural subtyping概念就此应运而生。其实,我们之前的设计,错就错在,一个正方形是一个长方形没有错,但是一个square对象并不是一个ractangle对象。因为我们除了逻辑上的继承关系,变量上的继承关系,我们还需要考虑并且满足子类的行为继承关系。
LSP原则其实让我们意识到了,通过is-a进行继承的思考与实现是与对象的公有的行为息息相关的,满足这个才算真正实现了is-a。
在每个方法中保证行为一致性当我们让客户使用基类/接口使用对象,客户知道的仅仅是其基类的前置条件和后置条件。也就是说其一定只会从这两个条件做出自己的假设。那么我们从这个基类中延展出来的衍生类,就必须要支持基类中的条件,也就是说,其前置条件一定要弱于基类的前置条件,其后置条件一定要强于基类的后置条件。
只有这样我们才可以保证基类和衍生类的行为等价性。放心的重用基类的函数,放心使用或修改衍生类的函数。
LSP:The Liskov Substitution Principle by Robert C. Martin “Uncle Bob”

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值