后端面试38讲_15_13丨软件设计的里氏替换原则正方形可以继承长方形吗
你好,我是李智慧。
我们都知道啊,面向对象编程语言有三大特性,分装、继承、多态。
这几个特性呢,你也许可以很快就学会,但是如果想要用好,可能就要花很长的时间了。
通俗的说,接口的多个实现就是多态,多态呢可以让程序在编程时面向接口进行编程,在运行期绑定具体类,从而使类之间不需要直接耦合,就可以关联组合,构成一个强大的整体对位提供服务。
绝大多数的设计模式其实都是利用多肽的特性玩的把戏。
前面两篇学习的开闭原则和依赖倒置原则,也是利用多肽的特性。
正是多肽使得编程有时候像变魔术,如果能够用好多态,可以说就掌握了绝大多数的面向对象编程技巧。
分装是面向对象编程语言提供的特性,将属性和方法分装在类里,用好分装的关键是知道应该将哪些属性和方法分装在某个类里。
一个方法应该分装在a类里,还是分装在b类里。
这个问题呢其实就是如何进行对象的设计,使于研究进去里面也有很大的学问。
继承似乎比多态和分装都要简单一点。
但实践中继承的误用也很常见,使于如何设计继承关系,怎样使继承不违反KB原则。
实际上有一个关于继承的设计原则,叫里式替换原则。
这个原则说啊,若对每个类型t一的对象o一都存在一个类型t二的对象o二使得所有针对t二编写的程序p中用o一替换o二以后程序p的功能行为。
不则这t一是t二的子类型上面这句话比较学术,通俗的说就是子类型必须能够替换掉他们的鸡类型。
再稍微详细点呢,就是说程序中所有使用鸡类的地方都应该可以使用,子类代替语法上,任何类都可以被继承。
但是一个继承是否合理,在这承关系本身是看不出来的,需要把继承放在应用场景的上下文中去判断使用鸡类的地方是否可以使用子类代替文章。
这里呢我改出一个马的继承设计,白马和小马驹都是马白,以都继承了马,这样的继承是不是合理呢?我们需要放到应用场景中,在这个场景中是人骑马。
根据这里的关系,继承了马的白马和小马驹应该都可以代替马,白马代替马当然没有问题了,人可以骑白马,但是小马驹代替马可能就不合适了,因为小马驹还没长好,没办法被人骑。
那么很显然,作为子类的白马可以替换掉鸡类马,但是小马不能替换马,因此小马继承马就不太合适了,违反了李斯替换原则。
下面我们看一个违反李斯替换规则的例子,这里有一处代码,这段代码中啊,circle和square都继承了鸡肋shape.然后在应用的方法中呢,根据输入shop对象类型进行判断,根据对象类型选择不同的绘图函数,将图形画出来。
这种写法的代码既常见又糟糕,它同时违反了KB原则和里式替换原则。
首先看到if else代码,我们就可以判断违反KB原则了当增加新的shape类型的时候,必须修改这个方法,增加else if代码。
其次,也因为同样的原因,违反了里式替换原则。
当增加新的shape类型的时候,如果没有修改这个方法,没有增加LSF代码,那么这个新类型就无法替换,积累,shrap程序无法正常运行。
要解决这个问题其实也很简单,只要在基类shelp中定义job p方法就可以了。
所有shab的子类、circle square都实现这个方法就可以了。
而join shep代码也可以变得很简单。
这段代码呢既满足KB原则,增加新的类型,不需要修改,任何代码,也满足里式替换原则。
在使用鸡类的这个方法中,可以用子类替换程序正常运行。
一个基层设计是否违反里式替换原则,需要在具体的场景中考察。
我们再看一个例子,假设我们现在有一个长方形的类,你在文章中可以看到这个类的定义。
这个类呢满足我们的应用场景,在程序中多个地方被使用,一切良好。
但是现在我们有个新的需求,我们需要一个正方形。
通常情况下呢,我们判断一个基层是否合理会使用is r进行判断。
类b可以继承类a我们就说类b依载类a比如白马依扎马轿车一扎车。
那正方形是不是依载长方形呢?通常我们会说正方形是一种特殊的长方形,是长和宽相等的长方形。
那从这个角度讲,正方形依载长方形,也就是可以继承长方形。
具体实现上,我们只需要在设置长方形的长和宽的时候,同时设置长和宽就可以了。
这样看的话,正方形类设计看起来很正常,用起来似乎也没有问题显是真的没有问题吗?继承是否合理?我们需要用里式替换原则来判断。
之前也说过,是否合理,并不是从继承的设计本身看,而是从应用场景的角度看,如果在应用场景中,也就是在程序中子类可以替换父类,那么继承就是合理的。
如果不能替换,那么继承就是不合理的。
这个长方形的使用场景是什么样的呢?你可以去文章中看一下使用代码程序。
在这个场景中,如果用子类square替换父类rect angle le,而是从calculate error将返回十六,而不是十二程序是不能正确运行的。
这样的继承不满足里氏替换原则是不合适的继承。
根据理森替换原则,可以引出一个基本的推论类的公有方法。
其实是对使用者的一个契约。
使用者呢按照这个契约使用类并期望类按照契约用性返回合理的词。
当脂类继承父类的时候啊,根据里氏替换原则,使用者可以在使用负类的地方使用脂类替换。
那么从契约的角度看,脂类的契约就不能比父类更严格,否则使用者在使用脂类替换负类的时候,会因为更严格的契约而失败。
在上面这个例子中,正方形继承了长方形,但是正方形有比长方形更严格的契约,即正方形要求长和宽是一样的。
因为正方形比长方形有更严格的契约。
那么在使用长方形的地方,正方形会因为更严格的契约无法替换长方形。
我们开头提到的小马继承马的例子也是如此。
小马比马有更严格的要求,既不能骑,那么小马继承马就是不合适的。
在类的继承中,如果父类中的方法访问控制是protected,那么子类override这个方法的时候啊,可以改成是public,但是不能改成provate.因为private访问控制比protected更严格,能使用父类protected方法的地方,不能用子类private方法替代,否则就是违反里式替换原则。
相反,如果子类方法的访问控制改成public就没有问题,即子类可以比父类有更宽松的契约。
同样子类类类类的类方法的的时候,能将类类的public方法改成protected,因则会出现编译错误。
通常说来,子类比父类的契约更严格,都是违反历式替换原则的。
子类不应该比父类更严格,这个原则看起来既合理又简单。
但在实践中,如果你不严格的审视自己的设计,就有可能违背历史替换原则。
在GDK中,类properties继承子类、hash table类是decon继承子vector.这样的设计其实呢就是违反了历史替换原则的properties,要求处理的数据类型是string,而它的父类hush table要求处理的数据类型是object子,类比父类的契约更严格。
Stake是一个占数据结构,数据只能先进先错误。
而它的父类vector是一个线性表子,类比父类的契约更严格。
这两个类都是从GDK一就已经存在的,我想,如果能够重新再来GDK的工程时,一定不会这样设计。
这也从另一个方面说明,不恰当的继承是很容易就发生的。
设计继承的时候需要更严谨的审视。
在实践中,当你仅仅为了复用父类中的方法,就继承父类的时候,很有可能你离错误的继承就已经不远了。
一个类如果不是为了被继承而设计,那么最好就别去继承它。
粗暴的一点说,如果不是抽象类或者接口,最好不要继承它。
如果你确实需要使用类的一个方法,最好的办法是组合这个类,而不是继承这个类。
这就是人们通常所说的组合优于继承。
我在这里给了一个例子,这个例子中如果类b需要使用类a的方法,这个时候不要去继承类a而是去组合类a也能达到使用类a的方法的效果。
这其实就是对象适配器模式了。
使用这个模式的话,类b不需要继承类a一样可以拥有类a的方法,同时还有更大的灵活性,比如可以改变方法的名称,以适应应用接口的需要。
当然,继承接口或者抽象类也并不能保证你的继承设计就是正确的最好的方法。
还是用理式替换原则,检查一下你的设计。
使用副类的地方是不是可以用子类替换?违反历时替换原则,不仅仅是发生在设计继承的地方,也可能发生在使用父类和子类的地方。
错误的使用方法也可能导致违反历时替换原则是子类无法替换。
父类下面给你留一道思考题吧。
父类中有抽象方法,f抛出异常a exception子类override.父类这个方法以后,想要将抛出的异常改为b exception,那么b exception应该是a exception的父类还是子类,为什么呢?请你用理式替换原则说明,并在评论区写下你的思考,我会和你一起交流,也欢迎你把这篇文章分享给你的朋友,或者同事一起交流一下。