里氏原则为良好的继承定义了一个规范。
一句简单的定义包含了4层含义。
1.子类必须完全实现父类的方法
我们举个例子来说明这个原则,非常经典的游戏CS,我们来描述一下里面的枪支。类图如图所示
枪的主要作用是射击,如何射击正各个具体的子类中定义,手枪是单发射程比较近,步枪威力大射程远,机枪用于扫射。在士兵类里面定义一个方法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();
}
}
有人,有枪,有场景,运行结果如下所示。
在这个过程中,我们可以给三毛这个士兵一把步枪,当然也可以给三毛其他的枪,只需要修改sanMao.setGun(new Rifle()) 修改成 sanMao.setGun(new MachineGun()) 即可。在编写Soldier士兵类根本就不用知道是哪个型号的枪被传入。
注意 在类中调用其他类时务必使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。
我们再来想一想,如果我们有一个玩具手枪,该如何定义呢?我们先在类图上增加一个类ToyGun,然后继承于AbstractGun类,修改后的类图如图所示。
首先我们想,玩具枪是不能用来射击的,杀不死人的,这个不应该写在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();
}
}
把玩具枪传递给三毛用来杀敌,代码运行结果如下
坏了,士兵拿着玩具来杀敌人,射不出子弹呀!如果在CS游戏中,那就等着爆头吧,然后看着自己凄惨倒地。在这种情况下,我们发现业务调用类已经出现了问题,正常的业务逻辑已经不能运行,那怎么办么?好办,有两种解决办法:
在Soldier类中增加instanceof的判断,如果是玩具枪,就不用来杀敌人。这个方法可以解决问题,但是你要知道,在程序中每增加一个类,所有与这个父类相关的类都必须修改,你觉得可行吗?显然这个方案被否定了。
ToyGun脱离继承,建立一个独立的父类,为了实现代码的复用,可以与AbstractGun建立关联委托的关系。
例如:例如可以将AbstractToy中声明将声音、形状都委托给AbstractGun处理,仿真枪嘛,形状和声音都要和真枪一样了,然后两个基类下的子类自由延展,互不影响。
在Java的基础知识中都会讲到继承,Java的三大特征嘛,封装、继承、多态。继承就是告诉你拥有父类的方法和属性,然后你就可以重写父类的方法。按照继承原则,我们上面的玩具枪继承AbstractGun是绝对没有问题的,玩具枪也是枪嘛,但是在具体应用场景中就要考虑下面这个问题了:子类是否能够完整地实现父类的业务,否则就会出现像上面的拿枪杀敌人时却发现是把玩具枪的笑话。
注意 如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。