怪怪 | Nothing, Everything

"有过一个发疯的时刻,有感觉的钢琴以为它是世界上仅有的一架钢琴,宇宙的全部和谐都发生在它身上." - 狄德罗
随笔 - 104, 文章 - 3, 评论 - 2004, 引用 - 44
数据加载中……

不需要强类型, 需要强测试?

这是Joel的观点, 据文章中说, Bob大叔也是这个观点。 他的这篇文章(应该是他引用并做了个前言的这篇Bruce Eckel的文章,多谢J老兄指正)认为编译器的检查, 实际上就是编译期的测试。 而且他俩, 据说都达到了“测试成瘾”的境界。

我当然没他俩牛, 考虑到Knuth他老人家也已经那么大年纪了, 程序正确性自动化证明工作现在又是起步阶段,也就没必要拉别人壮胆了。很多人说我是要特立独行, 我自己早就说过, 那我就是哗众取宠好了。不过要说明的一点是, 我并不喜欢当少数派, 我只是希望有一个清晰的概念, 任何不清晰的, 就是我要发问的; 所以反调还是要唱下去:

1. 编译器甚至运行时的检查, 很难说是测试。 如果测试和检查是一码事, 就根本不会是两个词了。 检查是如下过程: 我们描述一个东西A, 它具有特性B和C,那么所谓的检查, 就是观察一下是不是有B和C。 而测试呢,则是我不管什么A、B、C,先用一下, 如果没有用成功,我就给出一个错误报告。无论在运行时,还是在编译期, 如果是前者, 就是检查, 如果是后者就叫测试。

所以即使是运行期类型检查, 仍然是检查, 不能理解为测试(当然,那些为了执行运行时检查所写的代码本身是测试)。 另一个例子是汽车碰撞的测试, 我们无法找出一个办法知道一辆车的强度是不是能够承受多少公斤的力, 所以我要撞一下它。 如果有测量方式, 让我无需撞一下, 那么这就是检查了。

2. Joel有道理的地方在于,检查并不能找出所有错误: 比如过程代码在逻辑上的错误。因为这些过程代码或者逻辑, 是无法被测量的。就好比, 假设有一个不完备的检查车辆强度的方法, 你测量一下, 觉得行了, 没有做碰撞测试; 但是既然隐藏着你没有考虑到的因素, 则很有可能在撞车时,人还是被夹死了。但我个人认为, 这不是干脆全盘用测试代替检查的理由。

比如,很多对日系车的评价, 就从另一个方面体现了这些问题。 比如用吸能来保证成员的安全这件事, 日系车总是用NACP几颗星来说, 也就是用测试说话。 如果测试不是面面俱到的, 比如某种特殊角度的撞击没有测试, 当这种撞击发生的时候, 很多事情就一点保证也没有了。而欧系车在吸能的同时,还增加车体的刚性,虽然也不能说特殊角度的碰撞就一定不会烂,但是总能通过一些推导得知会不会有改进。

所以, 这里面存在三个概念:

1. 完全性。 无论是测试, 还是检查, 都存在完全性的问题。 出不出问题, 不是看你是检查还是测试, 而是看你是不是完全。

2. 可测量性。 只有可测量的东西, 才能用检查完成, 而剩下的则有可能需要使用测试。 为什么说有可能呢?

3. 可推导性。因为某些情况下, 从可测量的东西可以推导出不可测量的东西的情况, 这样就无需测试了。

我想这应该是一个比较明确的、 “回”字有多少种写法的描述了。 那么继续。

关于完全性的问题, 实用主义出发的,我的疑问目前有两点: 单元测试的真实覆盖率是不是真的能做到完全? 见《再问TDD: 扩散角模型》。 单元测试造成的重复表达问题, 见《三问TDD: 单元测试总是好的吗?》; 另外, 对结果进行采样式的确认, 并无法保证被测目标的逻辑100%正确。 暂时还不实用的、我在研究的问题:对于所有运行前就可以确认的东西,检查是不是真的不能做到完全?

这就涉及到了第二点, 可测量性。 而一个设计、一段程序的可测量性, 从根本上讲是确定的。 以我目前的见识看来, 是否可测量, 除了测量目标的特征之外,在于我们使用的工具(如某一种编程语言)是否支持对那些符合特征的目标进行测量。主要是还是我们能不能把这些运行前可以确认的东西表达到系统内部, 不能表达或者工具忽略它们, 导致不能测量, 那就是现有工具的错, 而不代表这些地方应该被测试占领。 (另外,可表达还代表着这些知识的可使用, 而测试是外部的, 也丧失了这种好处。)

