Palm OS代码重用思想

类别:软件工程 点击:0 评论:0 推荐:
作者:Robert Mykland,Palm OS Programming from the Ground Up 的作者,该书由 Osborne McGraw-Hill 出版发行


一些快速简单的设计方法的使用,能使 Palm OS 应用程序的大部分代码可以应用到下一个 Palm OS 应用程序。本文中我要介绍的内容是有关 C 语言中的公共头文件、事件抽象、方法和接口的分离、数据封装、多态性,以及可重用的 UI 定制和类型抽象。

许多人(包括我在内)被 C++ 和 Java 所吸引,因为它们生成的代码支持重用和分组合作。但是,这些大型语言限制了 Palm 手持设备的资源扩展性,而且很难使 68k Palm 应用程序达到预期的性能。随着基于快速基于 ARM 的掌上电脑的出现,这一问题不再那么严重。但是,用户即使不再为性能担心,他们也会抱怨这些语言生成的应用程序过大。

由于这些原因,自版本 1.0 后,C 已经而且会继续成为大多数专业 Palm 开发人员的首选编程语言。C 提供了最小的代码尺寸,并且在所有高级语言中性能最好。而且,在使用 C 时,如果能遵循一些设计原则,那么你能从这一更加面向对象的语言中受益匪浅。最终达到面向对象这种理想状态。如果使用不正确的话,就不能从 C++ 和 Java 中获得这些益处。

数据封装
数据封装是一种面向对象的思想。它将数据封装在对象中,这个对象以外的代码无法直接访问这些数据。通常只能在某个范围内修改数据,这样就避免因为在此范围外修改数据而导致出现 bug(当然,可怕的指针除外)。数据封装也有利于代码重用。由于数据并不是乱糟糟地遍布整个代码,也不能随便访问、修改、编译,所以可以很容易就能把其中一部分代码重用到其它地方。如果没有数据封装,一些关键的错误代码就会散布到整个应用程序的范围内。这样,就要费时间记着(或提醒)以后再修正它。

我认为 C 语言中最好的数据封装形式是把代码组织到 *.c 模块中,并且没有全局变量,这样,数据就只能在指定的 *.c 模块中修改。明确定义的 Palm 代码角色通常只有很少的几个,因此即使对于大型的 Palm 项目,这种办法也非常容易管理。在我的 Palm 应用程序中,每种数据库的代码都被组织到不同的模块中。每个窗体的代码都被放在不同的模块中。每个菜单项通常也有它自己的模块。自定义控件和专门的标准 Palm 控件分别有自己的模块。在新应用程序中可以混合搭配着使用这些模块。

数据抽象
所有的变量都声明成函数内部变量或静态变量,您可能想知道,(例如)如果一个窗体需要为某个数据库设置记录属性,该怎么办呢?答案就是把变量在数据库模块内部声明成静态的(如果需要永久存在的话,甚至也可以声明成数据库的全局变量),并且编写一个访问函数来修改这些数据。访问函数仅仅用来修改数据,或者返回数据的一份拷贝。这些额外工作值得吗?绝对值得,因为:1)这个模块可以很干净地从一个应用程序移动到另外一个应用程序,2)可以具体控制其它模块如何访问这些数据,这一点也许是最重要的,3)当出现 bug 时可以知道该在何处设置断点。

举例来说,某个模块有一些用来使用控件创建自定义表格的代码。这个表格基于数据库中的条目,或者基于某个由其它模块(如,数据库模块)生成的静态数据。当第一个模块要访问第二个模块中的数据时,应该调用一个用于数据封装的自定义函数,例如 getTableEntryControlType(),而不是直接从第二个中读取数据结构。

函数 getTableEntryControlType() 可能会像下面这样简单:


UInt16 getTableEntryControlType( UInt16 u16Row )
{
return( u16aControlTypes[u16Row] );
}

刚才我们编写的程序还有一个重要的功能,只要这个函数按照指定格式返回一个双字节数据,我们就可以改变内部数据的表达格式。例如,我们可以把它储存到数据库里,而不是放在静态表格中。而外边的调用方法却不必改变。

这就是数据抽象的威力。如果编写了一个专门的控件,它的所有配置数据都是通过函数调用取得的,那么就不必关心那些数据是来自常量、静态变量还是数据库。不论它是控件、窗体或者其他对象,立刻就可以发挥作用,而且在任何情况下都可以重用。

还有其它的办法借助数据抽象使代码可以重用,那就是常量。给整个应用程序定义一个通用头文件,用于定义常量。我总是给它命名为 app.h。通过这种办法,可以混合搭配使用我的窗体、主例程和数据库资源。它们都只引用这一个头文件,所需要的数据也都从这个头文件中获取,例如,在不同应用程序上下文中定义自己行为的数据。这样做不仅方便,还有一个很大的好处,当修正 bug 时,即使修改了顶部的头文件,也不用改动重用模块。

