2007/02/11

用例

测试真是个麻烦的事情。想侥幸是不行的。还是得老老实实写好用例,然后仔细的Track一遍。

因为算法的改变,尤其是记忆算法的引入,巨硬三的用例无论在数量还是复杂性上都比巨硬一增加很多。而且还有个64K的限制摆在那儿。真是麻烦透了。到现在为止,我还没有想出比较好的debug策略。在一些情况下发生错误的话,可能导致巨硬无法继续运行,我没想明白该在什么情况下清除输入历史和记忆激活状态。 还要好好想想才行。

2007/02/08

PalmOS C++

巨硬写了差不多两个月了,大的改动也不少了。总结一点经验之谈:

1)
代码阅读和注释

代码阅读是比迫不及待的在模拟器或实机上运行程序的更好的办法。在代码阅读阶段可以完善那些在写代码时没有来得及写上去的注释。这也是填写注释的最佳时机,等代码都完成得时候大概也没兴趣添加注释了。这个时候不要修改代码,无论它多么的不易理解。因为往往写代码的时候考虑得很深入,而阅读的时候会考虑得比较浅一些,为了追求简单和美观去修改代码时常会把代码改错的。

2)
完整性

完整性是非常重要的问题。尤其是存储数据的完整性,尽可能避免多次写入,如果中间发生失败,那就完蛋了,写入就要一次性完整写入。在语言一级是没有Transaction的概念的,这个必须保证,否则可能会遇到很奇怪的问题,必须大量撰写脚手架代码才能发现问题。

3)
有效性

对象分为两种。一种是务必保证其内部数据正确有效的,这要求在发生内部数据修改的时候,先在临时内存上工作,如果发生错误,抛弃所有临时内存,返回错误,数据对象仍然是正确的。另一种是无法保证这一点的,在这种情况下,至少要保证对象可以被正确析构。打印出一份成员变量的表格,对照着检查每个例程的分支,在返回结果之前,是否做到了所有的成员都是有效的;另外打印一份表格,标注上所有的例程,哪些在返回错误时对象仍然可用的,哪些是发生错误时对象必须销毁的。对于必须销毁对象的例程,要整理到一个单独的列表中,以后会用得到。

4)
参数的方向

我个人的习惯是:单向传递参数使用&引用(类)和原形(基本类型),返回或双向传递的参数用*(指针),这样在看例程说明的时候很容易理解。书上说应该在所有可能地方使用const,这个我持保留意见,我建议在Beta版之后的RC版阶段做这个工作。

5)
参数有效性

验证所有参数是个好习惯,如果能做到这一点,恭喜。如果做不到,那底线是至少要验证所有Public的例程的参数有效性,如果参数无效,返回INVALID_PARAM或其它自定义的错误。

6)
返回值

要非常谨慎的使用voidBooleanMemPtrInt作为返回类型,除非你非常确定该例程不会发生错误,或者错误无需处理,不会导致严重后果 (尤其是读入数据失败导致程序认为数据源不存在的时候)。不要因为返回成功/失败或者是/否,就偏爱用Boolean作为返回类型,把Boolean* success或者Boolean* found放在参数列表中往往是更好的办法。

我建议写函数的时候首先写成返回Err类型的,直到你发现实在没有什么错误可能返回的,再从参数列表中拉一个能让形式上出现一点美感的并且不牺牲效率的参数作为返回值,如果这也找不到,那就干脆用void

7)
清理现场

这是在阅读代码时很容易检查到但是在调试时不易发现的东西。看看是不是所有的return之前,都把例程中分配在堆上的东西清理干净了,在PALMOS中,还要看看有没有LockUnlock不配对的情况。

8)
对象参数

大的对象参数可能导致栈空间溢出,除了基本类型之外要使用引用&来传递对象。为了避免意外的情况,可以给类写一个复制构造函数的生命,并且写成Private。这样在编译阶段就能找到什么地方有意外复制了。这种情况非常常见的出现在参数列表中。

9)
参数类型与Ripple Effect

