契约式编程

DbC的核心思想是对软件系统中的元素之间相互合作以及“责任”与“义务”的比喻。这种比喻从商业活动中“客户”与“供应商”达成“契约”而得来。例如:
  供应商必须提供某种产品(责任),并且他有权期望客户已经付款(权利)。
  客户必须付款(责任),并且有权得到产品(权利)。
  契约双方必须履行那些对所有契约都有效的责任,如法律和规定等。
 
同样的,如果在面向对象程序设计中一个类的函数提供了某种功能,那么它要:
  期望所有调用它的客户模块都保证一定的进入条件:这就是函数的先验条件—客户的义务和供应商的权利,这样它就不用去处理不满足先验条件的情况。
  保证退出时给出特定的属性:这就是函数的后验条件—供应商的义务,显然也是客户的权利。
  在进入时假定,并在退出时保持一些特定的属性:不变式。

 

those benefits   are  substantial:
●      The code is more reliable since arguments and assumptions are clearly specifi  ed
in writing. 
●      They' re checked every time the function is called so errors can' t creep in.
●      It' s easier to reuse code since the contracts clearly specify behaviors; one doesn' t
have to reverse engineer the code.
●      The contracts are a form of documentation that always stays in-sync with the
code itself. Any sort of documentation drift between the contracts and the code
will immediately cause an error. 
●      It' s easier to write tests since the contracts specify boundary conditions.
●      Function designs are clearer, since the contracts state, very clearly, the obligations
of the relationship between caller and callee.
●      Debugging is easier since any contractual violation immediately tosses an
exception, rather than propagating an error through many nesting levels.
●      Maintenance is easier since the developers, who may not be intimately familiar
with all of the routines, get a clear view of the limitations of the code.
●      Peace of mind. If anyone misuses a routine, he or she will know about it immediately.

 

 http://www.cnblogs.com/RicCC/archive/2007/11/28/Design-By-Contract-DBC.html

客户程序(Client) methodA调用提供者(Provider) methodB时,先需要按照methodB的要求准备好参数,这是methodA的责任;然后methodB需要按照功能规格正确的进行处理,将结果返回给调用者methodA,这是methodB的责任,也是methodA应得的利益。契约式设计就是要求提供一套机制,在客户程序与提供者之间明确的声明双方的职责与权利,即契约,并能够对这些契约进行验证。

正确性是一个相对概念,只有跟具体的需求规格联系在一起,才有正确与不正确之分。在契约式设计中,用断言(Assersion)来描述需求规格。 C、C++等语言中有assert指令,这些指令只是对面向对象方法中的断言非常有限的一种运用。
 
在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),如何准确地表达是一门艺术。另外也跟分析设计粒度结合在相关,例如一个类、方法,如果粒度过大所完成的任务过杂,准确地描述前置、后置条件、不变式可能就非常困难。

 

http://www.nowamagic.net/internet/internet_ContractProgramming.php

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

  在人类的社会活动中,契约一般是用于两方,一方(供应者)为另一方(客户)完成一些任务。每一方都期待从契约中获得利益,同时也要接受一些义务。通常,一方视为义务的对另一方来说是权利。契约文档要清楚地写明双方的权利与义务。契约合同能保障双方的利益,对客户来说,合同规定了供应者要做的工作;对供应者来说,合同说明了如果约定的条件不满足,供应者没有义务一定要完成规定的任务。

  同样的道理也适合于软件。设想一个软件单元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关系被打破,大家都是平等的,你需要我正确提供服务,那么你必须满足我提出的条件,否则我没有义务“排除万难”地保证完成任务。

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

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

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

assert (dest != NULL);

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

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

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的成就坐视不理。所有这些厂商都会在这个方向上有所行动。”

 

http://blog.donews.com/maverick/archive/2006/04/22/841290.aspx

 

Design by Contract的核心是断言(assertion)。所谓“断言”,是指永远为真的布尔型语句,如果不为真,则程序必然存在错误。通常情况下,检查断言的时机,应该局限于调试(debug)阶段,而不是代码的实际执行阶段。实际上,完成的程序永远不应期望断言会被检查。

Design by Contract使用了三类断言:后继条件(post-conditions),前提条件(pre-conditions),以及不变量(invariants)。前驱条件与后继条件都是针对操作(operation)而言的。所谓“后继条件”,是指操作执行完之后的情况。举例来说,如果我们定义对某个数的“求平方根”操作,则该操作的后继条件为:input = result * result,这里的result是输出结果,而input是输入的数值。在描述“做什么”(what we do)而不涉及“怎样做”(how we do it)——换言之,将接口和实现分离开来——时,后继条件是非常有用的方法。

所谓“前提条件”,是指在执行操作之前,期望具备的环境。对“求平方根”操作来说,前提条件可以定义为input >= 0。根据该前提条件,对某个负数执行“求平方根”操作本身就是错误的,而且结果无可预料。

初看起来,这一切显得乱糟糟的,因为我们必须设置一些检查,来保证“平方根”操作能够被正确执行。然而,重要的问题在于,哪一方负责进行这种检查。

根据前提条件来判断,检查的义务无疑落在调用方(caller)。如果责任划分不明确,则我们或者需要设置太多的检查——每一方都要检查,或者需要太少的检查——每一方都期望对方进行检查。检查太多是很糟糕的,因为它会造成大量重复的代码,增加系统的复杂程度。明确哪一方应该进行检查,能够避免这种问题。调用方或许会忘记进行检查,但我们可以通过调试(debugging)和测试(testing)来降低这种风险。

基于以上对前提条件和后继条件的定义,我们可以得到关于术语“异常”(exception)的严格定义。如果在满足前提条件的情况下调用某操作,不能满足后继条件,这种情况即称为异常。

所谓“不变量”(invariant),使关于类(class)的断言。例如,某个帐户类(Account class)可能有不变量表示balance == sun(entries.amount())(也就是说,余额等于所有账目记录的总和)。对于该类的所有实例来说,不变量应该恒为真(always true)。这里说的“恒”,是指“无论是否能对该对象调用某种操作”。

从本质上说,这意味着,不变量可以应用于特定类暴露的所有公开操作的前提条件与后继条件。在方法的执行过程中,不变量可能为假,但是,在其他任何对象能够与被调用方进行交互的时刻,不变量断言必须恢复为真。

在继承关系中,断言扮演着独特的角色。继承的风险之一在于,开发人员为子类重新定义的行为,可能会违背父类的行为。断言减少了这种风险。对某个类来说,其不变量和后继条件必须能够应用于所有的子类。子类可以加强这两类断言(也就是说,增加更多的限制——译者注),而不能削弱它们。而前提条件则只能削弱,而不能增强。

乍看起来,这显得有些古怪,但对于动态绑定(dynamic binding)来说,这是极其重要的。开发人员必须时刻记得,把子类对象作为父类的实例来对待。如果子类对象增强了前提条件,那么调用其父类的方法时,就可能出现错误。

 

 

 

 

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值