后面我还会接着讨论 app.h,此处还有一些类似数据抽象的用法。我喜欢定义程序可以运行的最小和最大 Palm OS 版本号。喜欢给启动窗体起一个象 StartForm 一样抽象的名字,并在 app.h 中针对特定应用程序对它进行定义。我还喜欢给应用程序定义一个创建者 ID,定义为字符串和四字符数字的形式:

// Define the creator ID
#define APP_CREATOR_ID_CHAR 'SRVR'
#define APP_CREATOR_ID_STRING "SRVR"

这些定义使 main.c在任何地方都可以重用。数据库代码也引用了这些创建者 ID,并且受益于这种抽象用法。

继承
继承是这样一种思想:在一个对象中处理基本特性,然后创建一个专用对象,来继承基本对象的特性。例如,在 C++ 中,你可以创建一个控件,它从另外一个对象继承一些图形文字;而后者又从第三个对象继承字符串表达和操作函数;第三个对象又从其它对象继承原始数据移动操作。


在 Palm 应用程序中,有几个地方使用 C 继承非常有用。一个是数据库。另一个是自定义控件。

Palm OS 提供了一些 API 函数,用于操作基于记录的数据库。我编写了几个不同的模块,可以组织这些 API。其中一个模块又小又快,只用来拆分和调整记录。另一个模块的功能比较丰富,甚至可以执行排序操作、类似查询的操作和关系操作。当我在 Palm 应用程序中需要用到专用数据库时,我经常重用这两个模块中的一个或全部作为专用数据库的基础。因为这两个通用数据库中的内容都在它们各自独立的模块中,所以将每个特定数据库都放在其各自独立的模块中。为了继承的需要,这些对象可以像对象绑定一样使用。

这种模块级继承,还可以很好地应用在定制和自定义的控件上。我编写了一些与标准 Palm OS 控件功能相似的基本控件。可以把这些控件混合搭配、合并成一个新的较复杂控件。新控件继承了组成它的基本控件的特性。

可重用的 UI 定制
我将专门谈论一下如何搭配使用所有我创建的定制和自定义控件。下面列出了支持复选框控件的函数:

void CbxCloseAll( void );
void CbxDraw( FormGadgetType* spCbx );
Boolean CbxEnabled( FormGadgetType* spCbx );
void CbxErase( FormGadgetType* spCbx );
Boolean CbxGetValue( FormGadgetType* spCbx );
Boolean CbxHandleEvent( EventPtr spEvent );
void CbxHide( FormGadgetType* spCbx );
void CbxHit( FormGadgetType* spCbx );
Uint16 CbxOpen( FormGadgetType* spCbx, Boolean oEnabled,
Boolean oValue );
void CbxSetEnabled( FormGadgetType* spCbx,
Boolean oEnabled );
void CbxSetValue( FormGadgetType* spCbx,
Boolean oValue );
void CbxShow( FormGadgetType* spCbx );
Boolean CbxValidatePointer( FormGadgetType* spCbx );

对于这里面的很多函数,Palm OS 中都有您熟知的函数提供类似功能。这些函数可以天衣无缝地应用到控件上,甚至可以直接用我的函数替换 Palm OS 函数,同时检查是否自定义控件,如果是标准控件就调用适当的 Palm OS 系统函数。这样,我的自定义控件就可以和 Palm OS 标准控件混合搭配着使用。上述函数只是这种无缝集成的其中一个实现方法。

函数 CbxCloseAll() 释放当前窗体上所有自定义复选框占用的内存。在 frmCloseEvent 事件处理函数中可能会用到这个函数。

正常运行时,函数 CbxDraw() 绘制一个复选框。

函数 CbxEnabled() 用来查询复选框的状态。


函数 CbxErase() 删除复选框。

对于复选框来说,CbxGetValue() 与 CbxEnabled() 功能相同。当合并一组控件时,同时支持这两个函数就表示你不必深究什么控件调用什么函数,以及以什么方式处理什么事件。推荐使用这个函数。

函数 CbxHandleEvent() 为窗体上的所有复选框处理所有特殊事件,使它们更像普通的 Palm OS 标准控件。例如,使用 formOpenEvent 和 formUpdateEvent 时就需要绘制复选框。而 penDown 和 penUp 事件则用来分析它们是否在某个自定义复选框内部发生。如果满足条件,用户定义事件 cbxEnterEvent 和 cbxExitEvent 就会被追加到队列中,并且用户定义的事件 cbxSelectEvent 也会被加入队列。然后,就可以像处理常规复选框一样在你的代码里处理这些控件了。

