一個插件系統需要什麼?
一個最小的插件系統當然需要插件本身,調用插件的容器,最後需要契約.
契約是什麼呢?契約就是兩個對象相互溝通的一個標準,這個標準應該統一,這樣容器才能和不同的插件通訊.我們可以使用接口來表述這個契約.例如
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來調用插件
coding吧
我們構造一個容器,它動態的載入一個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中的類來完成該調用.
over.
下面給出一個bplLoader類的代碼例子,它可以被你的主程序調用,就是插件管理類
{*******************************************************}
{
codemyth.Group
copyright 2004-2005
codemyth(at)gmail(dot)com
Create at 2005-7-20 11:22:26
插件容器類,用於載入插件
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單元,是插件接口聲明類
{*******************************************************}
{
codemyth.Group
copyright 2004-2005
codemyth(at)gmail(dot)com
Create at 2005-7-20 10:22:47
插件系統公用定義,容器和插件均應包含該單元定義
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.