设计模式之六大设计原则之《二》朦胧的里氏替换原则

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u013821237/article/details/83341231

参考书籍:设计模式之禅--秦小波

上篇回顾:上期讲到了单一职责原则(SRP),经验浅薄,也是读设计模式之禅的复习。讲述的不好,各位见谅!上篇留下来的接口Iphone是否符合SRP原则呢?好像我们平时确实是这么写的:拨号,通话,挂断。这些似乎都是打电话的过程。但是一旦开始开始编写细节代码就会发现,好像不是那回事:它实际包含了两个职责:一个是协议管理(根据号码接通挂断),一个是数据传送(模拟信号和数字信号的两次转换)。协议接通的方式改变了会导致这个接口或实现类的改变吗?会的。数据传送变成语音通话和视频通话,是不是也要修改类?是的。这就明显了,有两个原因会导致类的变化。这两个职责会相互影响吗?协议管理,不管是电信还是网通,只要能接通挂断就好,接通后并不关心发送的是语音还是视频。既然不相关,那就可以考虑拆分成两个接口:

这样,不管拨号方式变了,还是传输方式变了,都可以独立的修改,不再互相影响。


里氏替换原则:

定义:里氏替换原则,英文全称(Liskov Substitution Principle,LSP)。如何理解这个原则呢?一句话:如果对每一个类型为S的对象o1,都有类型为T的对象o2,使得以T定义的所有程序P在所有的对象o1都代换成o2时,程序P的行为没有发生变化,那么类型S是类型T的子类型。(If for each object o1 of type S there is an object o2 of type T such that for all programs P defined in terms of T,the behavior of P is unchanged when o1 is substituted for o2 then S is a subtype of T.)这个说法依旧有点生硬,通俗来说就是:使用父类的地方都可以直接替换成子类,且不会产生任何错误或者异常。

以面向对象的Java语言为例,大家都知道Java具有三大特性:抽象、继承和多态。里氏替换原则所指的替换即是父与子的替换原则。说白了是针对继承和多态的原则。为什么要有继承?因为继承有以下几个明显的优点:

  1. 减少代码量提高开发和运行效率。子类具有父类的属性和方法,这些代码都被复用省的再写一遍,是不是很方便。
  2. 提高扩展性。所谓提高,是指在原有的类的基础上衍生出更具独立特色功能的类,俗话说青出于蓝而胜于蓝就是这个道理。

同时,继承如果没有规范和策略也会带来负面影响:

  1. 首先是子类必须拥有父类的属性和方法导致的侵入性和臃肿。
  2. 其次父子类的耦合,父类的常量、变量、方法修改可能会引起子类的修改。

趋利避害!里氏替换原则来帮忙!

里氏替换原则可以分为以下几个约束:

  • 子类需要继承父类的抽象方法,但是不能覆盖父类的非抽象方法。

比如有个父类(也称超类)是鸟类(Bird),麻雀,大雁等具体鸟继承了Bird类,Bird类有一个fly()的方法,麻雀,大雁实现了这个方法。

Bird bird1=new Maque();
bird1.fly();//这个时候bird1虽然是Bird类,执行的却是麻雀的飞行方法。
Bird bird2=new Dayan();
bird2.fly();//这个时候bird1虽然是Bird类,执行的却是大雁的飞行方法。
//从这里是不是发现,当我们需要执行鸟类飞行的时候,无需关心具体实现是谁,这就是运行时多态。

这时来了个麻烦,鸵鸟也来继承Bird了,但是我们发现鸵鸟不会飞:

Bird bird3=new Tuoniao();
bird3.fly()//你...我...他...怎么办,鸵鸟不会飞。

我们需要在父类里做判断,如果是鸵鸟,就不让它飞了。但是如果写Bird类的人找不到了怎么办?修改带来的风险谁来背?当遇到这个情况的时候,一定不要拘泥于鸵鸟也是鸟的论断,事实上,鸟,鸵鸟这些都只是设计的一个类,一个抽象。如果子类不能完整的描述父类,就断开父子继承的关系使用其他比如依赖的方式代替。

  • 子类可以有自己的方法,这些方法正是青出于蓝的所在。

比如 动物作为父类Animal,猪,鸡都是具体的动物类(Animal)的子类,猪是哺乳的,鸡是卵生的,猪肥肥的很可爱,鸡身上有羽毛...这些都是子类特有的属性。但是反过来,我们又不能说猪和鸡不是动物,因为,他们确实就是动物啊。

  • 当子类覆盖或实现父类的方法时,方法的前置条件(即方法的形参)要比父类方法的形参更宽松。

这点有点不好理解,为什么?难道不应该父类更宽松吗?这里我们举个例子:

class Father{
      public void do(Number o)
        {
            System.out.print("爸爸有数有数");
        }
}

class Son extends Father{
      public void do(Integer o)
        {
             System.out.print("是整数啊");
        }
}

Father f = new Father();
f.do(1);//执行的是父类的方法
//根据里氏替换原则,父类存在的地方都可以换为子类,修改下
Son s=new Son();
s.do(1);//执行的子类的方法

也许你会觉得,没问题啊,子类对象调用自己的方法没问题啊。我们知道,父类一般都是抽象类,传入这样的子类歪曲了父类的意图,即你想让子类依旧能完成父类的任务,却发现无法调用到,直接被子类消化掉了。原因就出在参数范围上,java总是找到参数最匹配的,以保证方法调用的唯一可靠性。所以当子类的参数范围比父类小时,父类就会被屏蔽掉,反之,如果子类参数范围比父类大,则这时候优先匹配到的是父类的方法。

贴出重载和覆写的简单区别:

  1. 重载:方法名相同,参数不同(个数或者类型),与返回值无关。
  2. 覆写:子承父类,对相同的方法有不同的实现。
  • 当子类的方法实现父类的抽象方法时,方法的后置条件(即方法的返回值)要比父类更严格。

这点稍微好理解一点,比如父类的某个方法返回的是数字类型,那么子类实现的方法返回int型、float型都是符合的,因为他们都是数字。但是你返回String肯定就不行,与父类的初衷背道而驰。当然这里数字类型和String并无关系,只是举例。也就是说子类实现返回的值不应该超出父类的管控,一旦超出,用子类替换父类的时候,又出现了子类不可控的情况。同样,子类抛出的异常也要是父类异常的子类,宏观来看,父类控制了可能抛出异常的范围,如果父类无法限制子类,那么子类就又可以“为所欲为”了。

总结起来,里氏替换原则就是在处理继承关系时,维持继承结构健康稳固的一个原则。一切为父类服务,青出于蓝也要循规蹈矩。

这里我给出一个广为流传的说法,大家思考下是否符合LSP原则?我们在下一节分析。

正方形是长方形吗?

猜你喜欢

转载自blog.csdn.net/u013821237/article/details/83341231