契约式编程—程序开发人员测试指南(3)

发表于:2018-5-07 10:05

字体: | 上一篇 | 下一篇 | 我要投稿

 作者:朱少民、杨晓慧译    来源:51Testing软件测试网原创

  第5章 契约式编程
  把代码组织好,使软件可以被测试,从而提高软件被测试的概率,这并非是确保正确软件的唯一方法。另一条路是采用形式化方法,也就是数学证明。这一章,我们诠释另外一种方法,它将软件建模为客户端(Client)和供应端(Supplier)之间的交易,并按照某种契约要求双方遵守某些义务(见图5.1)。交易过程中,双方都能获得好处。如果契约遭到破坏,应用程序将停止。为了让这种方法生效,契约将在系统运行时不断地被检查,而不是偶尔运行一个测试套件或者提供关于某些程序的纸面证明材料。
  
图5.1  在契约式编程的术语中,方法或函数的调用者是客户端,
被调用者是供应端(因为它提供某些功能)
  当软件是按照这种方法写的,我们就称之为契约式编程(Programming by Contract) 。Eiffel语言的内置功能能够非常好地支持这种技术。 另外,虽然并非所有语言都支持这种技术,但它也是非常有用的。
  5.1  契约形式化约束
  契约定义了应用执行全过程中的约束。约束的生命周期取决于它的类型。有一些是在方法进入或退出时需要满足的;有一些则是贯穿应用生命周期的全过程。
  如果一个约束被违反了,应用程序必须马上带着错误信息退出,这些错误不应该被应用所处理和捕获。这种错误是不可恢复的,因为根据约定,违反约束就是一个退出条件。在实践中,契约式编程成为软件质量的最后一道防线,它也是使用验证逻辑和测试集的一个补充。仅仅依赖契约是不可行的,也无法操作,没有编程语言完全支持契约式编程,它们也无法代替编程中的验证和常识。
  契约构建块
  在契约式编程语言中,方法和函数的调用者是客户端,被调用者是供应端。契约的基本构建块是前置条件(Precondition)、后置条件(Postcondition)和类不变量(Class invariant)。
  前置条件是调用供应端所需要满足的限制条件。通常,它们是供应端参数的某种条件,或是供应端的某种内部状体。如果约束没有被满足时,供应端在执行前将停止服务。前置条件的有效时间很短。只在进入方法中,前置条件才需要被检查。下面是一些可能的例子:
  ·  当从一个索引集合中检索一个元素时候,索引的位置是正数还是零?
  ·  当从栈中取出一个元素,这个栈是不是空的?
  ·  当计算一个输入的验证码时,这个输入的格式是否正确?
  后置条件是供应端的内部状态或返回值的约束。在完成调用之前,这些后置条件必须满足。如果这些约束没有被满足,供应端将在返回前中断服务。后置条件生效时间通常非常短。它们只是在返回调用客户端结果时才做检查。下面都是一些合理的后置条件:
  ·  账号间的资金转账,在一个账号增加的金额,必须和另外一个账号减少的金额相等。
  ·  在创建对象时,所有成员变量被初始化为有效值。
  ·  在一个链表中增加一个元素,新的元素成为链表头,它指向之前的链表头。
  不变量(invariant)是契约的第三种构建块。不变量的两种基本类型是类不变量和循环不变量。类不变量是指保持类内部的某种状态的约束。例如,我们有一个类,使用整数标识时间的小时和分钟,一个合理的类不变量就是该整数必须在0~23和0~59之间。类不变量的约束通常在程序生命周期中一直生效。例如,思考一下银行账号集合的一个类不变量,所有交易总量应该和余额总计相等。
  Eiffel中的契约
  下面的程序是使用Eiffel编写,前置条件是检查参数的有效性,后置条件是验证返回值的合理性。我们可以看到,这个语言可以清晰地支持契约检查。
