软件构造心得(11):里氏替换原则(Liskov Substitution Principle)保持行为一致的动机与目的

13 篇文章 2 订阅
11 篇文章 1 订阅

我们逐渐对于软件构造有了更深的要求。
设计良好的代码需要做到可以不通过修改而扩展,新的功能通过添加新的代码来实现,而不需要更改已有的可工作的代码。
抽象(Abstraction)和多态(Polymorphism)是实现这一原则的主要机制,而继承(Inheritance)则是实现抽象和多态的主要方法。而提高这一质量指标,打下构建可维护性和可重用性代码的基础,我们首先需要知道一个重要的原则。

本文参考了一些资料和课内内容,将着重解释,1)什么是里氏替换原则 2)里氏替换原则保持行为一致的动机与目的

什么是里氏替换原则

里氏替换原则(Liskov Substitution Principle),由Barbara Liskov 在 1988 年提出。
在这里插入图片描述
一句话概括:就是基类指针可以在任何不知道衍生类的条件下使用衍生类的对象。

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思想的一个生动的例子

在运行时查看其所属于的子类型,我们在之前的学习中就曾经遇见过这种风格的代码。其显然破坏了多项原则,其中由于我们需要了解其中的 S h a p e Shape Shape的子类的信息,所以其违背了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; }
  }

这个程序正常工作,没有问题。我们不妨设想,它被成功地部署到了各个位置,发起作用。然后,假如某一天,我们需要一个正方形类型 s q u a r e square square类来处理正方形。
通常来讲,继承关系是一种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”

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值