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

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

一、定义

(1)、所有使用基类的地方必须能够使用子类进行替换,而程序的行为不会发生任何变化(替换为子类之后不会产生错误或者异常)。

只有这样,父类才能真正被复用,子类能够在父类的基础上增减新的属性和行为。才能真正的实现多态行为。

(2)、当子类继承父类的时候,子类就拥有了父类属性和行为。(注意:只是类型而已) 但是如果子类覆盖父类的某些方法,那么

来使用父类的地方就可能出现错误。(如何理解呢?表面上看是调用的是父类方法,实际运行的时候子类方法覆盖了父类的方

法,注父类方法其实是存在的,通过作用域限定符可以访问到,两个方法的实现可能不一样,这样不符合LSP里氏替换原则。)

(3)、里氏替换原则是实现开闭原则的重要方式之一。由于使用基类对象的地方可以使用子类对象,因此程序中尽量使用基类类型进

定义,而在运行的时候确定子类类型,子类对象替换父类对象。 (有点面向接口编程的味道,对外提供接口,而不是实现类)。

或者可以实现公共父类(父类中公共属性和行为)。

编程实验:长方形和正方形的驳论

1、正方形是一种特殊的长方形(is-a关系):类图:

正方形类继承于长方形类。

  1. int main()
  2. {
  3. //LSP原则:父类出现的地方必须能用子类替换
  4. Rectangle* r = new Rectangle();//Square *r = new Square();
  5. r->setWidth(5);
  6. r->setHeight(4);
  7. printf("Area = %d\n",r->getArea()); //当用子类时,结果是16。用户就不
  8. //明白为什么长5,宽4的结果不是20,而是16.
  9. //所以正方形不能代替长方形。即正方形不能
  10. //继承自长方形的子类
  11. return 0;
  12. }
2、改进的继承关系---符合LSP原则(面向接口编程)

类图:


  1. int main()
  2. {
  3. //LSP原则:父类出现的地方必须能用子类替换
  4. QuadRangle* q = new Rectangle(5,4); //Rectangle* q = new Rectangle(5,4);或Square *q = new Square(5);
  5. printf("Area = %d,Perimeter = %d\n",q->getArea(),q->getPerimeter());
  6. return 0;
  7. }

3、鸵鸟不是鸟类
  1. //面向对象设计原则:LSP里氏替换原则
  2. //鸵鸟不是鸟的测试程序
  3.  
  4. #include <stdio.h>
  5.  
  6. //鸟类
  7. class Bird
  8. {
  9. private:
  10. double velocity; //速度
  11. public:
  12. virtual void fly() {printf("I can fly!\n");}
  13. virtual void setVelocity(double v){velocity = v;}
  14. virtual double getVelocity(){return velocity;}
  15. };
  16.  
  17. //鸵鸟类Ostrich
  18. class Ostrich : public Bird
  19. {
  20. public:
  21. void fly(){printf("I can\'t fly!");}
  22. void setVelocity(double v){Bird::setVelocity(0);}
  23. double getVelocity(){return Bird::getVelocity();}
  24. };
  25.  
  26. //测试函数
  27. void calcFlyTime(Bird& bird) //参数是引用 父类引用子类的时候,会有多态的行为
  28. {
  29. try
  30. {
  31. double riverWidth = 3000;
  32. if(bird.getVelocity()==0) throw 0;
  33. printf("Velocity = %f\n",bird.getVelocity());
  34. printf("Fly time = %f\n",riverWidth /bird.getVelocity());
  35. }
  36. catch(int) //异常处理
  37. {
  38. printf("An error occured!") ;
  39. }
  40. }
  41.  
  42. int main()
  43. {
  44. //遵守LSP原则时,父类对象出现的地方,可用子类替换
  45. Bird b; //用子类Ostrich替换Bird
  46. b.setVelocity(100); //替换之后,会直接调用子类的方法
  47. calcFlyTime(b); //父类测试时是正常的,子类时会抛出异常,违反LSP
  48. return 0;
  49. }
这种小程序都比较简单,我也是看的别人的,主要是用来测试历史替换原则。

二、历史替换原则的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()是合法的 子类赋值父类转是合法的,父类赋值给子类是不合法

利用设计模式之禅上面的例子更能详细的说明这点:

实验:(实验也是网上的,但是能用设计模式之禅上面的例子更好)

  1. #include <iostream>
  2.  
  3. using namespace std;
  4.  
  5. //定义两个空类型用于实验
  6. class Shape
  7. {
  8. };
  9.  
  10. class Rectangle : public Shape
  11. {
  12.  
  13. };
  14. //C++中的抽象类就相当于java中的接口实现
  15. //C++中普通的父类(带有虚函数的,抽象方法)相当于java中的抽象类
  16. class Father
  17. {
  18. public:
  19. virtual void drawShape(Shape s) //
  20. {
  21. printf("Father:drawShape(Shape s)\n");
  22. }
  23.  
  24. virtual void showShape(Rectangle r) //
  25. {
  26. printf("Father:ShowShape(Rectangle r)\n");
  27. }
  28.  
  29. Shape CreateShape()
  30. {
  31. Shape s;
  32. printf("Father: Shape CreateShape()");
  33. return s;
  34. }
  35. };
  36.  
  37. class Son : public Father
  38. {
  39. public:
  40.  
  41. //对于C++而言,重载只能发生在同一作用域。显示Son和Father是不同作用域
  42. //下面发生的是管下列函数中的形参是否比父类更严格,只要同名,父类virtual一律被隐藏。
  43.  
  44. //子类的形参类型比父类更严格,
  45. void drawShape(Rectangle r)
  46. {
  47. printf("Son:drawShape(Rectangle r)\n");
  48. }
  49.  
  50. //子类的形参类型比父类严宽松:表示的是父类
  51. void showShape(Shape s)
  52. {
  53. printf("Son:showShape(Shape s)\n");
  54. }
  55.  
  56. //返回值类型比父类严格
  57. Rectangle CreateShape()
  58. {
  59. Rectangle r;
  60. printf("Son: Rectangle CreateShape()");
  61.  
  62. return r;
  63. }
  64. };
  65.  
  66. int main()
  67. {
  68. //当遵循LSP原则时,使用父类地方都可以用子类替换
  69.  
  70. //Father* f = new Father(); //该行可用子类替换
  71. Son* f = new Son(); //用子类替换父类出现的地方
  72.  
  73. Rectangle r;
  74.  
  75. //子类形参类型更严格时,下一行输出结果会发生变化,不符合LSP原则
  76. f->drawShape(r); //Father类型的f时,调用父类的drawShape(Shape s)
  77. //Son类型的f时,发生隐藏,会匹配子类的drawShape
  78.  
  79. //子类形参类型更宽松时,对于C++而言,会因发生隐藏而不符合LSP原则。但Java发生重载,会符合LSP
  80. f->showShape(r); //Father类型的f时,直接匹配父类的showShape(Rectangle r)
  81. //Son类型的f时,因发生隐藏,会匹配子类的showShape(Shape s)
  82.  
  83. //子类的返回值类型更严格
  84. Shape s = f->CreateShape(); //替换为子类时,返回值为Rectangle,比Shape类型小,这种赋值是合法的
  85.  
  86. delete f;
  87. cin.get();
  88. return 0;
  89. }

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