在本讲中,我来为大家介绍一下软件设计原则里面的第二个原则,即里氏代换原则。

概述

首先,大家应该知道,里氏代换原则是面向对象设计的基本原则之一。那什么是里氏代换原则呢?里氏代换原则是指任何基类可以出现的地方,子类一定可以出现。这句话不好理解,但大家可以通俗理解成子类可以扩展父类的功能,但不能改变父类原有的功能。现在,这句话就好理解很多了,指的就是在Java里面通常都会有父子类的关系,一般而言,我们都会将子类中的功能抽取到父类中,以提高代码的复用性,而在子类中,我们只需要去定义子类特有的功能即可。

换句话说,子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。为什么呢?因为如果通过重写父类的方法来完成新的功能,这样写起来虽然简单,但是整个继承体系的可复用性就会比较差,特别是运用多态比较频繁时,程序运行出错的概率会非常大。

你想啊,要是在父类中已经声明了一个方法,而你又在子类中再进行了一个重写,那么在父类中定义的方法是不是就没有任何意义了?如果说父类定义规则,要求子类必须重写,那么在父类中只需要定义成抽象的方法就可以了。

经过我上面的描述,相信大家对里氏代换原则有了一个简单的认识。接下来,我就为大家介绍里氏替换原则中的一个经典的案例,即正方形不是长方形。

案例

案例分析

在数学领域里,正方形毫无疑问是长方形,它是一个长宽相等的长方形。所以,如果我们要开发一个与几何图形相关的软件系统的话,那么就可以顺理成章的让正方形继承自长方形了。

请看一下下面这张类图。

可以看到,这张类图里面有三个类,第一个是长方形类,长方形类里面有两个成员变量,一个是length,表示长,一个是width,表示宽,而且它里面还提供了相应的getter和setter方法,相对来说,这个类还是很简单的,比较好理解。

第二个是长方形类的子类,即正方形类,该类要重写父类中设置长和宽的这两个方法。为什么要重写呢?因为正方形里面的长和宽是相等的。

以上两个类介绍完之后,再来看最后一个类,即测试类,在测试类中应该提供这么几个方法:

  • 主方法:这里面我没有写出来
  • resize方法:扩宽方法。长方形里面的宽是要比长小的,如果宽比长小的话,那么我们就可以通过该方法来进行判断,然后再将宽给它扩长,直到比长大就OK了
  • 打印长和宽的方法:该方法只是为了更好的看到效果而已

注意了,在resize方法和打印长和宽的这两个方法里面,还需要传递一个长方形类型的参数,也就是说测试类其实是依赖于长方形类的,所以它俩之间是一个依赖关系。

把以上这三个类以及依赖关系理清楚了之后,接下来我们就要编写代码来实现这个案例了。

案例实现

打开咱们的maven工程,然后在com.meimeixia.principles包下创建一个子包,即demo2,接着在com.meimeixia.principles.demo2包下再创建一个子包,即before,我们首次是在该包下来存放咱们编写的代码的。接下来,我们就要正式开始编写代码来实现以上案例了。

首先,在com.meimeixia.principles.demo2.before包下新建第一个类,即长方形类,名字可取做Rectangle。

package com.meimeixia.principles.demo2.before;/*** 长方形类* @author liayun* @create 2021-05-27 13:26*/
public class Rectangle {private double length;private double width;public double getLength() {return length;}public void setLength(double length) {this.length = length;}public double getWidth() {return width;}public void setWidth(double width) {this.width = width;}
}

然后,新建第二个类,即正方形类,名字可取做Square,记住要让该类去继承长方形类,并重写父类中设置长和宽的方法。那么应该如何去重写呢?很简单,就拿重写父类中设置长的setLength方法来说,我们只需要调用父类中的设置长和宽的方法把方法中的length参数设置给长和宽即可,因为长和宽必须保持一致。当然,重写父类中设置长的setWidth方法也是同理。

package com.meimeixia.principles.demo2.before;/*** 正方形类* @author liayun* @create 2021-05-27 13:32*/
public class Square extends Rectangle {@Overridepublic void setLength(double length) {super.setLength(length);super.setWidth(length);}@Overridepublic void setWidth(double width) {super.setLength(width);super.setWidth(width);}}

接着,我们就要编写测试类了,名字就叫RectangleDemo。根据我们上面的分析,相信你一定能写出下面的代码,只不过现在还未在主方法中编写测试代码。

