前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >delphi bpl插件系统开发

delphi bpl插件系统开发

作者头像
Vaccae
发布2019-07-24 11:24:38
1.6K0
发布2019-07-24 11:24:38
举报
文章被收录于专栏:微卡智享微卡智享

一个插件系统需要什么?

一个最小的插件系统当然需要插件本身,调用插件的容器,最后需要契约.

契约是什么呢?契约就是两个对象相互沟通的一个标准,这个标准应该统一,这样容器才能和不同的插件通讯.我们可以使用接口来表述这个契约.例如

type IPlugin =interface ['{48BF4000-B028-4B57-9955-B1A8305DA394}'] procedure Execute; end;

容器,它可以配置加载哪些插件,并能调用插件的功能,并和插件交互数据,这种数据应该有统一性,因此我们的目标当然是需要和插件能够交互TObject,因为我们可以封装任何的数据在TObject中去,至于这个TObject中是些什么什么数据,只需要插件和容器知道就可以了.那么我们修改契约如下:

type IPlugin =interface ['{48BF4000-B028-4B57-9955-B1A8305DA394}'] function GetObject: TObject; procedure SetObject( value: TObject ); procedure Execute; end;

插件,我们使用实现了接口的一个bpl来构建插件,让容器动态载入一个bpl,然后访问其中的IPlugin来调用插件

我们构造一个容器,它动态的载入一个bpl,并且通过预定义的名称来访问其中的IPlugin,并调用IPlugin.Execute,这个预定义的名称其实是在bpl中实现了IPlugin的类的名称,这个类的名称我们可以通过修改bpl的名称或者同时发布一个配置文件来让容器获得.现在我们先暂时写死在程序里,毕竟这个问题是个小问题

构建插件

new->package生成一个package,就用'package1'的缺省名称,new->unit

unit TPluginImpl1;

interface

uses uIPlugin, dialogs, Classes;

type {$M+} TPlugin =class( TInterfacedPersistent, IPlugin ) function GetObject: TObject; procedure SetObject( value: TObject ); procedure Execute; private FMsg: string; public procedure AfterConstruction; override; end; {$M-} implementation

{ TPlugin }

procedure TPlugin.AfterConstruction; begin inherited; FMsg :='init String'; end;

procedure TPlugin.Execute; begin showmessage(FMsg ); end;

function TPlugin.GetObject: TObject; begin result :=TObject( FMsg ); end;

procedure TPlugin.SetObject( value: TObject ); begin FMsg :=string( Value ); end;

initialization RegisterClass( TPlugin );

finalization UnRegisterClass( TPlugin );

end.

TPlugin实现了IPlugin接口,并且注册了该组件,使它能够被容器访问到.

compile,之后,会在�lphi%\bpl目录生成package1.bpl.

构建容器

procedure TForm3.Button1Click( Sender: TObject ); var theClass : TPersistentClass; thePlugin : TPersistent; IPlug : IPlugin; FPackege : Cardinal; begin FPackege :=LoadPackage( 'package1.bpl' ); //加载包

theClass:= GetClass( 'TPlugin' ); //通过字符串获得类定义 if theClass= nil then begin ShowMessage( 'TPlugin not load' ); exit; end; thePlugin :=theClass.Create; //创建实例 Supports(thePlugin, StringToGUID('{48BF4000-B028-4B57-9955-B1A8305DA394}' ), IPlug); //转换成IPlugin接口 try IPlug.Execute; //执行插件的 finally IPlug := nil; end; UnloadPackage( FPackege); //卸载包 end;

project->options->package

点选build with runtime package

修改成vcl;rtl,确定

可以发布测试了

拷贝你的project1.exe,package1,windowSystem32目录下的vcl70.bpl,rtl70.bpl到一个目录,把他们拷贝到一个目录下,发布到一个没有delphi的机器上试试吧.

下一节让容器和插件交互数据

插件规范-----------插件必须实现一个接口,该接口通过GetObject,SetObject方法让容器和插件能够交互数据. IPlugin = interface ['{48BF4000-B028-4B57-9955-B1A8305DA394}'] function GetRunResult: TObject; //用于向容器返回执行Execute后的结果 //用于容器传如执行参数,通常会显示一个Form让用户输入,如果用户存入了 procedure SetRunParam; function GetInfo: TPluginInfo; //向容器返回插件的信息 { 用于容器调用配置插件的持久性配置, 通常会显示插件内的一个配置Form, 并可以将Form中的用户输入存入插件配置目录 } procedure EditConfig; procedureExecute; //执行插件

上面是插件接口的定义(与上一节的,略有不同),这样的定义具有通用性,我们定义的原则就是不能有任何特定于某个插件的东西.

