看麦兜的时候,我总是想尽力记住麦太说的“纸包鸡,鸡包纸,包鸡纸包鸡,包纸鸡包纸……”,但是每次我都发现我的智商太低,我的记忆力太差,一方面我难以推导出来这看似简单的罗圈话里面的人生真谛,另一方面我的嘴完全不够灵活……
最近,在调试和修改一个老的Mp3代码的时候,我又一次感受到了类似的智力挑战。按下F4后会执行保存Mp3播放列表的功能。
下面是按下F4键后,函数调用关系的一个简单示意:
VK_F4(dlg_list.c) -> gmp3_save_song(dlg_sub.c) -> MP3_insert(mp3a_main.c) -> mp3_main_insert(mp3e_main.c) -> mp3_save-list(mp3e_list.c) ->mp3_save_head(mp3e_list.c)
值得一提的是MP3_insert的函数体
int MP3_insert( struct mp3_item_s *list, int sum )
{
return mp3_main_insert( list, sum );
}
我充分怀疑这个调用只是为了让调试的人感觉到不爽……
据说这样的结构是来自于当年公司的一种规划,GUI/内部Api/引擎的三层结构,从文件名很容易看到这种倾向,dlg_*就是GUI源代码,mp3a_*就是Api层的代码,而mp3e_*就是引擎层的代码。我们姑且不谈在这样的小程序里面包来包去究竟有何意义,且看看他包的到底好不好!
分层一向是解决复杂问题的法宝,为什么这个程序的分层让我感觉很不爽呢?
- 分层没有机制的保证,流于形式
因为分层仅仅是通过文件名和函数名的一些约定来进行的,所以分层容易流于形式,各层的调用方式也无法做的规范。因为没有机制的保证,层之间使用同一个全局变量,是层之间存在过度的依赖关系。
- 横向的分层和纵向的调用关系
分层或者文件是横向布置的,然而调用方向完全是纵向的,最后代码是纵横交错,完全没有办法看。
- 没有文档
这样的过度的分层即使有文档也是非常难以看懂的,何况没有文档。
火炬在对.net技术的争论之言论摘抄中说,“包装包装再包装,一层一层包上来,过一段估计就要有对.net的包装和抽象了,原因同上。”不无讽刺意味……
“你有权保持沉默,如果你放弃这种权利,你所说的一切将成为呈堂证供。”这句话,大家都很熟悉。那天我的一个朋友在Msn上面问我为什么最近没有写新的Blog的时候,这句话突然在我的脑子里面冒了出来。
是啊!我有权保持沉默,为什么我要写那么多东西出来呢?最初,写这个Blog的目的,是为了让我的一些思路保存下来,这也许就是Log的意义吧。慢慢的,Blog变成了某种交流工具,我的很多朋友都是我的Blog读者。这种交流方式完全不同电话,短信,QQ,有时候感觉更类似于信件,不过是发给所有想看的人的信件,大家可以在任何想看的时候看,在任何想发表意见的时候发表意见,这种方式让人感觉很自由。
Blog也给我带来了很多的机会,因为我的文章提到我在为某电子词典厂商工作,所有有人写信问我是否承接外包设计工作;因为我写文章写过一个IE插件的思路,所以有人找我来帮忙写一个IE插件;甚至因为我贴出了照片,也说过我很胖,我结识了很多也很胖也钟爱编程的朋友。Blog给我带来的交际圈子变化是那么的鲜明,半年前我来到北京直到现在,我在北京的朋友基本上都是写Blog的。
最近一段时间,我换了一个Team,工作开始忙了起来。而且因为我就坐在大厅入口处,所以现在上网也变成有点偷偷摸摸的了。晚上回到宿舍又不能上网。这些综合在一起,造成我最近很少写Blog,感觉人总是空荡荡的。我不喜欢晚上把Blog写好,早晨上班的时候发的感觉,我写Blog的时候喜欢写了按一个按钮就能直接发出去的感觉。所以,最近我的Blog一直没怎么更新,所有也许担心Tiny,也许觉得无所谓的朋友们,可以告诉你们的是Tiny的心情最近虽然很波折,但是总体上,很好。最近在技术上面总有新东西可以学到,感觉很舒畅。
最近对正则比较感兴趣,但是grep支持的正则不全,C++的话还要安装库都很麻烦。那天安装Wincvs的时候,我顺便安装了Python和tcl/tk,心想干脆拿它们做点东西吧。
Python的上手果然很快,昨天一个晚上基本上语法就清楚了,今天我开始写这个日志文件的IP统计器。边看资料边写,就这么写好了,代码很短,和在一起还不到100行。我认为主要归功于,List的特殊的循环语句,实在是太简洁了,我喜欢。
判断ip是否合法的正则表达式,我用的是:
[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}
当然这个正则表达式还不够,还需要判断每个数字都不能大于255。所以函数如下:
import re
def isIp(Str):
"""
本函数利用[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}这个正则
表达式来判断ip的格式,然后检查4个数字是否都小于256。
"""
p=re.compile('[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}')
m=p.match(Str)
if m:
if len([k for k in Str.split(".") if int(k)<256])==4:>
主界面用了tk,显示了一个窗口,一个打开文件按钮和一个退出按钮,统计结果的输出是直接利用终端来进行的。
运行结果如图所示

