契约式编程

契约式设计(Design by Contract),也被称为契约式编程,契约优先式开发或代码合约等,是一种设计软件的方法。这种方法要求软件设计者为软件组件定义正式的,精确的并且可验证的接口,这样,为传统的抽象数据类型又增加了先验条件、后验条件和不变式。这种方法的名字里用到的“契约”或者说“契约”是一种比喻,因为它和商业契约的情况有点类似。

DbC的核心思想是对软件系统中的元素之间相互合作以及"责任"与"权利"的比喻。这种比喻从商业活动中"客户"与"供应商"达成"契约"而得来。例如:

  • 供应商必须提供某种产品(责任),并且他有权期望客户已经付款(权利)。
  • 客户必须付款(责任),并且有权得到产品(权利)。
  • 契约双方必须履行那些对所有契约都有效的责任,如法律和规定等。

同样的,如果在面向对象程序设计中一个类的函数提供了某种功能,那么它要:

  • 期望所有调用它的客户模块都保证一定的进入条件:这就是函数的先验条件—客户的义务和供应商的权利,这样它就不用去处理不满足先验条件的情况。
  • 保证退出时给出特定的属性:这就是函数的后验条件—供应商的义务,显然也是客户的权利。
  • 在进入时假定,并在退出时保持一些特定的属性:不变条件。

方法

当契约的概念扩展到了方法的级别。对于一个方法的契约通常包含下面这些信息:

  • 可接受和不可接受的值或类型,以及它们的含义
  • 返回的值或类型,以及它们的含义
  • 可能出现的错误以及异常情况的值和类型,以及它们的含义
  • 副作用
  • 先验条件
  • 后验条件
  • 不变条件
  • 性能上的保证,如所用的时间和空间(不太常见)
契约式编程

继承中的子类型可以弱化先验条件(但不可以加强它们),并且可以加强后验条件和不变式(但不能弱化它们)。这些原则很接近Liskov代换原则。所有类之间的关系就是客户与供应商的关系。一个客户在调用供应商的功能时有义务不去违反供应商所需的状态。相应的,供应商也有义务为客户提供它所需的状态和数据。其它的设计契约还有不变式。不变式保证类的状态在任何功能被执行后都保持在一个可接受的状态。

使用

当使用契约时,供应商不应对契约条件是否被满足进行校验。大体的思想是,利用契约条件校验为保护网,在契约被违反的情况下代码会“硬性失败”(fail hard)。DbC的"硬性失败"概念让对契约行为的调试变简单,因为每个过程的行为意图被定义得很清楚。它和一种叫作防御性编程的方法明显不同,在那种方法里,供应商要负责解决先验条件不满足的情况。相对通常的情况下,在DbC和防御性编程中,如果客户违反了先验条件供应商都会抛出异常—由客户来负责解决这种情况。DbC让供应商的工作更简单。

例如下面的代码就是防御性编程的例子

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

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

而契约方式如下:

1
assert(dest != NULL);

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

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

DbC同时也定义了软件模块的正确性条件:

  • 如果对一个供应商的调用之前类的不变式和先验条件是真,那么在调用后不变式和后验条件也为真。
  • 当调用供应商时,软件模块应保证不违反供应商的先验条件。
  • 因为契约条件在程序运行中不应被违反,它们可以只作为调试代码,或者在发布版本中被移除从而得到更好的性能。

DbC也能帮助代码重用,因为每段代码的契约都被很好的文档化了。模块的契约可以被当做软件文档来描述模块的行为。一般来说,在面向对象技术中,我们认为“接口”是唯一重要的东西,接口定义了组件,接口确定了系统,接口是面向对象中我们唯一需要关心的东西,接口不仅是必要的,而且是充分的。然而,契约观念提醒我们,仅仅有接口还不充分,仅仅通过接口还不足以传达足够的信息,为了正确使用接口,必须考虑契约。只有考虑契约,才可能实现面向对象的目标:可靠性、可扩展性和可复用性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class DICTIONARY{
public void __invariant(DICTIONARY self){
// 不变式
if(0 > self.count){
throw new DbcInvariantException("not 0 <= self.count");
}
if(self.count > self.capacity){
throw new DbcInvariantException("self.count <= self.capacity");
}
}
public void put(String key,String value){
// require 先验条件
if(this.count >= this.capacity){
throw new DbcPreConditionException("self.count < self.capacity");
}
if(StringUtils.isEmpty(key)){
throw new DbcPreConditionException("key is empty")
}
this.__invariant();
try{
int old_count = self.count
// Real logic is implemented in self.put_impl()
this.put_impl(x, key)

//ensure 后验条件
if(!self.has(x)){
throw new DbcPostConditionException("self.has(x)")
}
if(self.get(key) != x){
throw new DbcPostConditionException("self.item(key) == x")
}
if(count != old_count + 1){
throw new DbcPostConditionException("count == old_count + 1")
}
}finally{
# invariant, again
this.__invariant()
}
}
}

参考