原来我是想使用这样的架构思想来构建一个完全由插件构成的软件,如同eclipse,但是发现这样的想法有点空中楼阁的感觉,为什么这样说呢?我来举个例子:

我们设想这样的一个系统,它打开数据库,并打开一个表,修改记录并提交更新.这是一个数据库系统最基本的应用.

容器的工作大概情况是这样:

从Database.bpl得到一个adoConnection,

传入adoConnection参数给OpenQuery.bpl,并得到返回数据TClientDataset;

传入这个TClientDataset参数给ProcessData.bpl,它将数据载入界面并显示给用户,执行完毕后,容器会得到一个Delta封包,包含了用户所做的更新.

将该Delta封包参数和adoConnection参数传递给UpdateData.bpl,由它做数据库的更新.

传入adoConnection参数给OpenQuery.bpl,并得到返回数据TClientDataset;

传入这个TClientDataset参数给ProcessData.bpl,它将数据载入界面并显示给用户,执行完毕后,容器会得到一个Delta封包,包含了用户所做的更新.

将该Delta封包参数和adoConnection参数传递给UpdateData.bpl,由它做数据库的更新.

传入adoConnection参数给OpenQuery.bpl,并得到返回数据TClientDataset;

传入这个TClientDataset参数给ProcessData.bpl,它将数据载入界面并显示给用户,执行完毕后,容器会得到一个Delta封包,包含了用户所做的更新.

将该Delta封包参数和adoConnection参数传递给UpdateData.bpl,由它做数据库的更新.

传入adoConnection参数给OpenQuery.bpl,并得到返回数据TClientDataset;

传入这个TClientDataset参数给ProcessData.bpl,它将数据载入界面并显示给用户,执行完毕后,容器会得到一个Delta封包,包含了用户所做的更新.

将该Delta封包参数和adoConnection参数传递给UpdateData.bpl,由它做数据库的更新. 容器负责了整个工作的调度,它完全采用插件来完成每一步工作,我们可以实现不同的bpl来替换其中的相应角色,例如:

使用Database4SqlServer.bpl来提供对另一个数据库的访问(当然这可以使用不同的connectionString达到同样的效果,而且更简单,这里只是为了说明) 使用ProcessDataByRzLib.bpl来给用户呈现不同的界面控件 使用UpdateDataAndLog.bpl来更新数据,使在更新数据的同时写入日志 而我们的容器不需要做任何的更改,它只明白,需要4个不同的类可以完成工作,而各个角色如何来完成角色工作,他并不关心,它能驱动这些类,让系统运转起来.

这样的系统看起来已经很不错了,但是容器本身必须知道自己要干什么,必须知道如何组织载入的插件,以及它们的调用顺序,数据如何通过容器做为中转在插件之间交互.我们可不可以让容器也被什么东西来驱动起来呢?或者说容器的行为能够被配置起来,由外部来告知容器,这样容器本身就具有了可移植性.

有关面向接口编程

面向接口编程意味着系统中由一个管理程序,它组织许多的接口协调完成任务,它区别于旧式的系统在于被管理者是接口,而不是对象,这样的模式给了我们开发系统时松耦合的可能.但基于delphi的程序,我们可以对某个接口实现n个类,并在编译过程中确定由哪一个类来具体进行工作,这样的系统可以说扩展性很好了,举个例子来说,如果需要从外部文件读入信息,

传统方式:

function ReadConfig:string; begin withTIniFile.Create(someFile) do begin try result :=ReadString(aSection,aConfigName); finally Free; end; end; end;

当需要更改为从xml读取文件后,需要修改这个函数,或者重载这个函数,不可避免的所在单元的代码将不断扩大, 而使用面向接口方式将会这样来撰写

定义接口

IConfig = interface function GetConfig:string; end;

ini实现

TIniConfig = class(TInterfacedObject ,IConfig) function GetConfig:string; end; end;

调用者的代码将不再是:

ReadConfig;

而是

(TIniConfig.Create as IConifg).GetConfig;

当实现了

TXmlConfig = class(TInterfacedObject ,IConfig) function GetConfig:string; end; end;

那么调用者可以

(TXmlConfig.Create as IConifg).GetConfig;

这表示调用者可以使用不同的类来为自己提供服务,例如可以声明一个ITransaction,定义事务的3个方法,

那么,你可以有两个实现-----基于bde的实现和基于ado的实现,当你切换数据连接时将非常的方便.

然后这样的系统在架构上已经达到了我们的要求,唯一不太完美的是一旦有了切换,我们需要重新编译整个程序,分发....怎么解决它,我们需要一个可以动态载入到程序中的实现,并能配置容器告知容器我们切换了实现..

