hBifTs

山自高兮水自深!當塵霧消散,唯事實留傳.荣辱不惊, 看庭前花开花落; 去留随意, 望天上云展云舒.

导航

契约思想的一个反面案例[转载]

Posted on 2004-05-12 23:55  hbiftsaa  阅读(991)  评论(0编辑  收藏  举报

契约思想的一个反面案例

[原创] myan 2004-01-02
--------------------------------------------------------------------------------
 
--------------------------------------------------------------------
序言

    契约式设计的本意很简单,就是在设计和编码阶段向面向对象程序中加入断言(assertion)。它是为了给从事实际开发工作的一线程序员提供一种方法,以便在尽可能小的代价下开发出可靠性出众的软件系统。其应用却可以贯穿于软件创建的全过程,从分析到设计,从文档到调试,甚至可以渗透到项目管理中去。2003年10月中国软件工程领域又一大喜事便是孟岩先生翻译完成的《Design by Contract原则与实践》一书正式出版,该书为中国第一本关于契约式设计的书籍。孟岩先生在平日交往当中多次向我提到“DbC是一项必将对未来软件开发技术起到重要影响的技术”。遂邀先生为本站网友发布他的几篇关于契约式程序设计的著作,以期为中国软件开发技术引入新鲜血液。希望能对读者了解契约,利用契约有所裨益。
                                                     曾毅
                                      于北京工业大学计算机软件与理论研究室

--------------------------------------------------------------------

刚刚发表了《什么是契约》一文,突然发现自己通篇都在写理论,没有实例来证明。所以赶快补充一个反面案例——C++ IOStream。说是反面,不是因为IOStream库设计得不精彩(恰恰相反,你很难找到比IOStream设计更为精彩的C++库了),而是想展示一下,在没有契约概念的思想体系里,组件设计将为权责不清的错误处理付出多大的代价。

大家知道,C++ IOStream库非常经典,最先起源于Bjarne Stroustrup的Stream库,之后经过Jerry Schwartz、Martin Carroll、Andy Koenig等人的改进,成为IOStream库,并被并入Bell实验室发行的USD C++库中,广为传播。后来USD库逐渐消亡了,而IOStream由于获得广泛应用,得以幸免,并以新的形式被置于标准库中。

对于错误处理,当IOStream库诞生的时候(大约1985-1987),C++还没有异常机制。因此,Jerry Schwartz发明了这样一套错误处理机制:

例1:经典的IOStream错误处理:

  ifstream ifs("filename.txt", ios::in);
  if (!ifs) {  // 这里实施了向void*转型的操作
    // 文件打开失败,实施错误处理
  }

先测试文件是否打开,再实施具体操作,这是经典IOStream库的一个惯用法(idiom)。

我们现在设想用户没有很好地执行这个idiom:

  int val;
  ifstream ifs("filename.txt", ios::in);
  ifs >> val; 

如果filename.txt打开失败,会发生什么情况?

如果哪位还有当年的Borland C++ 3.1,可以试着测试一下。我估计是什么也不发生,或者说,程序处于极端危险的“undefined behavior”状态。

这种情况对C++库开发者来说是不能接受的。因此,尽管问题的出现是由于用户的错误(他们没有正确地测试流状态),但是由于非契约思想体系下的权责不清,IOStream库的开发者开始追求足以应对用户错误的组件开发技术。由此,IOStream开始在一个方向上被拖入了复杂性的泥潭之中。

我们看看标准库中的对付这种情况采用什么办法。标准IOStream有一个成员函数叫做exceptions(),专门用来帮助程序员切换异常模式。缺省情况下,异常触发并没有打开,所以情形跟经典IOStream库相同。如果你在操作IOStream之前如下调用:

strm.exceptions(std::ios::eofbit | std::ios::failbit |
                std::ios::badbit);
则当流不处于good状态时,执行类似 strm >> val;这样的操作时,将会抛出异常。

这样做看起来不错,是吗?

我觉得不是。请恕我C++标准的异议,这是我第一次正式对C++标准中的设计提出异议。

这种设计带来的缺点,首先是复杂。在Nicolai Josuttis的The C++ Standard Library中,对这个机制整整用了5页纸来解释,还意犹未尽。复杂的设计必然带来复杂的使用规则,而面对复杂的使用规则,用户是可以投票的,那就是你做你的,我不用!读这篇帖子的人,谁在实际项目中使用过exceptions()?事实上,我个人是害怕exception甚于害怕undefined behavior。

而对于用户来说,你可以不用,却不得不为对它付出运行性能和空间的代价。诸位有兴趣,不妨追踪一个IOStream功能的实现,看看为了支持这个异常,IOStream库的设计这耗费了多大的心力,而你的CPU又为此耗费了多少clock。

缺点之二,是异常本身的问题——即使你抓到了异常,又当如何?程序可能已经完全离开了发生异常时的执行环境,也许你连异常为什么发生都搞不清楚,谈何处理?也无非就是通知用户一声:“我完了,因为一个异常发生在XXXXXXXX处,你要报告的话给我发Email吧。” 是啊,你还能做什么呢?

我们试着用契约观点来分析这一状况,如果说“先测试,再使用”在传统上是一个idiom,那么在contract思想里上升为一个契约。对于C++来说,应该将“流处于good状态”作为一个契约,在每一个成员函数里进行检查。甚至你还可以设置一个调试期标志,专门用来核查用户是否检查过流状态。在必要的操作进行之前,你可以先用断言检查用户是否检查过流状态,满足了契约。这样一来,在契约之下,用户将被迫以正确的方式使用组件,从而大幅度简化组件开发的复杂度。

再来考虑异常。如果真正发生了异常,在Eiffel中提供了retry机制。Bjarne Stroustrup说过,retry可以做到,但是往往没有意义。为什么没有意义呢?因为C++中没有契约的思想,异常的产生可能根本就是程序员的bug。在这种情况下,无论retry多少次,结果都是一样的糟。可是在Eiffel里情形不同。如果各方面对于契约都做到很好的遵循,那么真正发生异常的时候,我们大可以比较有把握的说,这可能是一个很偶然的事件导致的。比如说网络环境下,另一个用户在那一瞬间突然对文件实施了一个操作,或者硬件的一次偶然异常。对于这种情况,“再试一次”成了合情合理的选择。我们很可能将异常扼杀在摇篮之中,从而不给上层模块带来任何影响。

谁说契约思想不伟大呢?