程序的大概思路是把文件读入一个list,然后对这个list的所有成员提取记录的客户端IP信息。然后用isIp函数验证,如果合法则加入到iplist里面。然后对iplist的所有成员进行统计,形成一个dictionary,key是ip,value是ip对应的重复次数。
对于今日截止到现在的访问日志,得到的统计结果为:
-------------------
Ip counts is 1183
Log item counts is 6235
One Ip average Pageview is 5
Max pageview is 526 It's Ip is 219.238.144.234
-------------------
这个Max pageview不用问了,肯定是我自己了,我没事儿干的时候就会无聊的点击自己的网站。人品问题,居然点了那么多次,寒死我了!
从今天的编程体验看,Python确实是一种非常好用的语言,只要有手册在手,使用是非常方便的。下午的时候yayv问我是不是有一种basic的感觉,我说,感觉有点像lisp,虽然我没有见过lisp。
鸣谢:
Python中文社区 http://python.cn/(我在那里找到了很多的资料)
Python学习笔记 作者:王纯业(主要是这本书教会我怎么编python程序的。)
源代码下载:
主程序 logtools.py
库文件 loglibs.py
通讯插件系统
通讯系统的设计要求是什么呢?就是可以支持所有的现有机型(实际上做的时候,一些早已退出市场机型并没有做。),也支持将来的新机型。
这里就涉及到两个比较复杂的问题,就是不同的机型有着不同的底层通讯协议,还有着不同的操作流程(比如上传文件和数据的时候,是PC机选择还是在电子词典上面选择等等。)。
底层通讯协议就是说串口还是USB,怎么握手,怎么传输。
操作流程,简单地说我们分为两种模式:
模式1:连接的时候,PC机可以要求电子词典提供文件列表,在PC机上面指定需要上传什么文件。一次握手可以传输多个文件。
模式2:PC机不能得到电子词典的文件列表,所以上传的时候必须在电子词典上选择要传输什么文件。一次握手只能传输一个文件。
这两种模式带来的实际问题是他们在传输中需要的界面也完全不同,所以问题变得很麻烦。
最后我的解决方案是三层的结构。
- 插件管理系统,它给主EXE提供抽象的传输接口,服务性的接口包括枚举机型列表,设定通讯参数等,功能性的接口包括上传文件/数据库,下载文件/数据库,升级。这些接口的具体实现是它调用不同的传输插件和界面插件实现的。
- 界面插件,它实现了和插件管理系统的功能性接口同名的所有接口,或者说在功能方面它直接给插件管理系统服务。而它的接口的实现是通过调用某个特定的传输插件完成的。
- 传输插件,它提供了比较底层也是比较实质的传输接口(这些接口直接为界面插件服务),另外还包括了一些注册自己所需要的接口(直接为插件管理系统服务)。
这三层结构大致如图所示:
这张图不能完全表示出来这个结构的思路。
我们这里通过实际工作中的两种不同的流程来介绍。
- 动态注册流程,每次通讯插件管理系统启动动态的提取所有通讯插件的信息注册在一个内存结构体内。具体实现方式是,每次通讯插件管理系统启动分配一个足够大的内存区域,然后在传输插件目录枚举所有的DLL,并对每个DLL调用我们定义的标准接口GetInfo,得到DLL支持的机型,以及它所需的UI类型。如果DLL没有这个标准接口,则认为它不是我们的标准通讯插件。注意,这里我们并没有枚举界面插件。原因在于,不同的通讯插件可能对应同一个界面插件,也就是说他们是多对一的关系。而且,如果存在一个流程类型(一个流程类型对应一个界面插件,一个界面插件实际上就是实现一种传输流程。),但是没有对应的机型和它对应,那么它实际是不会被使用的。或者说,我们只给用户选择机型的界面,没有选择界面插件的界面,因为机型确定了,所需要的界面类型也就确定了。或者说界面类型是用户完全不需要关心。这个流程在图中用蓝色线表示。
- 传输流程。每次用户点击上传功能,或者下载功能。插件管理系统首先查看当前机型是什么。如果没有设定当前机型,就提醒用户来设定。得到了当前设定的机型后,就在注册信息表(我们的内存结构)里面,寻找这个机型需要的界面插件信息,找到后调用这个界面插件,并把当前机型的传输界面的名字作为一个参数。这个过程用红色线表示。
在动态注册流程,三层的关系是插件管理系统->传输插件->界面插件(没有实际调用界面插件)。在传输流程中,三层关系是插件管理系统->界面插件->传输插件。
我负责编写的是,插件管理系统(dll),模式1的界面插件(dll),模式2的界面插件(dll),还有一个用来调试的工具。限于篇幅和公司利益问题,这里我不可能把具体的接口公布出来。我要说的是界面插件真难写,麻烦死了。幸好在后面的维护工作中我发现,虽然我们的插件系统设计为可以支持任意多种的界面模式,实际上,我们所有的自主产品和外包产品都可以在这两个模式其中一直之下很好的工作。
新的工作方式
这个项目已经完成了快半年了,这半年来我们工作方式因为这个项目的成功也发生了改变。现在一旦出新产品,我们就负责培训产品项目组的工程师如果按照我们的接口方式来编写传输插件。这对我们对他们都并不困难,因为实际上我们的传输插件接口模式,就是对一般的很普遍的上传下载进行封装。多个产品在这个期间推出,可以负责任的说,因为这样的新模式,我们花在联机软件上面时间和精力都变得很少,而且系统也很少出现Bug。特别是对于外包产品,外包公司不需要自己写一个联机软件,只需要把他们的底层传输协议用我们的DLL接口来封装一下。我们也很方便,节省了很多沟通需要的时间和成本。而且也避免了其它部分重要代码的外流(在这个模式下,外包公司甚至不需要知道我们上层的软件是怎么样的。当然为了调试方便,当然会告诉他们我们的系统的运作方式)。
这个项目中还有很多值得总结的东西,但是可能和我主要想谈的系统结构,系统的模块划分粒度问题,用层次来解决统一接口和特定实现的问题,关系不是很大,所以这里不提了。项目中也有些遗憾,我在内存分配,版本控制(插件的),等方面还有很多设计不合理的地方。这是经验和能力的问题,也是对系统理解能力的问题。
了解我的朋友都知道,我的爱好很广泛,什么方向都涉猎,同时呢,因为漫天撒网牵扯了时间精力做事情经常半途而废。所以在我的Blog里面也有很多文章是写了一半的。比如这篇“
一个具体项目的重构(一)”。我知道这个习惯不好,所以,我会慢慢地把我没有写完的文章都写完的,呵呵。一般我们把写一个半截的文章叫做挖坑,所以现在我开始埋坑了,所以叫做埋坑计划。
这个文章源于我来到公司的第一个项目,也是我在公司职责所在的项目。当然我主要是负责其中一个功能模块的而已。这里不是写工作总结,所以提到的我也许指的是我们,我们也许是我,这样说是因为这个项目是7,8人的劳动成果,不是我一个人的工作,但是这里不是讲每个人的功劳,而是描述我们的软件架构问题。
书接上文,我们总结了一个软件需求:
1. 可以在线升级。
2. 可以支持公司所有的(流行)产品。
3. 可以比较方便的进行扩充。
4. 一个全新的界面设计
5. 完成原有联机软件的所有功能。
在线升级可以说并不是一个复杂的问题,这个模块的具体实现我不是很清楚,基本上服务器上面放置一个最新版本的主程序和插件的列表,当然包括了他们的生成时间的信息,每次检查更新就是对比本地的文件和服务器上面提供的最高版本的日期差异,然后进行更新操作。这个功能工作得很好,我们的产品已经更新过相当多次了,没有任何失效的例子。
既然我们的题目叫做重构,实际上我最关心的话题是怎么提高软件的灵活性,怎么降低系统维护的成本。
首先我们来仔细看看,我们原有软件的维护难问题是怎么出现的。
首先,从软件结构上来看。这个系统由两个可执行文件构成,一个EXE文件是主界面,提供了完成的用户操作界面,包括名片、行程等的编辑功能,包括文件选择,包括选择上传下载等功能的界面;另一个EXE文件实际执行传输任务。他们之间的数据传输方式是通过对一个共享文件进行读写。这样划分的主要原因是,做界面的程序员和做实际传输模块的程序员往往不是一个人,甚至可能不是一个项目组的。主界面程序由我们的Team编写和维护,实际执行传输的程序往往是对应项目组负责传输协议的程序员来写。从难易程度和方向来讲,这么划分也还是有理由的,因为主界面程序员不需要了解任何通信底层接口,而传输程序的程序员主要是对通讯底层熟悉,他对一般的Win界面编程往往不是很熟悉。
在这样划分粒度下,理论上可以做到在底层通讯协议改变的情况下(每个产品都是有不同的通讯协议,这也是公司存在的一个问题,不过已经不在我可以影响的范围之内了。当然这有时候是必然的,比如外包产品,理论上讲外包产品当然会有不同的协议。),只修改一个EXE就可以的效果。但是实际上,这个问题很复杂不同的协议也可以造成完全不同的操作流程。这时候光修改通讯的EXE显然已经不够了。而且这个问题是这么的复杂,所以,在公司一直以来,有两种完全不同的传输模式存在,他们的联机软件是从完全不同的两个软件发展出来的。不仅仅是传输协议的不同了,这两套软件是完全不同的界面,完全不同的操作方式。这里虽然我不想批评我的公司。但是实际上我认为这对品牌战略是非常不利的。但是这是现状。
然后我们看代码维护和复用方式上面的问题。这里涉及到刚才提到的两种程序员的问题,负责联机软件界面的程序员和负责写通讯程序的程序员是不同 Team的,大部分时间他们是没有任何交流的。只有当一个新产品要准备推出的时候,他们才会临时组成团队。他们各自会找到最相近的一个老代码来进行修改(所以这里还是有一些代码复用的)。但是问题在于这里没有任何的版本控制,这造成了很多混乱,一些修改过的Bug,还会被多次重复的被修改,因为没有人来协调不同产品代码中的相同Bug。甚至可能因为采用了不好的开始代码,某个新产品会包括一些老产品都已经解决的Bug。另外,这样的模式必然会造成程序代码的迅速臃肿。为了安全,大家改代码都喜欢用注释老代码的方法。同时也出现了大量的条件编译语句用来弥合不同的机型之间的一些差异。可以想见,当公司有了 10多个产品之后,加上繁体版简体版的问题,代码中的垃圾和一些难以理解的成分就开始具有惊人的行数了。当一个程序员离职的时候,他根本没有能力给接班者讲清楚代码里面的来龙去脉,加上文档的欠缺。任何一个新从事这项工作的程序员读代码的工作就变得非常的沉重。
对程序的扩充是通过加入外挂程序实现的,一般的外挂是类似于一些特殊文件格式的编辑器,或者察看器之类东西。他们往往都很简单,所以不被重视。但是,他们也存在版本问题,也存在某一个版本已经修改了的Bug,在后续产品中又会出现的问题,这和项目组制度有关系。这里想抱怨一下,因为产品采用项目组的形式,而且缺乏一些必要的中间协调结构和标准化的制度,会出现一些很让人苦笑不得的问题。比如某种词典数据的格式定义是与平台无关的,但是先前的实现里面都莫名其妙的在词典数据的文件头上面加上了型号的标记,而且词典查看工具会读取这个标记。这样就必须把完全没有区别的词典数据文件复制成多份,修改他们的文件头然后,放置在不同的网站页面上。这也给用户带来了麻烦,用户下载词典的时候必须选择相应机型的对应词典,否则不能使用。其实解决起来也很简单,我们最后规定在机器上的词典查看工具忽略这个标记,这样就解决了一切问题。然而,这样的小问题确实在公司里面存在了数年,也给用户和工作人员带来了一些麻烦和困扰。(当然这属于题外化了,但是这个题外话,蕴含了我的一个重要的观点,在应该标准化的地方必须标准化,这样可以提高效率,而且避免给用户困扰。)
新的结构
从上面大家可以看到,我认为主要问题在于软件模块划分的粒度太粗了。在新的实现里面,我们把整个软件划分为主EXE,通用UI插件系统,通讯插件系统,外挂系统三个部分。系统之间的模块可以用EXE来分割,也就是用进程间通讯的模型,我们公司早期的系统就是这么做的。这样的好处是简单,但是问题在于采用共享文件作为通讯方式,速度慢,可靠性低。还可以采用的方法有DLL,或者COM。我们采用的是DLL,这是程序员的普遍水平决定的,事实上有些程序员之间连DLL都没有写过。如果采用COM,那么我们整个项目周期可能都不够培训的时间。
主EXE调用通用UI插件系统,通讯插件系统和外挂系统。这里,我将主要介绍通讯插件系统的设计,因为这是我主要负责的部分,也是我比较得意的部分。