Liskov替换原则(Liskov Substitution principle)
1 定义
子类型(subtype)必须能够替换掉他们的基类型(base type)。Barbara Liskov首次写下这个原则是在1988年。她说道:
这里需要如下替换性质:若对每个类型S的对象o1,都存在一个类型T的对象o2,使得在所有针对T编写的程序P中,用o1替换o2后,程序P行为功能不变,则S是T的子类型。
返回目录
2 问题&解决方案
对LSP的违反常常会导致对OCP的违反。遇到这种情况通常的解决方案是在运行时对类型进行鉴别,然后强制转换为子类型从而实现对方法的调用。
2.1 一个违反LSP的简单例子
代码清单
- Point.java
- Shape.java
- Circle.java
- Square.java
- LspDemo1.java
1 Point.java
请参考下文2.1章节代码清单第二个类的编写:
https://blog.csdn.net/weixin_37624828/article/details/111028394
2 Shape.java
public class Shape {enum ShapeType {/*** square*/square,/*** circle*/circle}Shape.ShapeType itsType = null;Shape(Shape.ShapeType t) {this.itsType = t;}
}
3 Circle.java
public class Circle extends Shape {double itsRadius;Point itsCenter;Circle(double itsRadius, Point itsCenter) {super(ShapeType.circle);this.itsRadius = itsRadius;this.itsCenter = itsCenter;}public void draw() {System.out.println("绘制圆形");System.out.println(toString());}@Overridepublic String toString() {return "Circle{" +"itsRadius=" + itsRadius +", itsCenter=" + itsCenter +", itsType=" + itsType +'}';}
}
4 Square.java
public class Square extends Shape {double itsSide;Point itsToLeft;public Square(double itsSide, Point itsToLeft) {super(ShapeType.square);this.itsSide = itsSide;this.itsToLeft = itsToLeft;}public void draw(){System.out.println("绘制方形");System.out.println(toString());}@Overridepublic String toString() {return "Square{" +"itsSide=" + itsSide +", itsToLeft=" + itsToLeft +", itsType=" + itsType +'}';}
}
5 LspDemo1.java
public class LspDemo1 {public static void main(String[] args) {Shape shape1 = new Circle(1, new Point(1, 2));Shape shape2 = new Square(1, new Point(2, 2));drawShape(shape1);drawShape(shape2);}/*** @param shape 图形对象*/private static void drawShape(Shape shape) {if (shape.itsType == Shape.ShapeType.circle) {((Circle) shape).draw();} else if (shape.itsType == Shape.ShapeType.square) {((Square) shape).draw();}}
}
促使开发人员编写上述代码的动机可能是因为让程序多态的开销很大,大到让人无法接受,但是这种认知是不正确的。Square和Circle在派生时没有实现对Shape类中的draw方法进行重写。问题在于Shape类中没有draw方法的定义,所以子类型无法对父类进行替换。
解决方案:在Shape类中定义draw方法,子类重写该方法即可遵循LSP原则。
2.2 一个更微妙的违反LSP的例子
这里有一个矩形类,现需要定义一个正方形类,因为正方形是矩形,所以自然会想到使用矩形类来派生正方形类,又根据常识,正方形的类宽高是相等的,所以有可能对设置宽和高的方法加入一些常识逻辑,当针对矩形类编写的方法validArea时,因为重写了矩形的设置宽高的方法,会引发一个更微妙的错误,具体例子参考如下代码。
代码清单:
- Rectangle.java
- Square.java
- LspDemo2.java
1 Rectangle.java
public class Rectangle {private Point itsTopLeft;private double itsWidth;private double itsHeight;Rectangle(Point itsTopLeft){this.itsTopLeft = itsTopLeft;}public Point getItsTopLeft() {return itsTopLeft;}public void setItsTopLeft(Point itsTopLeft) {this.itsTopLeft = itsTopLeft;}public double getItsWidth() {return itsWidth;}public void setItsWidth(double itsWidth) {this.itsWidth = itsWidth;}public double getItsHeight() {return itsHeight;}public void setItsHeight(double itsHeight) {this.itsHeight = itsHeight;}public double getArea(){return itsWidth * itsHeight;}
}
2 Square.java
public class Square extends Rectangle {Square(Point itsTopLeft) {super(itsTopLeft);}@Overridepublic void setItsWidth(double itsWidth) {super.setItsWidth(itsWidth);super.setItsHeight(itsWidth);}@Overridepublic void setItsHeight(double itsHeight) {super.setItsHeight(itsHeight);super.setItsWidth(itsHeight);}
}
3 LspDemo2.java
public class LspDemo2 {public static void main(String[] args) {Square square = new Square(new Point(1,1));validArea(square);}/*** 校验矩形的面积是否为20* 输出结果为否** @param rectangle 图形集合*/private static void validArea(Rectangle rectangle) {rectangle.setItsHeight(5);rectangle.setItsWidth(4);System.out.println(rectangle.getArea() == 20 ? "是": "否");}
}
问题就在于重写了设置宽高的方法,引入了先验逻辑,违反了Rectangle的不变性。具体如图所示:
2.3 解决方案
LSP让我们得出了一个非常重要的结论:一个模型,如果孤立地看,并不具有真正意义上的有效性。模型的有效性只能通过它的客户程序来表现。在考虑一个特定设计是否恰当时,不能完全孤立地看这个解决方案。必须要根据该设计地使用者所做的合理假设来审视它。解决这个问题通常最好的方法是只预测那些最明显的对于LSP的违反情况而推迟所有其他的预测,直到出现相关的脆弱性的臭味时,才会处理它们。
那究竟为什么会出现这个问题呢?是因为派生类和父类之间存在IS-A的关系,即子类是父类。有的方法的设计考虑对象是父类对象,子类不是真正的父类,导致了程序发生了上述微妙的错误。如何解决上述问题呢?有以下几种完美或者不完美的解决方法可供参考。
2.3.1 契约设计
使用DBC,类的编写者显式地规定针对该类地契约。客户代码的编写者可以通过该契约获悉可以依赖的行为方式。契约是通过为每个方法声明的前置条件(preconditions)和后置条件(postconditions)来指定的。要使一个方法得以执行,前置条件必须要为真。执行完毕后,该方法要保证后置条件为真。
某些语言,比如Eiffel,对前置条件和后置条件有直接的支持。你只需声明它们,运行时系统会去检验它们。C++和Java都没有此项特性。在这些语言中,我们必须自己考虑每个方法的前置条件和后置条件,并确保没有违反Meyer规则。此外,为每个方法都注明它们的前置条件和后置条件是非常有帮助的。
2.3.2 提取公共方法代替继承关系
考虑一个问题,尽管上述矩形和正方形很像,但是它们在getArea方法上真的是“父子关系”吗?如果将两个类中地公有方法提取出来,让这两个类均派生于该类,使之成为“兄弟关系”就可以完美的解决这个问题。
代码清单:
- Pet.java
- Cat.java
- Dog.java
- LspDemo3.java
1 Pet.java
public class Pet {protected String type;public Pet() {this.type = "宠物";}void run(){System.out.println("我是"+ type + ", 跑呀、跑呀、跑呀");}void eat(){System.out.println("不知道吃什么");}void doSomething(){System.out.println("做很多事情");}public String getType() {return type;}public void setType(String type) {this.type = type;}
}
2 Cat.java
public class Cat extends Pet {public Cat() {this.type = "猫";}@Overridevoid eat() {System.out.println("我是" + type + ", 吃猫粮");}@Overridevoid doSomething() {System.out.println("失效");;}
}
3 Dog.java
public class Dog extends Pet {public Dog() {this.type = "狗";}@Overridepublic void eat() {System.out.println("我是" + type + ", 吃狗粮");}
}
4 LspDemo3.java
public class LspDemo3 {public static void main(String[] args) {List<Pet> pets = new ArrayList<>();pets.add(new Cat());pets.add(new Dog());pets.add(new Pet());petDaily(pets);}private static void petDaily(List<Pet> pets){for(Pet c : pets){c.run();c.eat();}}
}
注意: 派生类中的退化函数有可能导致基于基类编写的函数失效
在子类里添加退化函数
public class LspDemo4 {public static void main(String[] args) {List<Pet> petList = new ArrayList<>();petList.add(new Cat());petList.add(new Pet());// 退化函数petDoSomething(petList);}private static void petDoSomething(List<Pet> pets){for(Pet c : pets){c.doSomething();}}
}
3 结论
OCP是OOD中很多说法的核心。如果这个原则应用有效,应用程序就会有更多的可维护性、可重用性以及健壮性。LSP是使OCP成为可能的主要原则之一。正是子类型的可替换性才使得使用基类类型的模块在无需修改的情况下就可以扩展。这种可替换性必须是开发人员可以隐式依赖的东西。因此,如果没有显式地强制基类类型的契约,那么代码就必须良好地并且明显地表达出这一点。
在设计种需要考虑基类和派生类是否真的是“父子关系”,可能因为它们很像,但有可能它们之间是“兄弟关系”。
返回目录
Liskov替换原则(Liskov Substitution principle)相关推荐
- 编码最佳实践——Liskov替换原则
Liskov替换原则(Liskov Substitution Principle)是一组用于创建继承层次结构的指导原则.按照Liskov替换原则创建的继承层次结构中,客户端代码能够放心的使用它的任意类 ...
- Liskov替换原则(LSP)
Liskov替换原则(The Liskov Substitution Principle) 子类型(subtype)必须能够替换掉它们的基类型(base type). 一个违反LSP的简单例子 pub ...
- 五大软件设计原则学习笔记3——Liskov 替换原则
五大软件设计原则SOLID: 单一职责原则(Single responsibility principle,SRP) 开放封闭原则(Open–closed principle,OCP) Liskov ...
- liskov替换原则_构造函数与打破Liskov替代原则
liskov替换原则 At the risk of being targeted by the PHP hate-mongers, I must confess that I'm pretty com ...
- Liskov替换原则
Liskov替换原则 文章目录 Liskov替换原则 案例引入 如何理解 子类型可以替换 基类型 ? 违反LSP的危害 总结 参考文档 今天我继续来说 软件设计的另一个原则, LSP原则 里氏代换原则 ...
- liskov替换原则_坚实原则:Liskov替代原则
liskov替换原则 以前,我们深入研究了坚实的原则,包括单一责任和开放/封闭原则. Liskov替代原则(LSP)是子类型关系的一种特殊定义,称为(强)行为子类型, 假设对象S是对象T的子类型,则可 ...
- 软件设计原则(三)里氏替换原则 -Liskov Substitution Principle
里氏代换原则(Liskov Substitution Principle LSP)面向对象设计的基本原则之一. 里氏代换原则中说,任何基类可以出现的地方,子类一定可以出现. LSP是继承复用的基石,只 ...
- Java设计模式之里氏替换原则(Liskov Substitution principle)
2019独角兽企业重金招聘Python工程师标准>>> Java是面向对象的语言,那么什么是面向对象程序设计呢? 面向对象程序设计(英语:Object-oriented progra ...
- ABAP设计模式之---“里氏替换原则(Liskov Substitution Principle)”
1. 定义 子类型必须能够替换掉他们的父类型. 2. 解读 子类可以扩展父类的功能,但不能改变父类原有的功能.也即: a) 子类可以实现父类的抽象方法,但不能覆盖/重写父类的的非抽象方法 b) 子类可 ...
最新文章
- 做正确的事情和把事情做正确
- windows下安装cygwin及配置
- 线结构光标定详细步骤与实现HALCON
- java 线程池 状态_【Java多线程】线程状态、线程池状态
- 2014中国企业面对的五大挑战
- MySQL Date 函数
- Problem C: 结构体---点坐标结构体
- python copy函数用法_python shutil模块函数copyfile和copy的区别
- 稀疏内存模型sparsemem memory model | 文章
- 贵大计算机学院陈力,CCF贵州大学学生分会完成换届
- 活动合作 | 2018GAITC大会:AI领袖、应用指南、窥见风口,还能从中获得更多
- 谷歌推出全能扒谱AI:只要听一遍歌曲,钢琴小提琴的乐谱全有了
- eclipse汉化包
- 世界三大统计分析软件sas splus spss
- linux ls按着文件数字大小排列
- MIT6.828学习之homework2:shell
- STM32实现德飞莱LED滚动效果
- 给idea设置默认使用的maven配置
- OpenOffice源文件中的文件扩展
- 《我要好工作》读书笔记