然后我们从易用的角度来看看第三点,是不是足够好用的语言, 就必须使用测试或者动态检查: Joel的文章, 主要提到duck typing等设施的方便之处, 我想这个又是一个混淆, 这些易用没有一样是放弃静态检查带来的。duck typing和其它设施本身并不会造成真正的未知。duck typing和其它设施,并不会造成在任何情况下都未知的情况(多谢脑袋的提醒)。 因为哪怕可以临时的扩展比如一个类的方法, 只要这些是写在代码中的,并且不和运行时造成的变化纠缠在一起,就已经留下了足够的信息, 而导致剩下的事实变成可推导的。

也就是说:只要有足够的信息, 并且这些信息是机器可获取、可理解的, 至少一般来说要么可测量、要么可推导。而所有可测量或者可推导的问题的验证,都可以放到运行前进行。那么导致当前流行的表达工具, 要么选择运行时检查甚至不检查,要么表达能力有缺陷, 其问题是什么呢?

最终的实现不同,我想这是因为不同的作者关注点不同的缘故。不过,也许是实现成本: 现在很多人都在说编译器前端没难度, 我想这很可能是因为大多数编译器前端处理的问题还不够多; 另一个问题是编译计算的成本, 很显然, 如果我改了两行代码, 计算了3分钟, 告诉我是错的, 那我还不如试一试算了。 很多人认为占用CPU时间比占用程序员值, Joel就是这么说的; 但他也承认, 如果动态检查, 则可能导致比如一个Web服务器或者集群提供需要10倍的计算能力。 在这方面,Knuth认为(虽然不是在这个问题上), 如果能改进一次, 永久受益, 那么程序员付出代价是值得的。

另一方面, 是对现有方式的保护, 就好比HTML的缓慢进化一样, 一个新的表达工具, 不可能突然冒出来, 并且大家都很快的掌握它。 从另一个角度讲, 对大型商业公司而言, 这样的更新换代, 可能比从C# 1.0直接到C# 4.0还要大一些; asp -> asp.net,就让很多人投靠了php阵营。不过这些也只能当作一个八卦讲讲而已。有一点是确定的: 实现一个有很大不同的表达工具,如何保留老的软件资产和知识资产, 确实是一个不得不关注的问题。

我有时候会把问题归结为一个统一的可行性问题: 要么是, 要么不是; 也许我太机械了,还是要具体问题具体分析。 但是Joel本人也曾一天到晚的思考, 是不是有一个最佳的方法,可以尽量少的付出代价, 尽量多的得到不同做法的优点; 不过他、Fowler、Robert Martin, 似乎已经都放弃了, 他们放弃的对吗?



不同的人如何阅读这篇文章:

1. 这些都不关心的兄弟, 点右上角的X, 或者骂两句也可。

2. 对如何舒服、合理的进行测试,测试、检查以及正确性之间的关系有见解的兄弟,请不吝赐教。但我知道TDD不只在于保证正确性, 本文涉及的仅仅是一个很小的方面,不要扩展它。

3. 不关心用不着的东西的兄弟, 可以通过我这篇文章的脉络, 看看有哪些词汇是未来可能要了解的;这些词汇包括Python/单元测试/duck typing等等, 他们所代表的一些特征, 很多已经开始流行了;其它的,几乎可以肯定说,很快也会出现在.NET平台上。而比如: 自动推导、机器验证等,就当没看见。

4. 正好对这个话题本身感兴趣的兄弟, 跟你们就没啥可多说的了,我自己准备好小板凳,等待上课 :)。



题外话:

最后在征集一次英文文章的合著者, 可惜我英文水的不行, 又不想写一篇文章1个小时, 翻译10个小时。我知道社区里大多数人对这些问题不感冒, 还有以为我仅仅是在对流行趋势唱反调的; 但是据我观察, 国外有不少爱讨论这些的社区,我想去试试。 我的目标是, 收集出一个当前表达工具支持度还不够、 而实际上是有改善的可能的特性列表, 也许我比较菜, 不能够实现这个列表上的东西, 但是我想即便是这个收集和论证工作本身也有些好处。

大家可能觉得我像个牛皮糖,天天琢磨不能让任何人挣钱或变轻松的东西, 还在别人耳边嗡嗡,一点也没有自知之明; 但是第一是我确实感兴趣, 第二是我也相信自己讨论这些的意义。 不是说我对那些刺耳的话一点感觉没有, 说实在的, 我每次都感觉很难受。 我过去有个想法, 就是大家说中文, 这是一个非常大的优势; 老外的东西大多咱们都看得懂, 但是咱们的东西老外看不懂。

