六大设计原则(二)LSP里氏替换原则

里氏替换原则LSP(Liskov Subsituation Principle)

里氏替换原则定义

所有父类出现的地方可以使用子类替换并不会出现错误或异常,但是反之子类出现的地方不一定能用父类替换。

LSP的四层含义

  • 子类必须完全实现父类的方法
  • 子类可以自己的个性(属性和方法)
  • 覆盖或实现父类的方法时输入参数可以被放大
  • 覆盖或实现父类的方法时输出结果可以被缩小

LSP的定义含义1——子类必须完全实现父类的方法

假设如下场景:定义一个枪支抽象类,一个场景类,三个枪支实现类,一个士兵类。此处,三个枪支完全实现了父类的方法。

关联关系:实线箭头
泛化关系:实线空心箭头(继承关系)
依赖关系:虚线箭头(使用关系)一个类需要另一个类的协助

抽象枪支类:射击功能

package des.lsp;

/**
 * 抽象类 枪支
 */
abstract class AbstractGun {
    //射击功能
  public abstract void shoot();
}

子类实现

package des.lsp;

/**
 * 手枪
 */
public class HandGun extends AbstractGun {
    @Override
    public void shoot() {
        System.out.print("手枪可以射击");
    }
}
package des.lsp;

/**
 * 手枪
 */
public class MachineGun extends AbstractGun {
    @Override
    public void shoot() {
        System.out.print("步枪可以射击");
    }
}
package des.lsp;

/**
 * 步枪
 */
public class Rifle extends AbstractGun {
    @Override
    public void shoot() {
        System.out.print("步枪可以射击");
    }
}

士兵类:士兵类使用的是抽象枪支类,具体的需要在场景类中指定。

类中调用其他类必须使用父类或接口,若不能使用则其实已经违背了LSP原则。

package des.lsp;

public class Soldier {
    private AbstractGun gun;
    public void setGun(AbstractGun _gun){
        this.gun = _gun;
    };
    public void killEnemy(){
        System.out.print("士兵开始杀人...");
        gun.shoot();
    }

}

场景类

package des.lsp;

public class Client {
    public static void main(String[] args) {
        // write your code here
        Soldier s = new Soldier();
        s.setGun(new Rifle());
        s.killEnemy();
    }
}

如果加入一个玩具枪类,即玩具枪类同样继承抽象枪支类,此时就会存在子类不能实现枪支类方法的情况,因为玩具枪和枪最本质的区别是玩具枪不能射击的,是无法杀死人的。但是,玩具枪的其他属性,比如颜色等一些属性可以委托抽象枪支类进行处理。

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

玩具枪继承枪支抽象类的情况:射击方法不能被实现,如果实现里面具体逻辑为空则毫无意义,即正常情况下不能实现父类的shoot方法,shoot方法必须去掉,从LSP来看如果去掉,则违背了LSP的第一个原则:子类必须实现父类方法。(代码层面来看如果去掉则会报错)

package des.lsp;

public class ToyGun extends  AbstractGun {
    @Override
    public void shoot() {
        //此方法不能实现,玩具枪不能射击
    }
}

解决方法:单独建立一个抽象类玩具类,把与枪支共有的如声音、颜色交给抽象枪支类处理,而玩具枪所特有的玩具类的属性交给抽象玩具类处理,玩具枪类实现玩具抽象类

LSP的定义含义2——子类可以含有自己的特性

如图引入,步枪的实现类即步枪由不同的型号。AUG:狙击枪可以由望远镜功能zoomOut方法。

此处Snipper是狙击手类,狙击手与狙击枪是密不可分,属于组合关系,所以狙击手类直接使用子类AUG。

package des.lsp;
//狙击枪
public class AUG extends Rifle {
    //狙击枪特有功能
    public void zoomOut(){
        System.out.print("通过望远镜观察敌人...");
    }

    @Override
    public void shoot() {
        System.out.print("AUG射击敌人...");
    }
}
package des.lsp;
//狙击手
public class Snipper {
    //此处传入参数为子类,组合关系
    public void killEnemy(AUG aug){
        //观察
        aug.zoomOut();
        //射击
        aug.shoot();
    }
}
package des.lsp;

