一直以来,yqj2065都认为,学习里氏替换原则(Liskov SubstitutionPrinciple、LSP),如同学习下围棋一样,易学难精。
因为易学,所以在《编程导论(Java)》中安排在2.1.1节。
简单地说。子类必须能够替代父类,这在面向对象语言中如同常识。所以紧接其后,在[2.1.2 啊,我看到了多态]中介绍向上造型、多态、改写(override);
所谓难精,我们将继承加以分析,符合LSP的继承有实现继承、拓展继承、接口/协议继承和多继承;在介绍接口与实现分离时,强调什么是设计良好的接口,到[4.1.2类的接口]中,说明【p133:如果一个对象能够接受X类的接口的全部操作请求(方法调用),则称对象具有X类型。正是因为Java的子类能够满足父类的接口(尽管可以改写),所以子类的对象能够同时具有类层次中的多个类型】,直到[8.3.2断言的使用指南]中,偷偷摸摸地说:继承关系即Is-A关系,本质上要通过接口的一致来衡量。
很多人使用了“正方形不是一个长方形”这个例子,介绍LSP。事实上,不管你采用什么手段和技巧,我可用反证法告示你:(在例程所示的场景中)如果你能够将正方形设计成长方形的子类,那么你必然可以将圆设计成长方形的子类。
[java] view
plaincopy
- class Square1 extends Rectangle{
- private double side;
- public Square1(double side){
- this.side= side;
- }
- void setSide(double s) { side = s; }
- double getSide() { return side; }
- @Override double getS() { return side*side;}//求面积
- }
所谓难精之二,在于五花八门的解释和介绍。yqj2065曾经非常惊讶,这样的里氏替换原则,他还写了设计模式的系列博客。例如,
[java] view
plaincopy
- 里氏替换原则通俗的来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含以下4层含义:
- 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法。
- 子类中可以增加自己特有的方法。
- 当子类的方法重载父类的方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。
- 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。
LSP仅仅说明可替代性或父子类的接口一致性,实现继承是符合LSP的;实现继承的问题,在于设计时没有更好地封装变化,使得子类不得不override父类的代码。plaincopy
- 里氏替换原则,简单地讲就是:子类可以替代父类。它包含以下含义:
- 子类可以实现继承(直接继承或 改变父类原有的功能实现)、拓展继承(扩展父类的功能)、接口/协议继承和多继承。【但是从OCP或封装变化的角度,应该避免其任何父类中已经实现的方法。】
- 子类中不可退化继承,如鸵鸟不会飞。
- 为了保证父类和子类的接口一致性,要注意:接口包括文档(方法的意图)和方法头。按契约设计(Design by Contract)是一致性判断的良好手段(Java断言机制是工具)。
前置条件,通常意味着方法参数的合法性检查(而非重载的类型问题)。如show(Baby b, int age),验证前置条件时,谁会验证出场的人是不是Baby?通常检查它不得为null;而age要大于10,小于16等都是方法文档中已经说明的前置条件,会考虑参数是否double?【子类重载父类的方法时,方法的形参要比父类方法的参数更宽松】,什么意思呢?重载和方法的形参更宽松或狭窄有什么关系?“重载”是敲错了?
子类的override 方法,返回的可以是父类方法返回类型的子类,Java语法支持。