面向对象设计原则实践:之四.里氏代换原则
五、里氏代换原则(LSP--Liskov Substitution Principle)
1. 定义
a). 如果对每一个类型为S的对象o1,都有类型为T的对象o2,
使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有变化,
那么类型S是类型T的子类型。
b). 子类型必须能够替换它的基类型。LSP又称里氏替换原则。
对于这个原则,通俗一些的理解就是,父类的方法都要在子类中实现或者重写。 。
2.分析
a). 在软件中如果能够使用基类对象,那么一定能够使用其子类对象。
把基类都替换成它的子类,程序将不会产生任何错误和异常,
反过来则不成立,如果一个软件实体使用的是一个子类的话,那么它不一定能够使用基类。
b). 里氏代换原则是实现开闭原则的重要方式之一,
由于使用基类对象的地方都可以使用子类对象,因此,
在程序定义中,尽量使用基类类型来对对象进行定义,
在程序运行时,再确定其子类类型,用子类对象来替换父类对象。
3. 实例一
某系统需要实现对重要数据(如用户密码)的加密处理,
在数据操作类(DataOperator)中需要调用加密类中定义的加密算法,
系统提供了两个不同的加密类,CipherA和CipherB,它们实现不同的加密方法,
在DataOperator中可以选择其中的一个实现加密操作。
如图所示:
里氏代换原则-图-1
里氏代换原则-图-2
图(一)和图(二)分析:
(图一)中,
如果需要更换一个加密算法类或者增加并使用一个新的加密算法类,
如将CipherA改为CipherAA, 增加一个CipherC,则需要修改客户类Client和数据操作类DataOperator的源代码,违背了开闭原则。
现使用里氏代换原则对其进行重构,使得系统可以灵活扩展,符合开闭原则。
(图二)中,
在程序定义时:
子类CipherB继承了基类CipherA,
并在DataOperator类中定义了基类对象cipherA
在程序运行时,
客户端类Client使用了配置文件config.xml,配置文件指明了运行时使用的子类加密算法CipherB
4. 实现二
对于依赖倒置原则,说的是父类不能依赖子类,它们都要依赖抽象类。
这种依赖是我们实现代码扩展和运行期内绑定(多态)的基础。
因为一旦类的使用者依赖某个具体的类,那么对该依赖的扩展就无从谈起;
而依赖某个抽象类,则只要实现了该抽象类的子类,都可以被类的使用者使用,从而实现了系统的扩展。
但是,光有依赖倒置原则,并不一定就使我们的代码真正具有良好的扩展性和运行期内绑定。
请看下面的代码:
public class Animal{
private string name;
public Animal(string name){
this.name = name;
}
public void Description(){
Console.WriteLine("This is a(an) " + name);
}
}
//下面是它的子类猫类:
public class Cat : Animal{
public Cat(string name){
}
public void Mew(){
Console.WriteLine("The cat is saying like 'mew'");
}
}
//下面是它的子类狗类:
public class Dog : Animal{
public Dog(string name){
}
public void Bark(){
Console.WriteLine("The dog is saying like 'bark'");
}
}
//最后,我们来看客户端的调用:
public void DecriptionTheAnimal(Animal animal){
if (typeof(animal) is Cat){
Cat cat = (Cat)animal;
Cat.Decription();
Cat.Mew();
}
else if (typeof(animal) is Dog){
Dog dog = (Dog)animal;
Dog.Decription();
Dog.Bark();
}
}
通过上面的代码,我们可以看到虽然客户端的依赖是对抽象的依赖,
但依然这个设计的扩展性不好,运行期绑定没有实现。
是什么原因呢?
其实就是因为不满足里氏替换原则:
子类如Cat有Mew()方法父类根本没有,Dog类有Bark()方法父类也没有,
两个子类都不能替换父类。
这样导致了系统的扩展性不好和没有实现运行期内绑定。
现在看来,一个系统或子系统要拥有良好的扩展性和实现运行期内绑定,有两个必要条件:
第一是依赖倒置原则;
第二是里氏替换原则。
这两个原则缺一不可。
我们知道,在我们的大多数的模式中,我们都有一个共同的接口,然后子类和扩展类都去实现该接口。
下面是一段原始代码:
if(action.Equals(“add”)){
//do add action
}
else if(action.Equals(“view”)){
//do view action
}
else if(action.Equals(“delete”)){
//do delete action
}
else if(action.Equals(“modify”)){
//do modify action
}
我们首先想到的是把这些动作分离出来,就可能写出如下的代码:
public class AddAction{
public void add(){
//do add action
}
}
public class ViewAction{
public void view(){
//do view action
}
}
public class deleteAction{
public void delete(){
//do delete action
}
}
public class ModifyAction{
public void modify(){
//do modify action
}
}
我们可以看到,这样代码将各个行为独立出来,满足了单一职责原则,
但这远远不够,因为它不满足依赖颠倒原则和里氏替换原则。
下面我们来看看命令模式对该问题的解决方法:
public interface Action{
public void doAction();
}
//然后是各个实现:
public class AddAction : Action{
public void doAction(){
//do add action
}
}
public class ViewAction : Action{
public void doAction(){
//do view action
}
}
public class deleteAction : Action{
public void doAction(){
//do delete action
}
}
public class ModifyAction : Action{
public void doAction(){
//do modify action
}
}
//这样,客户端的调用大概如下:
public void execute(Action action){
action.doAction();
}
看,上面的客户端代码再也没有出现过typeof这样的语句,扩展性良好,也有了运行期内绑定的优点。
5. 总结
子类型必须能够替换掉它们的父类。
LSP优点:
1)、保证系统或子系统有良好的扩展性。
只有子类能够完全替换父类,才能保证系统或子系统在运行期内识别子类就可以了,
因而使得系统或子系统有了良好的扩展性。
2)、实现运行期内绑定,即保证了面向对象多态性的顺利进行。
这节省了大量的代码重复或冗余。
避免了类似instanceof这样的语句,或者getClass()这样的语句,这些语句是面向对象所忌讳的。
3)、有利于实现契约式编程。
契约式编程有利于系统的分析和设计,指我们在分析和设计的时候,定义好系统的接口,然后再编码的时候实现这些接口即可。
在父类里定义好子类需要实现的功能,而子类只要实现这些功能即可。
使用LSP注意点:
1)、此原则和OCP的作用有点类似,其实这些面向对象的基本原则就2条:
A:面向接口编程,而不是面向实现;
B:用组合而不主张用继承
2)、LSP是保证OCP的重要原则
3)、这些基本的原则在实现方法上也有个共同层次,就是使用中间接口层,以此来达到类对象的低偶合,也就是抽象偶合!
4)、派生类的退化函数:
派生类的某些函数退化(变得没有用处),Base的使用者不知道不能调用f,会导致替换违规。
在派生类中存在退化函数并不总是表示违反了LSP,但是当存在这种情况时,应该引起注意。
5)、从派生类抛出异常:
如果在派生类的方法中添加了其基类不会抛出的异常。
如果基类的使用者不期望这些异常,那么把他们添加到派生类的方法中就可以能会导致不可替换性。
面向对象设计原则实践:之四.里氏代换原则相关推荐
- 从零开始学习Java设计模式 | 软件设计原则篇:里氏代换原则
在本讲中,我来为大家介绍一下软件设计原则里面的第二个原则,即里氏代换原则. 概述 首先,大家应该知道,里氏代换原则是面向对象设计的基本原则之一.那什么是里氏代换原则呢?里氏代换原则是指任何基类可以出现 ...
- 带你认识六种设计原则(开闭原则、里氏代换原则、依赖倒转原则....)
前言 1. 设计原则 1.1. 开-闭原则 1.2. 里氏代换原则 1.3. 依赖倒转原则 1.4. 接口隔离原则 1.5. 合成/聚合原则 1.6. 迪米特法则 前言 学习设计模式之前先要了解其中的 ...
- 里氏代换原则C#详解
看了一大堆的资料讲解关于里氏代换原则,在这里我想分享给大家. 下面这段话来自百度百科,是这么解释里氏代换原则的: 里氏代换原则(Liskov Substitution Principle LSP)是面 ...
- 里氏代换原则 举例 分析
里氏代换原则 定义:里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一. 目的:里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现.L ...
- 面向对象五大设计原则-里氏代换原则
1.里氏代换原则 里氏代换原则(Liskov Substitution Principle)是指,任何基类出现的地方,子类一定可以出现.反之则不一定成立. 通俗的讲便是:"老鼠儿子会打洞&q ...
- 面向对象设计原则之里氏代换原则
里氏代换原则由2008年图灵奖得主.美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出.其严格表述如下:如果对每一个类型为S的 ...
- 面向对象设计原则之三:里氏替换原则
里氏替换原则(Liskov Substitution Principle LSP) 里氏替换原则是面向对象设计的基本原则之一.任何基类可以出现的地方,子类一定可以出现.LSP是继承复用的基石,只有 ...
- 软件设计原则——里氏代换原则
里氏代换原则 里氏代换原则是面向对象设计的基本原则之一. 里氏代换原则:任何基类可以出现的地方,子类一定可以出现. 通俗理解:子类可以扩展父类的功能,但不能改变父类原有的功能. 换句话说,子类继承父类 ...
- 面向对象——依赖倒转原则和里氏代换原则
什么是依赖倒转原则 下面三个就是 高层模块不依赖低层模块 抽象不依赖细节 细节依赖抽象 为什么需要这个原则? 假设现在需要开发一个软件,其中某个功能需要和数据库进行操作 那么,马上能想到的方法是不是就 ...
最新文章
- 使用 ASP+ DataGrid 控件来创建主视图/详细资料视图
- linux下collada-dom编译,COLLADA DOM Tutorial
- jenkins 安装配置
- 计算机考博哪个学校好考,管理学博士哪个学校好考
- python亲密度_Python OpenCV 图像2D直方图,取经之旅第 25 天
- 不仅是人类的shooow
- 仿IOS圆角可自定义扩展对话框
- Python项目--飞机作战完整版(附带图片素材)
- ThreeJS 跨域
- php swfupload,ThinkPHP+swfupload多图上传实例 经典实用的php多图上传
- cityscapes数据集上的类别统计分布
- Android下WPS打开Excel2007版也有问题
- 计算机桌面图标有哪两类,计算机基础100题
- python的十句名言_程序员的二十句励志名言,看看你最喜欢哪句?
- R语言使用cph函数和rcs函数构建限制性立方样条cox回归模型、使用rms包的Predict函数计算指定连续变量在不同分组变量下和风险比HR值的关系、使用ggplot2可视化变量与风险值HR的关系
- 软件技术基础(一):绪论
- 3X技术点亮发布会 雄迈十年产业融合再出发
- keras基于CNN和序列标注的对联机器人
- 【LadyBug】thirdScriptError:api request success callback function,Cannot read property undefine参数
- java list高效去重_简述Java List去重五种方法