一、定义
(1)、所有使用基类的地方必须能够使用子类进行替换,而程序的行为不会发生任何变化(替换为子类之后不会产生错误或者异常)。
只有这样,父类才能真正被复用,子类能够在父类的基础上增减新的属性和行为。才能真正的实现多态行为。
(2)、当子类继承父类的时候,子类就拥有了父类的属性和行为。(注意:只是类型而已) 但是如果子类覆盖父类的某些方法,那么
原来使用父类的地方就可能出现错误。(如何理解呢?表面上看是调用的是父类的方法,实际运行的时候子类方法覆盖了父类的方
法,注意父类方法其实是存在的,通过作用域限定符可以访问到,两个方法的实现可能不一样,这样不符合LSP里氏替换原则。)
(3)、里氏替换原则是实现开闭原则的重要方式之一。由于使用基类对象的地方可以使用子类对象,因此程序中尽量使用基类类型进
行定义,而在运行的时候确定子类类型,子类对象替换父类对象。 (有点面向接口编程的味道,对外提供接口,而不是实现类)。
编程实验:长方形和正方形的驳论
1、正方形是一种特殊的长方形(is-a关系):类图:
正方形类继承于长方形类。
int main() { //LSP原则:父类出现的地方必须能用子类替换 Rectangle* r = new Rectangle();//Square *r = new Square(); r->setWidth(5); r->setHeight(4); printf("Area = %d\n",r->getArea()); //当用子类时,结果是16。用户就不 //明白为什么长5,宽4的结果不是20,而是16. //所以正方形不能代替长方形。即正方形不能 //继承自长方形的子类 return 0; }2、改进的继承关系---符合LSP原则(面向接口编程)
类图:
int main() { //LSP原则:父类出现的地方必须能用子类替换 QuadRangle* q = new Rectangle(5,4); //Rectangle* q = new Rectangle(5,4);或Square *q = new Square(5); printf("Area = %d,Perimeter = %d\n",q->getArea(),q->getPerimeter()); return 0; }
3、鸵鸟不是鸟类
//面向对象设计原则:LSP里氏替换原则 //鸵鸟不是鸟的测试程序 #include <stdio.h> //鸟类 class Bird { private: double velocity; //速度 public: virtual void fly() {printf("I can fly!\n");} virtual void setVelocity(double v){velocity = v;} virtual double getVelocity(){return velocity;} }; //鸵鸟类Ostrich class Ostrich : public Bird { public: void fly(){printf("I can\'t fly!");} void setVelocity(double v){Bird::setVelocity(0);} double getVelocity(){return Bird::getVelocity();} }; //测试函数 void calcFlyTime(Bird& bird) //参数是引用 父类引用子类的时候,会有多态的行为 { try { double riverWidth = 3000; if(bird.getVelocity()==0) throw 0; printf("Velocity = %f\n",bird.getVelocity()); printf("Fly time = %f\n",riverWidth /bird.getVelocity()); } catch(int) //异常处理 { printf("An error occured!") ; } } int main() { //遵守LSP原则时,父类对象出现的地方,可用子类替换 Bird b; //用子类Ostrich替换Bird b.setVelocity(100); //替换之后,会直接调用子类的方法 calcFlyTime(b); //父类测试时是正常的,子类时会抛出异常,违反LSP return 0; }这种小程序都比较简单,我也是看的别人的,主要是用来测试历史替换原则。
二、历史替换原则的4层含义(良好的继承定义规范,主要包括4层含义)
java里面的接口可以直接定
义接口对象。
(1)、步枪、手枪和机关枪都继承于AbstractGun接口类,都必须实现shoot(射击)的功能。
(2)、玩具枪不能直接继承AbstractGun。因为玩具枪不能实现父类的shoot功能(即子类不能完全实现父类的方法,违反LSP原则)。
按照继承原则,上面的玩具枪继承AbstractGun是没有问题的,玩具枪也是枪,但是在具体的应用场景中就要考虑这个问题了:子类
是否能够完整的实现父类的业务,否则就会出现拿枪杀敌人时是把玩具枪的笑话。
因此,ToyGun不能继承于AbstractGun,而是继承AbstracToy,然后仿真枪的行为。因为士兵类要求传入的参数AbstractGun类的对
象,所以不能使用玩具枪杀人。
感觉用C++表示这种关系比较牵强。
(3)、如果子类不能完整的实现父类的方法,或者父类的某些方法在子类中已经发生"畸变",则建议断开父子继承关系,采用依赖、
聚合、组合等关系代替继承。
2、子类可以扩展功能,但不能改变父类原有的功能(理解:不能出现方法覆盖的情况,多态可以)
(1)、子类可以有自己的属性和方法。因此,里氏替换原则只能正着用,父类出现的地方可以用子类替换,但是不能反过来用。即子
类出现的地方,父类未必可以替换。例如:Snipper类的killEnemy方法中不能传入Rifle类的对象,因为父类中没有子类的zoomOut
方法。
(2)、父类向下转换是不安全的,可能会调用只有在子类中出现的方法造成异常。
java里面的接口其实就是C++里面的抽象类,而java里面的抽象类其实就是C++里面的普通的父类(可以有成员变量和方法)。
多继承的实现:单继承+多接口
3、子类可以实现父类的抽象方法,但一般不要覆盖父类的非抽象方法。
注意:父类抽象方法(多态),一般不要覆盖非抽象方法(子类中公有的父类成分)
4、如果覆盖或实现父类方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。方法的后置条件(方法的返回值)要比父类更严
格。
(1)、子类只能使用相等或者更宽松(表示使用的是父类类型)的前置条件来替换父类的前置条件。相等时表示覆盖,不同时表示的是重载(java中)。
为什么是放大?因为父类方法的参数类型相对较小,所以当传入父类方法的参数类型,重载的时候优先匹配父类的方法,而子类的重载方法不会匹
配,因此仍保证执行父类的方法(子类继承的时候其实操作的是子类中的父类成分),所以业务逻辑不会改变(C++中,父子类的同名函数发生隐藏而不
是重载,因为父类的函数被隐藏,当用子类替换父类时,永远不会调用父类的函数,LSP将无法遵守)。若是覆盖时,子类的方法会被执行。
(2)、只能使用相等或更强的后置条件来替换父类的后置条件。即返回值应该是父类方法返回值的子类或更小。
如果是重载,由于前置条件的要求,会调用到父类的函数,因此子函数不会被调用。
如果是覆盖,则调用子类的函数,这时子类的返回值比父类要求的小。因为父类调用函数的时候,返回值的类型是父类的类型,而子类的返回值更小,
赋值合法。
Father F = ClassF.Func();//;用子类替换时Father F = ClassC.Func()是合法的 子类赋值父类转是合法的,父类赋值给子类是不合法的
利用设计模式之禅上面的例子更能详细的说明这点:
实验:(实验也是网上的,但是能用设计模式之禅上面的例子更好)
#include <iostream> using namespace std; //定义两个空类型用于实验 class Shape { }; class Rectangle : public Shape { }; //C++中的抽象类就相当于java中的接口实现 //C++中普通的父类(带有虚函数的,抽象方法)相当于java中的抽象类 class Father { public: virtual void drawShape(Shape s) // { printf("Father:drawShape(Shape s)\n"); } virtual void showShape(Rectangle r) // { printf("Father:ShowShape(Rectangle r)\n"); } Shape CreateShape() { Shape s; printf("Father: Shape CreateShape()"); return s; } }; class Son : public Father { public: //对于C++而言,重载只能发生在同一作用域。显示Son和Father是不同作用域 //下面发生的是管下列函数中的形参是否比父类更严格,只要同名,父类virtual一律被隐藏。 //子类的形参类型比父类更严格, void drawShape(Rectangle r) { printf("Son:drawShape(Rectangle r)\n"); } //子类的形参类型比父类严宽松:表示的是父类 void showShape(Shape s) { printf("Son:showShape(Shape s)\n"); } //返回值类型比父类严格 Rectangle CreateShape() { Rectangle r; printf("Son: Rectangle CreateShape()"); return r; } }; int main() { //当遵循LSP原则时,使用父类地方都可以用子类替换 //Father* f = new Father(); //该行可用子类替换 Son* f = new Son(); //用子类替换父类出现的地方 Rectangle r; //子类形参类型更严格时,下一行输出结果会发生变化,不符合LSP原则 f->drawShape(r); //Father类型的f时,调用父类的drawShape(Shape s) //Son类型的f时,发生隐藏,会匹配子类的drawShape //子类形参类型更宽松时,对于C++而言,会因发生隐藏而不符合LSP原则。但Java发生重载,会符合LSP f->showShape(r); //Father类型的f时,直接匹配父类的showShape(Rectangle r) //Son类型的f时,因发生隐藏,会匹配子类的showShape(Shape s) //子类的返回值类型更严格 Shape s = f->CreateShape(); //替换为子类时,返回值为Rectangle,比Shape类型小,这种赋值是合法的 delete f; cin.get(); return 0; }