对于开发 0 bug 代码的思考——Design by Contract 契约设计

前言

最近在开发一个验证框架,希望能够降低代码的bug率,提升质量;不知不觉就来到了Design By Contract,感觉这是个方向。

本文主要是批判一下现有的契约设计问题,提出自己的看法,很希望得到一些牛人的指教。

 

研究现状简单分析

Design by Contract(DbC)是个天才(我觉得)叫Bertrand Meyer提出来的。那个家伙同时还搞出了个Eiffel的东西,是对DbC的实践(Practise)。我先简单介绍下DbC, 下文是一个范型字典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)是xxxx;这个方法干了什么(do),这个方法结束后,保证了什么输出(ensure)。

具体可以看:http://www.eiffel.com/developers/knowledgebase/design_by_contract.html

换句话说,一个方法有了前置条件(pre condition)、后置条件(post condition),调用起来就有了保证。

 

 

然后,一些人就开始做文章了。在c#领域里面,ms也没有闲着。首先是Spec#,个人感觉是一种模仿Eiffel的语言,一个例子:

Spec#

代码
    static void Main(string![] args)
        requires args.Length 
> 0
    {
        
foreach(string arg in args)
        {
            Console.WriteLine(arg);
        }
    }

这个例子算简单了,(该死的找不到一个恶心的例子,也许ms的工程师们也觉得恶心,没放上来)。用了一个repuires去约束了args.具体可以看:

http://en.wikipedia.org/wiki/Spec_Sharp

http://research.microsoft.com/en-us/projects/specsharp/#documentation

 

没有这么牛逼的,就在.net基础上去做,比如使用断言(Assert)和属性去实现(Attribute)。比如:

http://research.microsoft.com/en-us/projects/contracts/

代码

这就是个例子,或者直接用TestDriven里面的Assert()去实现。例子太多了,比如:

  1. http://geekswithblogs.net/Podwysocki/archive/2008/01/22/118770.aspx
  2. http://devlicio.us/blogs/billy_mccafferty/archive/2006/09/22/design_2d00_by_2d00_contract_3a00_-a-practical-introduction.aspx
  3. http://puzzleware.net/nContract/nContract.html#ConfiguringContractChecks

这些实践的一个极端的例子:

代码

[FormallySpecified]
[ModelField(
typeof(List<char>), "Contents",
  
@"new List<char>(this.ToString().ToCharArray())")]
[RepresentationalInvariant(
"numberOfChars == stringBuilder.Length")]
public class CharBuffer 
{
  [Pre(
"value != null")]
  [Post(
"Contents.Count == value.Length")]
  
protected CharBuffer(string value) { }

  

  [Pre(
@"index >= 0 && index <= Contents.Count && value != null")]
  [Post(
@"Contents.Count == old.Contents.Count + value.Length")]
  [ExceptionalPost(
typeof(ArgumentOutOfRangeException),
    
"index < 0 || index > Contents.Count")]
  
public virtual void Insert(int index, string value) { }

  
  
  
// Member fields 
  protected StringBuilder stringBuilder;
  
protected int numberOfChars;
}

不知道大家怎么去想的,我看见了就想吐。。。

 

当然,微软里面有个比较牛的家伙,开发了个叫LinFu的框架,使用了AOP去操作。这个家伙牛在直接用Emit自己实现了aop,号称性能比其他框架好很多。

  1. http://www.codeproject.com/KB/cs/LinFu_Part5.aspx
  2. http://www.codeproject.com/KB/cs/LinFuPart1.aspx

感觉是差不多了,可是就是心里还是觉得有道砍,非常的不爽。

 

我对Design by Contract的实践

DbC提出来是1986年,现在都什么年代了,为什么还停滞不前。引用柯南的一句话:真相只有一个。因为路走错了。各位ms大牛们,每天埋头钻牛角尖的,因为他们把契约设计看成了一种程序代码、一种语言

