前言 大多数Delphi程序员都像使用Visual Basic 那样使用他们手头上开发工具而丝毫没有意识到Delphi的强大功能更谈不上使用这些功能了(写到这里编辑惶恐的举起了手怎么可能呢?)Delphi和Visual Basic不同Delphi完全建立在面向对象结构上这不仅影响到VCL的结构而且影响到使用Delphi开发的每一个程序
在本文中我不想涉及到面向对象编程(OOP)的所有理论只是提出一些简单的经验规则希望这些规则能够帮助改善你的程序结构无论你开发的是何种类型的程序这些经验规则都是适用的你应当把他们当作一些建议记住他们并把他们应用到你开发的程序中去
关于面向对象编程我想强调的一个关键原理是封装我们都希望创建一些灵活而且强健的类因为这样的类允许我们以后修改他们的实现方法而不影响到程序中的其他部分这正是封装给我们带来的好处虽然封装不是创建一个好的面向对象程序的唯一标准但是它构成了面向对象编程的基础所以在本文中我也许会过多的强调封装性请不要感到奇怪我有足够充分的理由这么做
最后我想说明这样一个事实本文将主要集中说明窗体(Forms)的开发(虽然其中的一些规则对于组件的开发同样适用)因此这些规则对于所有的Delphi程序员都是适用的那些编写组件的程序员必须把面向对象编程和类(Class)作为核心的元素但是对于那些使用组件编程的程序员他们时常会忘记面向对象对于他们本文可以当作一个提示提醒他们始终记住面向对象编程
第一部分窗体是类(A Form is A Class)(rule rule )
程序员常常将窗体看作是对象而事实上窗体是类两者的差别在于你创建基于相同的窗体类的多个窗体对象令人感到疑惑的是Delphi为你定义的每一个窗体类创建了一个默认的全局对象这对于新手来说是相当方便的但是这同样会使他们形成坏习惯
第二部分继承(Inheritance)(rule rule )
在讲述了一系列关于类特别是关于窗体类的规则后第二部分将是一些关于类的继承性以及可视化窗体继承的建议和技巧
关于代码
如果你想使用这些代码请注意构造器必要的初始化设置以及私有组件参照同时有必要设置好窗体的OldCreateOrder属性否则带有组件的窗体构造器的初始化代码将在窗体的OnCreate事件之前得到执行
规则一为每一个类创建一个单元(One ClassOne Unit)
请始终牢记这一点类的私有(private)和保护(protected)的部分只对于其他单元中的类和过程(procedure)才是隐藏的因此如果你想得到有效的封装性你应该为每一个类使用一个不同的单元对于一些简单的类比如那些继承其他类的类你可以使用一个共享的单元不过共享同一个单元的类的数目是受到限制的不要在一个简单的单元里放置超过个复杂的类虽然Borland公司的VCL代码曾经这样做过
如果你使用窗体的时候Delphi会默认的遵循一个类使用一个单元的规则这对于程序员来说也是十分方便的当你向你的项目中添加一个没有窗体的类时Delphi也会创建一个新的独立的单元
规则二为组件命名(Name Components)
为每一个窗体和单元给出一个有意义的名字是十分重要的窗体和单元的名字必须是不同的不过我趋向于为他们两者使用相似的名字如对于关于窗体和单元可以为他们使用AboutForm 和Aboutpas
为组件使用带有描述性的名字同样十分重要最常见的命名方式是使用类的小写字母开头再加上组件的功能如BtnAdd 或者editName采用这样的命名方式为组件命名可能会有很多相似的名字而且也没有一个最好的名字到底应该选择那一个应该依据你的个人爱好而定
规则三为事件命名(Name Events)
对于事件处理方法给出合适的名字更加重要如果你对于组件给出了一个合适的名字那么系统默认的名字ButtonClick将变成BtnAddClick虽然从这个名字中我们可以猜到这个事件处理程序的功能但是我认为使用一个能够描述该方法的作用的名字而不是采用Delphi附加的名字是一种更好的方式例如BtnAdd按钮的OnClick事件可以命名成AddToList这会使得你的程序可读性更强特别是当你在这个类的其他方法中调用这个事件处理程序时而且这会帮助程序员为类似的事件或是不同的组件选用相同的方法不过我必须声明使用动作(Actions)是目前开发重要的程序时我最喜欢的方法
规则四使用窗体方法(Use Form Methods)
窗体都是一些类因此窗体的代码是以方法组织的你可以向窗体中添加事件处理程序这些处理程序完成一些特别的功能而且他们能被其他方法调用除了事件处理方法外你还可以向窗体添加完成动作的特别定义的方法以及访问窗体状态的方法在窗体中添加一些公共的(Public)方法供其他窗体调用要比其他窗体直接操作他的组件要好
规则添加窗体构造器(Add Form Constructors)
在运行时创建的第二个窗体除了一个默认的构造器(从Tcomponent 类继承而来)外还会提供其他特殊的构造器如果你不需要考虑和Delphi以前的版本的兼容性问题我建议你重载(Overload)Create方法添加必要的初始化参数具体代码可参见下面的代码:
Public
Constructor Create(Text:string): reintroduce ; overload;
Constructor TformDialogCreate(Text:string);
Begin
Inherited Create(Application);
EditText:=Text;
End;
规则避免全局变量(Avoid Global Variables)
应该避免使用全局变量(就是那些在单元的interface 部分定义的变量)下面将会有一些建议帮助你如何去做
如果你需要为窗体存储额外的数据你可以向窗体类中添加一些私有数据这种情况下每一个窗体实例都会有自己的数据副本你可以使用单元变量(在单元的implementation部分定义的变量)声明那些供窗体类的多个实例共享的数据
如果你需要在不同类型的窗体之间共享数据你可以把他们定义在主窗体里来实现共享或者使用一个全局变量使用方法或者是属性来获得数据
规则永远不要在Tform类中使用Form(Never Use Form in Tform)
你应该避免在类的方法中使用一个特定的对象名称换句话说你不应该在TForm类的方法中直接使用Form如果你确实需要使用当前的对象你可以使用Self关键字请牢记大多数时候你都没有必要直接使用当前对象的方法和数据
如果你不遵循这条规则当你为一个窗体类创建多个实例的时候你会陷入麻烦当中
规则尽量避免在其他的窗体中使用Form(Seldom Use Form In Other Forms )
即使在其他窗体的代码中你也应该尽量避免直接使用全局变量如Form定义一些局部变量或者私有域供其他窗体使用会比直接调用全局变量要好
例如程序的主窗体能够为对话框定义一个私有域很显然如果你计划为一个派生窗体创建多个实例这条规则将是十分有用你可以在主窗体的代码范围内保持一份清单也可以更简单地使用全局Sreen对象的窗体数组
规则移除Form(Remove Form)
事实上我的建议是在你的程序中移除Delphi自动创建的全局窗体对象即使你禁止了窗体的自动添加功能这也有可能是必要的因为在Delphi随后仍然可能添加这样的窗体我给你的建议是应该尽量避免使用全局窗体对象
我认为对于Delphi新手而言移除全局窗体对象是十分有用的这样他们不至于对类和全局对象两者的关系感到疑惑事实上在全局窗体对象被移除后所有与它有关的代码都会产生错误
规则添加窗体属性(Add Form Properties)
正如我已经提到过的当你需要为你的窗体添加数据时请添加一个私有域如果你需要访问其他类的数据可以为你的窗体添加属性使用这种方法你就能够改变当前窗体的代码和数据(包含在它的用户界面中)而不必改变其他窗体或类的代码
你还应该使用属性或是方法来初始化派生窗体或是对话框或是访问他们的最终状态正如我前文所说的你应该使用构造器来完成初始化工作
规则显示组件属性(Expose Components Properties)
当你需要访问其他窗体的状态时你不应该直接访问它的组件因为这样会将其他窗体或其它类的代码和用户界面结合在一起而用户界面往往是一个应用程序中最容易发生改变的部分最好的方法是为你需要访问的组件属性定义一个窗体属性要实现这一点可以通过读取组件状态的Get方法和设置组件状态的Set方法实现
假如你现在需要改变用户界面用另外一个组件替换现有的组件那么你只需做的是修改与这个组件属性相关的Get方法和Set方法而不必查找修改所有引用这个组件的窗体和类的源码详细实现方法请参见下面的代码
private
function GetText:String;
procedure SetText(const Value:String);
public
property Text:String;
read GetText write SetText;
function TformDialogGetText:String;
begin
Result:=EditText;
end;
procedure TformDialogSetText(const Value:String);
begin
EditText;=Value;
end;
规则属性数组(Array Properties)
如果你需要处理窗体中的一系列变量你可以定义一个属性数组如果这些变量是一些对于窗体很重要的信息你还可以把他们定义成窗体默认的属性数组这样你就可以直接使用SpecialForm[]来访问他们的值了
下面的代码显示了如何将一个listbox组件的项目定义成窗体默认的属性数组
type
TformDialog =class(TForm)
private
listItems:TlistBox;
function GetItems(Index:Integer):String;
procedure SetItems(Index:Integer:const Value:String);
public
property Items[Index:Integer]:string;
end;
function TFormDialogGetItems(Index:Integer):string;
begin
if Index >=ListItemsItemsCount then
raise ExceptionCreate(TformDialog:Out of Range);
Result:=ListItemsItems[Index];
end;
procedure TformDialogSetItems(Index:Integer;const alue:string);
begin
if Index >=ListItemsItemsCount then
raise ExceptionCreate(TformDialog:Out of Range);
ListItemsItems[Index]:=Value;
end;
规则使用属性的附加作用(Use SideEffects In Properties)
请记住使用属性而不是访问全局变量(参见规则)的好处之一就是当你设置或者读取属性的值时你还可能有意想不到的收获
例如你可以直接在窗体界面上拖拉组件设置多个属性的值调用特殊方法立即改变多个组件的状态或者撤销一个事件(如果需要的话)等等
规则隐藏组件(Hide Components)
我经常听见那些面向对象编程的狂热追求者抱怨Delphi窗体中包含一些在published部分声明的组件这是和面向对象思想的封装性原理不相符合的他们确实提出了一个重要的议题但是他们中的大多数人都没有意识到解决方法其实就在他们手边完全不用重写Delphi代码也不用转向其他语言
Delphi向窗体中添加的组件参照可以被移到private部分使得其他窗体不能访问他们如果你这样做你就有必要设置一些指向组件的窗体属性(请参见规则)并且使用它们来访问组件的状态
Delphi将所有的这些组件都放在published部分这是因为使用这种方式能够保证这些域一定是在DFM文件中创建的组件当你改变一个组件的名称时VCL能够自动地将这个组件对象与它在窗体中的参照关联起来因为delphi使用RTTI和Tobject方法来实现这种功能所以如果想要使用这种自动实现功能就必须把参照放置在published部分(这也正是为什么delphi将所有的组件都放在published部分的缘故)
如果你想知道的更详细一点可以参看下面的代码
procedure TcomponentSetReference(Enable:Boolean);
var
Field:^Tcomponent;
begin
If Fowner<> nil then begin
Field:=FownerFieldAddress(Fname);
If Field<>nil then
Field^:=Self
else
Field^:=nil;
end;
end;
上面的代码是Tcomponent类的SetReference方法这个方法可以被InserComponentRemoveComponent和SetName等方法调用
当你理解了这一点后你应该不难想到如果你将组件参照从published部分移到了private段你将失去VCL的自动关联功能为了解决这个问题你可以通过在窗体的OnCreate事件中添加如下代码解决
Edit:=FindComponent(Edit) as Tedit;
你接下来应该做的就是在系统中注册这些组件类当你为他们注册过后就能使RTTI包含在编译程序中并且能够被系统所使用当你将这些类型的组件参照移到private部分时对于每一个组件类你只需为他们注册一次即使为他们注册不是一定必要的时候你也可以这样做因为对于RegisterClasses的额外调用有益无害通常你应该在单元中负责生成窗体的初始化部分添加以下的代码
RegisterClass([TEdit]);
规则面向对象编程的窗体向导(The OOP Form Wizard)
为每一个窗体的每一个组件重复上述两个操作不仅十分的烦人而且相当的浪费时间为了避免额外的负担我已经为此写了一个简单的向导程序这个程序将会生成一些可以完成以上两步工作的代码你需要做的仅仅是做几次复制和粘贴就行了
遗憾的是这个向导程序不能自动将代码放置到单元中合适的地方我目前正在修改这个向导程序希望能实现这个功能
规则可视化窗体继承(Visual Form Inheritance)
如果应用得当这将是一个强大的工具根据我的经验你所开发的项目越大越能体现它的价值在一个复杂的程序中你可以使用窗体的不同等级关系来处理一组相关窗体的多态性(polymorphism)
可视化窗体继承允许你共享多个窗体的一些公共的动作你可以使用共享的方法公用的属性甚至是事件处理程序组件组件属性组件事件处理方法等等
规则限制保护域数据的使用(Limit Protected Data) 当创建一些具有不同分级体系的类时一些程序员趋向于主要使用保护域因为私有数据不能被子类访问我不能说这没有其合理性但是这肯定是和封装性不相容和的保护数据的实现能够被所有继承的窗体所共享而且一旦这些数据的原始定义发生改变你必须更改所有的相关部分
请注意如果你遵循隐藏组件这样一条规则(Rule )继承窗体就不可能访问基类的私有组件在一个继承窗体中类似EditText=的代码就不会被编译虽然这是相当的不方便但是至少在理论上这是值得肯定的事情而不是否定的如果你感觉到实现封装性是最主要最需要的就请将这些组件参照放在基类的私有段
规则保护域中的访问方法(Protected Access Methods)
在基类中将组件参照放置在私有域中而为这些组件添加一些访问函数来得到他们的属性这将是一种更好的方法如果这些访问函数仅仅在这些类内部使用而且不是类接口的一部分你应该在保护域声明他们例如Rule 中描述过的GetText和SetText方法就可以声明成protected并且我们可以通过调用SetText()来编辑文本
事实上当一个方法被镜像到一个属性时我们可以简单地采用如下代码就可以达到编辑文本的目的Text:=;
规则保护域中的虚拟方法(Protected Virtual Methods)
实现一个灵活的分级制度的另一个关键点是定义一些你可以从外部类调用的虚拟方法来得到多态性如果这个方法使用得当将会很少出现其他公共的方法调用保护域中的虚拟方法的情况这是一个重要的技巧因为你可以定制派生类的虚拟方法来修改对象的动作
规则用于属性的虚拟方法(Virtual Methods For Properties)
即使是访问属性的方法也能定义成virtual这样派生类就能改变属性的动作而不必重新定义他们虽然这种方法在VCL中很少使用但是它确实十分灵活强大为了实现这一点仅仅需要将Rule 当中的Get 和Set 方法定义成Virtual基类的代码如下所示
type
TformDialog = class ( TForm)
Procedure FormCreate(Sender:Tobject);
Private
Edit:Tedit;
Protected
function GetText:String;virtual;
procedure SetText(const Value:String);virtual;
public
constructor Create(Text :String):reintroduce;overload;
property Text:String read GetText write SetText;
end;
在继承窗体中你可以添加一些额外的动作来重载虚拟方法SetText
procedure TformInheritSetText(const Value:String);
begin
inherited SetText(Value);
if Value= then
ButtonEnabled:=False;
end;
小结
要做到一个好的Delphi面向对象编程程序员远非我在上面提到的这些规则这么简单上面的这条规则中有一些可能需要足够的耐性和时间来实现因此我也不能强求你能遵循所有的这些规则但是这些规则应该被合适的运用到你的程序中而且当你开发的应用程序越大参与的程序员越多这些规则越重要不过即使是一些小程序始终记住这些规则并在合适的地方使用他们也会对你有所帮助
当然还有很多其他的经验规则我没有涉及到特别是存储器处理和RTTI问题因为他们十分的复杂需要专门的说明
我的结论是要遵循我上面列出的规则会付出一定的代价特别是额外的代码但是这些代价会让你得到一个更加灵活强壮的程序希望Delphi的后续版本能够帮助我们减少这些代价