基于LSP原则对方法的一点相关

2024-03-15 04:30
文章标签 方法 相关 原则 一点 lsp

本文主要是介绍基于LSP原则对方法的一点相关,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

方法

  方法,或者叫类的行为,定义了类能够实现的功能。在任何程序设计语言中,方法一个重要的特性就是名字的运用。我们创建一个对象时,会分配到一个保存区域的名字。方法名代表的是一种具体的行动。通过名字描述自己的系统,可使自己的程序更易人们理解和修改。它就像写散文——目的是与读者沟通。

构造方法

  构造方法是与一种与类同名且无返回值类型的方法。构造方法在实例化一个对象的同时进行编译,它分为无参构造和含参构造。无参构造顾名思义就是不含参数类型和参数的方法。

Static关键字

方法的实现

  通常在开发中,我们会定义一些接口或者抽象类,然后编码实现。在实现这些接口或者抽象类时,会进行方法的重载(overload)和覆写(override)。而重载和覆写区别在就在于重载是方法名相同,参数类型和参数个数不同;覆写是方法名和参数类型以及个数与父类方法完全相同。

子类必须完全实现父类的方法

  根据LSP原则,在此定义一个汽车类图

  汽车能够启动,而在车主类中定义一个方法racing,进行飙车,具体用什么车来飙车,调用车型的时候才知道,AbstractCar的代码如下:

1 public abstract class AbstratCar {
2 
3     //汽车能够启动
4     public abstract void drive();
5     
6 }

  跑车的实现类

1 public class SportsCar extends AbstratCar{
2     //跑车速度快
3     @Override
4     public void drive() {
5         // TODO Auto-generated method stub
6         System.out.println("跑车启动");
7     }
8 
9 }

  拖拉机的实现类

1 public class Tractor extends AbstratCar{
2     //拖拉机跑起来冒烟
3     @Override
4     public void drive() {
5         // TODO Auto-generated method stub
6         System.out.println("拖拉机启动");
7     }
8 
9 }

  卡车的实现类

1 public class Truck extends AbstratCar{
2     //卡车能拉货
3     @Override
4     public void drive() {
5         // TODO Auto-generated method stub
6         System.out.println("卡车启动");
7     }
8 
9 }

  司机的实现类

public class Owner {//定义司机的汽车private AbstracCar car;//给司机一张车public void setCar(AbstracCar _car){this.car = _car;}public void racing(){car.drive();System.out.println("老司机正在飙车");}
}

 

  汽车能够启动,但汽车是抽象的,具体是什么车启动需要在开车的时候(也就是场景中)才能通过drive方法确定,场景类Client实现入下:

 1 public class Client {
 2 
 3     public static void main(String[] args) {
 4         //产生一个老司机
 5         Owner oldDriver = new Owner();
 6         //给老司机一张车
 7         oldDriver.setCar(new SportsCar());
 8         oldDriver.racing();
 9         
10     }
11 
12 }

  运行结果如下:

跑车启动
老司机正在飙车

  在这个程序中,我们给了老司机一张跑车,然后老司机就开始飙车了。如果老司机要开拖拉机,直接修改oldDriver.setCar(new SportsCar())为oldDriver.setCar(new (Tractor))即可,在编写程序时车主类根本不用知道是哪一个型号的车(子类)被传入。

  在类中调用其它类是务必要使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背LSP原则。

  那再来看一看当出现一张玩具车时的定义。修改后的类图如下:

  在这里有个问题,玩具车是不能用来飙车的,所以不能写在drive方法中。

  玩具车实现

1 public class ToyCar extends AbstratCar{
2     // 玩具车不能启动,但编译器要求实现这个方法,只能虚构一个
3     @Override
4     public void drive() {
5         // 玩具车不能启动,故此方法不实现
6         
7     }
8 
9 }

  由于引入了新的子类,场景类中也使用了该类,Client稍作修改,如下:

 1 public class Client {
 2 
 3     public static void main(String[] args) {
 4         //产生一个老司机
 5         Owner oldDriver = new Owner();
 6         //给老司机一张车
 7         oldDriver.setCar(new ToyCar());
 8         oldDriver.racing();
 9         
10     }
11 
12 }

  运行结果如下

老司机正在飙车

  然而玩具车无法启动,因此不能够飙车,此时正常的业务逻辑无法继续运行,在此有两种解决办法:

    1.在Owner类中增加判断,如果是玩具车,就不能飙车。这可以解决问题,但是在程序中没增加一个类,所有与这个父类有关系的类就必须修改,因此这个方法行不通。

    2.ToyCar脱离继承,建立一个独立的父类,可以与AbastrctCar建立关联委托关系,如下图:

  在AbstractToy中奖部分数据的处理委托给AbstractCar中处理,然后两个基类下的子类自由延展,互不影响。

  按照Java三大特征,继承就是让子类拥有父类的方法和属性,然后可以重写父类的方法。按照继承原则,上面的玩具车继承AbstractCar是完全没有问题的,但是在具体的应用场景中需要考虑下面这问题:子类是否能够完整的实现父类的业务,否则就会想上面一样出现业务逻辑混乱的问题。

  如果子类不能完整的实现父类方法,或者父类的某些方法在子类中已经发生畸变,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。

