按合同设计 (DbC),也称为按合同编程和按合同设计编程,是一种设计计算机软件的方法。它规定软件设计者应该为软件组件定义正式的、精确的和可验证的接口规范,这些规范扩展了抽象数据类型的普通定义,带有前置条件、后置条件和不变量。根据商业合同的条件和义务的概念隐喻,这些规范被称为“合同”。 Wikipedia
捷径。
如果您遵循针对接口进行编码的良好实践,您就会知道接口定义了所有实现类必须遵守的约定。
我们设计了 Contract Java,它是 Java 的扩展,其中方法合同在接口中指定。我们确定了三个设计目标。
- 首先,合同没有合同的Java程序和有合同的程序
完全满足的合约应该表现得好像它们没有运行
Java 中的合约。
- 其次,使用传统 Java 编译器编译的程序必须能够与程序互操作
在 Contract Java 下编译。
- 最后,除非一个类声明它满足特定的约定,否则绝不能因为未能满足该约定而受到责备
合同。抽象地说,如果调用了一个类型为 t 的对象的方法 m,那么调用者应该只为
与 t 和 m 相关的前置条件合同只应归咎于相关的后置条件合同
与 t。
这些设计目标提出了几个有趣的问题,并要求做出平衡语言设计与
软件工程问题。本节描述了每个主要的设计问题、替代方案、我们的决定、
我们的理由,以及决定的后果。决策不是正交的;后来的一些决定
依赖于早期的。
Contract Java 中的合同是接口中方法签名的装饰。每个方法声明都可能来
带有前置条件表达式和后置条件表达式;两个表达式都必须计算为布尔值。这
前置条件指定调用方法时必须为真。如果失败,方法调用的上下文是
责备没有在适当的上下文中使用该方法。后置条件表达式指定什么时候必须为真
该方法返回。如果失败,则方法本身应归咎于没有建立承诺的条件。
契约 Java 不限制契约表达式。尽管如此,良好的编程纪律要求表达式
不应该对程序的结果做出贡献。特别是,表达式不应有任何副作用。
前置条件和后置条件表达式都通过方法和伪变量的参数进行参数化
这。后者绑定到当前对象。此外,合同的后置条件可以参考
方法名,绑定到方法调用的结果。
合同是根据方法调用的类型上下文强制执行的。如果对象的类型是接口类型,则
方法调用必须满足接口中的所有约定。例如,如果一个对象实现了接口 I,则调用
对于 I 的方法之一,必须检查 I 中指定的前置条件和后置条件。如果对象的类型是类
类型,该对象没有合同义务。由于程序员总是可以为任何类创建接口,我们
出于效率原因,不要选中具有类类型的对象。
例如,考虑接口 RootFloat:
interface RootFloat {
float getValue ();
float sqRoot ();
@pre { this.getValue() >= 0f }
@post { Math.abs(sqRoot * sqRoot - this.getValue()) < 0.01f }
}
它描述了提供 sqRoot 方法的浮点包装类的接口。第一种方法 getValue 没有
合同。它不接受任何参数并返回未包装的浮点数。 sqRoot 方法也不接受任何参数,
但有合同。前置条件断言展开的值大于或等于零。结果类型
sqRoot 是浮动的。后置条件规定结果的平方必须在浮点值的 0.01 范围内。
即使合约语言足够强大,可以在某些情况下指定完整的行为,例如
前面的例子,完全甚至部分正确不是我们设计这些合约的目标。通常,合同
不能表达方法的全部行为。事实上,在披露的信息量之间存在张力
接口和合约可以满足的验证量。
例如,考虑这个堆栈接口:
interface Stack {
void push (int i);
int pop ();
}
由于界面中只有 push 和 pop 操作可用,因此无法指定在 push 之后,顶部
堆栈中的元素是刚刚压入的元素。但是,如果我们用一个 top 操作来扩充接口,
显示堆栈中最顶部的项目(不删除它),然后我们可以指定 push 将项目添加到堆栈的顶部
堆栈:
interface Stack {
void push (int x);
@post { x = this.top() }
int pop ();
int top ();
}
总之,我们不限制合同的语言。这使得合约语言尽可能灵活;
合同表达式评估甚至可能有助于计算的最终结果。尽管具有灵活性
合同语言,并非所有合意的合同都是可表达的。有些合同是无法表达的,因为它们可能
涉及检查无法确定的属性,而其他属性则无法表达,因为接口不允许足够
观察。