2. 里氏替换原则(Liskov Substitution Principle,LSP)

2.1 定义

(1)所有使用基类的地方必须能透明地使用子类替换,而程序的行为没有任何变化(不会产生运行结果错误或异常)。只有这样,父类才能被真正复用,而且子类也能够在父类的基础上增加新的行为。也只有这样才能正确的实现多态

(2)当一个类继承了另一个类时,子类就拥有了父类中可以继承下来的属性和操作。但如果子类覆盖了父类的某些方法,那么原来使用父类的地方就可能会出现错误,因为表面上看,它调用了父类的方法,但实际运行时却调用了被子类覆盖的方法,而这两个方法的实现可能不一样,这就不符合LSP原则。(见后面的解决方案)

(3)里氏代换原则是实现开闭原则的重要方式之一,由于使用基类对象的地方都可以使用子类对象,因此在程序中尽量使用基类类型来对对象进行定义,而在运行时再确定其子类类型,用子类对象来替换父类对象。

【编程实验】正方形与长形的驳论

//1、正方形是一种特殊的长方形(is - a关系)?

#include <stdio.h>

//长方形类
class Rectangle
{
protected:
long width;
long height;
public:
void setWidth(long width){this->width = width;}
long getWidth(){return this->width;} void setHeight(long height){this->height = height;}
long getHeight(){return this->height;} long getArea(){return width * height;}
}; //正方形类(如果继承自长方形类)
class Square : public Rectangle
{
public:
void setWidth(long width)
{
this->width = width;
this->height = width;
} long getWidth(){return this->width;} void setHeight(long height)
{
this->width = height;
this->height = height;
} long getHeight(){return this->height;}
}; int main()
{
//LSP原则:父类出现的地方必须能用子类替换
Rectangle* r = new Rectangle();//Square *r = new Square(); r->setWidth();
r->setHeight(); printf("Area = %d\n",r->getArea()); //当用子类时,结果是16。用户就不
//明白为什么长5,宽4的结果不是20,而是16.
//所以正方形不能代替长方形。即正方形不能
//继承自长方形的子类
return ;
}

//2. 改进的继承关系——符合LSP原则

#include <stdio.h>

//抽象的四方形类
class QuadRangle
{
public:
//将四方形抽象出公共部分出来
virtual long getArea() = ; //面积
virtual long getPerimeter() = ;//周长
}; //长方形类(继承自抽象的四方形类)
class Rectangle : public QuadRangle
{
private:
long width;
long height;
public:
Rectangle(long width, long heigth)
{
this->width = width;
this->height = heigth;
} void setWidth(long width){this->width = width;}
long getWidth(){return this->width;} void setHeight(long height){this->height = height;}
long getHeight(){return this->height;} long getArea(){return width * height;}
long getPerimeter(){return (width + height) * ;}
}; //正方形类(继承自抽象的四方形类)
class Square : public QuadRangle
{
long side;
public:
Square(long side) {this->side = side;} void setSide(long side);
long getSide(){return this->side;}
long getPerimeter(){return * side;}
long getArea(){return side * side;}
}; int main()
{
//LSP原则:父类出现的地方必须能用子类替换
QuadRangle* q = new Rectangle(, ); //Rectangle* q = new Rectangle(5, 4);或Square *q = new Square(5); printf("Area = %d, Perimeter = %d\n",q->getArea(), q->getPerimeter()); return ;
}

【编程实验】鸵鸟不是鸟

