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,之后,會在%delphi%\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;
??????? procedure Execute;????????????? //執行插件
上面是插件接口的定義(與上一節的,略有不同),這樣的定義具有通用性,我們定義的原則就是不能有任何特定于某個插件的東西.
?? 原來我是想使用這樣的架構思想來構建一個完全由插件構成的軟件,如同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
??? with TIniFile.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 ); //載入位于某目錄下的插件
??????? procedure UnloadPlugins;??????? //卸載所有裁入的插件接口
??? 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重復
??? if FPluginList.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
??? with TStringList.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單元是一個由delphi XmlBinding向導生成的單元,用來讀寫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;
??????? procedure Execute;????????????? //執行插件
??? 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.
總結
- 上一篇: 查询时要处理好各种异常情况
- 下一篇: 我们都有冲动了的飞鸽传书2011