对于开发 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#
![](https://i-blog.csdnimg.cn/blog_migrate/81178cc93a2a3bb5048d90d76e7ec935.gif)
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/
![](https://i-blog.csdnimg.cn/blog_migrate/cdec0645add3fc3c328197dda5c76203.gif)
这就是个例子,或者直接用TestDriven里面的Assert()去实现。例子太多了,比如:
- http://geekswithblogs.net/Podwysocki/archive/2008/01/22/118770.aspx
- http://devlicio.us/blogs/billy_mccafferty/archive/2006/09/22/design_2d00_by_2d00_contract_3a00_-a-practical-introduction.aspx
- http://puzzleware.net/nContract/nContract.html#ConfiguringContractChecks
这些实践的一个极端的例子:
代码
[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) { }
![](https://i-blog.csdnimg.cn/blog_migrate/b99501c0ab8f77006783dde3ca54c4e3.gif)
[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) { }
![](https://i-blog.csdnimg.cn/blog_migrate/b99501c0ab8f77006783dde3ca54c4e3.gif)
// Member fields
protected StringBuilder stringBuilder;
protected int numberOfChars;
}
不知道大家怎么去想的,我看见了就想吐。。。
当然,微软里面有个比较牛的家伙,开发了个叫LinFu的框架,使用了AOP去操作。这个家伙牛在直接用Emit自己实现了aop,号称性能比其他框架好很多。
感觉是差不多了,可是就是心里还是觉得有道砍,非常的不爽。
我对Design by Contract的实践
DbC提出来是1986年,现在都什么年代了,为什么还停滞不前。引用柯南的一句话:真相只有一个。因为路走错了。各位ms大牛们,每天埋头钻牛角尖的,因为他们把契约设计看成了一种程序代码、一种语言。
结果导致了与业务毫不相干的、难看的(spec#)、奇怪的代码充斥我们优美的业务逻辑,然而DbC真正要解决的问题却没有解决。正如一个笑话说的:
苏联的优势在哪里?在于他解决了其他制度国家不存在的问题。(仅笑话,不要扯上政治)
我个人认为Design by Contract是一种设计模式!是一种习惯!是一种开发中的辅助语言!
先是一个简单的调用例子:
{
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)再去针对这些要求去做验证?比如:
代码
{
[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的严格验证,验证过程可以在新的类完成。比如以下伪代码:
代码
{
public void Foo(
[Supervise(CheckVar1)] // 对传入参数进行验证
string var1,
[Supervise(CheckVar2)] // 对传入参数进行验证
string var2)
{
int interval = 1 ;
B b = new B();
b.Foo(interval);
}
}
然后验证过程在新的方法实现了。这样开发,就变得非常的清晰了。
小结与后续
- 在design by contract的框架开发下,大部分的类的方法会使用自然语言去描述contract;到了关键的边缘区域(内部与外部交互的区域),会查询此区域的contract(当然是自然语言描述的集合),然后我们再针对这些contract去检视传入参数。
- 如果有些内部类会履行某个类的contract,那么这个类的履行也需要使用检视。
如果我的想法能够在现有的技术下实现了,我觉得出现一个全新的开发过程,一种新的practise。以后的程序员会在class标注各种contract,然后最终会在某些类上使用Supervise,同时在某些类可以查看他需要履行的contract是什么。
这样开发起来,bug会降低到0,不是梦想。
(偶狂敲了1个小时,吐了几千字的废话,希望各位支持一下,能给点思路,指出我的错误。在此感谢了!)
技术支持
zc22.cnblogs.com