All Stories

继续读《重构》

  《重构》中文版一直都放在公司电脑前,随时读上几页。有时不觉会为其中的精妙之处深深折服,大呼过瘾。看着一种一种的重构手法,该手法的使用场景,不时会有深有同感的感叹。  有些情况下,自己写的代码,写着写着,自己都觉得很不爽,但不知道问题出在哪里,功能是可以实现,但总会觉得不舒服,又不知道如何改进。在读《重构》时,就很可能能遇到甚至一模一样的所谓bad smell。  今天偶然跟同事谈起,我已经不再喜欢看跟语言相关的书了,同事就略带玩笑的语气说,这是架构师才会做的事。确实,我已经不再满足于那种与特定语言相关的奇技淫巧了,我更关心的是如何写出更少bug,更多弹性,更易理解,更好维护的代码。  但是在读这些书的过程中,我还深深地体会到熟练掌握并运行设计模式的重要性。现在的我,对设计模式几乎一无所知,能产出的代码也是最简单的过程化结构,往往能在最早期的时候快速写出一些代码,但后续的维护和扩展却越来越困难。从读《重构》一书中,体会到,也许只有设计模式,才能解决我目前所遇到的这些问题。不然,即使能看出问题所在,但不能运用正确的解决方法,依然是毫无用处。

理论考pass了

  今天去理论考了,考前还是有点担心的,因为感觉准备不充分,本来打算上周日好好看看书的,结果经不住诱惑去看小说了,还看得天昏地暗的。昨天晚上看完一遍,今天早起又开始从头开始看,在公司里也看了一上午,一直到11点半,才跟江江一起出去。先去四季花城,吃了顿快餐,凉拌的牛肉做菜,味道还不错。吃完看时间还早,便又在花城里面的椅子上坐着看书,有点大学里的感觉,平时不努力学习,到考试前夕就着急抱佛脚。  然后去花城门口等,等了一会儿接送车来了。在上面随着车的颠簸,有微微开始有点困意,确实本来这几天就因为看小说而睡得不多,加上一直来这个时候就是午睡时间,困是应该的。看江江也是眼睛一闭一闭的,呵呵。  一觉醒来,发现已经到车管所了,醒得还真是时候。上楼,签名,验指纹,排队等候。这是一个exam pool,一个人考完出来,交了座位牌,外面等的一个人才能拿着座位牌进考场。江江进去没几分钟,我也进去了,不过我不知道江江坐哪里。我拿着1号的牌,略带紧张。  每个座位左上方都有一个监控摄像头,座位之间都有挡板分隔。没有键盘,只有一个显示器和一个鼠标。我飞快地答完题,虽然还是有几题拿不准,不过我猜应该能pass吧,1分1题,最多允许错10题呢,拿不准的大概也是10题左右,再怎么也是有蒙对的吧。结果第1次交卷还太早,一定要开考后15分钟才能交,于是又把选择题看了一遍,再提交,有点紧张,94分,万幸,呵呵。  拿了成绩单,走下楼,江江还没出来,于是等了一会儿。江江出来时看到我还有点奇怪,说没听到我的名字,呵呵,她考了96,果然比我好呀!  到接送车的地方,开车的司机说要等到3点半,看看时间还2点半都不到,于是我和江江决定去外面自己找车走。刚走出,就遇到一帅哥问我们是否一路,于是又找了一美女,4人在路口等了好一会儿才拦到一辆红的,真的是,太少的士了。一直到公司,我拿了35块钱出来。  爽啊,就等上车了。