子类可以有自己的个性

  子类能够有自己的方法和属性,在LSP原则下,子类出现的地方,父类未必能胜任。依然以上文的汽车为例,将法拉第和奔驰引入跑车的子类。

  奔驰继承SportsCar类,霸道总裁(Ceo)直接用奔驰进行飙车。

  奔驰车类实现

public class Benz extends SportsCar{//豪车上一般会带个妹子public void girl(){System.out.println("香车美人");}public void drive(){System.out.println("奔驰启动");}
}

  霸道总裁类实现

 1 public class Ceo {
 2     private Benz benz;
 3     
 4     public void setCar(Benz benz){
 5         this.benz = benz;
 6     }
 7     
 8     public void racing(){
 9         //上车前先带个妹子
10         benz.girl();
11         //开车开车
12         benz.drive();
13     }
14 }

 

  业务场景类实现

 1 public class Client {
 2 
 3     public static void main(String[] args) {
 4         //产生一个老司机
 5         Ceo oldDriver = new Ceo();
 6         //给老司机一张车
 7         oldDriver.setCar(new Benz());
 8         oldDriver.racing();
 9         
10     }
11 
12 }

  运行结果如下

香车美人
奔驰启动

  在这里,系统直接调用了子类,也就是直接将子类传递进来,那么在这个时候能不能直接使用父类传递进来呢?修改一下代码如下:

 1 public class Client {
 2 
 3     public static void main(String[] args) {
 4         //产生一个老司机
 5         Ceo oldDriver = new Ceo();
 6         //给老司机一张车
 7         oldDriver.setCar((Benz)new SportsCar());
 8         oldDriver.racing();
 9         
10     }
11 
12 }

  运行结果如下:

Exception in thread "main" java.lang.ClassCastException: demo7.SportsCar cannot be cast to demo7.Benzat demo7.Client.main(Client.java:11)

  这个时候会抛出java.lang.ClassCastException异常,这也就是常说的,向下转型(downcast)是不安全的,也就是说,子类出现的地方父类未必可以出现。

覆盖或实现父类的方法时输入的参数可以被放大

  方法中的输入参数称为前置条件,在继承关系中前置条件范围的不同会造成什么关系呢,让我们测试一下。

1 public class Father {
2     public Collection doSomething(HashMap map){
3         System.out.println("父类被执行");
4         
5         return map.values();
6         
7     }
8 }

  定义父类代码

public class Son extends Father{public Collection doSomething(Map map){System.out.println("子类被执行");return map.values();}
}

  定义子类

 1 public class Client {
 2 
 3     public static void invoker(){
 4         //父类存在的地方,子类就应该能够存在
 5         Father f = new Father();
 6         HashMap map = new HashMap();
 7         f.doSomething(map);
 8     }
 9     
10     public static void main(String[] args) {
11         invoker();
12     }
13 
14 }

  定义场景类

父类被执行

  运行结果如上。根据LSP原则,父类存在的地方子类就应该能存在,如果将父类替换替换一下,会有什么结果呢。

 1 public class Client {
 2 
 3     public static void invoker(){
 4         //父类存在的地方,子类就应该能够存在
 5         Son f = new Son();
 6         HashMap map = new HashMap();
 7         f.doSomething(map);
 8     }
 9     
10     public static void main(String[] args) {
11         invoker();
12     }
13 
14 }

  运行结果如下

父类被执行

  两段运行结果都一样,这是因为父类方法输入的参数是HashMap类型,子类的输入类型参数是Map类型,也就是说子类输入参数类型的范围扩大了,子类代替父类传递到调用者中,子类的方法永远不会被执行。那如果修改一下将父类的前置条件扩大,会出现什么情况

1 public class Father {
2     public Collection doSomething(Map map){
3         System.out.println("父类被执行");
4         
5         return map.values();
6         
7     }
1 public class Son extends Father{
2     public Collection doSomething(HashMap map){
3         System.out.println("子类被执行");
4         return map.values();
5     }
6 }

  在父类的前置条件大于子类的前置条件的情况下,场景类实现如下

 1     public static void invoker(){
 2         //父类存在的地方,子类就应该能够存在
 3         Father f = new Father();
 4         HashMap map = new HashMap();
 5         f.doSomething(map);
 6     }
 7     
 8     public static void main(String[] args) {
 9         invoker();
10     }
11 
12 }

 运行结果如下

