我对于Design by Contract的最佳实践 通往无错代码之路 欢迎砸场(玩笑)

来源:互联网 发布:5.8毫米子弹相关数据 编辑:程序博客网 时间:2024/06/05 09:15

我对于Design by Contract的最佳实践 通往无错代码之路 欢迎砸场(玩笑)

 

前言

在上一篇文章中,我简单介绍了契约设计(Design by Contract)的现状,还有自己的想法。

http://www.cnblogs.com/zc22/archive/2009/11/30/1614142.html

这篇文章主要介绍我如何实现了这个设计。

 

阅读本文前,想再强调一下,本文介绍的是一种实践,而不是技巧,或者能够保证您代码绝对 0 bug的方法。但是在这种实践下,代码 0 bug 是可以达到的。

 

正文

一个简单的契约设计,就是约束了某个方法调用的要求、以及返回的承诺。比如:

代码

复制代码
    public class Log
    {
        
public void Info(string message)
        {
            
//do something here
        }
    }
复制代码

如果我要调用Log.Info的方法,前提必须满足message非空。否则这个方法不能保证返回需要的结果。

 

那么根据经典的计算机原则:在正确的输入下,能够得到正确的输出。我只要保证了我所有调用方法的前提要求,我的代码就是 0 bug。这个就是契约设计。

 

以往我们会通过各种判断去检测输入,这种设计模式叫做:防范式设计(请看code complete 2,里面有详细介绍)。防范式设计的前提就是:我们会假设调用这个方法是恶意的,因此要对所有可能出错的输入进行检测。

 

在这种设计模式下,就会导致了各种与业务逻辑无关的代码充斥着我们的程序,维护困难,看起来也恶心。因此契约设计是非常反对的。

 

可是非常可惜,现在连微软也还走不出这个圈子,一堆的Policy Injection / AOP / attribute / SPec#等等,仍然无法成为主流。

 

现在我就尝试去走出这个圈子。

 

首先,我会对Log.Info这个方法设置一个Contract.Constraint,如果用中文就是契约里面的约束(合同约束)。

代码

复制代码
    public class Log
    {
        [Contract.Constraint(
"message不能空")]
        
public void Info(string message)
        {
            
//do something here
        }
    }
复制代码

很简单,我什么都没有做,仅仅好像输入了一个注释,内容是messge不能为空

 

然后,我去尝试调用这个方法:

复制代码
代码
    public class TestCase
    {
        public void call001()
        {
            
string message = "hello world";
            Log log 
= new Log();
            log.Info(message);
        }
    }
复制代码

 

乍看一下,好像我还是什么都没有做啊???我是不是在愚弄大家啊???别急,咱们还没有进行契约的验证(合同验证)

 

复制代码
代码
    public class TestCase
    {
        
public void test001()
        {
            MethodInfo method 
= typeof(TestCase).GetMethod("call001");

            Console.WriteLine(
"check the call001 can be approved by contract.");

            Console.WriteLine(
"approval result = :" + Contract.Approval(method));

            
foreach (IConstraint cons in Contract.GetOpenConstraints(method))
            {
                Console.WriteLine(
"the constraint of \"" + cons.ConstraintName + "\" is not committed.");
            }
        }
        
public void call001()
        {
            
string message = "hello world";

            Log log 
= new Log();

            log.Info(message);
        }
    }
复制代码

 

在test001这个方法里面,我对契约进行了验证,因此得到了输出:

代码

check the call001 can be approved by contract.
approval result = :False
the constraint of "message不能空" is not committed.

结果是:approval result = FAlse

意思就是,契约的约束没有被履行,方法call001是个无效的方法。大家这个时候应该清楚什么意思了吧。

 

简单说明下,Log.Info有个契约的约束,要使用这个方法必须履行这个约束,可是我在call001这个方法里面没有履行这个约束,所以合同无效,也就不能保证代码是正确的。

 

如何保证呢?请看:

 

复制代码
代码
    public class TestCase
    {
        
public void test002()
        {
            MethodInfo method 
= typeof(TestCase).GetMethod("call002");

            Console.WriteLine(
"check the call002 can be approved by contract.");

            Console.WriteLine(
"approval result = :" + Contract.Approval(method));

            
foreach (IConstraint cons in Contract.GetOpenConstraints(method))
            {
                Console.WriteLine(
"the constraint of \"" + cons.ConstraintName + "\" is not committed.");
            }

        }
        [Contract.Commitment(
"input of log is not null")]
        
public void call002()
        {
            
string message = "hello world";

            Log log 
= new Log();

            log.Info(message);
        }
    }
复制代码

 

