里氏原则为良好的继承定义了一个规范。

一句简单的定义包含了4层含义。

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

我们举个例子来说明这个原则,非常经典的游戏CS,我们来描述一下里面的枪支。类图如图所示
image.png
枪的主要作用是射击,如何射击正各个具体的子类中定义,手枪是单发射程比较近,步枪威力大射程远,机枪用于扫射。在士兵类里面定义一个方法killEnemy,使用枪来杀死敌人,具体用什么枪来杀敌人,调用的时候才知道。

代码清单 枪支的抽象类

package cn.yxnu.pattern_2;
//定义一个抽象父类 枪的父类
public abstract class AbstractGun {
    //枪用来干什么?杀敌
    public abstract void shoot();
}

代码清单 手枪、步枪、机枪的类

package cn.yxnu.pattern_2;
//手枪类 
public class HandGun extends AbstractGun{
    //手枪的特点是方便携带,射程短
    @Override
    public void shoot() {
        System.out.println("手枪射击...");
    }

}
package cn.yxnu.pattern_2;
//步枪类
public class Rifle extends AbstractGun{
    //步枪的特点是射程远,威力大
    @Override
    public void shoot() {
        System.out.println("步枪射击...");
    }

}
package cn.yxnu.pattern_2;
//机枪类
public class MachineGun extends AbstractGun{
    //机枪的特点是扫射
    @Override
    public void shoot() {
        System.out.println("机枪扫射...");
    }

}

有了枪支,还要有能使用这些枪支的士兵

代码清单 士兵类

package cn.yxnu.pattern_2;
//士兵类
public class Soldier {
    //定义士兵的枪支
    private AbstractGun gun;
    
    //给士兵一只枪
    public void setGun(AbstractGun abstractGun){
        this.gun = abstractGun;
    }
    
    //士兵有杀死敌人的方法
    public void killEnemy() {
        System.out.println("士兵杀敌人...");
        //枪射击
        gun.shoot(); 
    }
    
    
}

士兵使用什么枪来杀敌,但是这把枪是抽象的具体是手枪还是步枪还是机枪,需要在上战场前(也就是Client中)通过setGun()方法来确定。

代码清单 场景Client类

package cn.yxnu.pattern_2;
//里式替换原则

//这是一个场景类
public class Client {
    public static void main(String[] args) {
        //生成一个名字叫 三毛 的士兵
        Soldier sanMao = new Soldier();
        
        //给三毛一只枪 
        sanMao.setGun(new Rifle());  //给三毛的是步枪, 也可以给三毛手枪 或者是 机枪
        
        //三毛开始杀敌人了
        sanMao.killEnemy(); 
    }
}

有人,有枪,有场景,运行结果如下所示。
image.png
在这个过程中,我们可以给三毛这个士兵一把步枪,当然也可以给三毛其他的枪,只需要修改sanMao.setGun(new Rifle()) 修改成 sanMao.setGun(new MachineGun()) 即可。在编写Soldier士兵类根本就不用知道是哪个型号的枪被传入。


注意 在类中调用其他类时务必使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。


我们再来想一想,如果我们有一个玩具手枪,该如何定义呢?我们先在类图上增加一个类ToyGun,然后继承于AbstractGun类,修改后的类图如图所示。
image.png
首先我们想,玩具枪是不能用来射击的,杀不死人的,这个不应该写在shoot方法中。新增加的ToyGun的源代码如代码清单所示。

package cn.yxnu.pattern_2;

public class ToyGun extends AbstractGun{
    //玩具枪是不能射击的,但是编译器又要求实现这个方法,怎么办?虚构一个呗!
    @Override
    public void shoot() {
        //玩具枪不能射击,这个方法就不实现了
    }
}

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

package cn.yxnu.pattern_2;
//里式替换原则

//这是一个场景类
public class Client {
    public static void main(String[] args) {
        //生成一个名字叫 三毛 的士兵
        Soldier sanMao = new Soldier();
        
        //给三毛一只枪 
        //sanMao.setGun(new Rifle());  //给三毛的是步枪, 也可以给三毛手枪 或者是 机枪
        sanMao.setGun(new ToyGun());  //给三毛的是玩具枪,玩具枪是不能用来杀敌的
        
        //三毛开始杀敌人了
        sanMao.killEnemy(); 
    }
}

把玩具枪传递给三毛用来杀敌,代码运行结果如下
image.png
坏了,士兵拿着玩具来杀敌人,射不出子弹呀!如果在CS游戏中,那就等着爆头吧,然后看着自己凄惨倒地。在这种情况下,我们发现业务调用类已经出现了问题,正常的业务逻辑已经不能运行,那怎么办么?好办,有两种解决办法:

在Soldier类中增加instanceof的判断,如果是玩具枪,就不用来杀敌人。这个方法可以解决问题,但是你要知道,在程序中每增加一个类,所有与这个父类相关的类都必须修改,你觉得可行吗?显然这个方案被否定了。

ToyGun脱离继承,建立一个独立的父类,为了实现代码的复用,可以与AbstractGun建立关联委托的关系。
image.png
例如:例如可以将AbstractToy中声明将声音、形状都委托给AbstractGun处理,仿真枪嘛,形状和声音都要和真枪一样了,然后两个基类下的子类自由延展,互不影响。

在Java的基础知识中都会讲到继承,Java的三大特征嘛,封装、继承、多态。继承就是告诉你拥有父类的方法和属性,然后你就可以重写父类的方法。按照继承原则,我们上面的玩具枪继承AbstractGun是绝对没有问题的,玩具枪也是枪嘛,但是在具体应用场景中就要考虑下面这个问题了:子类是否能够完整地实现父类的业务,否则就会出现像上面的拿枪杀敌人时却发现是把玩具枪的笑话。


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


2.子类可以有自己的个性

3.覆盖(重载)或实现父类的方法时输入参数可以被放大

4.覆写(重写)或实现父类的方法时输出结果可以被缩小


周周架构师
292 声望409 粉丝