如果咱们能通过有讨论者讨论、有实践者真干这一系列过程, 在一些虽然大多数人不会接触的关键领域作出突破, 那我们真有可能走在世界前面,尤其是软件学科是唯一一个不要求基础工业的学科了。 作为使用者, 不接触这些领域, 并不代表没有资格参与讨论; 而且只要敢于去想、去要求,也肯定能提出甚至重量级的意见。唯一一点小小的要求就是感受力: 感受到不爽的能力。

虽然就算有这种领先也不会持续多久, 但仍然会给我们挣点面子; 从投资环境讲, 如果能争取到这种声誉, 对我们每一个IT从业人员的就业等各个方面, 都会起到一些作用。 可能有人又要说了, 这种大而无当的事情, 根本不是你怪怪应该考虑的...

posted on 2008-07-18 06:19 怪怪 阅读(2241) 评论(21)  编辑 收藏

评论

#1楼    回复  引用  查看    

居然是怪怪的文章,一开始没看出来。
2008-07-18 06:43 | 金色海洋(jyk)      

#2楼    回复  引用  查看    

如果是技术文章,我凑合着能翻译,译不好,但至于能译
你这种,嘿嘿,等着大罗神仙降世吧
2008-07-18 07:37 | 丁学      

#3楼    回复  引用  查看    

文章看的好辛苦。。。
2008-07-18 07:39 | U2U      

#4楼 [楼主]   回复  引用  查看    

@金色海洋(jyk)
啊? 我变了?

@丁学
我这怎么不是技术文章啊...

@U2U
是内容没意思, 还是我写的不好呢?
2008-07-18 07:44 | 怪怪      

#5楼    回复  引用  查看    

看老怪的文章 常常看到类 Bob , Knuth ,Fowler、Robert Martin 等牛的名字^_^

有如下疑问,请善知识们赐教
1 是否存在可检查而不可测试的
2 是否存在可测试而不可检查的

如果以上二者存在,那么是不是说 ,检查和测试并不是问题的根本性问题

小弟才疏学浅,也可能是受了点佛法思想的影响,我总觉的吧,本来世界上没什么大不了的,可人们总想这想那,于是为了解决这那的问题 就有了各种方法论,方法论层出不穷新问题新困难也层出不穷,犹如六道轮回,转个不停。有人告诫我们要拿得起放得下,可在软件设计开发这个没有yd的小盒内,我们拿什么放什么呢?也许回家种白菜才是解决问题的好方法

ps:貌似人生病的时候喜欢啰嗦。
2008-07-18 08:13 | 戏水      

#6楼    回复  引用    

纠正一下,“Strong Typing vs. Strong Testing“是Bruce Eckel的文章。
http://www.mindview.net/WebLog/log-0025?PHPSESSID=f98dbda90e07f5dadde94eabd8583f32

《Joel谈优秀软件开发方法》只是周思博编者。对一些他认为好的文章做了个汇编,顺路评论一下而已。
2008-07-18 08:41 | J [未注册用户]

#7楼    回复  引用  查看    

这一切都是为了发展动态语言而寻找的理论基础,俺支持“强测试”观点。
2008-07-18 08:54 | 李战      

#8楼 [楼主]   回复  引用  查看    

@戏水
我个人目前的想法,都存在, 但情况是完全不同的。

可检查不可测试, 代表拥有所有条件,但测试代价可能无限高。

可测试不可检查, 代表存在未知条件。

另外, 检查和测试, 是一个实用性问题, 不是一个原则性问题。 其实我说的这些并非不实用的东西, 只是领域不同...

老子、和佛法的一部分思想, 必然导致回家种白菜的结果; 其实我个人坚信老庄的哲学, 不过我是XX分裂者, 在实际行动上采用的是西方那一套。

2008-07-18 08:57 | 怪怪      

#9楼    回复  引用  查看    

对于高手而言,可以采取任何的方法,都无所谓



对于低手而言,就只有一种方法,也就是目前流行的方法



对于这些所谓的大牛的过分吹鼓, 我已经非常反感
2008-07-18 08:58 | jjx      

#10楼 [楼主]   回复  引用  查看    

@李战
只要有额外的耗费, 就坚决不支持, 打到帝国主义 :P

我是能量最小化原则的忠实fans~

@ J
谢啦, 等下编辑一下。
2008-07-18 08:59 | 怪怪      

#11楼    回复  引用  查看    

继续看不懂.
2008-07-18 09:13 | 狼Robot      

#12楼    回复  引用  查看    

那不等于用我们的人工去抢编译器的工作?
2008-07-18 10:15 | deerchao      

#13楼    回复  引用  查看    

关键是测试覆盖率的问题。如果覆盖率达到100%,那么自然可以在测试逻辑的同时顺便也把语法错误和类型错误顺便测试了。现状是难以编写有关UI和数据库访问相关的单元测试,而对于信息系统来说这两块占开发时间的大半。

单元测试的优点是可以一小步一小步地前进,这样就节省了大量定位错误的时间,同时因为对刚刚编写的代码还有印象,改起来也快。所以我在编写一些工具类的时候非常喜欢用TDD的方法。

附言:希望将来可以有更容易编写单元测试的方法。越早发现Bug效率越高。
2008-07-18 11:46 | 1-2-3      

#14楼    回复  引用  查看    

我没看过本文提到的这几位大牛的文章。从本文来理解似乎是:“静态检查是鸡肋,单元测试是银弹。只要把银弹用彻底了,鸡肋就可以不要。”是这个意思不?其实我非常喜欢TDD,它确实是个可以极大提高效率和正确性的漂亮的子弹,只是目前它的射程还不够远,将来啥时候能射得远一些也还是未知数。
2008-07-18 11:57 | 1-2-3      

#15楼    回复  引用  查看    

@guaiguai
deerchao 也好这个。推荐下。
很羡慕你能交上laodai这样的朋友。
2008-07-18 12:04 | bmrxntfj      

#16楼 [楼主]   回复  引用  查看    

@jjx
我比较烦的一点是, 他们说的并非完全没有道理, 这样反而看起来费劲, 对的错的全都得自己挑...

@狼Robot
呃...

@deerchao
如果最终达到1-2-3形容的那个做法, 确实是这样...

@1-2-3
是地, 另一个完全相反做法, 是在Template C++里面的, 甚至通过表达, 把物理量用静态的方式约束、 使用编译器检查。

我个人的习惯和你正好相反, 只有靠别的方法确实还不如来一次单元测试性价比高的时候,我才用逐渐偏向测试驱动。

这个问题前段时间有个文章, 说80年代的“净室开发”完全没有测试, 也能得到相当高的正确率。 当然TDD在软工方面的其它优点先撇开不谈。
2008-07-18 13:25 | 怪怪      

#17楼 [楼主]   回复  引用  查看    

@bmrxntfj
这个问题, 我为你写了一个文章, 见这里:

http://www.cnblogs.com/guaiguai/archive/2008/07/18/1246033.html

其实我觉得这个大家应该讨论一下, 不过鉴于最近我比较不招人待见, 就不污染首页了。
2008-07-18 14:04 | 怪怪      

#18楼    回复  引用  查看    

@怪怪
实在无法想象完全没有测试也能得到相当高的正确率。不知道“净室开发”具体要怎么做。不知道有没有审核代码的人呀?如果有检查代码的步骤,这也是一种测试,叫“眼球测试”,吼吼。当然按照老怪物的说法,检查代码是先知先觉,应该算作“检查”;后知后觉的才叫“测试”。就好象我拔出枪来对准某人,他要是在我扣动扳机前就吓死了,那我顶多算误伤;他要是在我扣动扳机后被打死了,那我就是谋杀。呵呵,开个玩笑轻松下。

如果真有这样的项目,那它至少要满足5个条件:
1. 程序员从来不犯编译器检查不出的低级错误,例如:“string s = null;s.ToString();”或者“int[] i = new int[] {}; int j = i[0];”。或者,做“眼球测试”的人足够认真,足够聪明,足够耐心。
2. 程序员对业务和详细设计书理解得非常透彻,绝对没有一点马虎的地方。
3. 程序员的编程能力非常强,只要得到明确的要求,就可以写出优质无错的算法。
4. 这个项目只有一个程序员;或者程序员之间没有任何协助和依赖;或者程序员之间的交流非常顺畅,绝对没有误会的时候。
5. 程序不依赖任何第三方框架、库、控件和运行环境;或者它依赖的任何第三方的东西都是0 Bug的,并且程序员对这些第三方的东西了若指掌,没有一点误会的地方。
2008-07-18 14:42 | 1-2-3      

#19楼 [楼主]   回复  引用  查看    

@1-2-3
你说的情况5, 或者情况4的反面, 就落入了Knuth说的, 搞不清楚干了某事会取得什么效果的时候, 那测试是必然的。不过4要是一个外科手术式团队,就很难走向反面。

1和3是可以做到的, 而且不测试不代表不断言; 而且也不必太绝对, 习惯于不测试的人, 关键反而在于应该知道什么时候必须选用测试了,这有时会包括对目标复杂性和执行者能力的准确判断。