对那东西很失望

  今天考评,结果还是不出我所料,有点麻木地失望。不过这失望不是本篇blog的主题,这里要谈的是目前项目组里的重头项目,那个已经历了超过一年半开发的IDE。  这东西说要重构,我也已经开了个头,现在的情况是,老大说要在VSS上建分支。可是很明显的是,VSS根本就不支持这样的开发方式。这样就会引入一种新的问题,模拟分支,就有两份明显独立的拷贝,如果进行同步。修正问题在原有代码的基础上进行,重构则在新的一份拷贝上进行,以后合并代码对于现在的几个人来说,估计是个挑战,但我猜更多可能的直接是个灾难。  其次是架构和代码实现。现有的架构方式已经寸步难行了,但还是要在那个基础上继续进行。现在我是切实地体会到,编码产量高的,一点都不稀奇,相对来说架构设计则真的重要得多。一个文件,一个类实现,用C++代码实现,居然有18000行,这样的实现,功能确实做出来了,但后续的工作却举步维艰。一点小的修改,一点小特性的增加,都会牵扯到很多地方,可能引发很多问题。模块间的交互耦合,都没有经过良好的设计,都是觉得怎么容易实现,就怎么写,结果一堆一堆的重复代码,一个个超大的文件超大的类。  再次是加入特性没有经过认真评估,加入了一些完全不实用,花里胡哨的东西,而真正有用的功能并没有加入或增强、完善。比如about对话框,居然是一个视频文件,直接调用MCI接口播放实现。视频的周围则是一张背景图,背景图上的文字都是在图形处理程序中固定写死的,这样在以后每次显示信息修改的情况下,凭空增加了图片维护的工作量了。另一个是状态栏上的动态新闻,嵌入IE直接显示远程服务器上的内容,程序内部截获打开链接操作。大概是实现方式有问题,如果远程服务器宕掉了,则在程序启动时试图连接时,花费好几秒时间在那里阻塞等待。诸多问题,恕不一一列举。  最后是没有好好利用持续集成。虽然装了个CruiseControl,却只是为了实现点个按钮进行编译,连“持续编译”都算不上。一方面,没有实现“持续”,都是在需要时,手动点击一个按钮,进行force building;另一方面,只有编译,没有其他行为,比如代码度量、单元测试、打包发布等等,完全是个空架子。  其实回头看看这些问题,我觉得都是管理上的问题,单纯从管理层的角度出发,就能解决绝大多数。  总之,我对那东西很失望,因为管理者如果没有意识到问题根源,将在以后的很长很长一段时间一直存在,并继续恶化。而我能做的,大概就是假如有机会,把编辑器部分做完善点,在我能顾及到的范围内,实现一个类似Eclipse的扩展机制,仅此而已。因为今天听说,有人对编辑器评价很高,有人甚至就因为这个功能,就想把这东西拿到家里去用,这是我唯一的动力了。

我也TDD了

  TDD,即所谓的测试驱动开发,哈哈,我也开始用了。项目负责人花了两天时间总算建起一个可以正常跑起来的CppUnit工程,经过另外一个同事的验证,确实也能加入测试用例来跑。于是我兴高采烈地加入几个正儿八经的测试用例,结果发现用例是跑起来了没错,可是当从VC中想直接check in源代码到VSS中去时,VC就不干了,说两个工程不在同一个起始目录下。因为正式的功能代码是直接从原来的工程那里引用过来的,而偏偏测试工程是放在平行的目录下,因此有了这样的问题。万般无奈之下,只好亲自动手,把测试工程也移入正式工程的目录下,修改一下路径包含,一切OK,check in也没问题了。  几乎所有看得到的讲到TDD的书、资料上都说,在写功能代码前,要先写好测试用例。但是我发现直接这样做还是比较困难的,我现在的做法基本上是先想好,会有哪些功能类,然后就写好对应的测试类,再写好功能类中的成员函数声明,并用Visual Assist X生成一个空的函数实现,再跑到测试类中添加对应的测试函数,这样测试工程应该还是能跑起来。接着,我基本没按书上说的那样按部就班先写好测试函数内容再写功能函数内容,而是两边来回切换,一边写一点切换到另外一边再写一点,来回倒几次后,也基本都完成了。我不知道我这样做有没有什么不好的地方,反正用CppUnit这样做,进行单元测试确实方便,对于一些算法性的函数实现,直接在功能实现工程中往往不好测试,而放在CppUnit的测试工程里,则是再合适不过了。比如今天从别处抠来一段用WinINet进行Http下载的代码,略作修改,放在测试工程里测试,太方便了。  另外还有点体会是,设计模式是一定要学的,但总觉得GoF的《设计模式》看得太乏味,太枯燥,太艰苦了。Bob大叔的《敏捷软件开发》,Martin Fowler的《重构》都是要认真看的,还有《重构与模式》、《修改代码的艺术》、《反模式》都要好好看一下。提升个人能力,这些是必经之路。