个人认为应该尽可能避免传递对象参数,虽然这常常让C++程序员觉得方便。但是在修改对象的时候,它的Ripple Effect可能不会在编译阶段被发现。Ripple Effect指的是程序的数据结构或者对象界面发生修改时,波及的代码范围。

传递基本类型参数的例程,在对象被修改的时候,或者不会发生失效的情况,或者在编译时失效,这都更容易发现。

以上两点,我个人的原则是:尽在内部函数中一些可以让形式方便很多并且易于理解的地方传递一些大的对象参数,公共函数都用基本类型、流、字串、全局到处都会使用到的Struct/Enum、以及他们的集合类。复杂的类和结构通常仅应该限于在类内部使用,易于理解和使用方便,他们通常不该是需要其它类理解的东西。这被教科书成为类的内敛性。

10)
长参数列表

在读取对象的信息时,使用长参数列表并且接受NULL指针是一个很好的做法。可以减少代码体积,代码集中容易改动,而且不牺牲效率(参数指针为NULL 不返回结果)。不要因为效率问题为类似功能写多个函数版本。这种做法下唯一的小麻烦可能是某些返回参数是关联的,在多级读取的情况下,如果忘记读取其中的 一些参数,可能无法返回正确的结果,但是这是很容易被发现的错误,在调试时会遇到NULL指针对象的访问。

11) Operator=
与复制构造函数

重载所有对象的Operator=。为所有的对象提供复制构造函数,包括那些你不希望被复制的,提供一个Private声明。这可以避免大量的异常错误。

12)
错误处理

处理所有的错误。处理所有的错误。处理所有的错误。

13)
展开错误返回语句。

例如:
if (err)
{
return err;
}

在所有return err行标注断点。这样就可以在调试时看到所有失败的函数。

14)
检查危险例程

在前面提到过一些例程会导致对象的数据失败。理论上这种情况应该是不允许的,但是实际上,避免对象失效可能代价很高或者没有必要。把所有这类的函数都找出 来,给一个特殊的名字,不同的类之间不要使用同样的函数名,在代码完成的时候用搜索功能找到所有调用了这类函数的地方,看看是不是都在发生错误的时候立刻从例程中返回并且析构了这类对象。

15)
错误代码

Palm
的错误代码处理简单有效,值得效仿。给每个类分配一个ErrBase(避开系统已经定义的base),比如:

#define ERR_CLASS_A_BASE 0x6100

定义一段通用的错误信息,比如:

#define ERR_MEM_ERROR 0x0001
#define ERR_DATABASE_ERROR 0x0002

自定义的错误可以从某个位置开始,比如:

#define ERR_CLASS_A_INDEX_OUT_OF_RANGE 0x0080
#define ERR_CLASS_A_SEEK_NOT_FOUND 0x0081

把这些都声明在一个error.h文件中,在所有的cpp文件中include。遇到错误的时候返回最有意义的组合,比如(ERR_CLASS_A_BASE|ERR_INVALID_PARAM)。这样容易解决在什么地方提供用户级错误信息的问题,也方便Debug

16) []

如果要使用这个玩意,务必要使用C++的异常处理机制;否则无法处理指数越界问题。

17)
构造函数

和上一个问题类似,如果没有使用C++的异常处理机制,就不要在构造函数中写任何可能发生错误的代码。

18)
初始化对象

初始化的工作可以放在一个返回ErrInit()函数中完成。应该区分只能一次初始化的对象与可以多次初始化的对象。我的习惯是用Init()做初始化,用Reset()做多次初始化。

19)
结构

层状结构永远是最好的系统结构。如果设计不是层状结构的,或者需要大量的变量表示对象状态,这个设计就是不好的设计,应该重新设计。一个好的系统结构应该 只有一些工具类(集合类,字串,MemChunk操作),数据库封装,核心逻辑,显示和事件处理,核心逻辑部分接受的事件应该是虚拟定义的,这样方便代码 修改和移植。

20)
限制