package com.meimeixia.principles.demo2.before;/*** @author liayun* @create 2021-05-27 13:42*/
public class RectangleDemo {public static void main(String[] args) {// 测试代码...}// 扩宽方法public static void resize(Rectangle rectangle) {// 判断宽如果比长小,那么则进行扩宽的操作while (rectangle.getWidth() <= rectangle.getLength()) {rectangle.setWidth(rectangle.getWidth() + 1);}}// 打印长和宽public static void printLengthAndWidth(Rectangle rectangle) {System.out.println(rectangle.getLength());System.out.println(rectangle.getWidth());}}

紧接着,在主方法中编写如下代码进行测试。

package com.meimeixia.principles.demo2.before;/*** @author liayun* @create 2021-05-27 13:42*/
public class RectangleDemo {public static void main(String[] args) {// 创建长方形对象Rectangle r = new Rectangle();// 设置长和宽r.setLength(20);r.setWidth(10);// 调用resize方法进行扩宽resize(r);printLengthAndWidth(r);}// 扩宽方法public static void resize(Rectangle rectangle) {// 判断宽如果比长小,那么则进行扩宽的操作while (rectangle.getWidth() <= rectangle.getLength()) {rectangle.setWidth(rectangle.getWidth() + 1);}}// 打印长和宽public static void printLengthAndWidth(Rectangle rectangle) {System.out.println(rectangle.getLength());System.out.println(rectangle.getWidth());}}

这时,我们不妨来运行一下以上测试类,看看打印结果是啥?如下图所示,可以看到长是20,没变,宽是21,因为我们进行了一个扩宽的操作,此时,宽已经比长大了。

如果我像下面这样向resize方法中传入一个正方形类型的对象,那么可不可以呢?

package com.meimeixia.principles.demo2.before;/*** @author liayun* @create 2021-05-27 13:42*/
public class RectangleDemo {public static void main(String[] args) {// 创建长方形对象Rectangle r = new Rectangle();// 设置长和宽r.setLength(20);r.setWidth(10);// 调用resize方法进行扩宽resize(r);printLengthAndWidth(r);System.out.println("=============================");// 创建正方形对象Square s = new Square();// 设置长和宽s.setLength(10);// 调用resize方法进行扩宽resize(s);printLengthAndWidth(s);}// 扩宽方法public static void resize(Rectangle rectangle) {// 判断宽如果比长小,那么则进行扩宽的操作while (rectangle.getWidth() <= rectangle.getLength()) {rectangle.setWidth(rectangle.getWidth() + 1);}}// 打印长和宽public static void printLengthAndWidth(Rectangle rectangle) {System.out.println(rectangle.getLength());System.out.println(rectangle.getWidth());}}

从语法上来说是可以的,因为正方形是属于长方形的子类的,所以传递子类对象完全是可以的。

这时,我们再来运行一下测试类,看一下打印结果是什么,如下图所示,你会发现等了好久,结果却什么都没有打印出来。

难道是我们程序出现问题了吗?其实不是,你注意看以上控制台中的那个红色按钮,这表明程序还没有结束,它还在一直执行,为什么会出现这种现象呢?下面我就为大家解释一下其原因。

运行以上测试类中代码,你就会发现,假如我们把一个普通长方形对象作为参数传入resize方法中的话,是会看到长方形的宽度逐渐增长的效果的,当宽度大于长度时,代码就会停止,这种行为的结果符合我们的预期;假如我们再把一个正方形对象作为参数传入resize方法的话,就会看到正方形的宽度和长度都在不断增长,因为长和宽要保持一致,这是正方形的一个特点,那么代码就会一直运行下去,直至系统产生溢出错误为止。

所以,普通的长方形是适合这段代码的,而正方形不适合。而里氏代换原则又是指基类能使用的地方,那么子类也可以使用,因此很显然这违背了这一原则。

于是,我们可以得出这样一个结论:在resize方法中,Rectangle类型的参数是不能被其子类Square类型的参数所代替的,如果进行了替换就得不到预期结果。因此,Square类和Rectangle类之间的继承关系违反了里氏代换原则,它们之间的继承关系不成立,正方形不是长方形。

那么如何改进呢?下面我们再说。

案例改进

初步实现以上正方形不是长方形的案例之后,相信你也看到了其所在的问题,即违反了里氏代换原则。那么应该如何对该案例进行改进呢?

首先,我们要对类以及类和类之间的关系进行重新设计,重新设计出来的类图应该是下面这个样子的。

对于正方形和长方形而言,我们向上抽取,抽取出来一个四边形接口(即Quadrilateral),并在这个接口里面定义两个抽象的方法,一个是getLength,一个是getWidth,分别用于获取长和宽,然后让Rectangle类和Square类实现Quadrilateral接口;从以上类图中可以看到,我们还在Square类里面定义了一个名字为side的成员变量,也即正方形的边长,而且在该类里面,除了提供该成员变量的getter和setter方法之外,我们还重写了Quadrilateral接口里面的抽象方法;至于Rectangle类,依旧还是原先的设计,该类是没有任何变化的。

最后,大家不要忘了,还有一个测试类(即RectangleDemo),该测试类是没有成员变量的,只有如下三个方法:

