设计原则:里式替换原则(LSP)
系列文章
1 定义
里氏原则的英文是Open Closed Principle,缩写就是OCP。其定义有两种
定义1:
If S is a subtype of T, then objects of type T may be replaced with objects of type S, without breaking the program。
(如果S是T的子类型,则类型T的对象可以替换为类型S的对象,而不会破坏程序。)
定义2:
Functions that use pointers of references to base classes must be able to use objects of derived classes without knowing it。
(所有引用其父类对象方法的地方,都可以透明的使用其子类对象)
这两种定义方式其实都是一个意思,即:应用程序中任何父类对象出现的地方,我们都可以用其子类的对象来替换,并且可以保证原有程序的逻辑行为和正确性。
如何理解里氏替换与继承多态
很多人(包括我自己)乍一看,总觉得这个原则和继承多态的思想差不多。但其实里氏替换和继承多态有关系,但并不是一回事,我们可以通过一个例子来看一下
public class Cache {
public void set(String key,String value){
// 使用内存Cache
}
}
public class Redis extends Cache {
@Override
public void set(String key,String value){
// 使用Redis
}
}
public class Memcache extends Cache {
@Override
public void set(String key,String value){
// 使用Memcache
}
}
class CacheTest {
@Test
public void set() {
Cache cache = new Cache();
assertTrue(cache.set("testKey", "testValue"));
cache = new Redis();
assertTrue(cache.set("testKey", "testValue"));
cache = new Memcache();
assertTrue(cache.set("testKey", "testValue"));
}
}
我们定义了一个Cache类来实现程序中写缓存的逻辑,它有两个子类Redis和Memcache来实现不同的缓存工具,看到这个例子很多人可能会有疑问这不就是利用了继承和多态的思想吗?
不错,的确是这样的,而且在这个例子中两个子类的设计完全符合里式替换原则,可以替换父类出现的任何位置,并且原来代码的逻辑行为不变且正确性也没有被破坏。
但如果这时我们需要对Redis子类方法中增加对Key长度的验证。
public class Redis extends Cache {
public void set(String key,String value){
// 使用Redis
if(key==null||key.length<10){
throw new IllegalArgumentException("key长度不能小于10");
}
}
}
class CacheTest {
@Test
public void set() {
Cache cache = new Cache();
assertTrue(cache.set("testKey", "testValue"));
cache = new Redis();
assertTrue(cache.set("testKey", "testValue"));
}
}
此时如果我们在使用父类对象的时候替换成子类对象,那set方法就会有异常抛出。程序的逻辑行为就发生了变化,虽然改造之后的代码仍然可以通过子类来替换父类 ,但是,从设计思路上来讲,Redis子类的设计是不符合里式替换原则的。
继承和多态是面向对象语言所提供的一种语法,一种代码实现的思路,而里式替换则是一种思想,是一种设计原则,是用来指导继承关系中子类该如何设计的,子类的设计要保证在替换父类的时候,不改变原有程序的逻辑以及不破坏原有程序的正确性。
2 规则
其实历史替换原则的核心就是“约定”,父类与子类的约定。里氏替换原则要求子类在进行设计的时候要遵守父类的一些行为约定。这里的行为约定包括:函数所要实现的功能,对输入、输出、异常的约定,甚至包括注释中一些特殊说明等。
2.1 子类方法不能违背父类方法对输入输出异常的约定
前置条件不能被加强
前置条件即输入参数是不能被加强的,就像上面Cache的示例,Redis子类对输入参数Key的要求进行了加强,此时在调用处替换父类对象为子类对象就可能引发异常。
也就是说,子类对输入的数据的校验比父类更加严格,那子类的设计就违背了里式替换原则。
后置条件不能被削弱
后置条件即输出,假设我们的父类方法约定输出参数要大于0,调用父类方法的程序根据约定对输出参数进行了大于0的验证。而子类在实现的时候却输出了小于等于0的值。此时子类的涉及就违背了里氏替换原则
public void calculatePrice()
{
Strategy strategy= new Strategy();
BigDecimal price= strategy.getPrice();
if (price <= Decimal.Zero)
{
throw new ArgumentOutOfRangeException("price", "price must be positive and non-zero");
}
// do something }不能违背对异常的约定
在父类中,某个函数约定,只会抛出 ArgumentNullException 异常,那子类的设计实现中只允许抛出 ArgumentNullException 异常,任何其他异常的抛出,都会导致子类违背里式替换原则。
2.2 子类方法不能违背父类方法定义的功能
public class Product {
private BigDecimal amount;
private Calendar createTime;
public BigDecimal getAmount() {
return amount;
}
public void setAmount(BigDecimal amount) {
this.amount = amount;
}
public Calendar getCreateTime() {
return createTime;
}
public void setCreateTime(Calendar createTime) {
this.createTime = createTime;
}
}
public class ProductSort extends Sort<Product> {
public void sortByAmount(List<Product> list) {
//根据时间进行排序
list.sort((h1, h2)->h1.getCreateTime().compareTo(h2.getCreateTime()));
}
}
父类中提供的 sortByAmount() 排序函数,是按照金额从小到大来进行排序的,而子类重写这个 sortByAmount() 排序函数之后,却是是按照创建日期来进行排序的。那子类的设计就违背里式替换原则。
实际上对于如何验证子类设计是否符合里氏替换原则其实有一个小技巧,那就是你可以使用父类的单测来运行子类的代码,如果不可以正常运行,那么你就要考虑一下自己的设计是否合理了!
2.3 子类必须完全实现父类的抽象方法
如果你设计的子类不能完全实现父类的抽象方法那么你的设计就不满足里式替换原则。
// 定义抽象类枪
public abstract class AbstractGun{
// 射击
public abstract void shoot();
// 杀人
public abstract void kill();
}
比如我们定义了一个抽象的枪类,可以射击,也可以杀人。无论是步枪还是手枪都可以射击和啥人,我们可以定义子类来继承这个父类
// 定义手枪,步枪,机枪
public class Handgun extends AbstractGun{
public void shoot(){
// 手枪射击
}
public void kill(){
// 手枪杀人
}
}
public class Rifle extends AbstractGun{
public void shoot(){
// 步枪射击
}
public void kill(){
// 步枪杀人
}
}
但是如果我们在这个继承体系内加入一个玩具枪,就会有问题了,因为玩具枪只能射击,不能杀人。但是我经常看到很多人写代码会有这种套路。
public class ToyGun extends AbstractGun{
public void shoot(){
// 玩具枪射击
}
public void kill(){
// 因为玩具枪不能杀人,就返回空,或者直接throw一个异常出去
throw new Exception("我是个玩具枪,惊不惊喜,意不意外,刺不刺激?");
}
}
这时,我们如果把使用父类对象的地方替换为子类对象,显然是会有问题的(士兵上战场结果发现自己拿的是个玩具)。
而这种情况不仅仅不满足里氏替换原则,也不满足接口隔离原则,对于这种场景可以通过接口隔离+委托的方式来解决。
3 小结
面向对象的编程思想中提供了继承和多态是我们可以很好的实现代码的复用性和可扩展性,但继承并非没有缺点,因为继承的本身就是具有侵入性的,如果使用不当就会大大增加代码的耦合性,而降低代码的灵活性,增加我们的维护成本,然而在实际使用过程中却往往会出现滥用继承的现象,而里式替换原则可以很好的帮助我们在继承关系中进行父子类的设计。
系列文章
关注下方公众号,回复“代码的艺术”,可免费获取重构、设计模式、代码整洁之道等提升代码质量等相关学习资料
设计原则:里式替换原则(LSP)的更多相关文章
- java6大原则之单一职责原则,里式替换原则
单一职责原则:一个接口,一个类,一个方法,最好只做一类事,当然,在真实的项目中,一系列因素下,很难做到单一职责原则,但是针对接口是可以做到的,方法和类要尽量做到 里式替换原则:父类出现的地方,换成子类 ...
- 设计模式学习--面向对象的5条设计原则之Liskov替换原则--LSP
一.LSP简介(LSP--Liskov Substitution Principle): 定义:如果对于类型S的每一个对象o1,都有一个类型T的对象o2,使对于任意用类型T定义的程序P,将o2替换为o ...
- IOS设计模式的六大设计原则之里氏替换原则(LSP,Liskov Substitution Principle)
定义 里氏替换原则的定义有两种,据说是由麻省理工的一位姓里的女士所提出,因此以其名进行命名. 定义1:如果对一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1所定义的程序P中在o1全都替换 ...
- 深入理解JavaScript系列(8):S.O.L.I.D五大原则之里氏替换原则LSP
前言 本章我们要讲解的是S.O.L.I.D五大原则JavaScript语言实现的第3篇,里氏替换原则LSP(The Liskov Substitution Principle ). 英文原文:http ...
- Java设计原则之里氏替换原则
里氏代换原则由2008年图灵奖得主.美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing教授于1994年提出.其严格表述如下:如果对每一个类型为S的 ...
- 设计原则:开闭原则(OCP)
1.什么是开闭原则 开闭原则的英文是Open Closed Principle,缩写就是OCP.其定义如下: 软件实体(模块.类.方法等)应该"对扩展开放.对修改关闭". 从定义上 ...
- 设计原则:接口隔离原则(ISP)
接口隔离原则的英文是Interface Segregation Principle,缩写就是ISP.与里氏替换原则一样其定义同样有两种 定义1: Clients should not be force ...
- 【C#设计模式】里氏替换原则
今天,我们再来学习 SOLID 中的"L"对应的原则:里式替换原则. 里氏替换原则 里氏替换原则(Liskov Substitution Principle):派生类(子类)对象能 ...
- 【面向对象设计原则】之里氏替换原则(LSP)
里氏代换原则由2008年图灵奖得主.美国第一位计算机科学女博士Barbara Liskov教授和卡内基·梅隆大学Jeannette Wing 教授于1994年提出,所以使用的是这位女博士的性命名的一个 ...
随机推荐
- C++算法代码——n的全排列/全排列问题
题目来自:http://218.5.5.242:9018/JudgeOnline/problem.php?id=1541 题目描述 输入一个整数n,输出的n(n<=100)的全排列. 输入 输入 ...
- vue-axios插件、django-cors插件、及vue如何使用第三方前端样式库:element/jQuery/bootstrap
目录 一.vue的ajax插件:axios 1.安装axios 2.axios参数 二.CORS跨域问题(同源策略) 1.Django解决CORS跨域问题方法 三.前端请求携带参数及Django后台如 ...
- PBR:基于物理的渲染(Physically Based Rendering)+理论相关
一: 关于能量守恒 出射光线的能量永远不能超过入射光线的能量(发光面除外).如图示我们可以看到,随着粗糙度的上升镜面反射区域的会增加,但是镜面反射的亮度却会下降.如果不管反射轮廓的大小而让每个像素的镜 ...
- pytorch(09)transform模块(基础)
transforms transforms运行机制 torchvision.transforms:常用的图像预处理方法 torchvision.datasets:常用数据及的dataset实现,mni ...
- Django常见问题集锦
1. 解决pycharm终端/cmd运行python脚本报错"ImportError/ModuleNotFoundError:No Module named ..." 问题 项目结 ...
- C# 自定义时间进度条
这篇文章对我帮助极大,我模仿着写了两遍大概摸清楚了自定义控件的流程.https://www.cnblogs.com/lesliexin/p/13265707.html 感谢大佬 leslie_xin ...
- FTT简单入门板子
DFT : 1 #include <cstdio> 2 #include <iostream> 3 #include <cmath> 4 #include < ...
- Hexagon HDU - 6862
题目链接:https://vjudge.net/problem/HDU-6862 题意: 由六边形组成的圆形图案,要求不重复走遍历每一个小六边形. 思路:https://www.cnblogs.com ...
- 仿String()构造器函数 【总结】
需求 实现以下方法: 控制台结果: 需求分析: 首先确定new调用的this和什么对象绑定,如果跟默认返回的对象绑定肯定做不到 [ ] 这样的访问,所以要在构造器内部返回一个包装过的数组 1.leng ...
- 《逆向工程核心原理》——IAThook
hook逻辑写入dll中,注入dll. #include "pch.h" #include <tchar.h> #include "windows.h&quo ...