什么是契约——Eiffel的观点

DbC六大原则

原则1 区分命令和查询。查询返回一个结果,但不改变对象的可见性质。命令改变对象的状态,但不返回结果。(应当是不一定返回结果)
原则 2 将基本查询同牌生查询分开。派生查询可以用基本查询来定义。
原则 3 针对每个派生查询,设定一个后验条件,使用一个或多个基本查询的结果来定义它。这样我们只要知道基本查询的值,也就能知道派生查询的值。
原则 4 对于每个命令都撰写一个后验条件,规定每个基本查询的值。结合“用基本查询定义派生查询”的原则,我们现在已经能够知道每个命令的全部可视效果。
原则 5 对于每个查询和命令,采用一个合适的先验条件。先验条件限定了客户调用查询和命令的时机。
原则 6 撰写不变式来定义对象的恒定特性。类是某种抽象的体现,应当将注意力集中在最重要的属性上,以帮助读者建立关于类抽象的正确概念模型。

DbC六大准则

准则 1 在适当的地方添加物理限制。尤其是那些需要限制变量不应该为void的地方。
准则 2 先验条件中尽可能使用高效的查询。如果有必要,可以增加高效的派生查询,并在其后验条件中确保其与较低效的基本查询之间的等价。
准则 3 用不变式限定属性。如果一个派生查询被实现成一个属性,则应通过类中不变式的断言保证它与其他查询保持一致。
准则 4 为了支持特性的重定义,用相应的先验条件确保每一个后验条件。这样就允许在开发子类过程中进行各种不可预见的重定义。
准则 5 将预期发生的变化和框定规则这两种不同的限制分别放置在不同的类中。这使开发者在扩展你的类时享有更多的自由。
准则 6 若有保密性要求,则违背保密性的查询可以在契约中使用,然后被设为私有属性。这里的“设为私有”,是对“在契约中使用了该查询的类”之下的层次而言,至于该类之上的层次,这个查询当然是可使用的。
--------------------------------------------------------------------