父类被执行

  引入LSP原则,将父类替换为子类

    public static void invoker(){//父类存在的地方,子类就应该能够存在Son f = new Son();HashMap map = new HashMap();f.doSomething(map);}public static void main(String[] args) {invoker();}}

  运行结果如下

子类被运行

  这就出现了很严重的逻辑混乱问题。在子类没有覆写父类方法的前提下,子类被执行了,这会引起业务逻辑混乱,因为在实际应用中,父类一般都是抽象类,子类是实现类,传递这样一个实现类,就会“歪曲”父类的意图,引起一对意想不到的业务逻辑混乱,所以子类中方法的前置条件必须与超类中被覆写的方法的前置条件相同或者更宽松。

覆写或覆写父类的方法时输出结果可以被缩小

  父类的一个方法的返回值是一个类型T,子类的相同方法(重载或覆写)的返回值为S,那么S就必须小于T,也就是要么S和T是同一个类型,要么S是T的子类。这里分两种情况,如果是覆写,父类和子类的同名方法的输入参数是相同的,两个方法的范围值S小于等于T,这是覆写的要求。如果是重载,则要求方法的输入参数类型或数量不相同,在LSP原则下,即子类的输入参数宽于或等于父类的输入参数,也就是说这个方法不会被调用。

 

转载于:https://www.cnblogs.com/aladdin-light/p/5494172.html

这篇关于基于LSP原则对方法的一点相关的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/810790

相关文章

Java中的StringUtils.isBlank()方法解读

《Java中的StringUtils.isBlank()方法解读》:本文主要介绍Java中的StringUtils.isBlank()方法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑... 目录所在库及依赖引入方法签名方法功能示例代码代码解释与其他方法的对比总结StringUtils.isBl

CentOS7增加Swap空间的两种方法

《CentOS7增加Swap空间的两种方法》当服务器物理内存不足时,增加Swap空间可以作为虚拟内存使用,帮助系统处理内存压力,本文给大家介绍了CentOS7增加Swap空间的两种方法:创建新的Swa... 目录在Centos 7上增加Swap空间的方法方法一:创建新的Swap文件(推荐)方法二:调整Sww

QT6中绘制UI的两种方法详解与示例代码

《QT6中绘制UI的两种方法详解与示例代码》Qt6提供了两种主要的UI绘制技术:​​QML(QtMeta-ObjectLanguage)​​和​​C++Widgets​​,这两种技术各有优势,适用于不... 目录一、QML 技术详解1.1 QML 简介1.2 QML 的核心概念1.3 QML 示例:简单按钮

Oracle 通过 ROWID 批量更新表的方法

《Oracle通过ROWID批量更新表的方法》在Oracle数据库中,使用ROWID进行批量更新是一种高效的更新方法,因为它直接定位到物理行位置,避免了通过索引查找的开销,下面给大家介绍Orac... 目录oracle 通过 ROWID 批量更新表ROWID 基本概念性能优化建议性能UoTrFPH优化建议注

Pandas进行周期与时间戳转换的方法

《Pandas进行周期与时间戳转换的方法》本教程将深入讲解如何在pandas中使用to_period()和to_timestamp()方法,完成时间戳与周期之间的转换,并结合实际应用场景展示这些方法的... 目录to_period() 时间戳转周期基本操作应用示例to_timestamp() 周期转时间戳基

在 PyQt 加载 UI 三种常见方法

《在PyQt加载UI三种常见方法》在PyQt中,加载UI文件通常指的是使用QtDesigner设计的.ui文件,并将其转换为Python代码,以便在PyQt应用程序中使用,这篇文章给大家介绍在... 目录方法一:使用 uic 模块动态加载 (不推荐用于大型项目)方法二:将 UI 文件编译为 python 模

Python将字库文件打包成可执行文件的常见方法

《Python将字库文件打包成可执行文件的常见方法》在Python打包时,如果你想将字库文件一起打包成一个可执行文件,有几种常见的方法,具体取决于你使用的打包工具,下面就跟随小编一起了解下具体的实现方... 目录使用 PyInstaller基本方法 - 使用 --add-data 参数使用 spec 文件(

Python的pip在命令行无法使用问题的解决方法

《Python的pip在命令行无法使用问题的解决方法》PIP是通用的Python包管理工具,提供了对Python包的查找、下载、安装、卸载、更新等功能,安装诸如Pygame、Pymysql等Pyt... 目录前言一. pip是什么?二. 为什么无法使用?1. 当我们在命令行输入指令并回车时,一般主要是出现以

通过C#获取Excel单元格的数据类型的方法详解

《通过C#获取Excel单元格的数据类型的方法详解》在处理Excel文件时,了解单元格的数据类型有助于我们正确地解析和处理数据,本文将详细介绍如何使用FreeSpire.XLS来获取Excel单元格的... 目录引言环境配置6种常见数据类型C# 读取单元格数据类型引言在处理 Excel 文件时,了解单元格

MySQL连接池(Pool)常用方法详解

《MySQL连接池(Pool)常用方法详解》本文详细介绍了MySQL连接池的常用方法,包括创建连接池、核心方法连接对象的方法、连接池管理方法以及事务处理,同时,还提供了最佳实践和性能提示,帮助开发者构... 目录mysql 连接池 (Pool) 常用方法详解1. 创建连接池2. 核心方法2.1 pool.q