对的,在java下我们可以发布jar包,而jar包的类通过xxx.xxx.xxx方式保证了类的唯一性,java中各种框架的配置文件90%都有class="xxx.xxx.xxx"之类的声明,而Spring框架更是将这种插件的方式用到了一个可以说是理想的境界,这种机制叫做"依赖注入",而我们在delphi中该如何实现类似的应用(水平不够,不敢说相同的应用)

构思一下:

容器(即应用程序)完全按照面向接口编程 容器读入一个外部配置文件来确定每个接口的具体实现类的名称 载入bpl(bpl中注册了实现某接口的类,以让宿主程序可以访问到) 通过rtti(类似java的反射)创建类的实例 将该实例as 成接口,容器使用该实例完成工作. 当提供某个接口的不同实现时,发布bpl,更新容器配置文件,完成切换 这就是我想开发的插件系统,一个最花精力的事情就是容器到底需要哪些接口来完成一个应用.那么我们需要对现有的应用进行合理的分割,将可能出现变化的部分抽象成接口,将原有的代码实现在一个实现该接口的类中,设想一下一个完整的C/S结构的mis系统需要哪些接口来完成整个应用.

主程序

一个完全由接口驱动的程序,它调用各种接口完成软件的功能.(当然并不是绝对的,如果你的某个功能并不需要外部来提供的化)

插件s(注意,加了s复数形式)

放在同一目录下,一个完整的插件应该有两个同名文件,一个是含有实现某接口的bpl,一个是描述该插件功能的xml.

主程序启动时,将加载所有的插件,在运行过程中调用某个接口时,将会向一个PluginLoader请求该接口,该PluginLoader会返回一个插件变量给调用者,而它是使用在bpl中的类来完成该调用.

例子

下面给出一个bplLoader类的代码例子,它可以被你的主程序调用,就是插件管理类

{*******************************************************} {

插件容器类,用于载入插件

Change history:

} {*******************************************************}

unit uPluginLoader;

interface

uses codemyth.utils, codemyth.util.objectlist, uIPlugin,Xmlplugin, Classes, SysUtils;

type

TPluginLoader = class( TObject ) private FPluginList:TObjectList; //存储插件调用接口 function GetPlugin( const id: string ): IPlugin; function GetCount: integer; function GetPluginByIndex( const index: integer ): IPlugin; protected procedure UnloadPlugin( const id: string ); overload;//卸载指定的插件 procedure UnloadPlugin( const index: Integer );overload; //卸载指定的插件 procedure LoadPlugin( const XmlFile: string ); //载入位于某目录下的插件 procedureUnloadPlugins; //卸载所有裁入的插件接口 public constructor Create; destructor Destroy; override; public procedure LoadPlugins( Directory: string ); //载入插件 property Plugin [const id: string]: IPlugin read GetPlugin; property PluginByIndex [const index: integer]: IPlugin read GetPluginByIndex; property Count: integer read GetCount; end;

implementation

{ TPluginLoader }

constructor TPluginLoader.Create; begin FPluginList:= TObjectList.Create; end;

destructor TPluginLoader.Destroy; begin UnloadPlugins; FPluginList.Free; inherited; end;

function TPluginLoader.GetCount: integer; begin result :=FPluginList.Count; end;

function TPluginLoader.GetPlugin( const id: string ):IPlugin; var index : Integer; begin index :=FPluginList.IndexOfName( id ); Check( index>= 0, Format( '未找到%s插件.', [id] ) );

result :=GetPluginByIndex( index ); end;

function TPluginLoader.GetPluginByIndex( const index: integer ):IPlugin; begin Check( Index< FPluginList.Count, IntToStr( index ) + '超出范围 ,没有该索引.' );

result :=IPlugin(Pointer(FPluginList.Objects [index])); end;

procedure TPluginLoader.LoadPlugin( const XmlFile: string); var BplFile : string; XmlRoot : IXMLPluginType; ImplClass : TPersistentClass; obj : TPersistent; Intf : IPlugin; BplHandle : Cardinal; begin BplFile :=ChangeFileExt( XmlFile, '.bpl' ); XmlRoot :=Xmlplugin.Loadplugin( XmlFile );

//载入bpl BplHandle :=LoadPackage( BplFile );

//存入接口变量 ImplClass :=GetClass( XmlRoot.Class_ ); check(ImplClass <> nil, Format( '没有在%s中找到%s类.', [BplFile, XmlRoot.Class_] ) );