函数 CbxHide() 删除复选框,并设置它的“usable”属性(在动态分配的结构中定义)为 false。

函数 CbxHit() 把cbxSelectEvent 事件加入队列。

函数 CbxOpen()初始化复选框,为使用该复选框做好准备。

您可能也猜到了,函数 CbxSetEnabled() 用来启用/禁止复选框。

函数CbxSetValue() 接受一个 Boolean(布尔)值,并用它来设置复选框。

函数 CbxShow() 使复选框可用,并将其绘制出来。

函数 CbxValidatePointer() 用来识别自定义控件是否是真正的复选框。

有了这类函数,就可以创建能与 Palm OS 标准控件无缝混合使用的自定义控件了。

多态性
相对于简单事情而言,多态性是一个很大的话题。基本上,我们所做的一切都是在处理对象(对于我们的 C 来说,就是代码体),它们一般用来封装各种操作。从这一点来看,需要比 C++ 更仔细地组织 C。但这样也就更突出了代码重用的效果。

我在加载窗体的时候使用多态性。我的主执行例程完全通用,并且已经被我重用了很多次。当加载一个新的窗体时,它需要装载该窗体的事件处理函数。完成这项工作的函数调用实际上是用 #define 定义的,我把它放在通用头文件 app.h 里了。所以,装载事件处理函数的代码如下所示:

// Load a form
if( sEvent.eType == frmLoadEvent )
{
Uint16 u16FormID; // The form ID
FormPtr spForm; // Points to the form

// Get the ID
u16FormID = sEvent.data.frmLoad.formID;

// Initialize the form
spForm = FrmInitForm( u16FormID );

// Establish the event handler
FrmSetEventHandler( spForm,
getEventHandler( u16FormID ) );

// Point events to our form
FrmSetActiveForm( spForm );

// Draw the form
FrmDrawForm( spForm );
}

函数 getEventHandler() 位于 app.h 的一个 define 语句中,如下所示:

// This creates the function getEventHandler()
// that returns the event handler for a given form
// in this application.
#define EVENT_HANDLER_LIST \
static FormEventHandlerPtr getEventHandler(\
Uint16 u16FormID )\
{\
switch( u16FormID )\
{\
case AboutForm:\
return( aboutFormEventHandler );\
case DestinationForm:\
return( destinationFormEventHandler );\
case FeaturesForm:\
return( featuresFormEventHandler );\
case HomesForm:\
return( homesFormEventHandler );\
case NotesForm:\
return( notesFormEventHandler );\
case RemarksForm:\
return( remarksFormEventHandler );\
case ThisHomeForm:\
return( thisHomeFormEventHandler );\
}\
return( NULL );\
}

然后在 main.c 中实例化该函数。在 main.c 接近顶部的地方有这样一行代码:

// The event handler list
EVENT_HANDLER_LIST

这样,main 函数就不必知道它能加载或者将要加载什么样的窗体,因为它们都包含在通用头文件中定义的该函数中。

另外,我在处理致命错误和程序退出/清理的方式,也使 main 函数和事件处理代码能够重用。在这种情况和下面几种情况下,多态性被变成了某种事件抽象。因为,尽管没有到处都显式地处理像 appStopEvent 这样的重要事件,可是如果做了充分的准备,就能在任何合适的地方从容不迫地处理它。

主要问题是,就像其它事件驱动的应用程序一样,Palm OS 应用程序也需要有一个可以控制事件循环的环境。有时,需要以相同的方式拦截特定的事件,避免对它们进行分析和处理。有时,需要临时添加事件,例如更新动画,使它按照正常速度运行。

要做到这些,最好的办法就是借助 Palm OS 提供的工具,即 ErrThrow 和 ErrCatch。在我的 main.c 中,事件循环是这样的:

// Begin the try block
ErrTry
{
// Initialize all parts of the application
appInit();

// Wait indefinitely for events
while( true )
processEvent( -1 );
}

// Service any fatal alerts
ErrCatch( u32Alert )
{
if( u32Alert )
FrmAlert( (Uint16)u32Alert );
} ErrEndCatch

事件处理函数 processEvent() 需要一个参数。这个参数表示等待事件的时间,单位是 1/100 秒。同样,还可以在其它地方创建一个小型事件循环,用来实现以下功能:


while( oExitAnimation == false )
{
processEvent( 10 );
animationStep();
}

在上面的例子中,每隔 1/10 秒左右,程序就会更新一次动画。这样做有一个很大的好处:如果在事件循环中检索到 appStopEvent 事件,不论自定义事件循环嵌套得有多深,都可以通过一个 ErrThrow( 0 ) 调用返回到 main.c 中的 catch 语句。如果不能以多态性的方式实现这一点,就只能在每一个可能收到 appStopEvent 的地方处理它。那样就会带来大量的工作和 bug。