//面向对象设计原则: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();}
double getVelocity(){return Bird::getVelocity();}
}; //测试函数
void calcFlyTime(Bird& bird)
{
try
{
double riverWidth = ; if(bird.getVelocity()==) throw ; 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(); calcFlyTime(b); //父类测试时是正常的,子类时会抛出异常,违反LSP return ;
}

2.2 LSP原则的4层含义

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

  ①步枪、手枪和机关枪都继承于AbstractGun,因此都实现了shoot(射击)的功能。

  ②玩具枪不能直接继承于AbstractGun。因为玩具枪不能去实现父类的shoot功能(即子类不能完全实现父类的方法,违反LSP原则),否则这样的武器拿给士兵去杀敌会闹笑话。因此,ToyGun不能继承于AbstractGun,而是继承于AbstracToy,然后去仿真枪的行为。这样对于士兵类来讲,因要求传入的是AbstactGun类的对象,所以不能使用玩具手枪杀人。

(2)子类可以扩展功能,但不能改变父类原有的功能

  ①子类可以有自己的属性和操作。因此,里氏替换原则只能正着用,不能返过来用。即子类出现的地方,父类未必就可以替换。如Snipper类的killEnemy方法中不能传入Rifle类的对象,因为Rifle类中没有zoomOut的方法。

  ②父类向下转换是不安全的,可能会调用到只有在子类中出现的方法而造出异常。

(3)子类可以实现父类的抽象方法,但一般不要覆盖父类的非抽象方法。

4)如果覆盖或实现父类方法时,方法的前置条件(即方法的形参)要比父类方法的输入参数更宽松。方法的后置条件(即方法的返回值)要比父类更严格

  ①子类只能使用相等或更宽松的前置条件来替换父类的前置条件。当相等时表示覆盖,不同时表示重载。

  为什么只能放大?因为父类方法的参数类型相对较小,所以当传入父类方法的参数类型(或更窄类型)时,重载时将优先匹配父类的方法,而子类的重载方法不会匹配,因此保证了仍执行父类的方法,所以业务逻辑不变(对于C++而言,父子类之间的同名函数发生隐藏而不是重载,因父类的函数被隐藏,当用子类替换父类时,永远调用不到父类的函数,LSP将无法被遵守)。若是覆盖时,必须清楚其逻辑要义,因为覆盖时子类的方法会被执行)

  ②只能使用相等或更强的后置条件来替换父类的后置条件。即返回值应该是父类返回值的子类或更小

  如果是重载,由于前置条件的要求,会调用到父类的函数,因此子类函数不会被调用

  如果是覆盖,则调用子类的函数,这时子类的返回值(S类型)比父类要求的小(T类型),这是被允许的,因为父类调用函数的时候,返回值至少是T类型,而子类的返回值S(类型小),给T类型的变量赋值是合法的。

  Father F = ClassF.Func();//;用子类替换时Father F = ClassC.Func()是合法的

【编程实验】前置条件和后置条件

#include <stdio.h>

class Shape
{
}; class Rectangle : public Shape
{ }; 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一律被隐藏。 //子类的形参类型比父类更严格
virtual void drawShape(Rectangle r)
{
printf("Son:drawShape(Rectangle r)\n");
} //子类的形参类型比父类严宽松
virtual 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; return ;
}

第2章 面向对象的设计原则(SOLID):2_里氏替换原则(LSP)的更多相关文章

  1. 【面向对象设计原则】之里氏替换原则(LSP)

    里氏代换原则由2008年图灵奖得主.美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing 教授于1994年提出,所以使用的是这位女博士的性命名的一个 ...

  2. 面向对象设计原则三:里氏替换原则(LSP)

    里氏替换原则(LSP)定义:在任何父类出现的地方都可以用它的子类类替换,且不影响功能.解释说明:其实LSP是对开闭原则的一个扩展,在OO思想中,我们知道对象是由一系列的状态和行为组成的,里氏替换原则说 ...

  3. 面向对象五大原则_1.单一职责原则&amp;2.里氏替换原则

    单一职责原则:Single Responsibility Principle (SRP) 一个类.仅仅有一个引起它变化的原因.应该仅仅有一个职责.每个职责都是变化的一个轴线.假设一个类有一个以上的职责 ...

  4. 【设计模式六大原则2】里氏替换原则(Liskov Substitution Principle)

      肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对 ...

  5. 设计模式六大原则(二):里氏替换原则(Liskov Substitution Principle)

    里氏替换原则(LSP)由来: 最早是在 妖久八八 年, 由麻神理工学院得一个女士所提出来的. 定义: 1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 ...

  6. 面象对象设计原则之三:里氏替换原则(The Liskov Substitution Principle,LSP)

    里氏代换原则由2008年图灵奖得主.美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出.其严格表述如下:如果对每一个类型为S的 ...

  7. C# 实例解释面向对象编程中的里氏替换原则

    在面向对象编程中,SOLID 是五个设计原则的首字母缩写,旨在使软件设计更易于理解.灵活和可维护.这些原则是由美国软件工程师和讲师罗伯特·C·马丁(Robert Cecil Martin)提出的许多原 ...

  8. Java设计原则—里氏替换原则(转)

    里氏替换原则(Liskov Substitution Principel)是解决继承带来的问题. 继承的优点: 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性: 提高代码的重用性: 子类 ...

  9. [设计模式]<<设计模式之禅>>关于里氏替换原则

    在面向对象的语言中,继承是必不可少的.非常优秀的语言机制,它有如下优点:● 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性:● 提高代码的重用性:● 子类可以形似父类,但又异于父类,“龙 ...

