设计模式|里氏替换原则(爱恨纠葛的父子关系)

在六大设计原则中,里式替换原则,是一个关于父类和子类关系的原则,这个原则规定了,在软件开发中,父类出现的地方,把父类替换成子类,系统的功能应该不能受到影响。
里氏替换原则的主要作用如下。

它克服了继承中重写父类造成的可复用性变差的缺点。 它是动作正确性的保证。即类的扩展不会给已有的系统引入新的错误,降低了代码出错的可能性。 加强程序的健壮性,同时变更时可以做到非常好的兼容性,提高程序的维护性、可扩展性, 降低需求变更时引入的风险。

里式替换原则为良好的基础关系定义了一个规范,里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含了4层含义。
1、子类必须完全实现父类的方法 2、子类可以有自己的个性 3、覆盖或者实现父类时的输入参数可以被放大 4、覆写或实现父类方法时输出参数可以被缩小

一、子类必须完全实现父类的方法 我们在做系统设计的时候,一般都会根据需求,首先定义一个接口或者是抽象类,然后再对接口或者是抽象类进行编码实现,调用类以接口或者抽象类作为函数的参数,接收接口或者是抽象类的实现,在这里,就已经使用了里氏替换原则了。里氏替换原则规定,在父类作为参数的方法中,传入任何一个子类,其功能不应该受任何影响。
让我们来举个例子说明一下,比如说,大家都玩过CS吧,我们可以使用不同的枪去和敌人突突突,用类图来描述这个场景:
设计模式|里氏替换原则(爱恨纠葛的父子关系)
文章图片

士兵装备好枪,然后用枪进行射击。
代码如下:
枪抽象类:
public abstract class AbstractGun {public abstract void shoot(); }

手枪实现类:
public class HandGun extends AbstractGun { @Override public void shoot() { System.out.println("手枪射击"); } }

步枪实现类
public class Rifle extends AbstractGun { @Override public void shoot() { System.out.println("步枪射击"); } }

士兵类:
public class Soldier {private AbstractGun gun; void setGun(AbstractGun gun){ this.gun = gun; }void killEnemy(){ System.out.println("士兵上战场"); gun.shoot(); }}

game类主方法
public class Game { public static void main(String[] args) { Soldier soldier = new Soldier(); soldier.setGun(new HandGun()); soldier.killEnemy(); soldier.setGun(new Rifle()); soldier.killEnemy(); } }

类的结构很简单,我们可以为士兵设置不同的枪,用不同的枪去射击,之后如果有新的类型的枪,再去定义新的枪,只要遵循完全实现父类(完成射击的需求),代码都不会出问题。
但是如果我们再去定义的枪是玩具枪呢?
玩具枪类:
public class ToyGun extends AbstractGun {//玩具枪也是枪,但是玩具枪无法射击呀? @Override public void shoot() { System.out.println("玩具枪不能发射子弹"); } }

这个时候,如果把玩具枪给士兵,玩具枪也是枪的子类,所以士兵会去接收玩具枪,然后用玩具枪上战场,那会是什么后果,直接被敌人爆头了。
那应该怎么办?
1、在soldier类中判断,如果是玩具枪,就被用来杀敌,做特殊处理,但是这样会有什么后果,这样做,意味着程序中所有AbstractGun作为参数的函数,全部都要做玩具枪枪的判断,这简直太可怕了,显然,这个方法被否决了。
2、断开继承关系,建立一个独立的父类,如下图。
设计模式|里氏替换原则(爱恨纠葛的父子关系)
文章图片

所以,里氏替换原则约束我们:子类是否可以完整的实现父类的业务,如果不能,那就得做其他考虑了。
二、子类可以有自己的个性。 子类当然会有自己独有的行为和特征,所以在第一条的基础上,里氏替换原则规定了,在子类出现的地方,父类未必能出现,换句话说就是,用子类作为参数的函数,传递父类进去,程序有可能会出错。
还是用枪来举例子:
步枪分为普通步枪和狙击步枪,普通步枪就比如AK-47,直接就能突突突,而狙击步枪,就是那种带瞄准镜的,可以先瞄准,再突突突,而狙击手,可以用狙击枪进行射击。
类图如下:
设计模式|里氏替换原则(爱恨纠葛的父子关系)
文章图片

狙击手需要一把AUG狙击步枪进行射击
AUG类:
public class AUG extends Rifle {public void zoomOut(){ System.out.println("用望远镜观察敌人"); } }

狙击手类:
public class Snipper { private AUG aug; void setAug(AUG aug){ this.aug = aug; }void killEnemy(){ System.out.println("AUG射击。。。。"); }}

main方法:
public class Client { public static void main(String[] args) { Snipper snipper = new Snipper(); snipper.setAug(new AUG()); snipper.killEnemy(); snipper.setAug((AUG) new Rifle()); snipper.killEnemy(); } }

运行结果:
设计模式|里氏替换原则(爱恨纠葛的父子关系)
文章图片

很明显,这个方法只能使用子类,如果用父类去作为参数,会报转换异常,从里氏替换原则来看,有子类出现的地方,父类未必可以出现。
三、覆盖或者实现父类时的输入参数可以被放大 【设计模式|里氏替换原则(爱恨纠葛的父子关系)】子类在重写父类方法的时候,如果父类的参数是HashMap,那么子类的参数就必须更加的宽松,比如Map或者Map,如果父类是Map,那子类只能是Map.
举个例子:
我有一个father类和son类,father类是son的父类
public class Father {public Collection doSomething(HashMap map){ System.out.println("父类被执行。。。。"); return map.values(); } }class Son extends Father { public Collection doSomeThing(Map map){ System.out.println("子类被执行。。。。"); return map.values(); } }public class Client {public static void main(String[] args) { //里氏替换原则中,父类出现的地方,子类也可以出现 //Father f = new Father(); Son f = new Son(); HashMap hashMap = new HashMap(); f.doSomething(hashMap); } }

上面的代码,定义了三个类,在father类中,父类的参数是Map,子类的参数是HashMap,我们知道Map是HashMap的父类,所以给父类的参数是hashmap,依然能正常运行,上面的代码,注释的代码
//Father f = new Father(); Son f = new Son();

不管用那个,执行结果都是一样,都是
设计模式|里氏替换原则(爱恨纠葛的父子关系)
文章图片

因为子类的参数比父类宽松,所以把父类替换成子类,结果不会受到影响。
如果父类参数比子类宽松(父类是Map,子类是HashMap)
public class Father {public Collection doSomething(Map map){ System.out.println("父类被执行。。。。"); return map.values(); } }class Son extends Father { public Collection doSomeThing(HashMap map){ System.out.println("子类被执行。。。。"); return map.values(); } }public class Client {public static void main(String[] args) { //里氏替换原则中,父类出现的地方,子类也可以出现 //Father f = new Father(); Son f = new Son(); HashMap hashMap = new HashMap(); f.doSomething(hashMap); } }

那么当把父类替换成子类后,执行结果便是:
设计模式|里氏替换原则(爱恨纠葛的父子关系)
文章图片

这和里氏替换原则中,父类替换成子类,执行结果不变相违背。
四、覆写或实现父类方法时输出参数可以被缩小 在子类覆写或实现父类方法时,子类返回的参数,应该和父类的参数保持一致,或者是父类参数的子类。
五、总结 采样里氏替换原则的目的,就是增加程序的健壮性,让项目在版本升级的同时,也可以保持非常好的兼容性,即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务,使用父类作为参数,传递不同的子类去实现业务逻辑,非常完美!

    推荐阅读