从扩展编辑器开始

  重构auto completion和call tips模块,因为总体设计都被改得天翻地覆,而且决定开始使用TDD的方式进行,所以进度相对来说慢了些,更因为我自己一直静不下心来好好干活,真是令人烦恼啊!  话说其他一些相对可独立分类的小特性大部分已被我清理出来,但剩下还有一些杂七杂八的功能,我却不想再动了,因为从很早的时候就考虑好了这部分功能不应该由C++代码完成,而应该由host提供的扩展机制使用外部extension实现。但是现在的情况看来,这扩展机制短期内是不会加入的,因为老大似乎不支持这样的做法。另外一点是,组内大概除了我,也再没有其他人有足够深厚的兴趣和迫切的需求实现这么一个框架。  最早虽然想到了要用扩展,但并没有考虑很多,只是觉得只要能用内部嵌入的那个Ruby解释器执行一段脚本,而那个解释器同时也能访问到内部的一些数据结构和算法,应该就能处理目前的需求了。当时预见得到的需求也确实简单,比如点击一个菜单项,在当前光标位置插入一个字符串,该字符串可能是当前日期或时间等等。我当时想法相当单纯,甚至想的是菜单项还是依旧在资源管理器中画好,消息映射也还是添加好,最后实现的时候再调用Ruby解释器解释一个外部的脚本文件就可以了。即使这样简单,也比纯粹使用C++实现有一定的优势,至少如果需要修改实现逻辑,就只需要修改那个脚本文件,不再需要修改C++代码,不需要重新编译。  但是自从前几天快速翻看了一遍《Contributing to Eclipse》之后,我就想,要做就做得灵活点,也许不要求完全跟Eclipse一样那么强大有弹性,但也至少要满足以下几个要求:1、在主菜单、弹出式菜单、工具栏(、状态栏)上都可以扩展;2、在扩展之上还可以预留扩展点继续扩展;3、既然用脚本扩展,那么扩展加载应该也像Eclipse那样“懒加载”;4、需要有一种打包策略,足以支持起复杂的多文件的扩展。  暂时就只想到这些,嗯!重构完auto completion和call tips,就悄悄做这个。

讨教Plugins设计

  下午去向人讨教了一下人家怎么设计实现Plugins机制的。据该人士讲述,当时他们的产品2.x版本面对各种千奇百怪的用户需求,以及进度压力(这与我们目前遭遇到的情况类似),经过研究COM相关技术和Eclipse(估计当时也是2.x版本)的Plugins机制,最后在3.0版本实现了目前(也是3.x吧)的Plugins机制。  首先,该软件产品一般的插件都是通过vbs脚本实现,他们内嵌了vbs脚本解释器,使用的当然是最流行最正规的IActiveScript之道。可以扩展的地方包括主菜单、工具栏、弹出式菜单、状态栏等。像Eclipse及其他很多软件一样,基本的界面元素是用XML描述的,以弹出式菜单为例,在XML中首先会定义视图之类的信息,而弹出式菜单是作为视图信息的子节点出现的,说明这个弹出式菜单是在视图上点击右键才会弹出。这里好像是XML里就直接指定了菜单项的ID,对应Windows程序里的命令ID,而ID范围是程序中已固化规定好的,不能超出这个范围。然后指定一个脚本文件,可能还会具体到其中某个函数,每当菜单项触发时,便调用这段脚本。可能还会有触发前、触发后的选项,可以继续使用自定义脚本扩展实现。另外,菜单项有一个状态,如disabled、checked等等,这是在另外一个属性中设置的,该属性同样是指定一个脚本文件或函数,刚好对应MFC中的ON_UPDATE_COMMAND_UI。还有一点是,菜单可能是针对某种具体的选中项才有项的,所以在XML中描述菜单时,可能会包含一个属性,用于指定对其有效的选中项类型,只有在该类型的界面元素被选中并在上面点击鼠标右键时,才会弹出这个菜单。再有就是菜单等界面元素不可避免的一个问题是国际化,他们是把各种语言对应的字符串信息分别写到不同的配置文件中,描述菜单信息的XML里通过指定个别名或ID来动态查找装入相应的字符串。如果激活扩展插件函数时,需要一些固有信息,而是通过规定的格式书写函数参数。  其次,除了支持脚本扩展,该软件还支持COM组件形式的Plugins,这种Plugins相比脚本Plugins肯定强大得多,也许就好比Firefox中的Plugins之于Extensions,不过在介绍时他们一带而过,似乎也不是很支持第三方的去使用这种机制。  最后说一点,为了实现能让vbs脚本访问host程序中的一些信息或方法,他们把所有的东西都用COM接口还实现。似乎这是目前而言,我们能找到的最简单的一种处理方式,所有的状态、中间数据信息都可以保存在host程序中,脚本应该只是描述逻辑,而不保存状态。但这也是我最不屑的,我还是比较欣赏Eclipse(3.0以后用OSGi的不知道有多少变化)的机制,希望我有机会能设计并把它实现!

