里氏替换原则(爱恨纠葛的父子关系)

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

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

里式替换原则为良好的基础关系定义了一个规范,里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。它包含了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);
        
    }
}

那么当把父类替换成子类后,执行结果便是:
在这里插入图片描述
这和里氏替换原则中,父类替换成子类,执行结果不变相违背。

四、覆写或实现父类方法时输出参数可以被缩小

在子类覆写或实现父类方法时,子类返回的参数,应该和父类的参数保持一致,或者是父类参数的子类。

五、总结

采样里氏替换原则的目的,就是增加程序的健壮性,让项目在版本升级的同时,也可以保持非常好的兼容性,即使增加子类,原有的子类还可以继续运行。在实际项目中,每个子类对应不同的业务,使用父类作为参数,传递不同的子类去实现业务逻辑,非常完美!

猜你喜欢

转载自blog.csdn.net/qq_45171957/article/details/123294726