  • 主方法:这里面我没有声明出来,主要作测试用
  • resize方法:扩宽方法。注意,该方法需要的是一个长方形类型的对象,正方形类型的对象此时是不能传入进来的
  • printLengthAndWidth方法:打印长和宽的方法。注意,该方法需要传递的是一个Quadrilateral接口的子实现类对象

这样一路分析下来,你就会发现该测试类不仅得依赖Quadrilateral接口,还得依赖Rectangle类。至此,我就给大家分析完以上类图了,接下来,我们就得编写代码来实现以上改进后的案例了。

首先,在com.meimeixia.principles.demo2包下再创建一个子包,即after,该包下存放的就是改进后的案例的代码。

然后,我们再创建一个四边形接口。

package com.meimeixia.principles.demo2.after;/*** 四边形接口* @author liayun* @create 2021-05-27 14:27*/
public interface Quadrilateral {// 获取长double getLength();// 获取宽double getWidth();}

接着,再来创建咱们的正方形类,注意了,该类是要去实现四边形接口的,这样,我们还必须得重写其中的方法。此外,在该类里面我们还得声明一个表示边长的成员变量,当然还得提供其对应的getter和setter方法。

package com.meimeixia.principles.demo2.after;/*** 正方形类* @author liayun* @create 2021-05-27 14:32*/
public class Square implements Quadrilateral {private double side;public double getSide() {return side;}public void setSide(double side) {this.side = side;}@Overridepublic double getLength() {return side;}@Overridepublic double getWidth() {return side;}
}

紧接着,再来创建咱们的长方形类,同理,该类也得实现四边形接口,重写其里面的抽象方法。

package com.meimeixia.principles.demo2.after;/*** 长方形类* @author liayun* @create 2021-05-27 14:37*/
public class Rectangle implements Quadrilateral {private double length;private double width;public void setLength(double length) {this.length = length;}public void setWidth(double width) {this.width = width;}@Overridepublic double getLength() {return length;}@Overridepublic double getWidth() {return width;}
}

最后,我们再来创建一个测试类。根据我们上面对类图的分析,相信你一定能写出下面的代码。

package com.meimeixia.principles.demo2.after;/*** @author liayun* @create 2021-05-27 14:49*/
public class RectangleDemo {public static void main(String[] args) {// 创建长方形对象Rectangle r = new Rectangle();// 设置长和宽r.setLength(20);r.setWidth(10);// 调用方法进行扩宽操作resize(r);printLengthAndWidth(r);}// 扩宽的方法public static void resize(Rectangle rectangle) {// 判断宽如果比长小,那么则进行扩宽的操作while (rectangle.getWidth() <= rectangle.getLength()) {rectangle.setWidth(rectangle.getWidth() + 1);}}// 打印长和宽public static void printLengthAndWidth(Quadrilateral quadrilateral) {System.out.println(quadrilateral.getLength());System.out.println(quadrilateral.getWidth());}}

此时,不妨来运行一下以上测试类,看看打印结果是不是我们所想要的,如下图所示,可以看到长是20,没变,宽是21,因为我们进行了一个扩宽的操作。

大家现在想一想,如果我们去调用resize方法时传入的是一个正方形对象,那么还可不可以呢?肯定是不可以的,因为正方形和长方形它俩现在没有父子关系了,所以在resize方法里面只能传递长方形对象,而不能再传递正方形对象了。这样,我们就通过以上改进完美的解决了案例之前所存在的问题。

从零开始学习Java设计模式 | 软件设计原则篇:里氏代换原则相关推荐

  1. 从零开始学习Java设计模式 | 软件设计原则篇:开闭原则

    从本讲开始,咱们就要开始学习第一章中的第三部分内容,即软件设计原则了. 在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量根据6条原则来开发程序,从而提高软件 ...

  2. 从零开始学习Java设计模式 | 软件设计原则篇:依赖倒转原则

    在本讲,我将为大家介绍软件设计原则里面的第三个原则,即依赖倒转原则. 概述 什么是依赖倒转原则呢?我们来看一下下面这段描述: 高层模块不应该依赖低层模块,两者都应该依赖其抽象:抽象不应该依赖细节,细节 ...

