东方有线发票稽核项目暂时告一段落,本文对项目开发阶段作一个总结。
发票稽核模块在原东方有线业务数据平台基础之上完成,系统环境Oracle9i + jdk1.5 + weblogic9.2 ,架构采用Spring + Struts + Hinernate。
由于需求较为简单,所以计划开发时间较短,计划开发任务从 10 月 9 日 始, 11 月 17 日 实施部署;项目负责人1人,主管需求;DM1人,负责软件质量、Demo及公用代码编写、解决疑难问题;编码3人,负责业务代码编写。任务安排上DM似乎承担了过多的编码工作,事实上也正是如此,这是后话。
实际上从 9 月 2 日 (没错,就是 9 月 2 日 ,我来方正的第三天)就开始着手准备需求及部分Demo 的编写。整个9月份平淡无奇,完善需求、设计表结构、设计界面、技术规范文档、测试用例……一切按计划进行。
十一后,我让每个人估算一下自己的工作量,以下是各人填写的结果:
图1 工作量估算
这是一份很怪异的结果,至少我认为并不是所有作估算的人都想努力完成工作。先来看看第一个人,三个“发票××维护”功能极为相似,只有简单的增、删、改、查操作,与以往不同的仅仅是增、改、查操作都在同一个页面进行(客户明确提出了此项需求,其它功能也是如此)。此前还有一个“业务类型维护”,我将其作为一个Demo已经完成,极为相似的功能竟然打算9天完成!那个“资金录入”也没有任何难度。第二个人的估算我认为过于乐观,其中有各种关于权限的判断需要反复测试,而且这部分如果没有好的设计极易产生大量的垃圾代码(一段时间后我担心的事发生了)。第三个人经历过一期的开发,我本以为能够给我一个很快的预算,结果恰恰相反(其中的“发票使用信息录入”十一之前就开始编写了)。
此时我犯了一个错误——也正是这个错误导致了修改时期时间紧迫——我没有开会或以其它任何方式调整这个有问题的估算。这似乎是有原因的,我觉得这个系统需求稳定、功能简单、时间充裕、开发人员至少有一、二年以上工作经验,我们甚至可以免去测试组的工作,下图是统计的需求变更数:
图2 需求变更数
此前我从没见过如此稳定的需求和如此好说话的客户,看来这将是个轻松的项目。
没想到的是,整个10月我都在繁忙中度过。
为了实现一些动态功能及简化开发,我编写了部分标签,在文档上注明了各种标签的用法。此后的一段时间里,这个文档的页数不断增加,我也渐渐忙了起来,要命的是,总有人向我提出各种技术问题,java调试问题、JS问题、框架问题、还有让我及其气愤的SVN用法(难道大家一直在用CVS?可是这两者又太大差别吗?)。每天晚上我会检查新提交的代码,让我奇怪的是,尽管我多次强调规范问题,仍然没有人注意!查了一下邮件,至少五封附件中有《技术规范手册》,口头强调的已经不计其数。我自认为手册写的不错,有命名规范、注意事项、页面布局规范、代码风格、文档规范等,难道手册中有大量的方言?
当公用问题过多的时候,我开始无暇顾及他人的代码质量。事实上大部分并非公用问题,只不过是稍微费时的问题都有我来解决罢了(比如不会写正则表达式,不知道一个处理方法),一个简单的系统会有什么复杂的问题吗?
两周后,进度表上部分完成度被标为100%,我点开了部分页面,发现到处都是“Page not found”,超过一半的输入框写入长度没有限制。此时我发现了另一个问题,大家似乎在等待进度表上的时间点,当一个功能提前做完时没有仔细测试,没有重构,也没有马上开始下一个功能,而是在MSN,在论坛。这样的工作太有情趣了。
例会上,我提出了这个问题,指明了部分页面的哪些条件没有限制,本意是给出通用的提示,期待大家各自找出解决的方法,这似乎是一个错误,至少在这个项目中如此,大家期待的是给出具体的解决方法和更多的细节。几天后我发现只有我指出的那些特定输入框加入了校验。
磕磕绊绊的10月过去了,测试组也正式开始了工作,大量低级bug被记录在案,以下是bug列表及代码数量:
图3 累计bug数量
图4 周bug捡出及关闭情况
图5 bug严等级记分布图
图6 代码量
这无疑是一份令人窝火的统计图表,估算表中的第三个人制造了大多数bug。为了避免在页面硬编码实现一些权限校验,我设计了权限配置方案,大致是通过XML进行页面元素的权限设置,并写了详细的文档。奇快的问题又出来了,当我再次看到有人在页面硬编码时我问为什么不用文档中的方案,没有收到邮件吗?没有看文档服务器吗?回答是:谁看邮件啊?写代码还得看文档?最终部署期限慢慢接近,bug数依然有增无减,我强压火气吩咐加班,但是遭到强烈反对(当时我怎么想的?要是我的双节棍带在身上多好)。这没错,我也反对,毕竟我一向认为生活第一,工作第二,但是有什么办法呢?我们不应该为自己的行为负责吗?下图是最后得出结果的估算表:
图7 得出结论的估算表
这些统计并没有算上全部修改的时间。值得注意的是第三个人的结果,这是我始料未及的。这个功能最终由我接手,我发现修改困难是有原因的,过紧耦合让整个代码像是一个小型衣柜,程序员想要将大量的衣服塞进去,但由于衣柜的质量不好(或许它是令人诟病的“中国制造”),在超过阈值时每次塞入一件衣服衣柜就会破裂,程序员不仅要塞入衣服(可能有些衣服还有不平整的包装袋),还要修补衣柜,如此反复……终于有一次全部赛进去了,高兴过度的程序员使劲拍了一下自己的杰作,于是,衣柜破碎了。我不想跟干木匠的强饭碗,所以我的结论是此部分没有修改的价值,重写的代价更小。
以下是原程序的代码片段:
- public String checkInvoiceInfRecord(String[] invoiceInfRecordIDArray)
- {
- boolean flag = true; // 是否全部可以复核
- boolean b = true; // 是否全部可以结存
- StringBuffer invoiceInfRecordIDCheckBuffer = new StringBuffer(); // 未复核且未结存的记录的ID数组
- String message = new String(); // 页面alert的提示信息
- for(String invoiceInfRecordID:invoiceInfRecordIDArray)
- {
- InvoiceInfRecord invoiceInfRecord = invoiceInfRecordDAO.getInvoiceInfRecordByID(invoiceInfRecordID);
- if(invoiceInfRecord.getAuditState().equals(String.valueOf(Variable.CHECKSTATUS_YES)))
- {
- flag = false;
- break;
- }else
- {
- if(invoiceInfRecord.getFreezeState().intValue() == Variable.FREEZESTATUS_NOT) // 以下表示未复核且未结存的记录
- {
- invoiceInfRecordIDCheckBuffer.append(invoiceInfRecord.getUseID()).append(",");
- }
- }
- }
- for(String invoiceInfRecordID:invoiceInfRecordIDArray)
- {
- InvoiceInfRecord invoiceInfRecord = invoiceInfRecordDAO.getInvoiceInfRecordByID(invoiceInfRecordID);
- if(invoiceInfRecord.getFreezeState().intValue() == Variable.FREEZESTATUS_YES)
- {
- b = false;
- break;
- }
- }
- if(flag) // 以下表示全部未复核
- {
- if(b) // 以下表示全部未结存
- {
- if(doCheckInvoiceInfRecord(invoiceInfRecordIDCheckBuffer.toString())) // 复核成功
- message = Variable.CHECK_SUCCESSFUL;
- else // 复核失败
- message = Variable.CHECK_FAILURE;
- }
- else // 以下表示全部未复核但是有部分已经结存
- {
- if(doCheckInvoiceInfRecord(invoiceInfRecordIDCheckBuffer.toString())) // 复核成功
- message = "复核成功!(但是已经结存的发票使用信息不能被复核)";
- else // 复核失败
- message = Variable.CHECK_FAILURE;
- }
- }else // 以下表示有部分记录已经复核
- {
- if(b) // 以下表示全部未结存
- {
- if(doCheckInvoiceInfRecord(invoiceInfRecordIDCheckBuffer.toString()))
- {
- if(invoiceInfRecordIDCheckBuffer.toString().trim().length() > 1)
- message = "复核成功!(但是已经复核的发票使用信息不能再复核)";
- else
- message = "复核成功!";
- }
- else
- message = Variable.CHECK_FAILURE;
- }else // 以下表示有部分记录已经复核和已经结存
- {
- if(doCheckInvoiceInfRecord(invoiceInfRecordIDCheckBuffer.toString()))
- message = "复核成功!(但是已经复核或者已经结存的发票使用信息不能再复核)";
- else
- message = Variable.CHECK_FAILURE;
- }
- }
- return message;
- }
看到那个超大的for循环和其中杂乱无章的if-else了吧,这就是传说中的上帝代码。重写后的代码如下:
- public MessageBean checkInvoiceInfRecord(String[] ids) {
- MessageBean msg = new MessageBean();
- for(String useId : ids) {
- InvoiceInfRecord invoiceInfRecord = invoiceInfRecordDAO.getInvoiceInfRecordByID(useId);
- IFreezesuminfo freezesuminfo = freezesuminfoDao.getIFreezesuminfoByID(invoiceInfRecord.getCompanyID(),
- invoiceInfRecord.getSubCategory().getCategoryId());
- if(!alreadyBalance(invoiceInfRecord, msg))
- break;
- else if(!emptyBalance(freezesuminfo, msg))
- break;
- else if(!moreThanBalance(freezesuminfo, invoiceInfRecord, msg))
- break;
- else if(!checkInvoice(freezesuminfo, invoiceInfRecord, msg))
- break;
- }
- return msg;
- }
我想原作者大概舍不得自己的中国制造。
东方有线项目并没有结束,还有很多问题需要改进,拭目以待吧。
把团队比作球队真的准确吗?球队可以凭借球星的力量赢得比赛,当年的魔术队可以凭借麦迪一人之力杀入季后赛,我们像球队吗?即使像,也没有球星,不过是个瘪脚的居民社区球队。团队更像一个乐队,即使一个长笛手在演奏中吹错了音符,整场演出也会以失败告终。