也许你会需要做一些清理工作。比如上面那个深层嵌套的自定义事件循环,当接收到 appStopEvent 时,就需要做一些清理工作。这样,我们就需要以另外一种方式使用多态性来绑定不同的单独模块,并生成 main.c 通用代码。在头文件 app.h 中,定义了两个函数,名为 appInit() 和 appStop()。这两个函数实际上都是宏,它们调用了一系列的初始化函数和终止函数。这些初始化函数和终止函数属于应用程序中的每个模块。在 main 例程中调用它们。注意,上面将 appInit() 调用包含在了一个 ErrTry 块中。这样,我就可以在这些模块的初始化函数中避免致命错误。对于上面的动画循环,当接收到 appStopEvent 并退出之前,需要释放一些内存。在这种情况下,最好把代码放在模块的终止函数中,并在终止函数中检查是否还占用内存,如果有则把它释放掉。

多态性的最后一种主要用法也可以用来重用代码,我就是使用它来处理菜单和菜单条的。在一个应用程序中,也许会重用另一个应用程序中的窗体。二者的菜单应该完全不同。这就表示即使想改变的仅仅是一个菜单,也要去修改窗体代码。

有一个办法可以解决这个问题。对于应用程序中的每一个窗体,都在 app.h 中定义了一个宏,用来处理该窗体的菜单事件。在 app.h 中,可以随便定义菜单事件的处理函数,使窗体及其菜单项分隔开来,这样就增加了窗体和菜单代码的重用程度。宏定义如下:


// The menu event handler macro for the "remarks" form
#define remarksFormMenuEventHandler(spEvent) \
{\
editMenuEventHandler( spEvent );\
optionsMenuEventHandler( spEvent );\
}

菜单代码本身位于另外一个单独的模块中。我一直在重用这个宏。要想修改菜单,就可以随便将它放在什么地方,并将处理事件的模块复制过去。我的 about 对话框窗体及其菜单项也是单独抽取的。在 app.h 中有一项定义,可以从应用程序的名称抽取 about 项:

// Menu ID name conversions
#define OptionsAbout OptionsAboutMyCoolNewApp

使用这种方法,就可以在一个新的应用程序中复用 about 对话框的窗体代码及其菜单项代码,而两个模块都不必改动。

类型抽象
一般来说,在 Palm OS 应用程序中,并没有多少字节的内容不是与 Palm OS 息息相关的。但是,或许有时候面对代码的时候,你也想把它们重用到其它平台上。这种情况可能会发生在连接代码上,它们在接口的两边都以一种通用的方式处理数据结构。

对于这种类型的代码我还有两点建议。
·第一,创建一套自己的类型定义,并在编码时用它们代替标准的 Palm OS 类型定义。这样可以使用恰当的 typedef 定义,在不修改其余代码的情况下同时满足接口两边的需要。

·第二点建议是,不同的情况下,要特别注意结构体的压缩规则。如果对结构体进行压缩和展开时各自使用相同的规则,就会导致一些最棘手的 bug。但是,如果使用不同的规则,进行通信时就会在接口的某一边产生错误的数据结构。

代码重用时一些易犯的错误
尽管做了上面那些努力,当我把代码从一个应用程序转向另外应用程序时还是遇到了一些困难。困难之一是不同的应用程序使用不同的方法验证用户输入的有效性。当窗体从一个应用程序移动到另一个应用程序时,提交给原数据库模块的信息也许就不能用于新应用程序的数据库。现在,我给自己制定了一条规则,要尽可能快速直接地检查用户在窗体上输入的内容。我想这样也能制作出更好的用户界面。如果您觉得错误检查很大程度上依赖于应用程序,就将其置于另外一个模块中,并且在窗体模块和错误检查模块之间使用通用的调用方式。

在编写应用程序之间的错误处理时,需要有一套明确的规则。如果在使用更新的模块编写 ErrThrow 技巧之前,就试图重用该代码就不好了!技巧应该是一致的,而且应该尽量以多态的方式处理错误,这样才能在复用以前的代码时,尽可能地少写代码。

对于可能被特定模块破坏的部分,要使用终止调用请求进行清理。要仔细检查,可能不只是内存分配那么简单。在这儿缺乏警惕性会浪费很多宝贵的时间。

以上是我对 Palm OS 上代码重用的一点认识和体会,感谢大家和我一起分享。我希望各位能从中受益,能在将来开发应用程序时节省时间。我会非常乐意并答复一些问题、评论,或者有关代码重用方面的好想法,以及 Palm OS 其它方面的内容。可以通过 [email protected] 与我取得联系。

本文地址:http://com.8s8s.com/it/it32767.htm