  3. 从零开始学习Java设计模式 | 软件设计原则篇:接口隔离原则

    在本讲,我将为大家介绍软件设计原则里面的第四个原则,即接口隔离原则. 概述 接口隔离原则是指客户端不应该被迫依赖于它不使用的方法,一个类对另一个类的依赖应该建立在最小的接口上面. 这句话可能不是很好理 ...

  4. 从零开始学习Java设计模式 | 创建型模式篇:原型模式

    在本讲,我们来学习一下创建型模式里面的第四个设计模式,即原型模式. 概述 原型模式就是指用一个已经创建的实例作为原型,通过复制该原型对象来创建一个和原型对象相同的新对象. 这段话读起来有点绕,是不是? ...

  5. 从零开始学习Java设计模式 | 创建型模式篇:抽象工厂模式

    在本讲,我们来学习一下创建型模式里面的第三个设计模式,即抽象工厂模式. 前言 前面介绍的工厂方法模式中考虑的是一类产品,如畜牧场只养动物.电视机厂只生产电视机(不生产空调.冰箱等其它的电器).计算机学 ...

  6. 从零开始学习Java设计模式 | 创建型模式篇:建造者模式

    在本讲,我们来学习一下创建型模式里面的最后一个设计模式,即建造者模式. 概述 建造者模式是指将一个复杂对象的构建与表示分离,使得同样的构建过程可以创建不同的表示. 读完这句话之后,我估计很多人都已经懵 ...

  7. Java设计模式-软件设计原则

    目录 1 开闭原则 2 里氏代换原则 3 依赖倒转原则 4 接口隔离原则 5 迪米特法则 6 合成复用原则 在软件开发中,为了提高软件系统的可维护性和可复用性,增加软件的可扩展性和灵活性,程序员要尽量 ...

  8. Java设计原则之单一职责原则、开闭原则、里氏代换原则

    文章目录 面向对象设计原则概述 单一职责原则 开闭原则 里氏代换原则 面向对象设计原则概述 软件的可维护性(Maintainability)和可复用性(Reusability)是两个非常重要的用于衡量 ...

  9. 设计模式-设计原则之里氏代换原则

    设计原则之里氏代换原则 里氏代换原则 案例(正方形不是长方形) 案例改进 里氏代换原则 里氏代换原则是面向对象设计的基本原则之一. 里氏代换原则:任何基类可以出现的地方,子类一定可以出现. 通俗理解: ...

最新文章

  1. 返回指针值的函数(1)
  2. 只需2小时,成本不到7块,你我皆可制作的3D机器人
  3. 腾讯翻译君在线翻译怎么翻译整个文件_藏语怎么翻译成中文?这两方法非常好用...
  4. react前端显示图片_如何在react项目中引用图片?
  5. windows temp用户问题
  6. 7个JavaScript在IE和Firefox浏览器下的差异写法
  7. windows7:“创建系统修复光盘”
  8. OLTP在线事务处理
  9. scanner读取带空格字符串_Scanner类提供了输入字符出的方法,下面哪个方法可以实现字符串的输入且该串可以含有空格()。-智慧树JAVA程序设计(山东联盟-山东农业大学)章节答案...
  10. 宁波市重点首版次软件认定申报,区块链可申请 | 产业区块链发展周报
  11. 无线网DNS服务器有错误,关于dns错误的原因和解决办法
  12. MacOS修改Hosts文件
  13. GRU实现时间序列预测(PyTorch版)
  14. 在vi 中设置tab键为4个空格,并显示行号,对文件中的TAB与空格进行相互转换
  15. 计算机的鼻祖---差分机的由来
  16. 4.CCNP闫辉视频笔记路由重分发
  17. Oracle删除非空表空间
  18. 【云原生之Docker实战】使用Docker部署Duplicati备份工具
  19. 推荐4款高大尚的网站外链跳转页源码
  20. 交通标识牌检测及识别c++代码实例及运行结果 (可自行在网上下载图片测试)

热门文章

  1. 如何构建标签和画像体系,助力零售企业数据化转型
  2. 快鲸智慧楼宇:六大子系统及其作用
  3. css 自定义字体 Internet Explorer,Firefox,Opera,Safari
  4. [NOI2007]生成树计数
  5. Mac OS + IntelliJ Idea +Git 开发环境搭建实战
  6. C++实现行列式的计算
  7. ibm服务器怎么接显示器,ThinkPad如何外接显示器或投影仪进行演示
  8. python 格式化打印列表_打印和格式化列表在Python中
  9. 如何理解POP,OOP,AOP之间的关系
  10. 刷脸无感支付的方式有助于降低交互的门槛