1.7消除窗体的过度耦合
在上一节中,我们整理出一个颇具雏形的框架(Framework),但这个框架还有一些不太理想的地方。
1.7.1消除ChildFormDefine中对子类的依赖
首先我们先来看ChildFormDefine.pas的内容,
……
implementation
uses
MainForm;
……
if Action = caFree then
frmMain.RemoveMDIChild(self);
……
在上面的代码中,我们可以看到在TCustomChildForm父类窗体中居然要引用子类(TfrmMain)的实现,这是有违面向对象的设计思想的,一旦新工程的MDI主窗体不是frmMain的话,那么我们这个框架就会出错了。那我们怎样去改进呢?
在Delphi中,我们可以通过Application对象中的MainForm属性就可以获取应用程序的主窗体,但返回的主窗体类型是TForm的,但我们可以肯定这个主窗体一定是TCustomMainForm的一个子类。所以我们可以把代码改写成下面的形式:
……
implementation
uses
MainFormDefine;
……
if Action = caFree then
(Application.MainForm as TCustomMainForm).RemoveMDIChild(self);
……
这样,子窗体的父类就不再依赖于主窗体的子类了。
1.7.2消除MainForm中的子窗体引用
第二个不理想的地方在MainForm.pas中:
……
implementation
uses
ChildFormC, ChildFormD;
……
在这里我们可以看到,每当我们新增一个子窗体时,我们就必须修改MainForm中的内容,把这一新增的子窗体的单元名引入到上面的代码中,如果工程中有100个子窗体的话,MainForm中就会有100个子窗体的引用,看起来多恐怖啊!而且,当某一子窗体的文件要改名的话,那么MainForm中的内容也要接着修改;更麻烦的是,如果这些子窗体是由不同的开发人员各自开发的话,那么当这个工程要最后连编时,混乱的MainForm一定会带来不少的麻烦。那有什么好的方法去解决呢?
我们来看一下,为什么MainForm中要引用这些这窗体,原因在于在MainForm中要调用CallChildForm这一方法,在这方法中是使用了某个子窗体的类型的作为参数的,这样一来,就必须引用该子窗体的单元了。也就是说,如果CallChildForm不使用子窗体的类型作参数的话,那么就可以解决这个问题了。因此我们打算把CallChildForm改成是这种型式:
由CallChildForm(Sender, TfrmChildC); 改为 CallChildForm(Sender, 'TfrmChildC');
也就是把第二个参数由子窗体类型改为字符串类型,通过这样的修改,我们就不在需要引用某一子类了,为了达到这个从类名转变为实际类的效果,我们需要一个类工厂。我们在../FlexMDIFramework中新增一个单元文件:FormClassMgrUnit.pas
{
MDI Tutorial v1.7
Written by flexitime.
}
unit FormClassMgrUnit;
interface
uses
Forms, Windows, Classes, SysUtils;
type
TFormClass = class of TForm;
TFormClassItem = class
FormClass : TFormClass;
end;
TFormClassMgr = class
private
FList : TList;
public
constructor Create;
destructor Destroy; override;
function Items(index : integer) : TFormClassItem;
function Count : integer;
function GetItemByName(sFormClassName : string) : TFormClassItem;
function CreateForm(sFormClassName: string): TForm;
procedure Add(aFormClass : TFormClass);
end;
var
GFormClassMgr : TFormClassMgr;
implementation
uses
StrUtils;
{ TFormClassMgr }
procedure TFormClassMgr.Add(aFormClass: TFormClass);
var
ai : TFormClassItem;
begin
ai := TFormClassItem.Create;
ai.FormClass := aFormClass;
FList.Add(ai);
end;
function TFormClassMgr.Count: integer;
begin
result := FList.Count;
end;
constructor TFormClassMgr.Create;
begin
FList := TList.Create;
end;
function TFormClassMgr.CreateForm(sFormClassName: string): TForm;
var
ai : TFormClassItem;
begin
result := nil;
ai := GetItemByName(sFormClassName);
if ai = nil then exit;
result := TForm(ai.FormClass.NewInstance);
result.Create(Application.MainForm);
end;
destructor TFormClassMgr.Destroy;
var
i : integer;
begin
for i := 0 to Count - 1 do
begin
Items(i).Free;
end;
FList.Free;
inherited;
end;
function TFormClassMgr.GetItemByName(sFormClassName : string): TFormClassItem;
var
i : integer;
s1 : string;
begin
result := nil;
s1 := UpperCase(sFormClassName);
for i := 0 to Count - 1 do
begin
if (s1 = UpperCase(Items(i).FormClass.ClassName)) then
begin
result := Items(i);
exit;
end;
end;
end;
function TFormClassMgr.Items(index: integer): TFormClassItem;
begin
result := FList.Items[index];
end;
end.
在这个单元文件中,我们引入了一个全局的变量GFormClassMgr : TFormClassMgr; 这个变量引用的是一个子窗体的管理器,在使用这个管理器时,通过Add(aFormClass: TFormClass); 来把所有的子窗体类型都登记在这个管理器中,然后当需要创建某一个子窗体时,只要调用 CreateForm(sFormClassName: string): TForm; 这个方法就可以得到某一子窗体的实例了。代码比较简单,就不再详细说明了。
增加了上面的单元文件后,我们接着就要修改MainFormDefine.pas中的内容,增加一个新的CallChildForm方法,并且保留原来的方法,所以这里要使用重载。
……
function CallChildForm(Sender: TObject; AClass: TCustomChildFormClass): TCustomChildForm; overload;
function CallChildForm(Sender: TObject; AClassName: string): TCustomChildForm; overload;
……
function TCustomMainForm.CallChildForm(Sender: TObject;
AClassName: string): TCustomChildForm;
var
af : TForm;
begin
result := SearchMDIChild(Sender);
if result = nil then
begin
af := GFormClassMgr.CreateForm(AClassName);
if af is TCustomChildForm then
begin
result := af as TCustomChildForm;
AddMDIChildList(Sender, result);
end
else
result := nil;
end;
if result <> nil then
result.Show;
end;
在新的CallChildForm中,我们调用了GFormClassMgr.CreateForm(AClassName);这个方法来产生新的子类。
最后我们要修改的是MDI_Tutorial.dpr文件,
……
begin
GFormClassMgr := TFormClassMgr.Create;
GFormClassMgr.Add(TfrmChildC);
GFormClassMgr.Add(TfrmChildD);
Application.Initialize;
Application.CreateForm(TfrmMain, frmMain);
Application.Run;
GFormClassMgr.Free;
End.
我们在dpr文件中为GFormClassMgr进行初始化,并把所有的子类窗体都增加或者说是注册到GFormClassMgr中,只有进行这一步,才可以使得刚才的GFormClassMgr.CreateForm 代码能正常地工作。
把所有上面的工作完成后,我们就可以为MainForm.pas进行解耦了
……
var
frmMain: TfrmMain;
implementation
{$R *.dfm}
procedure TfrmMain.mnTestC1Click(Sender: TObject);
begin
inherited;
CallChildForm(Sender, 'TfrmChildC');
end;
procedure TfrmMain.mnTestD1Click(Sender: TObject);
begin
inherited;
CallChildForm(Sender, 'TfrmChildD');
end;
在这里,我们不再需要引用任何的子窗体文件了。
1.7.3进一步降低耦合度
我们完成了上一小节的工作后,整个框架的耦合度已经有了很大的改善。但我们回头来看一下最后修改的MDI_Tutorial.dpr文件,在这个文件里,我们把所有子类的增加(注册)都放到这里,这样就等于增加了这个文件与子窗体文件的耦合度了,虽然说这个MDI_Tutorial.dpr由于Delphi编译机制的原因,本来就必须引用这些子窗体文件的,所以这样的改变本来算不上什么,但如果我们每增加一个子窗体都必须修改这个dpr文件的话,这样也是非常不好的。所以我们把在dpr完成的注册过程交回给每个子窗体,让它们自己来完成注册这一步,这样就降低了耦合度了。那怎么去修改呢?我们来看ChildFormC.pas中的代码:
……
implementation
uses
FormClassMgrUnit;
……
initialization
GFormClassMgr.Add(TfrmChildC);
end.
我们在这个文件中,加入了FormClasMgrUnit的引用,然后在单元的结尾增加了一个初始化段(initialization),并在这个初始化段里面实现了类的注册。这里要说明一下initialization段,Delphi的编译器会在这个单元文件被第一次加载时就执行initialization段中的代码,因此我们把类的注册放到这里来了。
经过这样的修改,我们就不再需要dpr里面那些GFormClassMgr.Add()的代码了,但现在的程序能运行吗?答案是不行的,为什么呢?原因是当ChildFormC.pas中执行initialization中的内容时,GFormClassMgr还没有初始化!因此我们要接着修改FormClassMgrUnit.pas中的内容,在这个文件里面,我们也增加一个initialization段,
……
initialization
GFormClassMgr := TFormClassMgr.Create;
…..
最后要修改的是MDI_Tutorial.dpr文件中的uses顺序,我们要保证GFormClassMgr要在所有的子窗体注册前完成初始化。所以调整一下uses的顺序就行了。
在第一章中,我们通过一步一步的学习和提炼,完成了一个MDI程序的基本框架。通过这个框架,我们可以很简单的完成子窗体的创建、显示和关闭功能,同时也建立一个良好的读取、保存、退出提示的机制。在最后一节中,我们引用了类工厂的方式来为我们的框架解耦,这一工作从目前来看,好像并不能为我们带来直接的收益。但只有经过这样的解耦,我们才可能在后续章节中讨论如何动态加载DLL,BPL中的窗体。