Richie

Sometimes at night when I look up at the stars, and see the whole sky just laid out there, don't you think I ain't remembering it all. I still got dreams like anybody else, and ever so often, I am thinking about how things might of been. And then, all of a sudden, I'm forty, fifty, sixty years old, you know?

Design By Contract 契约式设计

契约式设计(Design By Contract)把类和它的客户程序之间的关系看作正式的协议,描述双方的权利和义务。Bertrand Meyer把它称作构建面向对象软件系统方法的核心。

契约式设计的提出,主要基于软件可靠性方面的考虑。可靠性包括正确性和健壮性,正确性指软件按照需求规格执行的能力,健壮性指软件对需求规格中未声明状况的处理能力。健壮性主要与异常处理机制相关 。正确性一方面包括对象元素内部运行的正确性,另一个重要方面是与其它对象元素交互时的正确性。客户程序(Client) methodA调用提供者(Provider) methodB时,先需要按照methodB的要求准备好参数,这是methodA的责任;然后methodB需要按照功能规格正确的进行处理,将结果返回给调用者methodA,这是methodB的责任,也是methodA应得的利益。契约的双方都有自己的责任和利益,在Building bug-free O-O software: An introduction to Design by Contract ™中,Bertrand Meyer给出了示例,从实际生活中的契约引申到对象元素间的契约。契约式设计就是要求提供一套机制,在客户程序与提供者之间明确的声明双方的职责与权利,即契约,并能够对这些契约进行验证。

Object-Oriented Software Construction中Bertrand Meyer提到,正确性是一个相对概念,只有跟具体的需求规格联系在一起,才有正确与不正确之分。在契约式设计中,用断言(Assersion)来描述需求规格。 C、C++等语言中有assert指令,这些指令只是对面向对象方法中的断言非常有限的一种运用。

Building bug-free O-O software: An introduction to Design by Contract ™中的例子:向一个有限容量的字典(使用字符串键值存取各个元素)中插入某个元素,契约如下:

 义务   权利 
 客户程序   (必须保证前置条件) 
 确保字典没有满,键值是非空字符串 
 (从后置条件获益) 
 更新字典使新的元素出现在字典中,并跟给定的键关联起来 
提供者  (必需保证后置条件) 
 将给定元素放入字典中,并跟给定的键关联起来 
 (可以假定前置条件成立) 
 如果字典满了,或者键值为空字符串,可以不处理 
 
使用Eiffel语法描述:假如这是一个范型类DICTIONARY[ELEMENT]的put方法
put(x: ELEMENT; key: STRING) is
-- Insert x so that it will be retrievable through key.
require count < capacity not key.empty
do ... Some insertion algorithm ...
ensure has (x)
item (key) = x
count =
old count + 1
end

require语句包含的是前置条件(precondition),ensure语句包含的是后置条件(postcondition),这些都是断言,也就是契约的描述。
前置条件:当前的元素个数count小于容量capacity;键值key不是空字符串。
后置条件:字典中存在元素x;使用健值key获取的元素是x;操作后元素个数count等于操作前元素个数old count加一(Eiffel中有old关键字)。

在整个软件过程中,从概念模型的business use case用例规约开始就会有很多前置、后置条件 ,这些条件被具体化到设计模型后,就是每个对象元素的契约了。
保证前置条件是客户程序的职责,保证后置条件是服务提供者(被调用方法)的职责。

条件的强弱(Strong and Weak condition):条件P1包含P2并且不等于P2,就说条件P1比P2强,P2比P1弱。
继承关系中,子类如果重写父类方法需要重定义断言,原则是:前置条件应当与父类相同或比父类弱;后置条件应当与父类相同或比父类强。这样在多态运用的地方可以确保:满足父类的前置条件一定可以满足子类,子类的后置条件一定会满足父类的后置条件。

前置条件和后置条件运用在各个方法上,有的契约描述需要运用在整个类层次上,这种契约(断言)叫做类不变量(Class Invariants)。例如DICTIONARY的例子中,不变量可能是下面这样:
invariant
    0 <= count and count <= capacity

Building bug-free O-O software: An introduction to Design by Contract ™中,Bertrand Meyer谈到了契约式设计其它的一些好处:
1. 文档。在文档中不仅包含签名描述,也能包含契约描述,如果能更进一步象VisualStudio智能提示那样呈现,能减少很多编码过程中犯下错误。
2. 测试、调试、品质保证。NUnit是比较完善的断言测试框架,如果语言本身提供契约式设计支持,我们可以使单元测试中断言的覆盖范围更广泛(考虑一般都是开发者自己做单元测试),能够帮助发现更多隐性的缺陷,断言测试代码的编写会更简洁。

我们习惯性的在函数内部检查各个前置条件是否符合要求,例如各个入参是否符合规定等(防御式编程-defensive programming),契约式设计反对这种做法。调用者可能会自己做检查,保证这些前置条件符合要求,这样这些条件可能被多次检查;另外将这些条件检查夹杂在逻辑代码中实现,使得契约并不是明确的描述出来,也增加了实现代码的复杂性。从微观角度来看(即一个个函数的角度),这并没有什么害处,但从宏观的角度看,复杂性是品质问题的主要原因。契约式设计中将这些条件集中明确的声明,确定职责归属,并由框架辅助进行验证。
契约式设计要求客户程序确保前置条件,在Object-Oriented Software Construction中Bertrand Meyer把这种方式叫做强制型方式(demanding style);相反地由服务提供者自己进行条件验证的方式称为宽容型方式(tolerant style)。他列举了现实中的一个例子,老职员对于不符合要求的申请,可能一概拒绝;而一个职场新手可能采取的态度是尝试自己为那些申请纠正错误、补充完整。
在接收外部输入的地方,还是必须做校验,应当使用专门的验证模块实现。

C、C++中使用assert断言以方便测试、调试;NUnit使用assert断言框架进行测试;Bertrand Meyer开发Eiffel语言,充分的展现语言层面对契约式设计的支持。如果这些断言使用方式可以看作是自底向上的运用契约思想,那TDD就可以看作是自顶向下运用契约思想的方式,并且深入到用它驱动整个软件过程。

CodeProject上有一个C#的契约式设计框架:Design by Contract Framework,实现很简洁,因为语言层面没有提供支持,它的实现方式基本上跟C、C++中的assert完全一样。

良好的契约编写能力是一个难点,我们是通过布尔型的断言来描述规格(specification),如何准确地表达是一门艺术。另外也跟分析设计粒度结合在相关,例如一个类、方法,如果粒度过大所完成的任务过杂,准确地描述前置、后置条件、不变式可能就非常困难。

目前Eiffel在语言层级支持契约式设计,但限于串行程序(sequential program),对于并行领域中契约式设计的运用,更在探索中。

posted on 2007-11-28 22:40  riccc  阅读(5402)  评论(1编辑  收藏  举报

导航