设置模式原则(2)里氏替换原则

里氏替换原则(LSP、Liskov Substitution Principle):Functions that use pointers or references to base classes must be able to use objects of derived classes without knowing it;所有引用基类的地方必须能透明地使用其子类的对象。
哪怕你不知道这个名字,你也会在开发中大量的运用。

在面向对象编程中很多时候会用到继承,继承的好处就不必多说了,先来说说这里带来的一些“麻烦”吧;继承是入侵性的,其降低了代码的灵活性,增强了耦合。
我们知道很难有两全其美的策略。但我们可以在一定范围内把优点进行放大,把缺点进行缩小或尽可能的规避。LSP正是一种放大继承优势的规则或说是策略。

以下是几条相关黄金规则

1、子类必须完全实现父类的的方法
相信这条是毫无疑问的了。就比如一个抽象类枪(Gun)带有一个方法shoot进行射击,可以有各种枪(手枪、步枪、冲锋枪)继承自Gun,那在继承的时候是不是要重写shoot方法呢?毕竟不同的枪有不同的射击方式嘛。
在实际业务中有了枪就会有枪的使用者:士兵(Soldier),在抽象代码设计中我们知道士兵只要知道手上有枪可以上战场了,而不需要关注具体是什么枪。所以对于士兵来说只要传入Gun对象就ok了。问题来了;如果传入的是玩具枪呢?那玩具枪可不能杀敌呀,这个时候重写shoot时就得区别对待了。那可以在Soldier中利用instanceOf对传入的对象进行判断吗?对应的功能是可以实现的。但这样的话是不是每增加一个Gun的子类都需要改动Soldier代码呢?
还有一种方法需要反过来思考一下。玩具枪是否是可以归属到Gun的类型呢?按我们的业务定义来看,Gun是可以用于杀敌的。按这个点出发,我们得考虑是否要新增一个玩具类了。
这里有了一条来自秦小波老师的建议:如果子类不能完整地实现父类的方法,或者父类的某些方法在子类中已经发生“畸变”,则建议断开父子继承关系,采用依赖、聚集、组合等关系代替继承。

2、子类可以有自己的个性
相信这也是不言而喻的了,子类若没有自己的个性还要子类干嘛!
我们知道,在接口设计时很多时候涉及到向上转型(子类对象传给父类),这可以大大的减少代码冗余,也是LSP的核心所在。那反过来向下转型是否可以呢?就比如步枪(Rifle)下面衍生一个狙击枪(Aug)子类,把Aug放在Soldier的传入参数中,实际测试代码传入的是Rifle(当然这里需要一个强转)。小伙伴们可能也猜到了结果了:ClassCastException;向下转型是不安全的,也就是说LSP不能反着用,其中原因之一就是子类有了自己的个性而父类中没有。

3、子类覆盖(重载)或实现父类的方法时输入参数可以放大
先来段代码热热身

class Father{
     public Collection getValues(HashMap map) {
          System.out.println("father running...");
          return map.values();
     }
}
class Son extends Father{
     public Collection getValues(Map map) {
          System.out.println("son running...");
          return map.values();
     }
}
public class T3 {  
     public static void main(String[] args) {
          Father father=new Father();
          HashMap map=new HashMap();
          father.getValues(map);
     }
}

以上代码的输出结果是啥?tip:注意看子父类方法的传入参数!结果是:father running…
问题又来了:如果把测试代码 Father father=new Father()改成Son father=new Son()呢?结果不变。
以上代码是重载哦!记得在重载的情况下根据传入参数会有一个就近匹配原则。对于以上结果也是很清晰的了。

另外一种情况:如果把子父类的传入参数类型调换一下呢?代码如下:

class Father{
     public Collection getValues(Map map) {
          System.out.println("father running...");
          return map.values();
     }
}
class Son extends Father{
     public Collection getValues(HashMap map) {
          System.out.println("son running...");
          return map.values();
     }
}
public class T3 {  
     public static void main(String[] args) {
          Father father=new Father();
          HashMap map=new HashMap();
          father.getValues(map);
     }
}

这个时候的输出依旧是father running…
测试代码换一种写法,还是和上面一样,按LSP,把测试代码 Father father=new Father()改成Son father=new Son()
结果是son running…
问题出现了!个人不知道这算是算是一种问题,从重载的角度来讲,这并没有问题;但从原本的重写设计角度来讲,子类在未重写父类方法的情况下,却调用了子类方法;这直接导致了业务的混乱。也即是说子类中方法的前置条件(传参类型)必须与超类中被覆写(重写)的方法的前置条件相同或者更宽松(为父类传参类型本身或其传参类型父类)。

4、覆写(重写)或实现父类的方法时输出结果可以被缩小
这算是LSP的另一种更加传统的解释,这也是在面向对象编程中需要遵循的规则。也就是说子类在重写父类方法时,返回值类型需要为父类返回值类型本身或其子类。究其根本,和上述第二条规则类似;向上转型。

参考文献
秦小波《设计模式之禅 》第二版

发布了130 篇原创文章 · 获赞 39 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/qq_34901049/article/details/104047435