基于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

相关文章

MySQL查询JSON数组字段包含特定字符串的方法

《MySQL查询JSON数组字段包含特定字符串的方法》在MySQL数据库中,当某个字段存储的是JSON数组,需要查询数组中包含特定字符串的记录时传统的LIKE语句无法直接使用,下面小编就为大家介绍两种... 目录问题背景解决方案对比1. 精确匹配方案(推荐)2. 模糊匹配方案参数化查询示例使用场景建议性能优

关于集合与数组转换实现方法

《关于集合与数组转换实现方法》:本文主要介绍关于集合与数组转换实现方法,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、Arrays.asList()1.1、方法作用1.2、内部实现1.3、修改元素的影响1.4、注意事项2、list.toArray()2.1、方

Python中注释使用方法举例详解

《Python中注释使用方法举例详解》在Python编程语言中注释是必不可少的一部分,它有助于提高代码的可读性和维护性,:本文主要介绍Python中注释使用方法的相关资料,需要的朋友可以参考下... 目录一、前言二、什么是注释?示例:三、单行注释语法:以 China编程# 开头,后面的内容为注释内容示例:示例:四

一文详解Git中分支本地和远程删除的方法

《一文详解Git中分支本地和远程删除的方法》在使用Git进行版本控制的过程中,我们会创建多个分支来进行不同功能的开发,这就容易涉及到如何正确地删除本地分支和远程分支,下面我们就来看看相关的实现方法吧... 目录技术背景实现步骤删除本地分支删除远程www.chinasem.cn分支同步删除信息到其他机器示例步骤

在Golang中实现定时任务的几种高效方法

《在Golang中实现定时任务的几种高效方法》本文将详细介绍在Golang中实现定时任务的几种高效方法,包括time包中的Ticker和Timer、第三方库cron的使用,以及基于channel和go... 目录背景介绍目的和范围预期读者文档结构概述术语表核心概念与联系故事引入核心概念解释核心概念之间的关系

在Linux终端中统计非二进制文件行数的实现方法

《在Linux终端中统计非二进制文件行数的实现方法》在Linux系统中,有时需要统计非二进制文件(如CSV、TXT文件)的行数,而不希望手动打开文件进行查看,例如,在处理大型日志文件、数据文件时,了解... 目录在linux终端中统计非二进制文件的行数技术背景实现步骤1. 使用wc命令2. 使用grep命令

Python中Tensorflow无法调用GPU问题的解决方法

《Python中Tensorflow无法调用GPU问题的解决方法》文章详解如何解决TensorFlow在Windows无法识别GPU的问题,需降级至2.10版本,安装匹配CUDA11.2和cuDNN... 当用以下代码查看GPU数量时,gpuspython返回的是一个空列表,说明tensorflow没有找到

XML重复查询一条Sql语句的解决方法

《XML重复查询一条Sql语句的解决方法》文章分析了XML重复查询与日志失效问题,指出因DTO缺少@Data注解导致日志无法格式化、空指针风险及参数穿透,进而引发性能灾难,解决方案为在Controll... 目录一、核心问题:从SQL重复执行到日志失效二、根因剖析:DTO断裂引发的级联故障三、解决方案:修复

C++ 检测文件大小和文件传输的方法示例详解

《C++检测文件大小和文件传输的方法示例详解》文章介绍了在C/C++中获取文件大小的三种方法,推荐使用stat()函数,并详细说明了如何设计一次性发送压缩包的结构体及传输流程,包含CRC校验和自动解... 目录检测文件的大小✅ 方法一:使用 stat() 函数(推荐)✅ 用法示例:✅ 方法二:使用 fsee

Java继承映射的三种使用方法示例

《Java继承映射的三种使用方法示例》继承在Java中扮演着重要的角色,它允许我们创建一个类(子类),该类继承另一个类(父类)的所有属性和方法,:本文主要介绍Java继承映射的三种使用方法示例,需... 目录前言一、单表继承(Single Table Inheritance)1-1、原理1-2、使用方法1-