一次程序调试带来的教训

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

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

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

GetCodeList查询
ContractedBlock.gifExpandedBlockStart.gifGetCodeList
1None.gifSELECT Code.city, Code.code
2None.gifFROM Code
3None.gifWHERE Code.code like ([(@prefix] & "*")
4None.gifORDER BY len(code) DESC , code;


DataBaseManager封装了GetCodeList查询

ContractedBlock.gifExpandedBlockStart.gifDataBaseManager
 1None.gifpublic class DataBaseManager
 2None.gif    {
 3None.gif        public static void EnsureDatabaseAvailable ()
 4ExpandedBlockStart.gifContractedBlock.gif        { /**//*代码省略*/ }
 5None.gif
 6None.gif        private static OleDbConnection GetConnection ()
 7ExpandedBlockStart.gifContractedBlock.gif        { /**//*代码省略*/ }
 8None.gif
 9None.gif        public static CodeList GetCodeList(string prefix)
10None.gif        {
11None.gif            string statement= "GetCodeList";
12None.gif            OleDbDataAdapter adapter = new OleDbDataAdapter (statement, GetConnection ());
13None.gif            OleDbCommand command = adapter.SelectCommand;
14None.gif            command.CommandType = CommandType.StoredProcedure;
15None.gif            command.Parameters.Add("@Code", prefix);
16None.gif            OleDbParameter parameter = command.Parameters.Add(new OleDbParameter("@Code", OleDbType.VarWChar, 255));
17None.gif            parameter.Value = prefix;
18None.gif            DataTable table = new DataTable("Code");
19None.gif            adapter.Fill(table);
20None.gif            return new CodeList(table);
21None.gif        }
22None.gif    }


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

ContractedBlock.gifExpandedBlockStart.gifCodeList
 1None.gifpublic class CodeList : IEnumerable
 2ExpandedBlockStart.gifContractedBlock.gif    dot.gif{
 3InBlock.gif        ArrayList mList = new ArrayList();
 4InBlock.gif
 5InBlock.gif        internal CodeList(DataTable table)
 6ExpandedSubBlockStart.gifContractedSubBlock.gif        dot.gif{
 7InBlock.gif            foreach (DataRow row in table.Rows)
 8ExpandedSubBlockStart.gifContractedSubBlock.gif            dot.gif{
 9InBlock.gif                mList.Add(row[0]);
10ExpandedSubBlockEnd.gif            }

11ExpandedSubBlockEnd.gif        }

12InBlock.gif
13InBlock.gif        public IEnumerator GetEnumerator()
14ExpandedSubBlockStart.gifContractedSubBlock.gif        dot.gif{
15InBlock.gif            return mList.GetEnumerator();
16ExpandedSubBlockEnd.gif        }

17ExpandedBlockEnd.gif    }

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

ContractedBlock.gifExpandedBlockStart.gifRegionCodeParser
 1ExpandedBlockStart.gifContractedBlock.gifpublic class RegionCodeParser    dot.gif{
 2InBlock.gif
 3ExpandedSubBlockStart.gifContractedSubBlock.gif        /**//// <summary>
 4InBlock.gif        /// 从一个电话号码中解析出区号
 5InBlock.gif        /// </summary>
 6InBlock.gif        /// <param name="phoneNumber">待解析的电话号码</param>
 7InBlock.gif        /// <param name="defaultValue">缺省值</param>
 8ExpandedSubBlockEnd.gif        /// <returns>解析出的区号;如果不含区号,返回缺省值</returns>

 9InBlock.gif        public string Parse (string phoneNumber, string defaultValue)
10ExpandedSubBlockStart.gifContractedSubBlock.gif        dot.gif{
11InBlock.gif            CodeList list = DataBaseManager.GetCodeList (phoneNumber.Substring(03));
12InBlock.gif            foreach (string code in list)
13ExpandedSubBlockStart.gifContractedSubBlock.gif            dot.gif{
14InBlock.gif                if (phoneNumber.IndexOf(code) >= 0return code;
15ExpandedSubBlockEnd.gif            }

16InBlock.gif            return defaultValue;
17ExpandedSubBlockEnd.gif        }

18ExpandedBlockEnd.gif    }


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

接下来是单元测试:

ContractedBlock.gifExpandedBlockStart.gifRegionCodeParserTestCases
 1None.gif[TestFixture]
 2None.gif    public class RegionCodeParserTestCases
 3ExpandedBlockStart.gifContractedBlock.gif    dot.gif{
 4InBlock.gif        [SetUp]
 5InBlock.gif        public void SetUp ()
 6ExpandedSubBlockStart.gifContractedSubBlock.gif        dot.gif{
 7InBlock.gif            DataBaseManager.EnsureDatabaseAvailable();
 8ExpandedSubBlockEnd.gif        }

 9InBlock.gif
10InBlock.gif        [Test]
11InBlock.gif        public void SmokeTest ()
12ExpandedSubBlockStart.gifContractedSubBlock.gif        dot.gif{
13InBlock.gif            const string Default = "999";
14ExpandedSubBlockStart.gifContractedSubBlock.gif            string[] phoneNumbers = new string[] dot.gif"02000000000""02100000000""59910000" }; //为保护客户隐私,电话号码已作马赛克处理,特此声明
15ExpandedSubBlockStart.gifContractedSubBlock.gif            string[] regionCodes = new string[] dot.gif"020""021", Default };
16InBlock.gif
17InBlock.gif            using (RegionCodeParser parser = new RegionCodeParser())
18ExpandedSubBlockStart.gifContractedSubBlock.gif            dot.gif{
19InBlock.gif                for (int i = 0; i < phoneNumbers.Length; i ++)
20ExpandedSubBlockStart.gifContractedSubBlock.gif                dot.gif{
21InBlock.gif                    string code = parser.Parse(phoneNumbers[i], Default);
22InBlock.gif                    Assert.AreEqual(regionCodes[i], code);
23ExpandedSubBlockEnd.gif                }

24ExpandedSubBlockEnd.gif            }

25ExpandedSubBlockEnd.gif        }

26ExpandedBlockEnd.gif    }