public class Client {
    public static void main(String[] args) {

        Snipper s = new Snipper();
        s.killEnemy(new AUG());
    }
}

LSP原则:父类不一定能替换子类

package des.lsp;

public class Client {
    public static void main(String[] args) {
        // write your code here
//        Soldier s = new Soldier();
//        s.setGun(new Rifle());
//        s.killEnemy();

        Snipper s = new Snipper();
        s.killEnemy((AUG) new Rifle());//此处用父类代替了子类
    }
}

报错代码

LSP的定义含义3——覆盖或实现父类方法时输入参数可以被放大

假设有如下场景
父类:方法入参<子类方法入参

场景类调用:父类调用自己方法。

package des.lsp;

import java.util.HashMap;

public class Client {
    public static void invoker(){
        Father f = new Father();
        HashMap map = new HashMap();
        f.doSomething(map);

    }
    public static void main(String[] args) {

        invoker();
    }
}

输出结果

使用里氏替换原则:把所有父类出现的地方替换为子类

package des.lsp;

import java.util.HashMap;

public class Client {
    public static void invoker(){
        Son f = new Son();
        HashMap map = new HashMap();
        f.doSomething(map);

    }
    public static void main(String[] args) {
emy((AUG) new Rifle());

        invoker();
    }
}

输出结果

我们的本意是调用子类重载的方法,入参为Map的方法,但实际程序执行是调用的从父类继承的方法。如果子类的方法中入参的范围大于父类入参的范围,则子类代替父类的时候,子类的方法永远不会执行。
从另外角度来看,假如父类入参的范围大于子类的入参的范围,则父类替换子类就未必能存在,这时候很可能会调用子类的方法执行。此句话较为抽象,实际情况如下。

父类和子类的代码如下

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

场景类:调用父类

package des.lsp;

import java.util.HashMap;

public class Client {
    public static void invoker(){
        Father f = new Father();
        HashMap map = new HashMap();
        f.doSomething(map);

    }
    public static void main(String[] args) {

        invoker();
    }
}

运行结果:不言而喻,是父类被执行

采用LSP后

package des.lsp;

import java.util.HashMap;

public class Client {
    public static void invoker(){
        Son f = new Son();
        HashMap map = new HashMap();
        f.doSomething(map);

    }
    public static void main(String[] args) {

        invoker();
    }
}

此时一般人会想,难道不是子类执行吗?因为子类的入参就是HashMap,肯定要调用这个。
但是此时要考虑一个问题,假如我们的本来意思是就是调用从父类继承的入参为Map的方法,但是程序执行的时候却自动为我们执行了子类的方法,此时就会导致混乱。
结论:子类中的方法的输入参数(前置条件或称形式参数)必须与父类中的输入参数一致或者更宽松(范围更大)。

LSP的定义含义4——覆盖或实现父类的方法时输出结果可以被缩小

理解:父类的返回类型为T,子类的返回类型为S,即LSP要求S<= T
此时分为两种情况

  • 如果时覆写,子类继承父类,继承的方法的入参必然相同,此时传入参数必须时相同或小于,返回的值必然不能大于父类返回值,这是覆写的要求。
  • 如果时重载,这时候要求子类重载方法的参数类型或数量不相同,其实就是保证输入参数宽于或等于父类输入参数,这时候就保证了子类的方法永远不会被执行,其实就是含义3。

LSP的目的及理解

  • 增强程序的健壮性
  • 保证即使增加子类,原有的子类仍然可以继续运行。
  • 从一方面来说,在程序中尽量避免直接使用子类的个性,而是通过父类一步一步的使用子类,否则直接使用子类其实就相当于直接把子类当作父类,这就直接导致父类毫无用途,父类和子类的关系也会显得没有必要存在了。
(0)

