32.根据契约进行替换

“深沉次的继承是很棒的。如果你需要其他类的函数,直接继承它们就好了!不要担心你创建的新类会造成破坏,你的调用者可以改变他们的代码。这是他们的问题,而不是你的问题。”

保持系统灵活的关键方式,是当新代码取代原有代码之后,其他已有的代码不会意思到任何差别。例如,某个开发人员可能想为通信的底层架构添加一种新的加密方式,或者使用同样的接口实现更好的搜索算法。只要接口保持不变,开发人员就可以随意修改实现代码,而不是影响其他任何现有代码。然而,说起来容易,做起来难。所以需要一点指导来帮助我们正确的实现。因此,去看看BarbaraLiskove的说法。

Liskov替换原则[Lis88]告诉我们:任何继承之后得到派生类对象,必须可以替换任何被使用的基类对象,而且使用者不必知道任何差异。换句话说,某段代码如果使用了基类中的方法,就必须能够使用派生类的对象,并且自己不必进行任何修改。

广告:个人专属 VPN,独立 IP,流量大,速度快,连接稳定,多机房切换,每月最低仅 5 美元

这到底以为着什么?假定某个类中有一个简单的方法,用来对一个字符串表进行排序,然后返回一个新的列表。并用如下的方式调用:Utils = new BasicUtils();…

sortedList = utils.sort(aList);现在假定开发人员派生了一个BasicUtils的之类,并写了一个新的sort()方法,使用了更快、更好的排序算法:Utils = new FasterUtils();…

sortedList = utils.sort(aList);注意对象sort()的调用是完全一样的,一个FasterUtils对象完美地替换了一个BasicUtils对象。调用utils.sort()的代码可以处理任何类型的utils对象而且可以正常工作.

但如果开发人员派生了一个basicUtils的子类,并改变了排序的意义--也许返回的列表以相反的列表进行排列--那就严重违反了Liskov替换原则。

要遵守Liskov替换原则,相对基类的对应方法,派生类服务(方法)应该不要求更多,不承诺更少;要可以进行自由的替换。在设计类的继承层次时,这是一个非常重要的考虑因素。

继承是OO建模和编程中被滥用最多的概念之一。如果违反了Lisakov替换原则,继承层次可能仍然可以提供代码的可重用性,但是将会失去可扩展性。类继承关系的使用者现在必须要检查给定对象的类型,以确定如何针对其进行处理。当引入了新的类之后,调用代码必须经常重新评估并修正。这不是敏捷的方式。

但是可以借用一些帮助。编译器可以帮助开发人员强制执行Liskov 替换原则,至少在某种程度上是可以达到的。例如,针对方法的访问修饰符。在java中重写方法的访问修饰符必须与被重写方法的修改符相同,或者可访问范围更加宽大,也就是说如果基类方法是保护的,那么派生重写方法的修饰符必须是保护的或者公共的。在C#和VB.NET中,被重写方法与重写方法的访问保护范围必须完全相同。

考虑一个带有findLargest()方法的类Base,方法中抛出一个IndexOut-OfRangeException异常。基于文档,类的使用者会准备抓住可能被抛出的异常。现在,假定你从Base类继承得到类Derived,并重写了findLargest()方法,在新的方法中抛出了一个不同的异常。现在如果某段代码期待使用Base类对象,并调用了Derived类的实例,这段代码就有可能接受到一个意想不到的异常。你的Derived类就不能替换使用到Base类的地方。在java中,通过不允许重写方法抛出任何新的检查异常避免了这个问题,除非异常本身派生自被重写方法抛出的异常类(当然,对于像RuntimeException这样的未检查异常,编译器就不能帮你了)。

不幸的是,java也违背了Liskov替换原则。Java.util.Stack类派生自java.util.Vector类。如果开发人员(不小心)将Stack对象发送给一个期待Vector实例的方法,Stack中的元素就可能被以与期望的行为不符的顺序被插入活删除。

当使用继承时,要想想派生类是否可以替换基类。如果答案是不能,就要问问自己为什么要使用继承。如果答案是希望在编译写新类的时候,还要重用基类的代码,也许要考虑转而使用聚合,聚合是指在类中包含一个对象,并且该对象是其他类的实例,开发人员将责任委托给所包含的对象来完成(该技术同样被称为委托)。

图6-3中展示了委托与继承之间的差异。在图中,一个调用者调用了CalledClass中的MethodA(),而它将会通过继承直接调用Base Class中的说法。在委托的模型中,Called Class必须要显式地将方法调用转向包含的委托方法。

Base Class

methodA()

Dekegate Class

methodA()

Called Class

methodA()?

1

Called Class

继承 委托图6-3 委托与继承

那么继承和委托分别在什么时候使用呢?

□ 如果新类可以替换已有的类,并且它们之间的关系可以通过is-a来描述,就要使用继承□ 如果新类只是使用已有的类,并且二者之间的关系可以描述为has-a或者user-a就使用委托吧。

开发人员可能会争辩说,在使用委托时,必须要写很多小方法,来将方法调用指向所包含的对象。在继承中,不需要这样做,因为基类中的公共方法在派生类中就已经是可用的了,仅凭这一点,并不能构成使用继承足够好的理由。

你可以在开发一个好的脚本或是好的IDE宏,来帮助编写这几行代码,或者使用一种更好的编程语言/环境,以支持更自动化形式的委托(比如Ruby这一点就做的不错了)。

通过替换代码来扩展系统。通过替换遵循接口契约的来,来添加并改进功能特性。要多使用委托而不是继承。

切身感受

这会让人觉得有点鬼鬼祟祟的,你可以偷偷地替换组件代码到代码库中,而且其他代码对比此一无所知,它们还获得了新的或改进后的功能。

平衡艺术

□ 相对继承来说,委托更加灵活,适应力也更强。

□ 继承不是魔鬼,只是长久以来被大家误解了。

□ 如果你不确定一个接口做出了什么样的承诺,或是有什么样的需求,那就很难提供一个对其有意义的实现了。