继续整理代码

  今天又整理了一下代码,又把初始化部分移出来了,居然相关的代码超过1100行,也算是神奇了。现在主要是把相对独立的功能剥离出来,所以并没有仔细地考究里面的代码细节是否写得完美。而且,总感觉现在的结构还不是我理想中的那种样子,但到底做成什么样,我自己也不知道,我想不明白怎么做才能做好。  另外在看代码的过程中,发现其中有一些是重新发明轮子了,其实有直接简单的实现,却因为对系统的不了解,而自己实现了一把,如果不考虑性能等方面的因素,只是关心代码的整洁清晰的话,当然不要自己的实现了。  还是要去整那一体化平台,心里总有种抵制的感觉。想去做图形编辑,想实现一个足够灵活的插件框架,唉!

整理了一下思路

  今天又没动手,人越来越懒了啊!总是在想,既然功能基本正常,就不要去动它了。就像Martin Fowler在书中说的一样。感觉有心无力,唉,心中有那么一点畏惧和抵制。  不过后来花了点时间,在纸上乱画,针对自动联想功能,清理了一下设计思路。现在的实现真的是很C风格化,所有的代码都是揉合在一个类中,所以相关的功能都在这个类里实现。重构时,希望尽量向OO的方向靠拢,那些设计准则,也要尽量地遵守。  从现存的代码看,自动联想从表现上看,可以分为3类,分别是Auto Completion、Call Tips、Code Snippet。其中Auto Completion是通过已输入的几个字符,根据上下文环境,给出候选的可能匹配的完整token,Call Tips则是在当输入函数调用的实参时,以tooltip的形式给出函数原型以提示,而Code Snippet则是在用户触发了事先绑定的事件时,根据已输入的字符,或者也可称为命令符,自动以代码片段替换该代表命令的字符串。总之这3种特性都是为了减轻编码者记忆负担,减少键盘敲击次数,降低出错概率。  当前的实现是当打开一个文件时,或者截获到换行操作时,进行扫描,记录当前文件及其相关包含的变量和常量的定义,除了名称,还需要类型。这样在输入变量时,可以根据名称进行提示,输入方法时,根据类型可以从数据库是查询到相关方法。所以扫描是为了Auto Completion和Call Tips作准备,Code Snippet不需要这些。  当初实现这些特性时,只考虑到了Ruby语言,所以紧紧围绕着Ruby这个主题开展,代码几乎没留下任何一丝空间以供后续扩展。不过既然这次要重构,无论从设计角度,对象职责上看,或是以后要添加扩展新语言支持,都需要把这块代码剥离出来。一个重要的原则是,剥离出来部分不能再跟界面有耦合了,不然单元测试几乎不能做了。  界面部分还是全都放在控件类中,控件类调用Auto Completion、Call Tips实现类获取待显示的内容,实现类应该从统一从一个抽象类继承,每个实现类可以对应一种(或一类)语法。这里需要引入一个工厂类。原来的助手类应该拆分成两部分,即代码扫描分析部分和信息存储查询部分。其中代码扫描部分也有一个抽象基类,从它派生出对应各种语法的具体实现类,把扫描分析的结果存储到信息存储部分,同时,信息查询部分从信息存储部分获取内容,信息存储部分的数据一部分来自于扫描分析部分,另一部分来自于固有持久化层,可能是一个数据库,可能是一个文件文件,可能是一种外部输入数据结构。这样,任何一部分的变化,都会尽可能少地影响其他部分的实现,而且这些实现除了界面相关部分,其他的应该都比较容易做单元测试,自我感觉设计得不错,只是可能工作量大了不少,很容易让人产生放弃的念头,呵呵!

在VC9下面用CppUnit

  因为主工程是用VC9的,所以想用CppUnit的话,一般说来也是用VC9的。不过很郁闷的是,无论怎么弄,总是不正常,不是编译不过,就是运行就崩溃的,真是奇怪呀!  如果实在不行,就换用VC7.1来用CppUnit算了,毕竟照在公司里使用CppUnit的经验,在VC7.1下是好好的,可以有经典的红条,绿条,也可以直接输出测试报告到文件中,嗯,今天就先到这里,又费了一晚的时间!