Seconds_in_24h: INTEGER = 86400
to_seconds (hour, minute: INTEGER):INTEGER
require
hour >= 0 and hour < 24
minute >= 0 and minute < 60
do
Result := hour * 3600 + minute * 60
Ensure
Result >= 0 and Result <= Seconds_in_24h
end
  5.2  实现契约式编程
  通过支持契约式编程的语言来进行软件开发,这是几种达到软件正确性的方法之一,同时契约式编程也可以作为各个层次测试强有力的补充。但是,所有的流行编程语言只能部分地支持契约式编程。另外一方面,这种技术内容丰富,无论语言是否能够全面支持这种技术,我们都可以使用它或部分使用它。在这一节,让我们一起探索,从契约式编程中我们能收获些什么,如何行之?
  契约的思考
  无论你喜欢的语言是否支持这些契约,利用契约的最大转变就是需要不断思考如何创建客户端和供给端,并且安排好它们的责任和义务。在原生支持契约的语言中,在编写代码之前首先确立契约是一种非常著名的设计实践。
  为我们的程序元素创建前置条件、后置条件和不变量也许会放慢我们的进度,但这是非常有益的方法。我们需要仔细思考责任的安置之处,以及每一部分代码应该做什么?是否需要在运行时始终保持契约,在我看来,这并非是第一位的事情。其实,制定契约才是这种技术最关键的部分。
  这听起来很显而易见,但是仔细想想:你曾有多少次思考如何划分方法/函数/子程序(Routine)的参数检查责任?在大部分我曾工作的软件系统中,这个问题往往被忽视或争论不休,因此产生了各种各样的方案:
  ·  调用者确保参数正确。类库和可复用组件常常采用这个方式,这也非常容易理解,库和组件无需纠缠于各种空指针和范围检查。如果有不正确的参数传入系统,类库的进程可能停止工作。因此,契约必须有十分清楚地说明。
  ·  被调用者检查参数。这是一个非常好的实践,被调用端检查输入参数(实际上是必须)并且提供公共使用。公用的远程过程调用(RPC)和Web Service都是很好的例子。由于它们不知道调用者的真实用意,调用目的可能是为了开玩笑或越权而破坏服务,因此服务必须有自己的保护方法。子程序可能会被不知名或不怀好意的客户端调用,因此需要精心地设计,并且检查它们的输入参数。一些常见的攻击方式包括缓冲区溢出和SQL注入攻击等,这些都是由于缺少或过于松懈的参数检查而造成的。
  好几代开发人员维护的遗留系统是另外一个例子,人们对这种系统缺少信心。这种系统慢慢会演变成孤岛,并应用一些防守式编程实践,详尽地检测所有参数。写代码的开发人员可能想:"所有这些代码都是有问题的,我不相信这些代码,但至少我可以确保我的子程序不会轻易地接受这些垃圾代码"。这也是确保某种契约的一个勇敢尝试。
  ·  责任不能够规范化。不同代的开发人员有不同的编程风格,加上缺少命名规范,这导致明显地缺少参数检查,重复代码和不同策略的大杂烩。
  契约,无论它是语言的一部分,或只是思考模型,都可以很自然地与面向对象设计融合在一起。如果开发人员能够非常清楚每个对象遵守哪种契约,特别是构造逻辑,那么繁琐和冗余的检查和验证都可以省略了。设想我们正在实现一个经典的时间差异功能:输入两个时间,返回其中的时间差异。一个朴素的实现是使用整数作为参数,并且检测输入参数必须是有效的日期,例如必须是yyyymmdd格式。在另外一个方面,如果相关函数接受两个日期对象作为参数,我们就无需担心参数验证和实现计算的问题了。换句话说,通过表示日期的类,日期差异函数就无需进行太多额外的检查。例如,通过将验证和参数检查外移出任务列表,这个例子也示意了契约如何帮助我们遵守单一责任原则(Single Responsibility Principle, Martin 2002)。
  5.3  强制契约
  一旦我们决定采用契约作为设计技术,我们可能有多种方式来强制推行契约。当前编程语言对于契约的支持力度,以及我们是否需要在运行时态进行强制检测或者通过某种间接方式来执行契约,这些都会影响我们的最后的选择。
  5.3.1  断言
  断言是实现契约验证最常用的方法。断言在运行时态检查布尔条件,当条件不被满足时,程序退出并且带上诊断信息。断言的一个功能是可以被关闭的,这意味着断言代码并非是整个程序执行中的核心部分。
  警告:不要试图尝试!
  断言可以被关闭,因此在断言中执行代码是非常糟糕的做法,例如增量计数器的值。
  assert(important++ < MAGIC_NUMBER);
  失败的断言将不做进一步处理,直接中断程序的执行,这也使得断言不适合用于验证公有函数和用户输入的参数。根据契约式编程的观点,契约检查应该发生在正常的验证逻辑之前。想象一个简单时间类的典型构造函数:

相关文章
版权声明:51Testing软件测试网获人民邮电出版社和作者授权连载本书部分章节。
任何个人或单位未获得明确的书面许可,不得对本文内容复制、转载或进行镜像,否则将追究法律责任。
《2023软件测试行业现状调查报告》独家发布~

关注51Testing

联系我们

快捷面板 站点地图 联系我们 广告服务 关于我们 站长统计 发展历程

法律顾问:上海兰迪律师事务所 项棋律师
版权所有 上海博为峰软件技术股份有限公司 Copyright©51testing.com 2003-2024
投诉及意见反馈:webmaster@51testing.com; 业务联系:service@51testing.com 021-64471599-8017

沪ICP备05003035号

沪公网安备 31010102002173号