PalmOS
有一些乱遭遭的限制,比如弹出的程序不能用全局变量(也就不能使用Virtual),栈空间只有2K,一个segment只能有64K等等,这些都有办法解决。不要因为这些问题影响主要程序结构。

21)
好的特性和坏的特性

PalmOS
有很多独特的东西,比如storage heap设计。但是开发者应该明白哪些是好的特性,哪些已经是过时的东西。比如storage的数据库记录方式就是效率很高的方式,其他平台上也有类似设 计(包括Windows MobileSymbian),但是和数据库相关的appInfo, sortInfo, category, 以及全局feature等特性通常是没必要使用的,这些特性在其它平台上都不被支持。如果大量使用这些特性,这个代码就很难移植了。

22) Palm Style

在其它平台上,一个窗口程序通常有全局对象,比如Document;但是在PalmOS上,应该考虑把这个东西放在数据库而不是动态内存中。在 PalmOS上的风格是,响应任何事件的时候,创建对象,修改或者读取需要显示的内容,然后立刻销毁对象,返回事件处理。这样做才是Thinking in Palm style

23)
用例遍历

第一步,写下所有可能的分支,遍历所有的情况,直至都可以顺利通过;第二步,记录下这些遍历过程使用了哪些例程的哪些分支,在代码中标注好。检查关键逻辑 部分未被使用过的代码范围,想办法设计一些用例可以用到这些代码,测试一遍;第三步,对于仍然没有被使用到的代码,考虑写一点脚手架代码,把重要的部分测 试一下。

24)
脚手架代码

脚手架代码转化成功能的可能性是很大的。尤其是检验和导入导出数据库的部分。这段代码甚至可以写在核心代码之前。

25)
开发顺序

我的代码顺序是首先是工具类,其次是存储类,然后是脚手架,在脚手架代码测试通过之后,工具和存储都基本上没有问题了,而且脚手架中通常会包含核心逻辑用得到的大量功能,比如自定义对象的显示和存储。最后开始写核心逻辑代码。这样在写核心逻辑的时候,之前的代码就提供了一个能运行的环境,就可以尝试做很多逻辑方式,检验各种不同的想法。做充分的比较并选择最优的方式。

26) 基础类

PalmOS提供的类库让人不齿。CodeWarrior中提供的POL,太重,是个形似MFC的东西,给MS程序员准备的。如果你不是只用MFC的选手,或者工程并不赶进度,没必要用这个东西。

PalmOS上的程序大多很简单。常用的类无非就是字串、文件和集合之类,自己写个轻量级的更好用。而且我建议准备一个MemChunk类,用于内存中的数据操作,支持一块内存的Insert/OverWrite/Remove/Move,这是PalmOS最常用的东西,会给开发带来极大方便。

27) 效率

简单的效率估计。把查找数据的过程分为三类,一类是索引,或者间接索引;一类是二分,令一类是遍历;索引是非常非常快的,几次二分也没问题;但是遍历就麻烦大了,一次遍历几千个数据的速度大约是1S左右。这种事儿只能有一次。Palm的数据库的UID查找记录就是遍历的。

数据库排序之后用二分是PalmOS的风格,应该尽量遵循这个原则设计数据结构。甚至对于单一记录,也应该这样设计。PalmOS提供二分的回调,使用很方便。当然,不需要排序的情况下使用索引是最快的。但是就要考虑删除记录之后的占位和重用问题。

防止对象被意外复制

一些对象不想被意外复制,比如避免出现在参数列表中,或者是全局唯一对象。简单的办法是写一个空的复制构造函数声明并声明为private,这样会在编译时报错。

Private:
CClass(CClass& cls);

外一则:
另外要注意在错误发生时处理好所有成员的有效性,避免出现在析构时Under lock或者无效的MemPtrFree。对象在运算过程中出错导致无法恢复内部数据是可能的,有时无法避免,只能在对象报废的时候返回错误并退出例程,让对象析构。但是要保证析构可以正常执行,并且不泄漏内存或者导致存储数据的不完整。

2007/01/29

super bug