obj :=ImplClass.Create; Check(Supports( obj, StringToGUID( '{48BF4000-B028-4B57-9955-B1A8305DA394}' ), Intf), ImplClass.ClassName + '不支持插件接口IPlugin.' );

//存入plugin,不允许id重复 ifFPluginList.IndexOfName( XmlRoot.Id ) = -1 then begin FPluginList.AddObject( XmlRoot.Id + '=' + IntToStr( BplHandle) , Pointer(Intf) ); end; end;

procedure TPluginLoader.LoadPlugins( Directory: string ); var i : Integer; begin withTStringList.Create do begin try Text := GetFilesList( Directory, '.xml' ); for i := 0 to Count - 1 do if FileExists( ChangeFileExt( Strings , '.bpl' ) ) then LoadPlugin( Strings ); finally Free; end; end; end;

procedure TPluginLoader.UnloadPlugin( const id: string ); var index : Integer; begin index :=FPluginList.IndexOfName( id ); Check( index>= 0, Format( '未找到%s插件.', [id] ) );

UnloadPlugin( index ); end;

procedure TPluginLoader.UnloadPlugin( const index: Integer); begin

UnloadPackage( StrToInt( FPluginList.ValueFromIndex [index] ));

FPluginList.Delete( index ); end;

procedure TPluginLoader.UnloadPlugins; var i : integer; begin for i :=FPluginList.Count - 1 downto 0 do UnloadPlugin( i ); end;

end.

XmlConfig单元,XmlPlugin单元是一个由delphiXmlBinding向导生成的单元,用来读写plugin的xml配置文件

uIPlugin单元,是插件接口声明类

{*******************************************************} {

插件系统公用定义,容器和插件均应包含该单元定义

Change history:

} {*******************************************************}

unit uIPlugin;

interface

type

//插件信息体 TPluginInfo= record Id:string; //插件id ,与xml文件中一样 Name:string; //插件名称 Version:string; //插件版本 Description:string; //插件简介描述 Vendor: string; end;

//插件接口,开发之插件应实现该接口,容器使用该接口调用插件 { 容器调用的例子,得到IPlugin的实例thePlugin后 1.显示插件信息 ShowMessage(thePlugin.GetInfo.Name); 2.配置插件执行环境参数 thePlugin.EditConfig 3.执行插件 thePlugin.SetRunParam; thePlugin.Execute; thePlugin.GetRunResult; //处理插件执行结果 } IPlugin =interface ['{48BF4000-B028-4B57-9955-B1A8305DA394}'] function GetRunResult: TObject; //用于向容器返回执行Execute后的结果 //用于容器传如执行参数,通常会显示一个Form让用户输入,如果用户存入了 procedure SetRunParam; function GetInfo: TPluginInfo; //向容器返回插件的信息 { 用于容器调用配置插件的持久性配置, 通常会显示插件内的一个配置Form, 并可以将Form中的用户输入存入插件配置目录 } procedure EditConfig; procedureExecute; //执行插件 end;

implementation

end.

