帝都的宇宙中心,古老文明的发源地,coding的传统在码农手中世代延续,CGI作为传承了一千多年的古老工艺,并没有被AJAX收割殆尽,仍在这里焕发着勃勃生机。--舌尖上的ABCD
背景
借着开发Stroy的机会,把一个古老的CGI脚本做了一下重构,有点心得,赶紧写下来,因为以后不太可能有机会经常接触perl的CGI了。
产品中现有的CGI并非像很多年以前那样用来产生web页面,而是作为系统操作的工具,在运行过程中修改Linux系统的一些配置以及进行其他一些比较底层的操作。
不想多说具体的编程规范方面的问题,如命名、注释等,虽然这些对于代码的可维护性、健壮性也非常重要,但本文主要想总结一下perl代码的功能区域、函数调用、异常处理等方面的问题。这些也谈不上架构设计,因为除了一些perl module外,其他的CGI脚本基本都是平级的,没有特别复杂的接口、模式之类的东西。
1.脚本功能内聚
CGI脚本最好按特性划分,通常一个脚本文件只实现一组相关的功能,只包含一个具体的特性。特性不宜过大,如果一个特性很大的话,可以通过目录的方式组织在一个文件夹下,每个脚本只完成一个小特性,同时修改服务器的路由配置。
每个文件的长度不要超过1000行,太长了就应该考虑一下是否可以拆分为多个文件,将不太相关的功能剥离开,以提高代码的可维护性和执行效率。
文件(夹)的命名应该有统一的约定,方便代码查找,在一个脚本内部,可以包括相关的CRUD及其他逻辑。
2.合理的代码布局
这里指的是一个CGI脚本内部包括的内容以及它们的位置。
Perl CGI脚本通常包括shebang,use modules, 全局配置,全局变量定义以及逻辑代码,上面的顺序也应该就是脚本中代码出现的顺序,如:
#!/usr/bin/perl # Copyright 2009-2015 ***. All rights reserved. use CGI; use warnings; use strict; $| = 1; ## turn autoflush on $ENV{"PATH"} = "/bin:/sbin:/usr/bin:/usr/sbin"; $< = $>; my $q = new CGI; my $error = 0; my $status = "SUCCESS"; print $q->header( { -type => 'text/plain' } ); #logical code here
前面几部分通常比较固定,不会有太大的变化。但是一个好的设计中,不应该有太多的全局变量,除了从url中获取传入参数、常量定义外,其他的变量应该都包含在函数内部,尤其是不能出现多个函数通过全局变量来传递状态的情况。perl解释器是C语言编写的,其实这个问题也是C语言开发过程中经常碰到的。
逻辑代码部分可能会比较复杂,因为要完成具体的业务逻辑,操作各种数据,然后返回处理结果等,内容比较多。CGI不需要main函数,全局部分的代码就相当于是main函数的代码,但是满篇位置随意的全局代码可读性非常差。建议提供一个main入口进行功能分发,其他逻辑以函数的方式被入口调用。这就要求在传入的参数中要包含行为参数,比如“operation=eat",然后使用switch(Swich模块提供)或者given(perl6内置)进行路由,当然实在不行,if...else...也可以,如:
switch ($operation) { case "eat" { eat(); } case "walk" { walk(); } case "swim" { swim(); } else {print "I don't know what to do!"; } }
main后面一般就可以exit 0了,看代码的话看到这里就知道这个脚本都能做什么事情了。
行为函数如果比较复杂的话,建议进一步拆分,拆出private的函数,行为函数只管流程调度,具体的实现放到private函数中。比如swim动作,拆分为几个私有函数,包括stroke、kick、breathe,swim函数的实现应该类似于
sub swim() { while(not reach the destination) { _stroke(); _kick(); _breathe(); } }
perl本身没有private/public的说法,在我这里行为函数表示从main入口的函数,可以认为是public函数,而private是被public函数或其他private调用的函数,private函数所需的参数除了常量外都要从@_处得到,区别起见,在命名上private函数以"_"开头。
3.结构化返回结果
前面说到,这里的CGI不是输出html网页,而是执行一些系统操作。纵然没有html格式的束缚,返回结果也需要有一个结构化的输出,便于调用者处理。常用的两种格式化语法是XML和json,我用过的几种语言都提供有两者的解析类库,如果是从javascript来调用,肯定是json无疑,直接就是javascript对象,方便操作,其他场景下,视情况而定,json灵活简单,XML严谨但有些冗余。产品里CGI返回结果使用的是XML,都按照下面的格式:
<response> <message><![CDATA[...]]></message> <errorcode>$error</errorcode> <status>$status</status> </response>
调用者看status中就能知道本次调用成功与否,从errorcode可以得到具体的错误,一些格式不固定的具体执行结果都放在CDATA部分中。
在实现上,可以定义两个工具函数,分别print以CDATA的内容分开的前后两部分,switch之前打印前半部分,在exit 0之前将结果的后半部分打印出来。
4.异常处理
看久了下面的代码总感觉有点胸闷气短,整个屏幕都是if...else...地处理错误码,密集恐惧症要犯了
my $error = doSomething(); if ($error == 0) {/*normal process*/; return;} elsif ($error == 1) {} elsif ($error == 2) {} ... else {}
实际上,这个问题就跟我刚刚接触Java的时候一样,可以返回错误码,也可以抛出自定义异常,到底用那种方式呢?好纠结,好纠结。发散一下,讲个笑话,昨晚刚听到的,说女人看到男人哭时会想:“他是不是有外遇了,他是不是瞒着我做了什么见不得人的事了...”; 而男人看到女人哭时总是想:“她又哭了。她怎么又哭了?她怎么又哭了!”
从现实主义的角度看,我琢磨着一是看返回值是否还有其他用处,二是能否让代码逻辑清晰、可读性更高,可能有时候可能还要考虑异常的性能开销。
在这次重构中,我将所有函数中出错需要return的地方都修改为die "message" if (something wrong)或者 doSomething() or die "message"的方式,在上面说的main入口处统一进行异常捕捉。封装了两个函数:
sub _throw { my $msg = shift; my $args = \@_; my $e = sprintf($msg, @$args); die $e."\n"; # "\n"不会让die输出行号,只有你自己写的错误信息 } sub _catch { my $e = [email protected]; $error = -1; #这个error是给返回结果用的 print "error reason ==> " . $e; }
上面的那个switch block变成下面的样子:
eval { switch ($operation) { ... } 1; } or _catch();
eval就相当于Java中的try,在编译时会有一点小开销,但不会影响运行效率(要区别于eval一个表达式)。这样所有的错误处理都可以在_catch中统一处理,比如记日志,格式化错误提示等,同时其他函数中的代码大大精简。
5.统一的常量定义
在开发业务逻辑时,不可避免地需要处理异常情况,很有可能临时起意写了一个错误码或者一个错误提示的字符串。但是,同样的错误可能在其他地方也会遇到,怎么办?再写一遍吗?写完了,如果需要修改的话是不是要满篇地查找啊?
另外,还有一些配置常量,比如说端口号,可能在很多地方都用到了,如果有一天要修改端口号,又是一堆的查找替换。
解决这些问题的方案可能都不必细说,因为在其他语言中都司空见惯了--使用统一的常量定义。Perl提供了const和Readonly关键字来定义常量。
6.复用公共逻辑
重构最常做的事情可能就是提取公共代码进行复用了。
多个CGI脚本之间可能会有一些公共的业务逻辑,比如对数据库操作的封装、对其他第三方工具的存取等,以及一些切面上的操作,比如防止代码注入的安全地执行shell的方法,记录操作日志等。
这些逻辑可以提取出来做成perl module,在CGI开头use一下就可以调用了,同时也可以查一下CPAN,有没有现成的module可以拿来用,省得再造一遍轮子。
7.执行进度显示
这个问题可以参考我之前的一篇文章《Java动态展现CGI执行进度》。但是目前在实现上还有些不尽如人意的地方,主要问题是侵入性太大,粒度较粗。
比如说,对于一个比较长的业务调用来说,逻辑可能拆分到各个子函数中去了,如果要将进度表现得详细些,就要在更多的地方打印百分比,而且在A函数中打印了10%,在B函数中要打印20%,比较奇怪。
在CPAN中能够找到几个progress相关的module,但是都带有一些UI元素,不是我想要的。
这个问题目前还是没有妥善解决,打算自己写一下,好用的话再来分享,不好用就算了。
以上,是我这段时间重构perl CGI的一点心得,都是我遇到并需要解决的实际问题,希望对大家有所帮助。不过,因为没有特别深入地去研究CPAN的各个module,也没有找到其他人关于这方面的一些实践经验,所以,上面说得可能不完全正确,如果有不对的地方,敬请指出,谢谢。