关于里氏替换原则LSP

前端之家收集整理的这篇文章主要介绍了关于里氏替换原则LSP前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。

一直以来,yqj2065都认为,学习里氏替换原则(Liskov SubstitutionPrinciple、LSP),如同学习下围棋一样,易学难精

1.要点

里氏替换原则/LSP是 OO 编程范式中规则范式的核心。
它说明(强调)了子类型必须具备替换属性这在面向对象语言中如同常识。(类层次 Is_a 替代)
即在一个软件系统中,基类出现的所有地方都必须能够被子类型替代。
  • 子类型必须能够替代其父类型——《敏捷软件开发:原则、模式与实践Robert C. Martin
  • substitution property: If for each object s of type S there is an object f of type F such that for all programs P defined in terms of F,the behavior of P is unchanged when f is substituted for s then S is a subtype of T..
    替换原则:对于类型S的任何一个对象s ,都存在一个类型F 的对象f ,在针对F编写的所有程序P中,如果用s替换f后,程序P的“行为”不变,则S是T的 子类型( subtype

LSP是正确设计类层次的指导原则,它检测和保证类层次的正确性,进而维护针对父类型编写的程序的正确性。(你针对父类型,进一步针对抽象类型编程,是OCP的管辖范围)

多态的本质,是不同子类替代父类变量后,可以对同一种消息,有不同的反应。

继承的本质,是子类承接了父类的接口。这是语言层面对LSP的支持。当然,程序员刻意用实现违背接口,没有人可以阻挡。




2.学习思路

因为易学,所以在《编程导论(Java)》中安排在2.1.1节。所以紧接其后,在[2.1.2 啊,我看到了多态]中才开始介绍向上造型、多态、改写(override);

所谓难精,我们

  • 继承加以分析,符合LSP的继承有实现继承、拓展继承、接口/协议继承和多继承;参考《4.2.3接口继承 Vs. 实现继承》
  • 在介绍接口与实现分离时,强调什么是设计良好的接口,到[4.1.2类的接口]中,说明【p133:如果一个对象能够接受X类的接口的全部操作请求(方法调用),则称对象具有X类型。正是因为Java的子类能够满足父类的接口(尽管可以改写),所以子类的对象能够同时具有类层次中的多个类型】
  • 直到[8.3.2断言的使用指南]中介绍正方形和长方形关系时偷偷摸摸地说:继承关系即Is-A关系,本质上要通过接口的一致来衡量

实现继承是符合LSP的——即使对具体父类方法的override有千般不好,它还是符合LSP的。你可以直接使用父类方法,或override空方法。你的方法 m()可以采用改进型override(总是调用super.m()一次)。你在遇到取代型改写时,问一问自己为什么?如果自己能够说服自己,那就用吧!谁敢咬你。

扩展继承是符合LSP的——即使向上造型导致扩展的方法(子类自己定义的方法)不能够用。白马非马,是好事。既保证了系统的扩展性——如果子类不能够定义自己的新方法,那样的语言你用?同时不影响LSP。比如说,Client依赖的Man接口只有eat(),你超人就算有一万个方法,你造型为Man时你也得憋着。要用子类的方法,(SuperMan)U。多重继承A、B时,你作为A,那么B的方法都被A视为扩展。

3.正方形和长方形

很多人使用了“正方形不是一个长方形”这个例子,介绍LSP。事实上,不管你采用什么手段和技巧,我可用反证法告示你:(在例程所示的场景中)如果你能够将正方形设计成长方形的子类,那么你必然可以将圆设计成长方形的子类

[java] view plain copy
  1. classSquare1extendsRectangle{
  2. privatedoubleside;
  3. publicSquare1(doubleside){
  4. this.side=side;
  5. }
  6. voidsetSide(doubles){side=s;}
  7. doublegetSide(){returnside;}
  8. @OverridedoublegetS(){returnside*side;}//求面积

4.吐槽(考试的时候,不要随便使用网上的某些文字)

所谓难精之二,在于五花八门的解释和介绍。yqj2065曾经非常惊讶,这样的里氏替换原则,他还写了设计模式的系列博客。例如,

copy
    里氏替换原则通俗的来讲就是:子类可以扩展父类功能,但不能改变父类原有的功能。它包含以下4层含义:
  1. 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
  2. 子类中可以增加自己特有的方法
  3. 当子类的方法重载父类方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
  4. 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
LSP仅仅说明可替代性或父子类的接口一致性,实现继承是符合LSP的;实现继承的问题,在于设计时没有更好地封装变化,使得子类不得不override父类代码 plain copy
    里氏替换原则,简单地讲就是:子类可以替代父类。它包含以下含义:
  1. 子类可以实现继承(直接继承或改变父类原有的功能实现)、拓展继承(扩展父类功能)、接口/协议继承和多继承。【但是从OCP或封装变化的角度,应该避免override其任何父类中已经实现的方法。】
  2. 子类中不可退化继承,如鸵鸟不会飞。
  3. 为了保证父类和子类的接口一致性,要注意:接口包括文档(方法的意图)和方法头。按契约设计(DesignbyContract)是一致性判断的良好手段(Java断言机制是工具)。

前置条件,通常意味着方法参数的合法性检查(而非重载的类型问题)。如show(Baby b,int age),验证前置条件时,谁会验证出场的人是不是Baby?通常检查它不得为null;而age要大于10,小于16等都是方法文档中已经说明的前置条件,会考虑参数是否double?【子类重载父类方法时,方法的形参要比父类方法的参数更宽松】,什么意思呢?重载和方法的形参更宽松或狭窄有什么关系?“重载”是敲错了?

子类的override 方法,返回的可以是父类方法返回类型的子类,Java语法支持


又有人写道:“里氏代换原则是实现开闭原则的重要方式之一”。我一直说LSP是OCP的前提条件实现方式Vs. 必要条件,我有点傻傻地分不清了。

你的Client依赖于IServer接口,符合OCP。为什么能够OCP,因为具体的Server可以替代IServer接口。如果(所有的)Server设计的烂,不能够LSP,你能不能OCP?(必要条件)

但是,我的Client依赖于IServer接口,符合OCP。如果我依赖于具体的Server就不OCP了。好在Server总能够替代IServer,这个LSP好使,所以我的Client就可以依赖于IServer接口,实现了OCP。(实现方式)好像也说得通。

总体上,这篇面向对象设计原则之里氏代换原则,还不如改为OCP之2。


猜你在找的设计模式相关文章