这个超级Bug的起因很不容易发现。

如果在使用MemMove的时候,算错了大小,导致写入越界,在使用MemPtrFree释放内存的时候会遇到错误。

实例:写入的时候错误格式是:
MemMove(dst, src, sizeof(TSeqAddr) * m_num - position);

这里的m_num - position忘记使用括号了。结果是第三个参数越界;然后在使用MemPtrFree释放dst的内存时,会遇到错误。这个错误在模拟器上不会报,在实机上立刻重启,无任何错误信息,#*377的结果是Fatal Exception。找到这个Bug花了我整整5个小时的时间。吐血ing....

外一则:
赋值的对象需要重载"=",尤其在使用集合类的时候,否则死得很难看。

2007/01/23

stage & stage set

今天定义了一套很变态的candidate填充方式。把所有可能拆解的因素都变成可以自定义的。这包括:

源:巨硬特殊的双源工作方式。
alpha/chinese:匹配中文还是英文
搜索模式:包括moreOrEqual, Equal, lessThan, decremental & oneByOne
渐进:all, lastOne, none

这些因素的组合构成一个搜索阶段,SearchStage,一组搜索阶段构成了搜索阶段集,可以完整定义所有可能的搜索组合。

但是在实际使用中,只打算定义两个集,一个是正常模式,一个是强制模式,用户可以在这两个模式之间切换。

低级错误

这个Blog快成了低级错误大全了。

*index--或者*index++

这样的写法会先运算(++)/(--),而不是把地址index存储的整数值 ++或--。

自己打脸一百遍呀一百遍。

2007/01/22

在集合类中使用的对象,务必重载assignment operator

不然会死的很难看。

在集合类重新分配指针的时候,要用到这个函数copy对象。如果系统不指定,那它就采用Copy内存的方式解决,这个最终在对象析构的时候就会崩溃了。

CMol& CMol::operator[](CMol& mol);

Copy Constructor与Virtual Function

没想到这种格式的写法下:

CString str = "";

编译器是直接调用Copy Constructor。我没有在Copy Constructor中初始化指针,但是在调用的Copy函数中首先释放指针的内存:

if (m_p) MemPtrFree(m_p);

结果就是完蛋了。

另外,所有的Virtual函数在弹出窗口模式下都无法使用,这可能和Virutal Function的表是全局变量有关,对编译原理我不是很熟,猜想应该是这样的原因造成的吧。

2007/01/20

segment问题及对策

PalmOS有一些比较变态的限制。

象巨硬这样的弹出程序,必须在程序的第一个64K segment。mHard3的算法复杂度提升,完成这个目标颇有一些难度。解决的办法是把哪些不会在 输入时用到的函数,都从类中拿出去,放到一个继承类中。程序的主界面和维护功能,可以使用继承类工作。这样可以尽可能的压缩核心输入用到的数据结构编译后的大小。如果还是不行的话,就职能考虑ARMlet了。

另外今天的好消息是,已经debug到导入导出数据库了。这个工作同时可以检验大量的数据对象操作的代码质量。这部分完成之后,就可以进入输入法部分的Debug了。照目前的进度,应该在春节前有能对付使用的版本了。

2007/01/17

脚手架代码

这个词真是有够形象。

大部分软件在进入Debug阶段的时候都会遇到这个问题。会写一些和主程序没什么关系,但是对于测试来说会带来很大方便的代码。这类代码被称为脚手架代码。因为脚手架代码通常不是最终发布版本的一部分,程序员就很容易偷懒,不做很好的结构设计和封装,不仔细考虑功能的设计,不做严格的错误处理,因为它的用户是非常理解这些代码会发生什么事的人。

当然这在程序开发阶段是正确的。但是谁又能保证在长期的维护阶段一直正确呢?缺乏好的文档和设计,在软件发布了一段时间之后,需要规划新的改进的时候,这些代码通常就很难读了。在一些数据结构被修改之后,这些代码就可能失效。给调试工作带来麻烦。

要知道想盖好一座楼,好的脚手架也是成功的保证。