hBifTs

山自高兮水自深!當塵霧消散,唯事實留傳.荣辱不惊, 看庭前花开花落; 去留随意, 望天上云展云舒.

导航

契约式设计的收益[转载]

Posted on 2004-05-12 23:49  hbiftsaa  阅读(1668)  评论(0编辑  收藏  举报

契约式设计的收益

[翻译] myan 2004-01-01
--------------------------------------------------------------------------------
 
--------------------------------------------------------------------------
本文节选自我翻译的《Design By Contract原则与实践》一书。《Design By Contract原则与实践》一书,是国内第一次引进以Design by Contract为主题的技术书籍,原文书《Design by Contract, by Example》2001年由培生集团出版。由于这本书所讲述的Design by Contract(本书译为“契约式设计”)技术并不像Java、.NET、C++等技术一样火爆,所以这本书的名气并不大。但对于希望了解和运用契约式设计的读者而言,这本书却是一本难得的佳作。Amazon.com上的读者一致给予了5星级的评价,有的评论称这本书是“契约式设计的最佳指南”,有的评论甚至称赞本书是“本年度最重要的技术书籍之一”。

本书篇幅不大,但是由于契约式设计本身并不复杂,因此内容十分充实,技术含量很高。从写作上来看,本书的作者并不打算拘泥于契约式设计的形式证明,而是单刀直入地着手解决程序员最关心的几个问题:契约式设计是什么,契约式设计为什么,契约式设计怎么做。全书的精华是作者从长期的开发和教学实践中总结出的契约式设计6大原则和6大导则。更难得的是,作者并没有停留在空泛地理论推演上,全书几乎每一个观念、每一项原则,都是通过实例导出的,这一点十分难得,凸现了作者深厚的实践经验。一个对契约式设计毫无观念的读者,只要能够认真阅读并且领会本书的内容,就可以迅速成为契约式设计的行家里手,运用这一杰出思想改进自己的工作质量。
--------------------------------------------------------------------------
8.1 几种优点
我们将使用契约式设计的优点大致分类如下:

·获得更优秀的设计。

·提高可靠性。

·得到更出色的文档。

·帮助调试。

·支持复用。

8.2 更优秀的设计
谨慎地运用契约式设计方法可以获得更优秀的设计,这是因为

· 组件服务的提供方和使用方各自的义务被表述得更清晰,从而使设计更加系统化、更清楚、更简单。

·子类特性的重定义得到周密的控制。

·异常的运用系统化、一致化。

更系统的设计 契约式设计鼓励程序员思考诸如“例程的先验条件是什么”这样的问题,这样有助于程序员理清概念。

如果你使用的编程语言直接支持先验条件的语言结构,并在执行时对它们进行检查,那么你可以很容易地尝试使用先验条件,通过实践来体会这种方法的实际意义。我们相信你将逐渐养成一种有益的习惯,只有确认类中的所有例程都在自己的require子句中清楚地描述了先验条件,才能对这个类放心。

更清楚的设计 使用者和提供者之间的权利和义务得到了共享,同时获得了清晰的描述。

例如,在字典类中,如果k已经存在字典中,put(k, v)还能照常工作吗?键值?设计者必须选择是由客户端确保不使用一个已经存在的键值调用put,还是由提供者处理这种情况,如果选择后者,则提供者还必须规定此时put的含义。put的契约明确了这个选择。

继续讨论put例程,put可以接受一个null实值吗?一个键值null键值呢?另外,value_for到底会不会偶尔返回一个null实值呢?如果你打算调用别人开发的特性来实现自己的代码,就必须回答这类问题。契约使答案一目了然。

更简单的设计 程序的先验条件清楚地描述了使用该程序的限制,而且非法调用的结果也很清楚,所以我们鼓励程序员不要开发过于通用的程序,而要设计小巧的、目标专一的例程。

再借用一下字典这个例子,value_for(k)不能返回一个字典中没有的键值的值,所以原则上,客户应该确保不使用字典中不存在的键值来调用value_for(k)。这个原则可以很容易地在实践中运用,我们为value_for(k)设置先验条件,则在开发过程中,若某个客户用字典中不存在的键值调用value_for(k),将得到一个异常。类的提供者,也就是类DICTIONARY中的value_for查询无需理会这个不守规矩的客户。

没有契约的帮助,value_for程序的设计者将不得不在程序体中包含一些代码,用来防备不守规矩的客户,这样一来,表明“这里是先验条件”的程序块和实际用来实现程序功能的代码块之间的界限就变得模糊,彼此互相缠绕,含糊混乱。

关于这个主题的更多内容请参见第8.8节。

控制对继承的使用 例如,当要用到多态性和动态绑定时,检查契约可以确保重定义例程的先验条件在子类中不会被加强。

另外,契约和继承之间的密切关系能够使类设计者敏感地认识到,要设计出今后能够为子类设计者所复用的类。

系统地运用异常 当例程被非法使用(先验条件失败)或者例程没有遵循契约的规定(后验条件或者不变式失败)时,就会发生异常。因此,异常的使命就清晰了:它是代码中错误的信号和标志,而不是一种因人而异、随心所欲的控制转移机制。这样一来,程序员之间彼此理解他人的代码就会容易得多。

8.3 提高可靠性
契约可以提高可靠性,因为

·编写契约可以帮助开发者更好地理解代码。

·契约有助于测试。

理解更加清晰,因此代码更加可靠 如果你按照两种不同的方式表达同一件事情,就能更好地理解这件事。

写契约时,你必须用先验条件和后验条件来说明每个例程的任务,之后还必须撰写程序体内的实现代码以完成任务。由于不得不按照两种稍有差别的方式思考这个程序,你将能更清楚地理解这个程序的任务和完成任务的方法。这样做也有助于你尽早发现错误。

测试更加到位,因此代码更加可靠 断言在运行时进行检测,从而确保程序符合它们既定的契约。

契约可以随意关闭或者启用,因此我们能够方便地反复测试程序的各个部分。譬如,你可以在设计类B的同时对先前设计的类A进行继续测试,从而发现两者在交互中暴露出来的新问题,而这些问题在你之前的设计中不可能预见到,这就使A的设计更加可靠。

很显然,任何有助于测试的工作都能减少代码中的错误。

8.4  更出色的文档
契约能够使文档更出色,因为

·契约乃是类特性的公用视图(public view)中的固有成分。

·契约是值得信赖的文档。

·契约是精确的规范,同时也可以作为测试的可靠指导。

更清晰的文档契约乃是类特性的公用视图(或称客户视图)中的固有成分

例如,用Eiffel你可以在一个源文件中写完所有代码。然后,使用某种工具提取出客户视图的内容,其中包括方法的签名式及契约。因此,“类接口”可以通过例程的签名式、非正式的注释和形式化的断言来清晰地描述。

更可靠的文档 运行时要检查断言,以便保证制定的契约与程序的实际运行情况一致。

开发项目临到发布时,程序员经常只顾修改代码而不更新文档。如果采用契约式设计,面对这种情况,你可以关闭断言检查,修改代码,当然这样一来断言也就跟不上代码的变化了。但是,一旦稍有闲暇,你就可以打开断言检查,很容易地暴露出文档中过时的地方,从而避免为日后留下隐患。

明确的测试指导 断言定义了测试的预期结果,并且由代码进行维护。

如果put程序的后验条件断言count将增加1,那么这就是调用put的一个预期结果。这样一来,如果运行一个要调用put的程序,你无需专门为了检查结果而编写测试代码。如果count没有增加1,就会产生一个后验条件错误。

支持精确规范 契约提供了一种方法,既能够获得精确规范所带来的益处,同时还使得程序员继续以他们所熟悉的方式工作。

我们需要非正式的注释帮助我们建立对类及其特性的任务的正确认识。但是,接下来我们就需要用精确的语言撰写正式文档,整理出特性的确切细节。断言正好提供了这样一种精确文档,特别是当我们用正式语言(编程语言当然是正式语言)来撰写断言时,这一点格外明确。

8.5 简化调试
契约可以简化调试工作,因为它们把错误牢牢地定位。

开发期间的支持 由于断言判断为假而在运行时展现出来的错误将被精确地定位。

通常,调试中最困难的部分就是对错误进行定位。如果断言失败,我们可以很清楚地知道错误的位置。

维护期间的支持 如果发布程序时打开了断言检查,客户就可以为开发人员提供更确切的错误信息。

听起来似乎不太现实。但是,你肯定可以提供一个测试版给特定的客户,在这个版本中有许多打开了的断言检查。这样一来,一旦出现错误,客户就会告诉你运行系统给出的信息,你也就能更好地找到并更正错误。

8.6 支持复用
通过提供下列内容,契约使得复用更简单:

·出色的文档

·正确运用了复用代码的运行时检测。

库使用者手中的优秀文档 契约清楚地解释了程序库中各个类、各个例程的任务,以及使用中的限制条件,从而减轻了“此功能未实现”综合症的危害程度。

对库使用者的帮助 运行时的契约检查为那些学习使用别人的类的人们提供了反馈。

没有契约的帮助,如果你在错误的情况下调用某个程序,就得不到有关出错位置的明确反馈信息。精心编写的契约,尤其是精心编写的先验条件,为客户程序员提供了关于出错位置的精确分析。

… …

8.9 契约的一些开销和限制
设计具有良好契约的程序需要相当的开销:撰写契约需要时间,开发者需要时间学习撰写良好契约的思想和技术,况且,并不是每个软件都需要那么高的质量。目前的主流语言中,只有Eiffel支持契约,而且仅仅支持顺序式程序(sequential program)的契约。

契约的撰写成本 编写契约当然要花时间。回报是节省了编写测试指导的时间、节省了编写文档的时间、节省了调试的时间以及由于复用节省下来的时间等等。尽管绝大多数开发人员都知道,在上游多花一点时间就能大量节省下游时间,但是仍然很难顶住压力去采用较优的开发方式。

需要大量实践 编写好契约是一门技术,需要花时间学习。你可能想在下一个项目中充分利用契约,但恐怕不太可能在一夜之间让团队中的所有人都变成契约式设计的行家里手,因此你将不得不花费一些项目资源来对他们进行培训。

我们在本书中列出一系列原则和导则的目的,就是要减少读者掌握契约式设计所必须投入的时间和精力。我们在前3章就介绍了一些基本知识,有了这些知识,读者就可以开始撰写良好的契约。

麻痹大意 契约并不能确保程序十全十美。使用契约式设计,程序确实可以得到提高,不过单凭它不足以使程序完美无缺。

质量并不总是主要目标 对于某些开发项目而言,最重要的目标是尽早发布版本,即使是一个有错误的版本。这种情况下,为了提高质量而降低开发速度恐怕得不偿失(根据我们的经验,企图用降低产品质量的手段来提前发布日期,其结果常常是事与愿违,不过是抬起今天的手打明天的脸,不过在这里宣扬我们的看法恐怕并无意义,这毕竟是另一码事。)

最好用于顺序式程序(sequential program) 契约可以用在并发程序和分布式程序中。但是在那些地方,游戏的本质已经截然不同。在契约式设计中,我们鼓励你针对那些谨慎的客户设计程序,所谓谨慎,就是说在调用程序之前,应该事先确定自己已经满足了先验条件。而在分布式系统中,你需要能够对付“先枪毙,后审判”的客户,这样的客户很可先调用X,然后才关心X是否有效。即使在这样的系统中,契约仍然是有意义的(参见第11章中的关于将设计级契约转换成显式产生异常的Java例子)。

除了能够节省网络连接的时间外,契约还为分布式系统提供了第2种保障。有些先验条件是关于同步的,而不是关于校验的。例如,考虑缓冲区 “get”程序的先验条件:“缓冲区非空”。在并发环境中,不管是不是分布式的,从缓冲区获取数据的对象可以和往缓冲区中放入数据的对象一起并发运行。因此,条件“缓冲区非空”可以被看作是一个wait条件。在放入数据的对象填满缓冲区之前,获取数据的对象只能等待。

据我们所知,目前还没有一种商业语言同时支持校验先验条件和等待先验条件。

语言不提供支持 如果你使用的不是Eiffel,那么基本上可以肯定你得不到由语言直接提供的契约式设计支持。目前,有越来越多的工具能够协助Java和C++等语言的程序员简化契约设计工作。但是这总有这样那样的不足。比如使用支持契约的预处理器,就意味着你用一种语言开发,而用另一种语言调试,这样做当然比只用一种语言要麻烦点儿。