`
helloyesyes
  • 浏览: 1272946 次
  • 性别: Icon_minigender_2
  • 来自: 武汉
文章分类
社区版块
存档分类
最新评论
阅读更多

江南这一带有过去有一种很流行的曲艺,叫做评弹。包括评话和弹词。前者又称“大书”,只是说,也就是苏州人的说书。后者又称“小书”,还要唱,用琵琶和三 弦伴奏(人多了还有月琴)。小时候也很喜欢听大书。说书人往往根据传统故事和小说改编,有时会添油加醋,作为噱头。其中印象最深是《三国》,其中有这么一 段:说是诸葛亮到东吴联合孙权一同抗曹。周瑜带兵在赤壁与曹操对峙。周瑜有心要杀诸葛亮,但又怕落下骂名,于是想借曹操之手。他请来了孔明,要他带关张去 聚铁山截曹操的粮草。诸葛亮一口答应下来。老实头鲁肃担心诸葛亮有所闪失,前来询问。诸葛亮大言不惭地说:这事只有他孔明能干,你们都不行。鲁肃问他为什 么。诸葛亮回答:我诸葛亮打仗,陆战、车战、马战、水战等等六战俱全(三 国演义里只提到四战,不知那两战是哪里来的。难道还有空战、电子战么?:-D)。哪里像你鲁肃只会打埋伏,周都督只会水战。鲁肃一肚子气,回去一五一十地 向周瑜禀报。周瑜一听就跳起来,叫鲁肃去跟诸葛亮讨回令箭,他要自己去。随后诸葛亮告诉鲁肃,不要去截曹操的粮草,太危险,周都督想借刀杀人,我才说这番 话,实际上我们谁都不能去,我们孙刘两家还是应该联合起来一起抗曹。
这只是一个故事,小说作者的杜撰和说书人的添油加醋。但是这里面却说出了一个重要的道理:作为一个领军打仗的将军,不能只会一种作战方式,应当具备多种作 战能力。用现代的话来讲,就是“多兵种协同作战”。引申开来,在编程领域,作为一个程序员,也不应只会用一种技术、方法,或者工具完成工作,也应当做到“ 六战俱全”。在日常开发工作中,我时常遇到试图只用一种技术手段或工具解决所有问题的情况。尽管最终可以完成任务,但却效率低下,并且存在着隐患。
这里,我打算用两个亲身经历的案例,来展示组合运用多种不同的技术和工具所代来的好处。两个案例中,前一个很简单,直观。后一个复杂些,但更能说明问题。

一天,一个项目经理来找我,说有一些数据需要导入。是银行的对帐数据,都是些文本。我跟他说:这东西我们做了不下十几遍了,有现成的代码。他到诉我,这回 不一样了。各家银行的对帐数据格式不一样,现在有两三家,将来还会有更多,它们的格式都不知道,得做一个通用的模块。而且时间不多,只有两三天。不然的 话,只能先做两家的,其他的到时候再说。即使这样,两三天也很紧。我考虑了一些时间,然后给了他一个方案。
过去,我们在程序里用vb、C#、C++等语言,直接读取文件,按格式手工解析。如果是我们自己的程序导入导出,vb提供的一些函数,可以把数据按一定格 式导出到文本,再倒回来。这样的做法已经无法解决眼前的问题。当时我听到这个需求的第一反应就是正则表达式。很奇怪,这么多年,那些作数据导入的程序员一 直没有想到使用正则表达式。只需写出正则表达式,便可以读取任何一种格式的文本数据。这样,我们只需针对每个银行编写一组正则表达式,便可以解决问题。对 文本文件中所代表的每个字段用命名组加以捕获,然后读出这些数据,再进行处理。
但是,事情并没有结束。银行对帐数据的格式不仅仅是字段的位置变化,还会有语义上的变化。比如,有的银行用一个字段表示借贷方向,另一个字段存放金额,两 者合起来就是一组对帐数据。但是另一些银行用一个字段表示借方金额,另一个字段表示贷方金额。这些格式都必须转换成我们软件中的数据格式。这样就会涉及到 数据的条件判断和计算。麻烦的是这些转换在将来可能会有新的变化,难以预测。考虑再三,我决定使用一种脚本来执行这种转换,脚本易于修改,可以满足未来的 变化。用哪种脚本合适呢?找来找去,还是xslt。于是便产生了最终的方案:
数据转换模块包含三个子模块。第一个按行读取文本,然后把这个字符串进行正则表达式匹配。获得的结果以xml格式写入一个流。然后把流送到第二个子模块。 第二个子模块进行xlst转换。转换获得的结果送到第三个模块。第三个模块把结果xml中的数据取出来,填充一个insert语句,更新数据库。(后来改 进的版本里,不再填充insert,而是直接填充一个DataTable,以块操作更新数据,提高性能)。正则表达式和xslt stylesheet都存放在一个xml文件中。这样,每种文本格式就对应一个xml文件,如果格式有变化,新增一个文件或者修改原有文件即可。
在确定方案之后,我和另一个同事分头做这些模块。当时,项目经理还有些担心这个方案实现起来太难,时间上不够,打算同时再做一个专用的导入模块以防万一。 但是仅仅过了半天,同我合作的那个同事便向项目经理信誓旦旦地保证我的通用方案比以前做得方法更简单,更快。因为半天的功夫他已经把正则表达式子模块做完 了,开始调试正则表达式。而我,也已经把主模块搭建起来。第二天,我们把剩下的子模块都做完。并且做出了两个银行数据的导入脚本。第三天,导入模块集成到 软件中,任务基本完成。最后,我们又多花了一点时间,做了一个小工具,用来测试正则表达式和stylesheet。
此后,我进一步考察了模块的性能。我担心在大数据量的情况下,性能会很坏,毕竟都是在进行文本处理。我询问了项目的业务专家,他告诉我,这些数据的导入只 要在5分钟以内都是可以忍受的。那么一般会有多少数据呢?几百条吧,顶多上千条。这下我便放心了。实测下来,几百条数据的读取和转换基本上都在毫秒级。而 数据库的更新也在百毫秒左右(因为网络的关系)。对于用户而言,都是一瞬间的事。
这个方案在三个方面简化了问题。首先,正则表达式引擎和xslt引擎消除了手工编写数据读取代码和数据转换代码。它们的编写更加简洁高效。其次,把正则表 达式和stylesheet存放在文件中,便于修改。最后,程序代码只需操作文本。由于数据转换和计算的工作由xslt完成,便无须涉及数据类型的转换。
说来也可笑,这个对于我们那班C#er如此新奇的方案,对于unixer而言,根本就是老生常谈。在unix中,grep执行正则表达式匹配,脚本进行数 据转换,顶多做一个把转换结果插入数据库的小程序便是了。unix长期以来一直遵循的Multilanguage原则,在这里得到充分的体现。
实际上,这个任务有更容易的解决方案。XQilla,一种XQuery引擎,可以直接读取文本文件,并且其上执行查询,返回所需的xml结果。这当然已经超出我们公司的技术消化能力了,只能说说,长长见识。

第二个案例就复杂了些。
我接手了一个老项目,是一个查询网站。原先的程序员使用asp.net。后来有了些新的需求,需要改代码。当我看到这些代码时,有些晕眩。整整百多行代码,仅仅为了在页面上显示一个表格。于是,我决定做一个新的版本,设法简化代码,并且是软件易于修改。
我大致查看了一下业务逻辑,发现造成代码复杂的原由:这个网站主要查询我们另一个软件中的工资数据。这些工资的数据有一个麻烦的地方。工资是每个月发放一 次(也有的单位一个月发放两次,甚至三次的),每次的工资项目可能会变化。另一方面,这个软件用于很多单位,各家的工资项目也都不一样。如果硬要高出一个 超集,囊括所有用户工资项目,不仅数量很大(至少有两三百),而且我们也无法知道用户可能会增加的工资项目。
这种情况下,可以将工资项目放在一个表中,然后通过<员工,工资项目,时间,金额>这样的triple进行关联。但是,当时开发软件的时候, 考虑到这种形式的性能可能会很差,因而使用了介于这两种方案之间的设计。把工资项目存放在一个表中,然后在工资表里放上100个字段,并冠以 F001~F100的字段名。项目表里存放着该项目对应的字段名称,通过这个字段名,可以找到相应的数据。也就是说,一个字段在不同用户,不同月份的含义 是不同的。通过这种方式,实现了字段的复用,同时,也使得查询和数据处理的性能大幅提高。唯一的问题就是程序中需要动态地拼接查询语句。
因为是查询网站,不同用户间的需求变化很大,有的只想让网站代替工资单,有的希望能够全面地查询若干年的工资,而有的则希望能够让部门领导查询手下员工的 工资情况。于是,我打算把查询内容脚本化,便于修改和扩展。过去,在这种情况下,我们一般都把相应的sql抽取出来,保存在数据库或者文件里。但是,这次 不同,由于上面提到的这种工资项目的动态特性(动态构建sql),使得我们无法直接保存sql语句,因为只有在运行时才会确定最终的sql。一种解决方法 就是引入一种脚本语言,利用它拼接查询语句。但这么做不仅增加网站的复杂度,而且和在C#里拼接查询语句没有本质区别。
经过一番考察,我找到了一种解决方法。我发现sql server 2005开始,支持for xml子句(实际上2000就开始了,但功能很弱,基本没用)。通过这个子句,我可以把查询结果集构造成一个有层次结构的xml文档。于是,我写了一个邪恶的查询:
select * from Salary where ... for xml path('salary'), type, root('SalarySet')
于是,就产生了这样的结果集:
<SalarySet>
<salary>
...
<F001>...</F001>
<F002>...</F002>
...
<F100>...</F100>
</salary>
</SalarySet>
也就是说,这个查询取出了所有的字段,但此时还不知道哪些字段有用,而哪个字段代表哪个项目。下一步就简单了,从工资项目表中取出相应月份的数据,同样生成xml数据:
select itemName as "@name", FieldID as "@fieldid" from Items where ... for xml path('item'), type, root('ItemSet')
这个查询产生的结果集:
<ItemSet>
<itemname="xxx" fieldid="F001" />
<itemname="xxx" fieldid="F002" />
<itemname="xxx" fieldid="F003" />
<itemname="xxx" fieldid="F004" />
...
</ItemSet>
然后,可以把这两个查询作为子查询,放到一个查询中,并且关联起来。最终可以形成这样的结果集:
<employee name="..." eid="...">
<month year="2008" month="1">
<ItemSet>
<itemname="xxx" fieldid="F001" />
<itemname="xxx" fieldid="F002" />
...
</ItemSet>
<salary>
...
<F001>...</F001>
<F002>...</F002>
...
<F100>...</F100>
</salsry>
</month>
...
</employee>
下面的工作,就需要另一种语言——xslt——来完成(XQuery是另一种选择,只是目前XQuery的实现较少,特别是在.net平台上)。做法并不 复杂,只需对每一个月的工资的ItemSet/item执行xsl:for-each指令,获得工资项目对应的字段名(放入xslt变量$fieldid 中),然后使用<xsl:value-of select="salary/*[name()=$fieldid]"/>取出相应的数据。意思就是取出salary节点下,name与 fieldid相同的子节点值。
就这样,通过使用for xml子句将查询结果转换成xml,然后运用xslt将数据组织成所需的形式。如此,只需一个sql和一个xslt stylesheet,即可完成查询,而避免拼接查询语句。其中的原理并不复杂,在运用for xml子句将查询结果集转换成xml的时候,已经将结果集半结构化,数据本身携带了结构信息。而xslt则利用这些结构信息(字段名),提取和组织数据。
在这个案例中,我们所面对的问题已经超出了结构化查询的能力范围。而传统上使用C、C++、java、C#等语言加以弥补(拼接查询语句)。但这样的硬编码失去了柔性和灵活性。当我们引入更合适,也更加符合数据特性的技术手段,则可以更加优化地解决问题。
然而,这样的技术方案要求开发者了解更多的技术和方法,而不仅仅会用一两种编程语言。我见过不少能够熟练地使用一种语言开发软件而沾沾自喜的人。他们可以做出软件,甚至可能做得很好。但在更多的情况下,他们却只能应付用 户的需求。而无法更加合理有效地解决问题。在第二个案例化中,有一点非常重要,作为一个查询网站,面对着近20家用户,每一家都存在不同的需求。当我试图 将用户的查询需求脚本化的时候,发现根本无法用单一的某种语言或者工具实现。其中关键的问题在于,数据结构的这种动态特性迫使我拼接查询语句。如果我想要 将查询语句存放在可编辑的文件中的话,那么脚本的组织必须拥有运行时拼接字符串的特性。我可以使用一个脚本语言,但这样更加复杂。反过来,如果我可以设法 避免拼接查询,那么也就无需复杂的脚本系统,一个带有结构的文本文件即可(比如xml)。此时,如果我不知道还有for xml和xslt这些技术手段,那么我也只能放弃这种想法,或者回到老路,或者把问题搞得更复杂。反过来,只要我了解了for xml和xslt,就能发现它们可以帮助我实现最简单和最有效的设计。

从上面这两个案例中可以看到,当我们了解并运用多种不同的技术,便可以更加高效地解决面临的问题。很多程序员因为拘泥于单一的技术和方法,从而沦为“码农”。换句话说,当你发现自己象机器人那样堆砌代码,而没有出头之日的时候,就先看看自己会多少种技术吧。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics