delphi bpl插件系統開發

一個插件系統需要什麼?

     一個最小的插件系統當然需要插件本身,調用插件的容器,最後需要契約.

     契約是什麼呢?契約就是兩個對象相互溝通的一個標準,這個標準應該統一,這樣容器才能和不同的插件通訊.我們可以使用接口來表述這個契約.例如

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.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章