第2章 面向对象的设计原则(SOLID):2_里氏替换原则(LSP)
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)的更多相关文章
- 【面向对象设计原则】之里氏替换原则(LSP)
里氏代换原则由2008年图灵奖得主.美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing 教授于1994年提出,所以使用的是这位女博士的性命名的一个 ...
- 面向对象设计原则三:里氏替换原则(LSP)
里氏替换原则(LSP)定义:在任何父类出现的地方都可以用它的子类类替换,且不影响功能.解释说明:其实LSP是对开闭原则的一个扩展,在OO思想中,我们知道对象是由一系列的状态和行为组成的,里氏替换原则说 ...
- 面向对象五大原则_1.单一职责原则&2.里氏替换原则
单一职责原则:Single Responsibility Principle (SRP) 一个类.仅仅有一个引起它变化的原因.应该仅仅有一个职责.每个职责都是变化的一个轴线.假设一个类有一个以上的职责 ...
- 【设计模式六大原则2】里氏替换原则(Liskov Substitution Principle)
肯定有不少人跟我刚看到这项原则的时候一样,对这个原则的名字充满疑惑.其实原因就是这项原则最早是在1988年,由麻省理工学院的一位姓里的女士(Barbara Liskov)提出来的. 定义1:如果对 ...
- 设计模式六大原则(二):里氏替换原则(Liskov Substitution Principle)
里氏替换原则(LSP)由来: 最早是在 妖久八八 年, 由麻神理工学院得一个女士所提出来的. 定义: 1:如果对每一个类型为 T1的对象 o1,都有类型为 T2 的对象o2,使得以 T1定义的所有程序 ...
- 面象对象设计原则之三:里氏替换原则(The Liskov Substitution Principle,LSP)
里氏代换原则由2008年图灵奖得主.美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出.其严格表述如下:如果对每一个类型为S的 ...
- C# 实例解释面向对象编程中的里氏替换原则
在面向对象编程中,SOLID 是五个设计原则的首字母缩写,旨在使软件设计更易于理解.灵活和可维护.这些原则是由美国软件工程师和讲师罗伯特·C·马丁(Robert Cecil Martin)提出的许多原 ...
- Java设计原则—里氏替换原则(转)
里氏替换原则(Liskov Substitution Principel)是解决继承带来的问题. 继承的优点: 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性: 提高代码的重用性: 子类 ...
- [设计模式]<<设计模式之禅>>关于里氏替换原则
在面向对象的语言中,继承是必不可少的.非常优秀的语言机制,它有如下优点:● 代码共享,减少创建类的工作量,每个子类都拥有父类的方法和属性:● 提高代码的重用性:● 子类可以形似父类,但又异于父类,“龙 ...
随机推荐
- Ahjesus获取自定义属性Attribute或属性的名称
1:设置自己的自定义属性 public class NameAttribute:Attribute { private string _description; public NameAttribut ...
- [Tool] Windows 8.1安装SQL Server
[Tool] Windows 8.1安装SQL Server 问题情景 因为工作的关系,需要在Windows 8.1.64Bit设备上安装SQL Server 2012.本来以为是个只要按下一步就可以 ...
- Python: 解决pip安装源被墙的问题
pip install <package> -i http://mirrors.aliyun.com/pypi/simple --trusted-host mirrors.aliyun.c ...
- iOS之Cookie
iOS之Cookie使用 简介 概念:Cookie中文名称叫做"小型文本文件",指某些网站为了辨别用户身份而存储在用户本地终端上的数据(通常经过加 密). Web服务器可以用过Se ...
- 转载:sql关联查询
inner join(等值连接)只返回两个表中联结字段相等的行 left join(左联接)返回包括左表中的所有记录和右表中联结字段相等的记录 right join(右联接)返回包括右表中的所有记录和 ...
- Windows 2003 Server C盘空间被IIS日志文件消耗殆尽案例
今天突然收到手头一台数据库服务器的磁盘空间告警邮件,C盘空间只剩下5.41GB大小(当系统磁盘剩余空间小于总大小的10%时,发出告警邮件),如下图所示: 由于还有一些微弱印象:前阵子这台服务器的C盘剩 ...
- java web中日期Date类型在页面中格式化显示的三种方式
一般我们经常需要在将服务器端的Date类型,传到页面进行显示,这就涉及到一个如何格式化显示Date类型的问题,一般我们有三种方式进行: 1)在服务端使用SimpleDateFormat等类格式化成字符 ...
- mongo学习笔记(五):分片
分片 人脸: 代表客户端,客户端肯定说,你数据库分片不分片跟我没关系,我叫你干啥就干啥,没什么好商量的. mongos: 首先我们要了解”片键“的概念,也就是说拆分集合的依据是什么?按照 ...
- Java锁(一)之内存模型
想要了解Java锁机制.引发的线程安全问题以及数据一致性问题,有必要了解内存模型,机理机制了解清楚了,这些问题也就应声而解了. 一.主内存和工作内存 Java内存模型分为主内存和工作内存,所有的变量都 ...
- HttpClent4.3 的例子
package com.unbank.robotspider.util; import java.io.IOException; import java.net.MalformedURLExcepti ...