`

浅谈契约式编程

阅读更多

契约式编程是编程的一种方法。那么什么是契约式编程呢?我想这个概念是从“合同”演变过来的。

在人类的社会活动中,契约一般是用于两方,一方(供应者)为另一方(客户)完成一些任务。每一方都期待从契约中获得利益,同时也要接受一些义务。通常,一方视为义务的对另一方来说是权利。契约文档要清楚地写明双方的权利与义务。

契约合同能保障双方的利益,对客户来说,合同规定了供应者要做的工作;对供应者来说,合同说明了如果约定的条件不满足,供应者没有义务一定要完成规定的任务。

同样的道理也适合于软件。设想一个软件单元E。它要达到它的目的(履行契约), E使用的策略可能会包括一系列的子任务,t1, ... tn。如果子任务ti 不是那么简单的,它得调用另一个功能例程(routine)R。换句话说,E把子任务转包给R。这样的情形应该被一个很好定义的“登记表”(roster)来管理双方的义务与权利--契约。

假如ti是一项任务,要求把一个给定的元素插入到一个有限容量的字典中。此处字典是一个(名-值)表,每一个元素(值)通过一个作为关键字的字符串(名)存取。(译者注:这里“元素”可以理解成一个任意的数据项)。

简而言之,就是函数调用者应该保证传入函数的参数是符合函数的要求,如果不符合函数要求,函数将拒绝继续执行。如果按照契约式编程的思想编写代码,就要求我们写函数时检查函数参数。有时候是简单的判断某个参数不能为空,或者数值不能小于0。如果在项目中全面应用契约式编程,则应该有一个“契约框架”帮我们来做这些事情。

契约与我们通常所说的商业契约很相似,有以下几个特点:

  1. 契约关系的双方是平等的,对整个bussiness的顺利进行负有共同责任,没有哪一方可以只享有权利而不承担义务。
  2. 契约关系经常是相互的,权利和义务之间往往是互相捆绑在一起的;
  3. 执行契约的义务在我,而核查契约的权力在人;
  4. 我的义务保障的是你的利益,而你的义务保障的是我的利益;

将契约关系引入到软件开发领域,尤其是面向对象领域之后,在观念上给我们带来了几大冲击:

一般的观点,在软件体系中,程序库和组件库被类比为server,而使用程序库、组件库的程序被视为client。根据这种C/S关系,我们往往对库程序和组件的质量提出很严苛的要求,强迫它们承担本不应该由它们来承担的责任,而过分纵容client一方,甚至要求库程序去处理明显由于client错误造成的困境。客观上导致程序库和组件库的设计和编写异常困难,而且质量隐患反而更多;同时client一方代码大多松散随意,质量低劣。这种情形,就好像在一个权责不清的企业里,必然会养一批尸位素餐的混混,苦一批任劳任怨,不计得失的老黄牛。引入契约观念之后,这种C/S关系被打破,大家都是平等的,你需要我正确提供服务,那么你必须满足我提出的条件,否则我没有义务“排除万难”地保证完成任务。

一般认为在模块中检查错误状况并且上报,是模块本身的义务。而在契约体制下,对于契约的检查并非义务,实际上是在履行权利。一个义务,一个权利,差别极大。例如上面的代码:

1 if (dest == NULL) { ... }

这就是义务,其要点在于,一旦条件不满足,我方(义务方)必须负责以合适手法处理这尴尬局面,或者返回错误值,或者抛出异常。而:

1 assert(dest != NULL);

这是检查契约,履行权利。如果条件不满足,那么错误在对方而不在我,我可以立刻“撕毁合同”,罢.工了事,无需做任何多余动作。这无疑可以大大简化程序库和组件库的开发。

契约所核查的,是“为保证正确性所必须满足的条件”,因此,当契约被破坏时,只表明一件事:软件系统中有bug。其意义是说,某些条件在到达我这里时,必须已经确保为“真”。谁来确保?应该是系统中的其他模块在先期确保。如果在我这里发现契约没有被遵守,那么表明系统中其他模块没有正确履行自己的义务。就拿上面提到的“打开文件”的例子来说,如果有一个模块需要一个FILE*,而在契约检查中发现该指针为NULL,则意味着有一个模块没有履行其义务,即“检查文件是否存在,确保文件以正确模式打开,并且保证指针的正确性”。因此,当契约检查失败时,我们首先要知道这意味着程序员错误,而且要做的不是纠正契约核查方,而是纠正契约提供方。换句话说,当你发现:

1 assert(dest != NULL);

报错时,你要做的不是去修改你的string_copy函数,而是要让任何代码在调用string_copy时确保dest指针不为空。

我们以往对待“过程”或“函数”的理解是:完成某个计算任务的过程,这一看法只强调了其目标,没有强调其条件。在这种理解下,我们对于exception的理解非常模糊和宽泛:只要是无法完成这个计算过程,均可被视为异常,也不管是我自己的原因,还是其他人的原因(典型的权责不清)。正是因为这种模糊和宽泛,“究竟什么时候应该抛出异常”成为没有人能回答的问题。而引入契约之后,“过程”和“函数”被定义为:完成契约的过程。基于契约的相互性,如果这个契约的失败是因为其他模块未能履行契约,本过程只需报告,无需以任何其他方式做出反应。而真正的异常状况是“对方完全满足了契约,而我依然未能如约完成任务”的情形。这样以来,我们就给“异常”下了一个清晰、可行的定义。

一般来说,在面向对象技术中,我们认为“接口”是唯一重要的东西,接口定义了组件,接口确定了系统,接口是面向对象中我们唯一需要关心的东西,接口不仅是必要的,而且是充分的。然而,契约观念提醒我们,仅仅有接口还不充分,仅仅通过接口还不足以传达足够的信息,为了正确使用接口,必须考虑契约。只有考虑契约,才可能实现面向对象的目标:可靠性、可扩展性和可复用性。反过来,“没有契约的复用根本就是瞎胡闹。(Bertrand Meyer语)”。

由上述观点可以看出,虽然Eiffel所倡导的Design By Contract在表象上不过是系统化的断言(assertion)机制,然而在背后,确实是完全的思想革新。正如Ivar Jacoboson访华时对《程序员》杂志所说:“我认为Bertrand Meyer的方向——Design by Contract——是正确的方向,我们都会沿着他的足迹前进。我相信,大型厂商(微软、IBM,当然还有Rational)都不会对Bertrand Meyer的成就坐视不理。所有这些厂商都会在这个方向上有所行动。”

  • 大小: 2.2 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics