一次程序调试带来的教训

周五的时候在写一个小程序,它的输入是电话号码,输出是该号码的区号。区号表用Access数据库存储。
解析算法的最初思路是:把整张区号表做成一个状态机,电话号码逐字符输入。这样效率应该比较高,但是要手工构造那个状态机就有点麻烦。“先让程序跑起来,然后让它快起来”——稍微简化一下,先用号码的头三位(为什么用前三位?那是因为国内区号最短就是三位)做模糊查询,过滤掉明显不匹配的区号,然后再用剩下的区号对号码进行最长子串匹配。嗯,就是这个主意。

数据库设计是酱紫滴:
RegionCode数据表

字段名 类型及约束 字段含义
City 文本,非空 区号对应的主要城市
Code 文本,主键 区号

GetCodeList查询

GetCodeList


DataBaseManager封装了GetCodeList查询

DataBaseManager


查询结果被传递给CodeList类,它封装了对结果集的遍历(这个类还会有其他方法,例如提供区号和城市之间的映射查询)

CodeList

接下来是解析区号的RegionCodeParser类——本来它没必要是个类,但考虑到以后可能会替换算法,还是这么做了:

RegionCodeParser


程序的流程大致是:
接受一个电话号码和->取出前三位作为参数调用数据库查询GetCodeList,获得记录集->遍历记录集,用每一个区号去匹配电话号码,直到穷尽或找到匹配区号(分别返回缺省值或者匹配区号)。

接下来是单元测试:

RegionCodeParserTestCases

顺利通过编译->执行测试用例->OMG,被NUnit狠狠踩了一脚煞车——Red bar~~

NUnit给出的提示是:
Parser.UnitTest.RegionCodeParserTestCases.SmokeTest :
 String lengths are both 3.
 Strings differ at index 0.
 
 expected:<"020">
  but was:<"999">
 -----------^
RegionCodeParserTestCases.SmokeTest方法中for循环底部的Assertion失败了。 看上去第一个数据都没能通过测试。

打个log检查一下,CodeList居然是空的!我没看错吧?揉揉眼睛——在Access里面测试得好好的。难道是参数传递的问题不成?尝试改变参数的类型和长度,毫无效果。早就听说Access的查询对Ado.net支持很差,难道是真的?好吧,我认输,换条路,GetCodeList查询的过滤条件不用like了,改成这样:

Revision: GetCodeList

NUnit再测,还是Red bar……

OK,再退一步,我现在已经完全不信任Access的查询了,我自己拼SQL,参数传递我也一样不信任,直接把参数拼进SQL串去:

DataBaseManager.GetCodeList


再测,依然Red bar……
别急,擦擦汗,验证一下SQL串的正确性——拼完串的地方加个断点->添加快速监视->粘出拼好的SQL,放在Access里面执行——见鬼了,查询结果完全没问题:

city code
020 广州市

为啥程序都查不到呢?难道Access对Ado.net的支持差到这种地步吗?
最后一招,关门放狗……googling到一些文字,说Access不支持命名参数传递,只能严格按照参数出现的位置顺序传递实参。可是,我用SQL串拼得完全不需要参数呀?而且GetCodeList只有一个参数,完全不存在顺序问题啊?

光阴似箭,日月如梭,半天时间已经报销了,还是毫无进展。休息一下先,到走廊上溜达溜达……

再次回到座位上,老老实实从头看一遍程序。突然一行代码从眼前闪过:

CodeList constructor

这里我偷了个懒,用索引代替字段类型,这是一个隐含的假设:结果集的第一个字段是区号。查一下SQL,OMG!第一个字段是city,我违反了自己的诺言。天哪!一个业余程序员才会犯的错误居然花了我半天宝贵的时间?!

痛定思痛,痛何如哉?总结教训,以为殷鉴:
1、在怀疑别人是否出了问题之前,先确定自己是正确的。
2、每个单元测试用例的范围应该尽量小,以便快速定位问题。
3、尽量避免“按约定编程”,这会给程序加入容易使人忽略的假设,难以发现和定位。
posted on 2007-07-30 00:07  omnislash  阅读(597)  评论(0)    收藏  举报