在这段代码里面,我的call002调用了log.Info。但是我在这个call002里面做了一个承诺[Contract.Commitment],我保证了这个代码满足了log.info的契约。所以,我得到的测试结果是:

代码

check the call002 can be approved by contract.
approval result = :True

结果是:approval result = TRUE,再简单说下,因为我在call002里面使用了承诺,因此在这个承诺范围内,所有契约的约束我都会被满足。

 

这下子我的思路就清楚拉吧!!老实说,我真的很兴奋!!!因为我找到了通往 0 bug 的道路了!

 

源码下载

 http://www.boxcn.net/shared/304p0ktnke

 

核心技术介绍

其实思路比较简单的,就是查看当前方法体内 所有使用了的方法是否存在了契约的约束,如果存在了但是没有被履行,那么就认为契约无效。

 

当然,为了实现这个技巧,我使用了Reflection.Emit,直接操作了IL语言去获取方法体内的信息。

 

后记

当一个问题无法解决的时候怎么办?采用最简单的方法。Design by contract是个绝对的精华,可惜大部分人总是再想怎么去用代码实现,怎么在编译解析、运行中去实现。

 

因此我尝试了退一步,海阔天空。就像TestDriven一样,难道我们会在代码里面放各种Assert吗?肯定不会。但是我们在coding的时候需要了。

契约设计也是一样,他是一种编码过程,而不是结果。只有这个思路清晰了,才能豁然开朗。

 

技术支持

reborn_zhang@hotmail.com

zc22.cnblogs.com

http://www.codeproject.com/KB/cs/DesignByContract_indotnet.aspx

 

 

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

 

精灵软件 火热讨论组

192700436点击这里加入此群

95755843 [满]

绿色通道:好文要顶关注我收藏该文与我联系

关注 - 14
粉丝 - 203
荣誉:推荐博客
+加关注
0
0
(请您对文章做出评价)
«博主前一篇:对于开发 0 bug 代码的思考——Design by Contract 契约设计
»博主后一篇:Pixysoft.Framework.Versions 版本控制框架 开发实录
posted @ 2009-12-01 18:41 辰 阅读(1128) 评论(20) 编辑 收藏

  回复引用
#1楼2009-12-01 19:28 |y-y[未注册用户]
沙发......顶一下,是个不错的想法
  回复引用
#2楼2009-12-01 22:02 |深山老林  
0bug恐怕还有很长时间的路要走的,如果真的通过技术手段实现0bug,测试人员恐怕就该丢饭碗了。呵呵。
支持(0)反对(0)
  回复引用
#3楼2009-12-01 22:47 |oec2003  
一直都不太相信0bug
不过楼主这思路貌似还不错
支持(0)反对(0)
  回复引用
#4楼[楼主]2009-12-02 02:43 |辰  
@深山老林

我自己尝试了在现有项目中使用了文章的框架,开发中测试时的确仍然有bug,但是这些bug主要来自逻辑上的不严谨,比如递归造成死循环等。

而关于输入不确定产生的bug就不会存在了。

design by contract假设前提是在正确的输入下有正确的输出。那么bug唯一来源就是:在正确的输入下 不能产生正确的输出。

不过感到很爽的是,现在在开发中找bug变得很简单了,因为很多参数问题可以忽略,专心在逻辑上。

而且以前很多对代码输是否需要进行验证这个现在变得机器话了,也不可能出错了。
支持(0)反对(0)
  回复引用
#5楼2009-12-02 06:20 |Jake Lin  
bug free的问题需要由Unit Test来解决。
支持(0)反对(0)
  回复引用
#6楼2009-12-02 08:03 |深山老林  
@辰
没错,这个特性的确有用,它可以减少程序员找Bug的时间,我认为这才是它真正的作用。即使逻辑再完善,bug也不能保证降为0.
如果想通过这个东东寻求bug0之路,我还是感觉夸大了某种技术本身。从软件工程的角度考虑,目前这个观点恐怕也是不被接受的。
解决软件质量有很多方法,可能您对这种方式情有独钟,我个人感觉不管是您推荐的这种方式、还是单元测试还有其它的解决软件质量的办法,综合作用下会好些。至于bug0嘛,只能算是个理想了。
支持(0)反对(0)
  回复引用
#7楼2009-12-02 09:02 |生鱼片  
其实你的契约还是你预先知道了导致bug的原因,不知道的怎么去解决?0 bug还是不现实的。
支持(0)反对(0)
  回复引用
