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。对象在运算过程中出错导致无法恢复内部数据是可能的,有时无法避免,只能在对象报废的时候返回错误并退出例程,让对象析构。但是要保证析构可以正常执行,并且不泄漏内存或者导致存储数据的不完整。