设计原则之里氏替换原则

里氏替换原则

定义:
如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有对象o1都替换成o2时,程序P的行为没有发生变化,那么类型S就是类型T的子类型。
所有引用基类的地方必须能透明地使用其子类对象。

首先看继承的优缺点:

继承的优点

1.代码共享,提高代码的复用性,每个子类都拥有父类的属性和方法
2.子类虽然拥有父类的方法,但是也可以拥有自己的特性。即可扩展性

继承的缺点

1.继承有侵入性,只要继承,那么就会拥有父类的属性和方法
2.因为子类拥有父类的属性和方法,所以子类的约束也就变多了,也就降低了子类的灵活性
3.增强了耦合性,当父类的常量,变量,方法被修改时,子类也要随之改变

里氏替换原则就是定义了继承的规范:

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

通常我们在设计系统时,会先定义接口或者是抽象类,然后编码实现,调用类则直接传入接口或抽象类。

①设计一个接口或者是抽象类

public abstract class AbstractGun {
    /**
     * 射击
     */
    public abstract void shoot();
}

②设计实现类

public class HandGun extends AbstractGun {
    @Override
    public void shoot() {
        System.out.println("手枪射击敌人");
    }
}

public class MachineGun 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 abstractGun;

    public void setAbstractGun(AbstractGun abstractGun) {
        this.abstractGun = abstractGun;
    }

    public void killEnemy() {
        //这里注意,在类中如果调用其他类时,务必要使用父类或者抽象类接口,否则就违背了里氏替换原则
        abstractGun.shoot();
    }
}

里氏替换原则第一条:

子类必须完全实现父类的方法
注意事项
1.在类中调用其他类时,一定传入的是接口或者抽象类,否则违背了里氏替换原则
2.如果子类不能完全实现父类的方法,或者父类的方法在子类中产生畸变,那么建议断开父子继承关系

子类可以有自己的个性

里氏替换原则第二条:

在父类能出现的地方子类一定能出现,但是子类能出现的地方,父类不一定能出现。

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

如果是重载被放大,则导致只会执行父类方法,子类方法不会被执行
public class Father {
    public Collection doSomething(HashMap map){
        System.out.println("父类的方法被执行");
        return map.values();
    }
}

public class Son extends Father {
    //注意这里是重载,不是重写(覆盖)
    public Collection doSomething(Map map){
        System.out.println("子类的方法被执行");
        return map.values();
    }
}

测试:

public class Client {
    public static void main(String[] args) {
        invoker();
    }

    public static void invoker(){
        Father f = new Father();
        HashMap map = new HashMap();
        f.doSomething(map);
    }
}

执行了父类的方法。

根据里氏替换原则,将父类出现的地方替换成子类。
测试:

public class Client {
    public static void main(String[] args) {
        invoker();
    }

    public static void invoker(){
        //根据里氏替换原则,父类出现的地方,子类一定可以出现,所以我们修改
        //Father f = new Father();
        Son f = new Son();
        HashMap map = new HashMap();
        f.doSomething(map);
    }
}

执行了父类的方法,子类方法永远不会被执行。

如果父类的前置条件大,子类的前置条件小
public class Father {
    public Collection doSomething(Map map){
        System.out.println("父类的方法被执行");
        return map.values();
    }
}

public class Son extends Father {
    //注意这里是重载,不是重写(覆盖)
    public Collection doSomething(HashMap map){
        System.out.println("子类的方法被执行");
        return map.values();
    }
}

测试:

public class Client {
    public static void main(String[] args) {
        invoker();
    }

    public static void invoker(){
        //根据里氏替换原则,父类出现的地方,子类一定可以出现,所以我们修改
        //Father f = new Father();
        Son f = new Son();
        HashMap map = new HashMap();
        f.doSomething(map);
    }
}

测试结果,子类的方法被执行了。但是这不对啊,如果父类是一个抽象类,你在没有重写父类抽象方法的情况下,子类方法被执行了,这是不符合你设计的业务逻辑的。

总结:在重写或实现父类的方法时,子类的前置条件应该比父类的前置条件相同或者更宽松,但是不能比父类的前置条件小。

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

意思就是,如果父类的方法返回值类型时T,子类重写或重载父类方法的返回值类型是S,那么要求S必须小于等于T。

总结:
继承给我们带来便捷,但是也带来了约束,在设计时,满足里氏替换原则必须规范以下4点:

  1. 子类必须完全实现父类的方法,在类中调用其他类时,必须使用接口或者抽象类,否则违反里氏替换原则
  2. 子类可以拥有自己的个性,里氏替换原则规定,在父类出现的地方一定能够出现子类,但是在子类能出现的地方,不一定能出现父类
  3. 重写或实现父类的方法时,子类的前置条件应该大于等于父类的前置条件
  4. 重写或实现父类的方法时,子类的返回值类型应该小于等于父类的返回值类型
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值