随机推荐

  1. VS 2013 Preview 自定义 SharePoint 2013 列表 之 两个Bug

    SharePoint 2013 已RTM了,对于程序员来说又要了解新功能了,同时 VS 2013 也将要 RTM了,两者同时应用定会有不新功能,我们先从 自定义 列表开始. SharePoint 20 ...

  2. H5实现的可自定义贪吃蛇游戏

    原创游戏,使用lufylegend.js开发 用canvas实现的贪吃蛇游戏,与一般的贪吃蛇游戏不同,图片经过美工设计,代码设计支持扩展和自定义. 游戏元素丰富,包括障碍物(仙人掌),金币(奖励),苹 ...

  3. [ javascript css clip ] javascript css clip 的奇思妙想之文字拼接效果

      语法: clip : auto | rect ( number number number number ) 参数: auto : 对象无剪切 rect ( number number numbe ...

  4. CSS 类选择器(四)

    一.类选择器 类选择用使用"."(英文点号)进行标识,后面紧跟类名 如: .red{color:red;} 类样式可以应用于文档中的多个元素,这体现了CSS代码的可重用性,帮助用户 ...

  5. Block的使用及循环引用的解决

    Block是一个很好用的东西,这篇文章主要来介绍:1.什么是Block?2.Block的使用?3.Block的循环引用问题及解决. 1.什么是Block? 说这个问题之前,我先来说一下闭包(Closu ...

  6. iOS支付宝集成详细流程

    实现支付宝支付的准备工作: 1.向支付宝签约,成为支付宝的商户 签约完成后,支付宝会提供一些必要的数据给我们 商户ID:partner 账号ID:seller 即支付宝账号 签约需要营业执照 2.获取 ...

  7. Cocos2d-X-3.0之后的版本的环境搭建

    由于cocos2d游戏开发引擎更新十分频繁,官方文档同步不够及时和完善.所以不要照着官方文档来照做生成工程. <点击图片就能进入网站> 具体的步骤: 1.获取cocos2d-X的源码v3. ...

  8. 傅里叶:有关FFT,DFT与蝴蝶操作(转 重要!!!!重要!!!!真的很重要!!!!)

    转载地址:http://blog.renren.com/share/408963653/15068964503(作者 :  徐可扬) 有没有!!! 其实我感觉这个学期算法最难最搞不懂的绝对不是动态规划 ...

  9. UVa 104 - Arbitrage(Floyd动态规划)

    题目来源:https://uva.onlinejudge.org/index.php?option=com_onlinejudge&Itemid=8&category=3&pa ...

  10. iOS开发者必备的10款工具

    当前iOS和Android两大移动操作系统“二足鼎立”,几乎覆盖了市面上大部分的智能手机.相比Android,iOS开发适配更简单,且随着各种实用工具和Swift语言的出现,iOS开发门槛地降低,开发 ...