#8楼2009-12-02 09:03 |yingql  
[Contract.Commitment("input of log is not null")]
public void call002()
{
string message = null;

Log log = new Log();

log.Info(message);
}
问个问题,像上面这个样子是不是也算履行了契约?
支持(0)反对(0)
  回复引用
#9楼2009-12-02 09:03 |Gideon Wu  
你只能证明程序有bug,而不能证明程序没有bug。
支持(0)反对(0)
  回复引用
#10楼2009-12-02 10:43 |fftt[未注册用户]
你这个怎么好了,怎么跳出圈子了。 你看过CodeContract了吗,.NET 4.0 beta已经自带了这个东西了。 Contracts.Require ...
  回复引用
#11楼[楼主]2009-12-02 16:17 |辰  
@yingql

从程序角度是履行了。

但是从我们coding角度,我们在发现没有履行的情况下,硬声明了履行。

有点像 我明知道有错,但是我还是去错了。
支持(0)反对(0)
  回复引用
#12楼[楼主]2009-12-02 16:20 |辰  
@yingql

我觉得bug的定义是:

在程序员尽了最大可能去避免之后,仍然存在的软件漏洞。

那么问题就是如何最大可能去避免了。以往如果没有一定的工具,也许我们会用这种输入验证。

现在通过designbycontract,系统在编译的时候就会通知我们什么方法可能存在漏洞。

如果这样我们都仍然犯下错误,那么是bug。所以bug和程序员素质就相关了。
支持(0)反对(0)
  回复引用
#13楼[楼主]2009-12-02 16:27 |辰  
@生鱼片

我觉得应该是,我的内部方法会声明调用的要求,如果不满足这个要求就会出差错。这个就是Contract.Constraint,契约约束。

当然我写这个方法就等于知道潜在的危险了,比如空指针之类的。

那么以后其他类使用了这个方法,如果不履行契约约束,就会存在现在的危险,所以其他的类就需要做出承诺Contract.Commitment


不如咱们把故事说远点。

如果以后我们调用.net x.0的类库的时候,使用了list.Get(index)这个微软提供的方法。

编译中间报错了,说这个方法要求index大于0,但是调用这个方法的类并没有履行契约。我们就会查看代码去检查传入的index是否保证了大于0.
当确认的确代码中间验证了index之后,再在我们自己的类中注明了契约承诺 Contract.COmmitment,这样就是一种契约设计。

这样的设计基本上杜绝了外界对程序内部产生的影响了。
支持(0)反对(0)
  回复引用
#14楼2009-12-02 16:56 |生鱼片  
@辰
这和.NET4.0的契约式貌似没有本质的区别吧,在.NET4.0中我们也可以指定Precondition(前置条件), Postcondition(后置条件) 以及其他一些Assert(断言),Assume(假设)等,这个只能解决一部分问题,这种方式是好的实践,但是达到0 bug可能还远远不够,Unit Test还是不可少的。
支持(0)反对(0)
  回复引用
#15楼[楼主]2009-12-02 17:11 |辰  
@生鱼片

我看了.net 4.0关于dbc的实现。他做到的基本上和我的比较像。问题在于,他对dbc的实现太细力度了。比如:

?
1
2
3
4
5
public static string GetTypeName(object obj)
{
    CodeContract.Requires(null!= obj);
    returnobj.GetType().Name;
}


的确,如果我实现是:
?
1
2
3
4
5
[Contract.Constraint("obj is not null")]
public static string GetTypeName(object obj)
{
    returnobj.GetType().Name;
}


相比之下,也许.net 4.0会在编译甚至运行的时候去执行
CodeCOntract.Require,然后报错。

但是这么简单的例子,现实生活中不可能出现的。

我现在的架构已经要面对非常复杂的输入情况,因此我选择了自然语言去描述,在程序设计时去遵守。Design by contract就像一个备忘录,一个编译时的warning,不会影响执行性能,但是会提醒程序员,这个会有问题哦!
支持(0)反对(0)
  回复引用
#16楼[楼主]2009-12-02 17:12 |辰  
@生鱼片

unit test肯定是需要的,不过在dbc+unit test下,代码质量会更高。
支持(0)反对(0)
  回复引用
#17楼2009-12-02 17:29 |生鱼片  
@辰
其实任何程序在特定的场景下(如msg不为空)不管是否使用dbc都好办,我们都会去加if(msg!=“”)。但很多时候往往导致bug出现的是我们不可预知的(如msg=“1”,msg=“2”),如果我们知道这些也好办了。我们在尽可能可以确定场景的范围的前提下,多一个dbc来check某些通用的场景当然是好事,也是一种约束。
支持(0)反对(0)
刷新评论刷新页面返回顶部

原创粉丝点击