1.3.1 操作契约
操作契约说明应该如何使用某个方法以及该方法的局限。在分析的时候就应该指明这一契约,在设计的时候完成规范,并且在代码中为该契约编写文档,特别是在头文件中。通过这种方法,使用代码的程序员就可以知道,为了让这个方法生成正确的结果应该遵循什么样的契约。
注释:模块的契约指明了模块的目的、假设、输入和输出
需要为方法提供一些信息。正如前面所讨论的那样,方法的接口指明了如何调用方法,以及该方法期望的参数的数量、顺序以及类型。还需要指明在方法调用之前哪些应该为真,方法调用结束后哪些应该为真。
在设计的时候,不仅要明确指明每个模块的目的,还要指明模块之间的数据流,这一点很重要。例如,需要为所有的模块提供以下问题的答案:在执行之前,这个模块可以使用哪些数据?这个模块做出了什么假设?在模块执行之后,完成了什么动作并且数据是什么样的?因此,应该为所有的模块详细指明假设、输入和输出。
注释:指明模块之间的数据流
例如,程序设计人员如果需要将某个整数数组排序,那么可能会为某个排序函数编写如下的规范:
这个函数将接收具有num个整数的数组,其中num>0。
这个函数将返回经过排序的整数数组。
可以将这个规范当作函数和调用函数的模块之间的契约的条款。
注释:规范是契约的条款
这个契约有助于程序员理解该模块对解决方案中的其他模块所承担的责任。任何编写排序函数的程序员都需要遵循这个契约。当编写完成这个排序函数并经过测试之后,契约告诉程序的其余部分如何正确调用这个排序函数,以及调用这个函数之后的结果。
然而需要注意,模块的契约并不负责让模块以某种特定的方式来执行任务。如果程序的其他部分对算法做出了某些假定,那么这部分程序应该对此负责。因此,如果后来用不同的算法重写了这个函数,则无须修改程序的其他部分。只要新的函数遵循原始的契约条款,程序的其余部分将不会知道这一修改。
注释:操作契约不应该描述模块如何执行任务
您应该已经知道这些内容,尽管先前可能并没有显式地使用术语"契约",但对这一概念应该很熟悉。当编写函数的前置条件和后置条件时也就编写了契约,前置条件是在函数执行前必须存在的条件,而后置条件是函数调用结束后存在的条件。例如,遵循前面契约的排序函数的伪代码如下所示:
注释:操作契约应该包括准确的前置条件和后置条件
- // Sorts an array.
- // Precondition: anArray is an array of num integers; num > 0.
- // Postcondition: The integers in anArray are sorted.
- sort(anArray, num)
注释:规范的初稿
实际上,这些前置条件和后置条件并不充分,这种情况只能在契约初稿中使用。例如,sorted意味着升序排列还是降序排列?num可以有多大?当实现这个函数时,可能会假定sorted意味着升序排列,num不超过100。当其他人使用sort函数试图将一个具有500个整数的数组进行降序排列时,就会发生问题。用户无法知道这个假设,除非对契约进行如下修改:
- // Sorts an array into ascending order.
- // Precondition: anArray is an array of num integers and 1 <= num <= MAX_ARRAY,
- // where MAX_ARRAY is a global constant that specifies the maximum size of anArray.
- // Postcondition: anArray[0]<= anArray[1]<= … <= anArray[num - 1];
- // num is unchanged.
- sort(anArray, num)
注释:修改后的规范
当编写前置条件时,首先描述了方法或者函数的输入参数,给出了该函数使用到的所有已命名的全局常量,最后给出了在此所作的所有假定。当编写后置条件时,描述了这个模块所做的改变。注意当方法或者函数具有返回值的情况下(这个值在技术上是后置条件的一部分),应该描述这个值。
新手程序员通常会忽视精确文档的重要性,尤其是他们同时担任某个小程序的设计员、程序员以及用户的时候。如果设计sort而不编写契约条款,那么在后面实现这个函数的时候还能记得它们吗?数周之后,还记得如何使用sort函数吗?为了回忆这些内容,您愿意检查程序代码还是阅读简单的前置条件和后置条件呢?当程序的规模变大时,良好的文档变得越来越重要,无论是单独工作还是在团队中都是如此。
注释:精确的文档非常重要
提示:操作契约完整地指明模块的目的、假设、输入和输出。
提示:使用模块的程序组件是模块的客户(client)。用户(user)是使用程序的人。