相关推荐

  • 方法重写

    方法重写 重写都是方法的重写,与属性无关 父类的引用可以指向子类,就是new一个子类的对象数据类型可以写为父类,new子类是可以指向父类的 重写的快捷键:Alt+insert 对静态方法调用的时候,调 ...

  • Java学习——35、子类的构造方法

    本文接上篇--34.类的继承. 子类可以继承父类的除了构造方法以外的所有成员,在子类创建对象时,必须对父类的变量进行初始化.但构造方法是不被继承的,故要在子类当中调用父类的构造方法. 如果子类中没有显 ...

  • 讲真,这三道Java入门级面试题,你也不一定能搞定

    想更好的应对面试,还是需要不断学习不断总结,下面我们来分析三道面试题. 涨薪必备的面试小抄 下面是一道入门级面试题,这道题基本上都是问初级的小伙伴比较多,但如果你是中级,或者高级.我觉得未必都能回答上 ...

  • 子类的构造方法

    子类可以继承父类的除构造方法和析构方法以外的所有成员,在子类创建对象时,必须对父类的变量进行初始化.但构造方法是不被继承的,故要在子类当中调用父类的构造方法. 如果子类中没有显式调用父类的构造方法,J ...

  • 面向对象(面向对象真的难吗,你只不过没有理清思路而已)

    面向对象真的难吗?其实我看不然,只不过我们学习的时候比较杂论,并且也没有真正领悟到他内部的强大.那么开始进入正题,众所周知面向对象三大概念:封装,继承,多态.封装中又可以实现构造方法,方法的重载.继承 ...

  • 最简单直接地理解Java软件设计原则之里氏替换原则

    理论性知识 定义 里氏替换原则,Liskov Substitution principle(LSP). 抽象定义是下面这样的 如果对每一个类型为T1的对象O1,都有类型为T2的对象O2,使得以T1定义 ...

  • 设计模式 - 七大设计原则(三)- 迪米特法则与里氏替换原则

    概述 简单介绍一下七大设计原则: 开闭原则:是所有面向对象设计的核心,对扩展开放,对修改关闭 依赖倒置原则:针对接口编程,依赖于抽象而不依赖于具体 单一职责原则:一个接口只负责一件事情,只能有一个原因 ...

  • 设计模式的七大原则(4) --里氏替换原则

    前言 上一节中我们介绍了,依赖倒置,依赖倒置利用抽象的稳定性来架构我们的系统,是我们经常能遇到的一种原则,比如说面向接口编程. 这一节中,我们来说说里氏替换原则,这个原则其实非常非常的简单,其实与依赖 ...

  • 六大设计原则(一)SRP单一职责原则

    单一职责原则SRP(Single reponsibility principle) BO(Business Object):业务对象 Biz(Business Logic):业务逻辑 SRP最简单的例 ...

  • 【资料】23种设计模式和六大设计原则

    程序IT圈 www.cxyquan.com 优秀的程序猿技术公众号 1 设计模式的六大原则 ☛开闭原则 对扩展开放,对修改关闭.在程序需要进行拓展的时候,不能去修改原有的代码,实现一个热插拔的效果.简 ...

  • 设计模式——六大设计原则

    文章目录 一.单一职责原则 二.里式替换原则 三.依赖倒置原则 四.接口隔离原则 五.迪米特法则 六.开闭原则 一.单一职责原则 单一职责原则简称 SRP,他想表达的就是字面意思,一个类只承担一个职责 ...

  • 【高考历史全能解题方法思路】第14讲 解答选择题遵循的六大基本原则(二)——全面原则

    与高考命题人对话  颠覆传统解题思路第14讲  解答选择题遵循的六大基本原则(二)--全面原则全面原则是指题干材料以分号.句号或省略号为标志,包含着并列关系的两层或三层意思,对应的正确选项也必须全面包 ...

  • 如何运用刻意练习原则(二十四)

    本书作者安德斯·艾利克森博士,是美国佛罗里达州立大学心理学教授,康拉迪杰出学者."刻意练习"法则研创者.他专注于研究体育.音乐.国际象棋.医学.军事等不同领域中的杰出人物如何获得杰 ...

  • 浅析交互设计的10种通用启发性原则,内附交互设计方法学习海报

    本文UXD将为同学们讲解由NN Group的Jakob Nielsen分享的交互设计的10条一般性通用原则.我们称之为"启发式"法则,因为它们是广泛的经验法则,而不是特定的可用性准 ...