另两个codemyth开头的单元是我自己的函数包,其中codemyth.util.objectList声明了TObjectList类,它继承自TstringList类,但它可以自动销毁Objects中存储的对象实例而已.你可以用TstringList代替它,但你就需要自己释放TPluginList中的接口变量列表(虽然接口不需要释放,他通过引用计数来自释放

我们可以想像这样一个系统,与mvc的思想比较相同,controller负责整个系统的调度,当用户执行了某个action后,controller将其处理后用某个特定的view来呈现给用结果.这就是mvc 先看看这个图

这图是我用Together6.1画的,关心的设计思想,而不是代码本省,而且together本身也不支持pascal语法生成(不知道有没有插件)

其中的IMisDriver就是mvc中的TController,它负责协调整个系统,驱动系统工作起来.在delphi中它就是一个全局变量,任何单元都可以包含它,并访问它的功能,在IMisDriver内部,将会用到我们上一章说到的TPluginLoader来持有所有的服务接口

下面解释一下各个接口的作用,

ITracer,这是一个用来写入跟踪信息的接口,它仿照了一些log4j的思想.

ILogin,它用于登录的到系统,至于它后台使用的机制,当然要靠我们的实现来进行验证

IUserInfo:它返回当前登录用户的各种信息,

IAuthentic用于验证当前用户是否具有某个操作的权限.

IDataService用于提供数据服务,它可以从数据库中取得数据,并支持事务,

IShortcutDispather它用于将用户的快捷输入转化为某个操作

IActionManager用于管理用户动作和该动作应采用的处理数据的类的对应关系,

IProcessData用于处理给定的数据.

我们还可以看到IView和IReport从IProcessData继承下来,他们同样用来处理数据,只不过

IVew用于给用户呈现数据的crud界面

IReport用于给用户呈现报表

继承自IView的几个接口,用于对同一数据呈现不同的操作界面,我在另一个项目FormLib中基本实现了这些功能.

那么mvc的的通常操作的流程是什么样子呢?

上图没有包含一些全局的操作,例如ITracer等.

设计给了我们对软件更清晰的认识,3年后的今天,算是远远的看到了软件设计的大门.用周xx的话来说,这个世界前所未有的清晰.....

这样的设计为什么能够说有扩展性呢?,

整个系统靠IMisDriver驱动起来,它使用接口来完成工作,每一个接口,你可以使用不同的方法来实现,并发布它(bpl形式),就像你从pc上拔掉了一个优盘,插上了另一个优盘,你就可以看到故事的后半部分.

再举个实际的例子:你原先的权限验证需要去掉,现在不再需要权限,那么你可以实现一个总返回"允许操作"的IAuthentic,发布出去,系统的执行行为整个就改变了.

这导致的结果是:IMisDriver说我需要哪些接口,你只要提供了相应数量和类型的接口,他就可以按照预先设定的调度来完成整个系统.

那么如果整个系统的调度需要变化怎么办呢?这在软件设计中简直就是灾难,但是在这样的插件系统下,你只需要修改IMisDriver,或者重新设计一个IDriver来驱动其他的接口,这样的改变已经最大可能的保证了软件的价值.

如何规划好你的系统,这将是日后软件复用,重构的重要因素,

理论不知道说的够清楚没有,之后的工作,将是枯燥的代码编写了,

总结一下,

1.面向接口,提供给插件式系统中插件开发成为可能.

2.bpl机制,很大程度上把我们从把插件本地化的工作中逃离,它的机制在delphi中特有,基于delphi我们能做的可能只有通过它来的最方便了

3.系统的设计对于哪怕是一个简单的系统来说,能更好的帮助你对产品有着更全面的思想.一定要做,那怕只是花两个圆呢.

在这样的构想下,我们来做一个demos,

我们来定义如下的被IMisDriver驱动的接口,加入现在能想到的简单的应用,我们要作的工作如下

定义被驱动的接口 撰写IMisDriver的一个实现,用它来驱动各个接口,IMisDriver通过调用TPluginLoader的获得接口实例. 撰写每个接口的实现,并生成多个bpl 用TPluginLoader来载入这些插件, 主程序,实例化一个TPluginLoader,然后取得IMisLoader的实例,运行它. 我们先来完成第一步

unit InterfaceDefine;

interface

uses DBClient, midasLib, Types, classes;

type

ITracer =interface ['{623B3A22-15CE-4555-B470-C3F4EBEE7EB4}'] procedure info( const msg: string ); procedure error( const msg: string ); procedure debug( const msg: string ); end;

ILogin =interface ['{082F9C02-B504-4417-ACEB-1C9E3410ADED}'] procedure login( const user, pwd: string ); function loginByCookie( const user, pwd: string ): string; end;

IUserInfo= interface ['{4DE53541-6FC3-44C7-BA27-49B0827625F0}'] function information: TObject; end;

IAuthentic = interface ['{0E4BCF53-D685-4AC8-9C38-614117E59365}'] procedure valid( const actionId: string ); procedure config; end;

IDataService = interface ['{722CE946-1F59-4C67-A0EA-6655F1B1D961}'] procedure beginTrans; procedure commitTrans; procedure rollbackTrans;

function doSelectSql( const theSql: string ): TClientDataset; function doSelectValue( const theSql: string ): string; procedure doUpdateSql( const delta: string ); procedure doUpdateSqls( const deltas: TStringDynArray );

end;

IShortcutDispather = interface ['{A2C08C9E-5B56-4DC9-934B-323CAEC1FF49}'] function actionOf( const input: TShortCut ): string; procedure config; end;

IProcessData = interface ['{9368710D-7240-466A-8BCF-0D8B2FF0502D}'] function process( var theData: TClientDataSet ); end;

IActionManager = interface ['{0FEE643C-7610-4442-9EB7-5D21A433788A}'] function processerOf( action: string ): IProcessData;

end;

IView =interface( IProcessData ) ['{5F0000F8-7A9D-4824-915C-20A95A7B01F4}'] procedure View( var theData: TClientDataSet ); end;

IReport =interface( IProcessData ) ['{DD5A8AE6-37D9-4B9F-A2C9-9BEA9F217F90}'] procedure report( var theData: TClientDataSet ); end;

implementation

end.

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-03-21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 微卡智享 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档