设计模式系列之设计原则(6)里氏替换原则

定义

从不同出发点,该原则的定义存在以下两种描述方式。定义一从条件出发,得出结论。定义二从结论出发,得出特点。定义一和定义二在描述方向上恰好相反,不过两者的目的都是要强调类间建立继承关系时需要遵循的条件

  • 定义一 描述了构成父子关系需要满足的条件,即类间能够透明替换----->是父子关系。
    如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有对象o1都替换成o2时,程序P的行为没有发生变化,那么类型T2是类型T1的子类型。
  • 定义二 描述了构成父子关系的类具备的特点 即类间是父子关系------>可以透明替换
    子类对象能够替换父类对象,所有引用父类的地方必须能够透明地使用其子类的对象,而程序逻辑不变。

作用

  • 里氏替换原则是实现开闭原则的重要方式之一。
    • 开闭原则中,扩展类有继承父类和实现接口两种方式。里氏替换原则是通过约束子类行为(可透明替换父类,不出出现逻辑混乱)。因此可以说里氏替换原则是实现开闭原则的重要方式。
  • 减少系统中的继承泛滥问题
    • 从定义可知,是否满足里氏替换原则可以作为类间是否能建立继承关系的依据。由于继承会增强类间的耦合性、降低程序可移植性等问题,一般会采用组合、聚合等方式替换继承关系。从这个角度出发,里氏替换原则就是为了提供一个准则,减少软件实践过程中滥用继承。通俗的讲,如果一定要用继承,就必须满足里氏替换原则。否则,就别用继承。
  • 减少继承带来的复杂度
    • 类中实现的方法实际上是定义了规范和锲约,子类如果重写父类的方法,会破坏继承体系,需要不断地思考不同派生类的实现在语义上的差异,此时继承就会增加软件的复杂度。在必须使用继承的地方,符合里氏替换原则的设计不必考虑派生类对父类的侵入,也就不会增加系统复杂度。

软件的首要技术使命就是管理复杂度。

实现方法

里氏替换原则通俗来讲就是:子类可以扩展父类的功能,但不能改变父类原有的功能。也就是说:子类继承父类时,除添加新的方法完成新增功能外,尽量不要重写父类的方法。在软件实践中,遵循里氏替换原则首先要确定类间是否能构成继承关系,这需要充分理解业务,将业务抽象为软件实体模型。在确定继承关系之后,再规范具体的技术细节,主要有以下几点:

  • 子类可以实现父类的抽象方法,但不能覆盖父类的非抽象方法
    • 对于抽象方法,子类是必须要实现的。对于非抽象方法,子类如果将其覆盖,比如说重写父类某一个非抽象方法逻辑,则在父类出现的位置,很可能子类无法透明的替换父类,系统逻辑发生变化。此时子类侵入了父类逻辑,影响了系统的稳定性。
  • 子类中可以增加自己特有的方法
    • 子类增加特有方法不会侵入父类的原有内容,子类依然可以透明地替换父类。同时,在子类中增加特有方法遵从了开闭原则,正是系统可扩展性的体现。
  • 当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松
    • 因为父类方法的参数类型相对较小,所以当传入父类方法的参数类型,重载的时候优先匹配父类的方法,而子类的重载方法不会匹配,因此仍保证执行父类的方法(子类继承的时候其实操作的是子类中的父类成分),所以业务逻辑不会改变。
    • 按照本条正确约束,父类方法F1(HashMap)的入参为HashMap类型,子类重载方法F1(Map)的入参为Map类型,则在客户端中调用F1(HashMap)方法处,将父类对象替换为子类对象。假设客户端向这个方法传递实参类型为HashMap类型,则子类对象依然调用父类的F1(HashMap)方法,而不是子类重载的F1(Map)方法。此时是一种透明替换。
    • 举个反例,父类方法F1(Map)的入参为Map类型,子类重载方法F1(HashMap)的入参为HashMap类型,则在客户端中调用F1(Map)方法处,将父类对象替换为子类对象,假设客户端向这个方法传递实参类型为HashMap类型,则子类对象调用的是子类新添加的重载F1(HashMap)方法,而不是父类中的F1(Map)方法,按照透明替换的要求,此处仍然应该调用父类的F1(Map)方法。如果子类的F1(HashMap)方法和父类的F1(Map)方法体的业务逻辑不一致,则系统的逻辑发生变化,此时不是透明替换。
    • 理解这一层含义要抓住三个点,“重载”、“替换”、参数为“小”类型。
    • 实例参看代码实践小节。
  • 当子类的方法实现父类的方法时(重写/重载或实现抽象方法),方法的后置条件(即方法的的输出/返回值)要比父类的方法更严格或相等
    • 按照本条正确的约束,父类方法F1的返回值是Map类型,子类的实现F1方法之后的返回值是HashMap类型。则在客户端中调用父类的F1方法的位置处,替换为子类对应的方法,由于HashMap也是一种Map,即发生了向上转型,此时系统的逻辑不发生变化,这是一种正确的替换。
    • 举个反例,父类方法F1的返回值是HashMap类型,子类的实现F1方法之后的返回值是Map类型。则在客户端中调用父类的F1方法的位置处,替换为子类对应的方法,由于Map不是一种HashMap,即发生了向下转型,这种情况无法通过编译器。

代码实践

以下代码主要验证里氏替换原则中“当子类的方法重载父类的方法时,方法的前置条件(即方法的输入参数)要比父类的方法更宽松”的含义

  • 正常情况,父类参数(HashMap)比子类参数(Map)范围“小”。
//父类
//setList的入参为HashMap
public class Base {
    public void setList(HashMap ma){
        System.out.println("执行父类方法逻辑");
    }
}
//子类
//setList的入参为Map
public class Children extends Base {
   public void setList(Map map){
       System.out.println("执行子类逻辑");
   }
}

//应用客户端
//系统逻辑不变,是透明替换
public class Test {
    public static void main(String[] args) {
        HashMap hashMap = new HashMap();
        //父类调用
        Base base = new Base();
        base.setList(hashMap); //输出:“执行父类方法逻辑”

        //替换为子类对象,仍然调用的父类的setList方法,系统逻辑不变
        Children children = new Children();
        children.setList(hashMap);//输出:“执行父类方法逻辑”
    }
}
  • 反例,父类入参(Map)比子类入参(HashMap)范围“大”
//父类
//setList的入参为Map
public class Base {
    public void setList(Map ma){
        System.out.println("执行父类方法逻辑");
    }
}
//子类
//setList的入参为HashMap
public class Children extends Base {
    public void setList(HashMap map){
        System.out.println("执行子类方法逻辑");
    }
}

//应用客户端
//系统逻辑发生变化,不是透明替换
public class Test {
    public static void main(String[] args) {
        HashMap hashMap = new HashMap();
        //父类调用
        Base base = new Base();
        base.setList(hashMap); //输出:“执行父类方法逻辑”

        //替换为子类对象,未调用的父类的setList方法
        //调用的是子子类的setList方法,系统逻辑发生变化
        Children children = new Children();
        children.setList(hashMap);//输出:“执行子类方法逻辑”
    }
}

编程知识点

  • 重载overload:方法名字必须一样,参数(类型或数量)必须不一样,可以通过方法名和参数确定一个方法。返回值可以一样,也可以不一样,无法根据返回类型区分函数重载。在同一个类中。
  • 重写override:比重载条件更严格,方法签名必须一样,包括方法名称、参数类型和数量、返回值类型。在子类中。

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值