结果导致了与业务毫不相干的、难看的(spec#)、奇怪的代码充斥我们优美的业务逻辑,然而DbC真正要解决的问题却没有解决。正如一个笑话说的:

苏联的优势在哪里?在于他解决了其他制度国家不存在的问题。(仅笑话,不要扯上政治)

我个人认为Design by Contract是一种设计模式!是一种习惯!是一种开发中的辅助语言

 

先是一个简单的调用例子:

    class A
    {
        
public void Foo()
        {
            
int interval = 1;
            B b 
= new B();
            b.Foo(interval);
        }
    }
    
class B
    {
        
public void Foo(int interval) { }
    }

A调用了B的Foo方法,传入参数interval。

 

如果B对interval的约束很简单,比如要求interval>0,这样很轻松,用之前的spec#、aop、attribute、assert之类的都容易实现。可是现实生活不是这样,假设:

B要求interval非负数,当小于10的时候必须连续、当大于10的时候,必须每连续2个数字之后断开1个数字。

这怎么办??亲爱的spec#们,傻眼了吧。因为他们的工具对precondition的描述太有限了,而我们的需求又太复杂了,所以导致了design by contract停滞不前。

 

针对这些问题,我们为什么要用程序语言去约束?为什么就不用自然语言?为什么设计的时候内部的类(internal)使用自然语言规定了传入的要求,然后最终暴露在外部的类(public)再去针对这些要求去做验证?比如:

代码

    class B
    {
        [Contract(
"interval小于零,大于0的时候,10以内连续,10以外每连续2次就断开1次")]
        
public void Foo(int interval) { }
    }

的确,对于B.Foo我什么都没有做,只是添加了语言去描述约束。但是,当我编写A的时候,我亲爱的VS20xx就会自动的去检测调用对象的情况,然后汇总contract。比如:

是不是感觉清晰了很多?如果A是个最终暴露给用户的类,我们只要在调用A.Foo的时候,对他的方法的contract都做个验证,就足够了。

Design by Contract理论形态

我们开发设计的时候,一定会分interval/public class去写,暴露给用户的public class要尽量的少,剩余的工作全部交给内部的类去实现。这样一般会采用Facet的设计模式,由他负责提供方法、提供对象,而不是让用户自己去new。这个是我DbC的前提。

 

Design by Contract深入的去思考,实际上是对类方法的传入参数的约束(请先不要考虑返回值,让我先解决50%的问题)。对于内部类而言,会默认传入参数符合调用要求,不会对传入参数进行验证。

这样,当类与类之间调用后,暴露在最外面的类就负责起了最终参数传入的验证工作,所谓一夫当关,只要最外面的类把好关,那么剩下的业务逻辑我们会默认"在正确的输入下,会得到正确的输出"。

因此,如果我们知道外部类的某个方法需要负责哪些contract,这样我的design by contract就完成了。

 

因此,首先技术上要解决的是,我的外部类的方法如何知道需要的contract。目前.net的语言来看,反射还不足以完成任务,也许需要使用emit等高级工具。因为有时候有些内部类的contract会被另外的内部类保证了,这样外部类需要负责的contract就少了。

 

其次,就是如何去负责这些contract,这个就可以使用合适的设计模式了,针对外部类每一个参数,进行一个contract的严格验证,验证过程可以在新的类完成。比如以下伪代码:

代码

    class A
    {
        
public void Foo(
            [Supervise(CheckVar1)] 
// 对传入参数进行验证
            string var1,
            [Supervise(CheckVar2)] 
// 对传入参数进行验证
            string var2)
        {
            
int interval = 1;
            B b 
= new B();
            b.Foo(interval);
        }
    }

然后验证过程在新的方法实现了。这样开发,就变得非常的清晰了。

 

小结与后续

  1. 在design by contract的框架开发下,大部分的类的方法会使用自然语言去描述contract;到了关键的边缘区域(内部与外部交互的区域),会查询此区域的contract(当然是自然语言描述的集合),然后我们再针对这些contract去检视传入参数。
  2. 如果有些内部类会履行某个类的contract,那么这个类的履行也需要使用检视。

如果我的想法能够在现有的技术下实现了,我觉得出现一个全新的开发过程,一种新的practise。以后的程序员会在class标注各种contract,然后最终会在某些类上使用Supervise,同时在某些类可以查看他需要履行的contract是什么。

这样开发起来,bug会降低到0,不是梦想。

 

(偶狂敲了1个小时,吐了几千字的废话,希望各位支持一下,能给点思路,指出我的错误。在此感谢了!)

 

技术支持

reborn_zhang@hotmail.com

zc22.cnblogs.com

 

 

 

posted @ 2009-11-30 23:31    阅读(2210)  评论(18编辑  收藏
IT民工