什么是契约——Eiffel的观点


    假设你现在正在面试,主考不紧不慢地给出下一道题目:“请用C语言写一个类似strcpy的函数。要考虑可能发生的异常情况。” 你会怎么做呢?很明显,对方不是在考察你的编程能力,因为复制字符串实在太容易了。对方是在考察你的编程风格(习惯),或者说,要看看你编码的质量。
    下面是多种可能的做法:
    void
    string_copy1(char* dest, const char* source)
    {
      assert(dest != NULL); /* 使用断言 */
      assert(source != NULL);
      
      while (*source != '/0') {
        *dest = *source;
        ++dest;
        ++source;
      }
      *dest = '/0';
    }
    void
    string_copy2(char* dest, const char* source)
    {
      if (dest != NULL && source != NULL) {  /* 对错误消极静默 */
        while (*source != '/0') {
          *dest = *source;
          ++dest;
          ++source;
        }
        *dest = '/0';
      }
    }
    int
    string_copy3(char* dest, const char* source)
    {
      if (dest != NULL && source != NULL) {
        while (*source != '/0') {
          *dest = *source;
          ++dest;
          ++source;
        }
        *dest = '/0';
        return SUCCESS;  /* 返回表示正确的值 */
      }                          
      else {
       errno = E_INVALIDARG;  /* 设定错误号 */
       return FAILED;         /*  返回表示错误的值 */
      }
    }
    
    // C++
    void
    string_copy4(char* dest, const char* source)
    {
       if (dest == NULL || source == NULL)
         throw Invalid_Argument_Error();  /*  抛出异常 */
       while (*source != '/0') {
         *dest = *source;
         ++dest;
         ++source;
       }
       *dest = '/0';
    }
    如果你是主考,不知道面对这样四份答卷,你的评分如何?当然,你可以心里揣着一个“标准答案”,“顺我者昌,逆我者亡”。但是如果以认真的态度面对这四份答卷,我想很多人都会难以抉择。
    因为这里涉及到了软件开发中的一个带有本质性的难题——错误处理。
    历来错误处理一直是软件开发者所面临的最大困难之一。Bjarne Stroustrup在谈到其原因时说道,能够探察错误的一方不知道如何处理错误,知道如何处理错误的一方没有能力探察错误,而直接采用防御性代码来解决,会使得程序的正常结构被打乱,从而带来更多的错误。这种困境是非常难以应对的——费心耗力而未必有回报。因此,更多的人采用鸵鸟战术,对可能发生的错误视而不见,任其自然。
    C++、Java和其他语言对错误处理问题的回答是异常机制。这种机制在正常的程序执行流之外开辟了专门的信道,专门用来在不同程序模块之间报告错误,解决上述错误探察与处理策略分散的矛盾。然而,有了异常处理机制后,开发者开始有一种倾向,就是使用异常来处理所有的错误。我曾经就这个问题在comp.lang.c++.moderated上展开讨论,结果是发现有相当多的人,包括Boost开发组里的很多专家,都认为异常是错误处理的通用解决方案。
    对此我不能赞同。并且我认为滥用异常比不用异常的危害更大。
    The Pragmatic Programmer是一本在国外程序员中间颇为流行的书,其中在讲到错误处理时,有一句箴言:
    
    “只在真正异常的状况下使用异常。”
    书中举了一个例子,如果你需要当前目录下的一个名叫“app.dat”的文件,而这个文件不存在,那么这不叫异常状况,这是你应该预料到的、并且显式处理的情况。而如果你要到Windows目录下寻找user.dat文件,却没找到,那才叫做异常状况——因为每一个正常运行的Windows系统都应该有这个文件。
    我非常赞成书中的那句忠告,可是究竟什么是“真正异常”的状况?书中的这个例子显然只是一个颇具感性的、寓言似的故事,具有所有寓言的共同特点——读起来觉得豁然开朗,收获很大,实际上帮不了你什么忙。这种例子对于我们的实际开发,仍然提供不了真正的帮助。
    究竟应该如何看待错误?怎样才能最好地错误处理?
    说实话,在这两个问题上,我们所见到的大部分语言都没有给出很好的回答。C秉承一贯风格,把所有的东西推给开发者考虑;Ada发明了异常,但是又为异常所累(知道阿里亚纳5火箭的处女航为什么失败吗?);C++企图将Ada的异常机制融合进自己的体系中,结果异常成了C++中最难以处理的东西;Java和C#显然都没有耐心重新考虑错误处理这桩事,而只是简单的将C++的异常机制完善化了事。
    与上述这些语言不同,Eiffel从一开始就把错误处理放在核心的位置上予以考虑,并以“契约”思想为核心,建立了整个的错误处理思想体系。在我了解的语言里,Eiffel是对这个问题思考最为深刻一个,因此,Eiffel历来享有“高质量系统开发语言”的声誉。(事实上,Bertrand Meyer很不喜欢别人称Eiffel为“编程语言”,他反复强调,Eiffel是一个Software Development Framework。不过本文只涉及语言特性,所以姑且称Eiffel语言。)
    Eiffel把软件错误产生的本质归结与“契约”的破坏。Eiffel认为,一个软件能够正常运作,正确完成任务,是需要一系列条件的。这些条件包括客观运行环境良好,操作者操作正确,软件内部功能正确等等。因此,软件的正确运行,是环境、操作者与软件本身三方面合作的结果。相应的,系统的错误,也是由于三者中有一方没有正确履行自己的职责而导致的。细化到软件内部,每个软件都是由若干不同的模块组成的,软件的错误,是由于某些模块没有正确履行自己的职责。要彻底杜绝软件错误,只有分清各自模块的责任,并且建立机制,敦促各模块正确履行自己的责任,然后才有可能做到Bug-free。(鉴于系统中错综复杂的关系,以及开发者认识能力的局限,我认为真正无错误的系统是不可能的。但是当前一般软件系统中的质量问题远远比应有的严重。)
    如何保证各方恪守职责呢?Eiffel引入了契约(Contract)这个概念。这里的契约与我们通常所说的商业契约很相似,有以下几个特点:
1. 契约关系的双方是平等的,对整个bussiness的顺利进行负有共同责任,没有哪一方可以只享有权利而不承担义务。
2. 契约关系经常是相互的,权利和义务之间往往是互相捆绑在一起的;
3. 执行契约的义务在我,而核查契约的权力在人;
4. 我的义务保障的是你的利益,而你的义务保障的是我的利益;

    将契约关系引入到软件开发领域,尤其是面向对象领域之后,在观念上给我们带来了几大冲击:
