设计模式六大原则之 里氏替换原则

里氏替换原则

里氏替换原则 英文名称Liskov Subtitution Principle, LSP

定义

传说中最正宗的定义:

If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T, the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.

如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型.

传说中第二种比较通俗的定义:

Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it.

所有引用基类的地方必须能透明地使用其子类的对象。

更通俗的一种定义:

只要父类能出现的地方子类就可以出现,而且替换为子类也不会产生任何错误或异常,使用者可能根本就不需要知道是父类还是子类。但是反过来就不行了,有子类出现的地方,父类未必就能适应。

举例

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

cs游戏中的枪支类图:
cs游戏枪支类图

代码清单:

枪支的抽象类

1
2
3
4
public abstract class AbstractGun {
//枪用来干什么的?杀敌!
public abstract void shoot();
}

手枪、步枪、机枪的实现类:

1
2
3
4
5
6
7
public class Handgun extends AbstractGun {
//手枪的特点是携带方便,射程短
@Override
public void shoot() {
System.out.println("手枪射击...");
}
}
1
2
3
4
5
6
7
public class Rifle extends AbstractGun {
//步枪的特点是射程远,威力大
@Override
public void shoot() {
System.out.println("步枪射击...");
}
}
1
2
3
4
5
6
public class MachineGun extends AbstractGun {
@Override
public void shoot() {
System.out.println("机枪射击...");
}
}

士兵的实现类:

1
2
3
4
5
6
7
8
9
10
11
12
public class Soldier {
//定义士兵的枪支
private AbstractGun gun;
//给士兵一支枪
public void setGun(AbstractGun _gun){
this.gun = _gun;
}
public void killEnemy(){
System.out.println("士兵开始杀敌人...");
gun.shoot();
}
}

场景类:

1
2
3
4
5
6
7
8
9
public class Client {
public static void main(String[] args) {
//产生三毛这个士兵
Soldier sanMao = new Soldier();
//给三毛一支枪
sanMao.setGun(new Rifle());
sanMao.killEnemy();
}
}

运行结果如下:

1
2
士兵开始杀敌人...
步枪射击...

在这个程序中,我们给三毛这个士兵一把步枪,然后就开始杀敌了。如果要使用机枪,当然也可以,直接把sanMao.setGun(new Rifle())修改为sanMao.setGun(new MachineGun())即可,在编写程序时Solider士兵类根本不用知道是哪个型号的枪(子类)被传入。

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

如果我们要加入一把玩具枪,该如何处理呢?

第一种处理方式 ToyGun继承AbstractGun
继承方式拓展玩具枪

但是玩具枪毕竟是玩具,不能当作真枪来杀敌人,所以这样拓展并不妥当,更好的处理方式应该是如下图:

委托的方式来拓展玩具枪

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

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

子类可以有自己的个性

子类当然可以有自己的方法和属性了,这里再次强调,是因为里氏替换原则可以正着用,但是不能反过来用。在子类出现的地方,父类未必就可以胜任。

以刚才对枪支为例,步枪有几个比较“响亮”的型号,比如AK47、AUG狙击步枪等,把这两个型号的枪引入后的Rifle子类图如下:

步枪扩展

代码清单:

1
2
3
4
5
6
7
8
9
public class AUG extends Rifle{
//狙击枪都携带一个精准的望远镜
public void zoomOut(){
System.out.println("通过望远镜观察敌人...");
}
public void shoot(){
System.out.println("AUG射击...");
}
}

狙击手代码:

1
2
3
4
5
6
7
8
public class Snipper {
public void killEnemy(AUG aug){
//首先看看敌人的情况,别杀死敌人,自己也被人干掉
aug.zoomOut();
//开始射击
aug.shoot();
}
}

场景类:

1
2
3
4
5
6
7
8
public class Client {
public static void main(String[] args) {
//产生三毛这个狙击手
Snipper sanMao = new Snipper();
sanMao.setRifle(new AUG());
sanMao.killEnemy();
}
}

运行结果:

1
2
通过望远镜观察敌人...
AUG射击...

那么我们能不能传递Rifle父类给sanMao呢?如下所示:

1
2
3
4
5
6
7
8
public class Client {
public static void main(String[] args) {
//产生三毛这个狙击手
Snipper sanMao = new Snipper();
sanMao.setRifle((AUG)(new Rifle()));
sanMao.killEnemy();
}
}

显示是不行的,会在运行期抛出java.lang.ClassCastException异常,这也是大家常说的向下转型(downcast)是不安全的,从里氏替换原则来看,就是有子类出现的地方父类未必就可以出现。

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

即子类方法的参数应该大于父类方法的参数,即子类方法的参数应该至少与父类相同或者或者是其父类

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

即子类方法的返回值应该小于父类方法的返回值,即子类的返回值应该至多与父类的返回值相同或者是其子类

以上两点 归根结底一句话:重载或实现 子类的方法输入范围>=父类,返回范围<=父类L

坚持原创技术分享,您的支持将鼓励我继续创作!