对于系统而言,机制就是系统设计的程序;对于单个问题而言,机制就是解决问题的方法摸板。本文以实例来说明机制是如何建立和运作的。(机制也可以称为模式,不过机制略多带一些管理控制方面的内容)
以前的编程过程中,虽然是按模块划分,或按对象实现封装,但从来没有考虑过建立问题解决机制这么一个概念。由于这次的软件项目大了,开发人员好几个,不再是一个人的事情,所以,不得不考虑多人协同开发的方法。 要想保证质量和进度地协同开发,将面对以下几个困难: 1)开发者的水平和开发习惯不一样,如何在统一思想的同时发挥个人能力;2)系统的需求分析和设计不可能一步到位,调整系统设计后对每个开发者的影响很难估计,有时可能需要重做。如何降低调整系统设计对部分内容的影响程度;3)开发人员的流动不可避免,技术资源的延续性如何得以保持仅仅靠详细的文档,而不考虑程序自身的可读性和可延续性是不够的(让新手了解每一个变量或函数的功能仍是很困难的)。4)当时间进度已经临近或者技术难度太大而要求简化系统功能时,可能出现某些部分的开发已经要求另一部分必须完成的矛盾,如何避免此类骑虎难下的局面。 此时,机制的建立就十分有必要了。这是一种编程标准的制定,这种标准不仅仅是变量、函数等命名规则,更多的是许多设计和编写程序处理问题的法则和规定。 对于系统而言,机制就是系统设计的程序;对于单个问题而言,机制就是解决问题的方法摸板。
这里有一个简单而有效的办法:步骤一:统一开发群体的基本开发思想(即基本开发机制)1)无论水平高低,每位成员必须以学习态度来到这里共同开发 对于任何一个未开发的项目,我们都是学生,即便是第二次开发此类项目,同样有待学习。在一起开发的团体,首先要解决的是思路定式。开发经验和惯例并不是定论,创新的东西或许更有价值。所以,端正学习的态度来开发,无疑是重新审视已有技术和创新新的技术的保证。2)不是在为自己开发程序,而是在为后人开发程序 这句话的意思是:你开发的那部分程序就是一个完整的产品,它的用户就是后来的开发者。所以,你必须考虑你的产品如何简单明了地交给其他开发者继续开发或者扩展功能。3)没有抽象出问题解决公式不要写代码 如果是接到任务就不管3721地按需求写出一堆代码,任务虽然是完成了,但这段代码也许跟着就废了:增加或修改功能等于重做一次软件。任务虽然是明确的,但任务中各项需求之间的关系和有可能更改需求的处理却是不太明确的,这需要综合分析。请看一个简单的例子: CAD图形设计中点、线、圆的绘制编辑和数据管理,要求有绘制、点选、删除、移动、修改功能。以下设计是完全根据需求实现: #define dotID 1 #define lineID 2 #define CircleID 3
struct TDot { long X,Y; }; struct TLine { TDot start; TDot end; }; struct TCircle { TDot center; long radius; }; struct TData { 1=点 2=线 3=园 long typeID; file://区别数据类型,1void *data; }; class TPaper ( TData *dataVal; void Draw(TData *data) { switch(data->typeID) { case 1: file://画点 case 2: file://画线 case 3: file://画圆 default: } }
void Delete(); void Move(); bool Select(long x,long y); Update(); }; ...... 抽象各类图形元素的设计:将点、线、圆的共性部分抽象成一个基类,使用虚拟函数,让各自图素各自解决画法和操作。
typedef MyDataType long
struct RPoint { MyDataType X,Y; } class TLink { file://数据指针链定义; } class TPaper : public TLink { virtual void Draw() { TPaper *temp; temp=FisrtData(); while(temp不到结束) { temp->Draw(); temp=temp->NextData; } } virtual void Delete(); virtual void Move(); virtual bool Select(long x,long y); virtual Update(); }; class TDot : public TPaper { RPoint crood; void Draw() { 表示点。file://以crood.X,crood.Y画一个十字交叉 } void Delete(); void Move(); bool Select(long x,long y); Update(); } class TLine : public TPaper { RPoint start; RPoint end; void Draw() { (end.X,end.Y)画一条线。file://以(start.X,start.Y)到 } void Delete(); void Move(); bool Select(long x,long y); Update(); } class TCircle : public TPaper { RPoint center; MyLengthType radius; void Draw() { 径为radius画一个圆。file://以(center.X,center.Y)为圆心,半 } void Delete(); void Move(); bool Select(long x,long y); Update(); } ......
两段程序的区别:第一段,初期思路清晰,但,当增加画圆弧或者二次曲线等时,程序的代码将改动的地方多,凡需要区分点、线、圆、以及其他的类别时,必须通过switch语句检查当前应该显示的类型数据。这样导致增加功能或者变更画法操作时,许多地方都要更改。第二段,初期设计时较难,经过分析设计后,所写代码有很好的扩展性。增加一个圆弧,也仅仅只增加一个圆弧的类即可,而且许多操作都封装到一个类中,不会对外部程序体没有太大的影响。如果再增添一个注册机制,将新增内容注册到TPaper中,这样扩展功能更是易如反掌,并且还保证了系统的可靠性。 如果觉得此方法的类占用了太多的内存空间(虚拟函数在类实例中需要空间),可以建立一个基本结构: struct RDataBase { TPaper *type; RDataBase *nextData; void Add(RDataBase *); void Remove(RDataBase *); } struct RDot : public RDataBase{ MyDataType X,Y; } struct RLine : public RDataBase { RPoint start,end; } struct RCircle : public RDataBase { RPoint center; MyDataType radius; } 将图素的数据分别实现为一个结构,将各自图素类的一个实例指针置入一个TPaper *type指针变量中,Paper的Draw()实现为: virtual void Draw() { TPaper *temp; RDataBase *tempData;
tempData=FisrtData(); while(tempData不到结束) { temp=tempData->type; temp->Draw(); tempData=tempData->nextData; } } 经过一系列的问题抽象形成可确定的公式后,即便程序没有实现太多的功能,我们可以认为它已经完成了90%的工作。因为,所有需要增加的内容已经被机制化,只要按照这样一个明了的机制去实现圆弧、二次曲线、矩形框、多边形、双线等,不需要了解太多,其他开发者都可以很快加入它的工作。 其实,面向对象的设计方法就是一种机制的建立,不过,面向对象偏重于类型的建立,注重封装,而机制概念则不仅考虑类的建立,同时还考虑非类部分的建立,即使程序处理得非常没有结构化,它的机制同样存在。一个类封装的不是非常好但机制确实很不错的例子: 上篇文章提到过的通用编辑器,为了实现编辑器摸板化,我们尽量将编辑器的共性设计在内部,而且让扩展者独立创建编辑器能够识别的数据类(没有任何派生继承)。所以,采用了一系列的接口函数。其中,显示接口函数更具有机制性: struct RData { file://扩展者自己定义 long ID; file://除零外的所有整数,由 char *data; file://可以是文本,也可以是图形、表格或控件
TFont *font; } 当前需要显示的内容 file://void *GetWord() file://是一个接口函数,返回 file://long UserTextRect(text,X,Y) file://是接口函数,返回特显内容的宽度 ShowText(long &X,long &Y) file://显示一行的内容 { RData *text; text=(RData *)GetWord(); while(text!=NULL) { if(text->ID==0) 作为文本,内部显示 X+=MyTextOut(text,X,Y); //
else 作为特显,外部显示 X+=UserDataOut(text,X,Y); //
text=(RData *)GetWord(); } } 这样的机制,外部只需要在当前位置显示应该显示的内容,比如图片。(当然,仅仅这样的数据信息还不够,至少还要行高。但,这样的机制是合理存在的。其他的数据管理机制不详写了)。在这个例子中,并不要求外部的扩展者有基类的限制,他仅仅提供接口函数即可。
步骤二:根据步骤一的基本开发机制,要求开发者提供两份文档:程序说明文档和程序使用文档。程序说明和使用文档同时留给项目管理者,程序使用文档交给其他参与此部分程序的开发者。保证核心技术的保密同时提供可供扩展完善的机制。项目管理者还可以根据进度要求和资金实力,临时招聘新的开发者,经过短期的培训后,迅速加入开发行列,从而加快完成产品的进度。
步骤三:定期阅读程序使用文档,检查机制是否具有包容性、灵活性。检查具体办法:1)机制能够适应多少种情况,常见情况是否全包容,特殊情况考虑了多少?2)如果考虑了特殊情况,那么不存在特殊情况时机制是否有效、效率如何?3)目前没有考虑的情况,以后增加是否容易;如果不容易,是否简单调整机制后能够解决?4)机制的理解是否容易,它的要求是否很多或者很难控制。 经过步骤三的检查,可最大程度地避免了系统更改(或简化)后,对某些部分之间产生的逻辑矛盾。
步骤四:精益求精,机制的建立不能将就,必须经常性反思机制的机能合理程度,随时修改完善。特别是才开始创建机制时,对机制要求更加细腻周全。
下面一个CAD命令行处理机制的例子:
定义:命令行只执行文本命令和提示信息。菜单、工具条、按钮、热键等均向命令行发送命令和数据。命令行运作图:(以画圆为例)
画圆指令 命令行 等待输入命令 | 输入命令"c" | 画圆程序开始 〈=============== 调用相应的画圆程序 发送提示出选项 ================〉 等待选择选项 | 是圆心、半径圆 〈=============== 传送给画圆所选"r" 发送等待输入半径命令的项"r" =======〉等待输入半径 | 画圆 〈=============== 得到半径 画圆结束 ================〉 等待输入命令
交互式一问一答,将画圆的所需参数一一输入完后,画圆。命令行从最开始就是等待输入命令的状态,当输入命令后,命令行根据输入命令调用相应的命令执行程序;在执行程序中,如果需要更多的参数信息,将向命令行发送选项或需要输入的命令,控制权有交给了命令行;命令行等待选择或输入参数后,将所的结果返回到执行程序中,执行程序如果还需要信息,继续重复与命令行的交互传送工作直到可以执行程序命令为止;执行完命令后,又将控制权交给命令行;命令行回到等待输入命令。 通过命令行这样的操作,我们可以编写一些命令批处理文本,当在命令行中粘贴这些批处理文本后,命令行依次执行命令。命令行支持"/" 缺省输入的控制等高级扩展功能。 有了命令行的机制,实现其他图素画法变得如同文本操作一样简单。
总之,如果设计出良好得机制,可以说就获得有了可靠得核心程序。
梦郎