1. 一般的观点,在软件体系中,程序库和组件库被类比为server,而使用程序库、组件库的程序被视为client。根据这种C/S关系,我们往往对库程序和组件的质量提出很严苛的要求,强迫它们承担本不应该由它们来承担的责任,而过分纵容client一方,甚至要求库程序去处理明显由于client错误造成的困境。客观上导致程序库和组件库的设计和编写异常困难,而且质量隐患反而更多;同时client一方代码大多松散随意,质量低劣。这种情形,就好像在一个权责不清的企业里,必然会养一批尸位素餐的混混,苦一批任劳任怨,不计得失的老黄牛。引入契约观念之后,这种C/S关系被打破,大家都是平等的,你需要我正确提供服务,那么你必须满足我提出的条件,否则我没有义务“排除万难”地保证完成任务。
2. 一般认为在模块中检查错误状况并且上报,是模块本身的义务。而在契约体制下,对于契约的检查并非义务,实际上是在履行权利。一个义务,一个权利,差别极大。例如上面的代码:
    if (dest == NULL) { ... }
这就是义务,其要点在于,一旦条件不满足,我方(义务方)必须负责以合适手法处理这尴尬局面,或者返回错误值,或者抛出异常。而:
    assert(dest != NULL);
这是检查契约,履行权利。如果条件不满足,那么错误在对方而不在我,我可以立刻“撕毁合同”,罢工了事,无需做任何多余动作。这无疑可以大大简化程序库和组件库的开发。
3. 契约所核查的,是“为保证正确性所必须满足的条件”,因此,当契约被破坏时,只表明一件事:软件系统中有bug。其意义是说,某些条件在到达我这里时,必须已经确保为“真”。谁来确保?应该是系统中的其他模块在先期确保。如果在我这里发现契约没有被遵守,那么表明系统中其他模块没有正确履行自己的义务。就拿上面提到的“打开文件”的例子来说,如果有一个模块需要一个FILE*,而在契约检查中发现该指针为NULL,则意味着有一个模块没有履行其义务,即“检查文件是否存在,确保文件以正确模式打开,并且保证指针的正确性”。因此,当契约检查失败时,我们首先要知道这意味着程序员错误,而且要做的不是纠正契约核查方,而是纠正契约提供方。换句话说,当你发现:
     assert(dest != NULL);
报错时,你要做的不是去修改你的string_copy函数,而是要让任何代码在调用string_copy时确保dest指针不为空。

4. 我们以往对待“过程”或“函数”的理解是:完成某个计算任务的过程,这一看法只强调了其目标,没有强调其条件。在这种理解下,我们对于exception的理解非常模糊和宽泛:只要是无法完成这个计算过程,均可被视为异常,也不管是我自己的原因,还是其他人的原因(典型的权责不清)。正是因为这种模糊和宽泛,“究竟什么时候应该抛出异常”成为没有人能回答的问题。而引入契约之后,“过程”和“函数”被定义为:完成契约的过程。基于契约的相互性,如果这个契约的失败是因为其他模块未能履行契约,本过程只需报告,无需以任何其他方式做出反应。而真正的异常状况是“对方完全满足了契约,而我依然未能如约完成任务”的情形。这样以来,我们就给“异常”下了一个清晰、可行的定义。
5. 一般来说,在面向对象技术中,我们认为“接口”是唯一重要的东西,接口定义了组件,接口确定了系统,接口是面向对象中我们唯一需要关心的东西,接口不仅是必要的,而且是充分的。然而,契约观念提醒我们,仅仅有接口还不充分,仅仅通过接口还不足以传达足够的信息,为了正确使用接口,必须考虑契约。只有考虑契约,才可能实现面向对象的目标:可靠性、可扩展性和可复用性。反过来,“没有契约的复用根本就是瞎胡闹。(Bertrand Meyer语)”。
    由上述观点可以看出,虽然Eiffel所倡导的Design By Contract在表象上不过是系统化的断言(assertion)机制,然而在背后,确实是完全的思想革新。正如Ivar Jacoboson访华时对《程序员》杂志所说:“我认为Bertrand Meyer的方向——Design by Contract——是正确的方向,我们都会沿着他的足迹前进。我相信,大型厂商(微软、IBM,当然还有Rational)都不会对Bertrand Meyer的成就坐视不理。所有这些厂商都会在这个方向上有所行动。”(参见《程序员》2002年第11期,P22)。
--------------------------------------------------------------------
续篇-契约思想的一个反面案例将于近日在计算机科学技术网发布
--------------------------------------------------------------------
作者简介:

孟岩:国内著名的软件工程与面向对象专家。多部软件工程与面向对象技术书籍的译者。翻译和审校了《C++标准库》《C++沉思录》《敏捷软件开发》《Design by Contract原则与实践》等多部著作。
技术方向: 标准C++,面向对象技术,范型程序设计技术和STL,设计模式,软件开发方法,嵌入式。
尊崇和喜爱的技术专家和作家:Richard Stallman和Bjarne Stroustrup是我的精神领袖,侯捷先生是我的良师益友。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值