里氏替换原则
定义:
所有引用父类的地方必须能够使用其子类的对象。
通俗点:
老爸能干的事情,儿子都能干,反过来儿子能干的事情,老爸未必可以。
案例讲解:
大家都打过CS吧,我们来描述下里面用到的枪:
枪的主要职责是射击,如何射击在各个具体的子类中定义,手枪是单发射程比较近,步枪威力大射程远,机枪用于扫射。在士兵类中定义类一个方法killEnemy,使用枪来杀敌人,具体使用什么枪来杀敌人,调用的时候才知道。
AbstractGun类的源程序代码:
手枪,步枪,机枪的实现类:
士兵的实现类:
注意:killEnemy方法中用枪杀敌,枪是抽象的,具体使用的枪需要通过setGun方法确定。
场景类:
有人,有枪,也有场景,运行结果:
士兵开始杀敌……
步枪射击……
在这个过程中,我们给三毛这个士兵一把步枪,然后开始杀敌了。如果三毛要使用机枪,当然也可以,直接把sanmao.setGun(new Rifle())修改为sanmao.setGun(new MachineGun())即可,在编写程序Solider士兵类根本就不用知道是那个型号的枪被传入。
注意:在类中调用其他类时务必使用父类或接口,如果不能使用父类或接口,则说明类的设计已经违背了LSP原则。
我们再来想一想,如果我们有一个玩具枪,该如何定义呢?
首先我们想,玩具枪是不能用来射击的,杀不死人的,这个不应该写在shoot方法中。新增加的ToyGun源码:
由于引入了新的子类,场景类中也使用了该类,Client稍作修改:
运行结果:
士兵开始杀敌人……
坏了,士兵拿着玩具枪来杀敌人,射不出子弹呀!如果在CS游戏中有这种事情发生,那你就等着被人爆头吧,然后看着自己凄惨的倒地。在这种情况下,我们发现业务调用类已经出现类问题,正常的业务逻辑已经不能运行,那该怎么办?
好办,两种解决方法:
-
在Soldier类中增加instanceof的判断,如果是玩具枪,就不用来杀敌人。这个方法可以解决问题,但是你要知道,在程序中,每增加一个类,所有与这个父类有关系的类都必须修改,你觉的可行么?如果你的产品出现了这个问题,因为修正了这样一个Bug,就要求所有与这个父类有关系的类都增加一个判断,客户非跳起来跟你干架不可!你还想要客户的忠诚于你么?显然,这个方案不行。
-
ToyGun脱离继承,建立一个独立的类,为了实现代码复用,可以与AbastractGun建立关联委托关系,如图:
例如:可以在AbastractToy中声明将声音,形状都委托给AbastractGun处理,仿真枪嘛,形状和声音都要和真实的枪一样了,然后两个基类下的子类自由延展,互不影响。
注意:如果子类不能完整的实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖,聚集,组合等关系代替继承。
子类可以有自己的个性:
还是刚才关于枪的例子,步枪有几个比较响亮的型号,比如:AK47,AUG狙击步枪等,把这两个型号的枪引入后Rifle子类如图:
很简单,AUG继承了Rifle类,狙击手(Snipper)则直接使用类AUG狙击步枪,源码如下:
有狙击枪就有狙击手,狙击手类的源码:
狙击手使用狙击枪来杀死敌人,业务场景Client类的源码:
狙击手使用G3杀死敌人,运行结果:
通过望远镜查看敌人……
AUG射击……
在这里,系统直接调用了子类,狙击手是很依赖枪支的,别说换一个型号的枪了,就是换一个同型号的枪也会影响射击,所以这里就直接把子类传递进来。这时候,我们能不能直接使用父类传递进来呢?
修改Client类:
显示不行的,会在运行期抛出java.lang.ClassCastException异常,这也是大家经常说的向下转型是不安全的,从里氏替换原则来看,就是有子类出现的地方父类未必就可以出现。
写在最后:
希望能通过上面的例子理解此原则,其实这个原则更多的是制定一种规则,在设计继承时,能够按照此原则来设计。
大旗不挥,谁敢冲锋-6大设计原则: