第3章 里氏替换原则(LSP)

来源:互联网 发布:c语言进阶书籍 编辑:程序博客网 时间:2024/04/20 22:48

一、定义

(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;}};//鸵鸟类Ostrichclass 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层含义)

1、子类必须实现父类中声明的所有方法

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发生重载,会符合LSPf->showShape(r); //Father类型的f时,直接匹配父类的showShape(Rectangle r)//Son类型的f时,因发生隐藏,会匹配子类的showShape(Shape s)//子类的返回值类型更严格Shape s = f->CreateShape(); //替换为子类时,返回值为Rectangle,比Shape类型小,这种赋值是合法的delete f;cin.get();return 0;}



0 0
原创粉丝点击