顺利通过编译->执行测试用例->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了,改成这样:

ContractedBlock.gifExpandedBlockStart.gifRevision: GetCodeList
1None.gifSELECT Code.city, Code.code
2None.gifFROM Code
3None.gifWHERE ((left(Code.code,len([@prefix]))=[@prefix]))
4None.gifORDER BY len(code) DESC , code;

NUnit再测,还是Red bar……

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

ContractedBlock.gifExpandedBlockStart.gifDataBaseManager.GetCodeList
 1None.gifpublic static CodeList GetCodeList(string prefix)
 2None.gif        {
 3None.gif            const string statement =
 4None.gif                      "SELECT Code.city, Code.code FROM Code" +
 5None.gif                      " WHERE ((left(Code.code,len([@prefix]))=[@prefix])) " +
 6None.gif                      "ORDER BY len(code) DESC , code;";
 7None.gif            statement = statement.Replace("@prefix", prefix);
 8None.gif            statement = statement.Replace("@prefix", prefix);
 9None.gif            OleDbDataAdapter adapter = new OleDbDataAdapter (statement, (OleDbConnection)connection);
10None.gif            OleDbCommand command = adapter.SelectCommand;
11None.gif            command.CommandType = CommandType.StoredProcedure;
12None.gif            command.Parameters.Add("@Code", prefix);
13None.gif            OleDbParameter parameter = command.Parameters.Add(new OleDbParameter("@Code", OleDbType.VarWChar, 255));
14None.gif            parameter.Value = prefix;
15None.gif            DataTable table = new DataTable("Code");
16None.gif            adapter.Fill(table);
17None.gif            return new CodeList(table);
18None.gif        }


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

citycode
020广州市

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

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

再次回到座位上,老老实实从头看一遍程序。突然一行代码从眼前闪过:
ContractedBlock.gifExpandedBlockStart.gifCodeList constructor
1None.gifinternal CodeList(DataTable table)
2ExpandedBlockStart.gifContractedBlock.gif        dot.gif{
3InBlock.gif            foreach (DataRow row in table.Rows)
4ExpandedSubBlockStart.gifContractedSubBlock.gif            dot.gif{
5InBlock.gif                mList.Add(row[0]);
6ExpandedSubBlockEnd.gif            }
7ExpandedBlockEnd.gif        }

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

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

转载于:https://www.cnblogs.com/omnislash/archive/2007/07/30/835781.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值