2呢, 就比较复杂了, 在这块我部分赞成迭代的做法, 毕竟迭代也是70~80年代就开始了的。 具体情况具体分析, 有些东西, 叫做原型比叫做测试更合适。

我倒不是对测试有什么意见, 只是总隐隐感觉, 有更好的方法(但我的感觉可能是错误的); 当前可以使用的手段, 比如我说的不同表达方式配合检查方式可以覆盖的, 至少从正确性来讲, 就不需要测试参与了。

净室确实要基于眼球, 但是最高境界我认为还是Knuth说的, prove it; 可惜估计全世界真敢拍胸脯的没几个了。另外, 据McConnell援引的调查报告里, 都说明了无论哪种测试, 相比机器检查和人肉检查, 效果有限的多。
2008-07-18 17:20 | 怪怪      

#20楼    回复  引用  查看    

@怪怪
小结一下下,有不对的地方请指正:
无论粒度是小(例如单元测试)还是大(例如功能测试);无论是在编码前、编码中还是编码后;无论是机器检查还是人肉检查;无论是叫检查还是测试^_^,测试的目的都是确保程序的正确性。测试手段的好坏不外乎效果和成本两个指标。单就(时间)成本来说,
时间成本=发现Bug所花的时间+定位Bug并修正的时间
所以,
prove it确实是最高境界,这和疾病重在预防差不多。
编译器检查,发现Bug的时间就是编译时间(这个可长可短,不过应该比眼球检查和编写测试代码要快),由于目前编译器只能检查一些低级错误,所以定位和修正bug应该也很快。
单元测试,发现Bug的时间就是编写测试代码的时间。由于单元测试是一小步一小步走的,所以定位Bug并修正的时间的时间会比较短,这也是TDD的最大好处。
功能测试和系统测试,发现Bug的时间可长可短,关键看测试人员的水平和运气(据说有些操作系统的Bug过了几十年才被发现),定位和修正Bug的时间要比较长,而且很容易出现改了一个Bug出现十个Bug的情形。

综上,如果所有Bug都能预防或者被编译器检查出来,那就最好不过了。如果做不到,用单元测试是个不错的选择,可惜单元测试的覆盖率又达不到100%,所以最后的功能测试和系统测试还是少不了(不过留到这个阶段的bug越少越好)。

由此,可以看出单元测试确实填补了编译器检查和功能测试之间的空白,是个相当好的idea。但是在单元测试覆盖率不理想的现状下,要把编译器检查去掉,似乎有增加测试成本的可能(除非能把Bug都Prove了,如何做到这点是个值得琢磨的课题)。
2008-07-18 18:14 | 1-2-3      

#21楼    回复  引用  查看    

俺认为像Joel此类人或许太关注他所在的软工领域了,而忽视了其他方面。

同意文中观点,检查和测试不一样,检查顶多也就是测试的冰山一角。

抛开 C# 的静态类型检查,因为它那运行时类型检测和可怜的类型推导(这还只是C# 3.0才有的功能),来谈下相对更厉害的C++。

要想做到编译时测试,非常困难。静态编译型语言编译之后会完全丢失其类型信息,当然像 C# 这样的有VM的除外。编译器只能在词法分析阶段检测到类型,如果无类型,它该如何示警?如果无法检测,开发人员的痛苦要加倍了,呵呵。

C++的模板元编程倒有点编译时测试的味道。但是如果没有强类型,那么类型推导会十分困难,如果没有没有类型推导,几乎就没有什么大用处。

更重要的是,逻辑上的错误在编译时该如何测试?更何况,即使测试也不能完全消灭Bug。

此外,强类型带来的好处还有性能上的巨大提升。俺在使用D做模板元编程时,真是深有体会。

俺认为,有所取舍比追求完美更划算。追求完美的代价太高,所以任何软工都是各方取舍的结果。要是听信 Joel 胡侃,会着魔嘀。哈哈。

PS:怪怪兄的文章很有意思。但俺每次都没耐心仔细阅读,有时候思维跳跃的厉害,导致俺这个笨人思维连贯不起来。而且力求逻辑完美,这就好像代码中太多的条件判断淹没了核心算法,让阅读代码的人吃力。
2008-07-18 19:44 | Angel Lucifer      

标题  
姓名  
主页
Email (博主才能看到) 
验证码 *  看不清,换一张 [登录][注册]
内容(请不要发表任何与政治相关的内容)  
博客园首页

新闻频道

社区

小组

博问

网摘

闪存

  登录  使用高级评论  新用户注册  返回页首  恢复上次提交      
该文被作者在 2008-07-18 10:09 编辑过
成果网帮您增加网站收入


相关链接: