Delphi制作客户端系统功能组件分离的架构设计
一. 目标:
a) 减少客户端exe程序的长度
b) 更好的组织源代码项目文件夹的结构
c) 使用基于接口的方式编程,减少模块之间的耦合度
d) 物理分离不同的功能组件
二. 实现方式:
a) 三类子项目:
l 主窗体和主框架项目
l 全局资源包项目(package BPL项目)
l 功能组件项目(com dll组件)
b) 主项目是普通的窗体项目或者MDI项目
l 主项目是整个项目的主框架,负责组织所有的功能组件
l 负责验证用户身份和初始化全局资源(如全局网络连接)
l 利用菜单或者子窗体调用功能组件
l 将“全局资源包”项目中的全局数据模块和全局变量单元增加到本项目中
c) 全局资源包bpl项目。
l 本项目封装全局使用的数据资源。
1. 本项目生成和安装后,其他项目将本项目产生的bpl包编译到里面
2. 主项目和功能组件项目都将本项目的包编译进去后,运行时本项目的所有变量均有一个唯一实例
3. 利用这一点可以共享全局数据以及不同项目之间进行数据交流
l 包含全局使用的数据模块
l 也可以放置全局变量。
d) 功能组件子项目是activeX Library项目
新建activeXLibrary项目,增加一个automachine object控件
l 令新接口继承IPlug接口
1. IPlug是插件的通用接口,所有功能组件都要支持这个接口
2. 本接口支持getname函数和run函数
3. getname函数用于返回插件名字
4. run函数用来运行插件(如果插件是个独立的子系统的话)
l 子功能组件在新接口中增加函数,对外提供子功能
l 将“全局资源包”项目中的全局数据模块和全局变量单元增加到本项目中
三. 缺点和需要注意的问题:
本方案只是利用部分OO思想主要解决在客户端桌面程序上的物理分离问题,并不是纯粹用OO的方法做系统分析和设计。
特别是各个功能模块(COM组件)访问全局的数据模块,是通过delphi的共享包(bpl)来解决的。利用这种方式,com组件可以直接在编程中访问数据模块单元。(如果将数据模块作为单独的一个dll或者com,复杂度太大而且面临无法随时修改的问题。所以采用了这种方式。)
但是这样一来不同的功能组件都依赖于这个共享包,增加了功能组件耦合度。所以最好在每个功能组件项目中增加自己的数据模块,存放本模块专用的数据库资源。在全局数据模块中只保存全局链接、通用的查询组件(例如登陆和权限)等。尽量减少功能组件对全局数据模块的依赖。
可以在全局数据包项目中增加一个单元,将通用的业务逻辑操作封装在内,供所有项目调用;
可以在每个功能组件项目中增加业务逻辑操作单元,以便减少业务逻辑功能代码对界面的依赖性;
客户端和com组件的参数传递:
com只能使用标准的数据类型,对于编程中其他类型只能通过间接的类型转换来实现
1)对于某些控件对象(Tform类型和TwinContrl类型等)delphi数据类型,可以使用int类型与指针类型进行类型转换来实现,(必须在同一个进程内,否则指针无法这个能取映射),例如下面的实现:
接口方法: |
ITLogin = interface(IPlug) ['{06B82963-EE63-49B6-8989-8D3021928447}'] procedure ShowSub(POwner: SYSUINT; ParentHand: SYSUINT); safecall; // POwner在com接口中参数类型是整形,在实现中当作指针来用 // ParentHand在com接口中参数类型是整形,在实现中当作HWND类型来用 |
Com的接口实现: |
procedure TTLogin.ShowSub(POwner: SYSUINT; ParentHand: SYSUINT); type PComp=^TComponent; begin if SubForm=nil then SubForm:= TSubForm.Create(PComp(POwner)^); //利用强制类型转换,先还原成^Tcomponent的指针类型,然后解除指针参照 subform.ParentWindow:=ParentHand;//直接赋值,隐式类型转换 subform.Show; end; |
客户端 |
var loginCom:ITLogin; begin if loginCom=nil then loginCom:=CreateComObject(CLASS_TLogin) as ITLogin; LoginCom.ShowSub(cardinal(@self),self.Panel1.Handle); // cardinal(@self) 先取地址,再显式类型转换 // self.Panel1.Handle 隐式类型转换 End; |
2)对于不同进程空间内的转换,就不能简单的像上面一样利用指针进行数据传递了。对于“记录”类型:对于下面一种方法,应该小心使用:
记录类型的类型转换,利用Move和SizeOf |
装入Variant: MyQuery: Record; V:Variant; P:Pointer;
V:=VarArrayCreate([0,Sizeof(Record)],varByte); P:=VarArrayLock(V); Move(MyQuery,P^,Sizeof(Record)); VarArrayUnLock(V);
取出 V:variant: var P:Pointer; MyQuery: Record; begin P:=VarArrayLock(V); Move(P^,MyQuery,Sizeof(Record)); VarArrayUnLock(V); |
四. 项目文件夹结构
项目文件夹 |
Bin |
Tmp |
Frame |
class |
Interface |
CommDataPk |
Com组件1 |
Com组件2 |
编译后的可执行文件以及dll文件 |
编译产生的dcu等中间文件 |
主程序框架 |
项目所需要的公共工具类 |
项目所需要的公共接口单元(例如IPlug) |
公共数据模块和公共数据 bpl项目 |
功能组件 子系统 |
2
五. 日后的改进和演化:
增加主框架对子功能组件的组织能力(例如快捷键、主菜单、com组件的载入器);
增加服务器的功能,配合子功能做一些编码,将单纯的数据访问演化为逻辑中间件;