一、引言
1. 問(wèn)題的引入
假設你設計的程序已經(jīng)部署到用戶(hù)的計算機上,并且能夠正常運行了。但是有一天,用戶(hù)打來(lái)了電話(huà)——他們要求增加新的功能。確定了用戶(hù)的需求后,你竟然發(fā)現原有的軟件架構已經(jīng)無(wú)法勝任新增任務(wù)的需求——你需要重新設計這個(gè)應用了!但問(wèn)題是,就算你又用了一個(gè)開(kāi)發(fā)周期完成了用戶(hù)需要的應用,卻不能保證用戶(hù)的需求不會(huì )再次變更。也就是說(shuō),需求蔓延的可能性依然存在。因此,這種情況下插件構架更能顯示出它的優(yōu)越性。
2. 幾個(gè)解決方案的對比
我總結了一下我所接觸到的插件構架,大致上可分為以下幾類(lèi):
| i> 腳本式 使用某種語(yǔ)言把插件的程序邏輯寫(xiě)成腳本代碼。而這種語(yǔ)言可以是 Python ,或是其他現存的已經(jīng)經(jīng)過(guò)用戶(hù)長(cháng)時(shí)間考驗的腳本語(yǔ)言。甚至,你可以自行設計一種腳本語(yǔ)言來(lái)配合你程序的特殊需要。當然,用當今最流行的 XML 是再合適不過(guò)了。 這種形式的特點(diǎn)在于,稍有點(diǎn)編程知識的用戶(hù)就可以自行修改你的腳本( ^_^ 假如你不加密它的話(huà))。我們無(wú)法論證這是好處還是壞處。因為,這種情況所造成的后果是不可預知的。 ii> 動(dòng)態(tài)函數庫 DLL 插件功能以動(dòng)態(tài)庫函數的形式存在。主程序通過(guò)某種渠道(插件編寫(xiě)者或某些工具)獲得插件 DLL 中的函數簽名,然后在合適的地方調用它們。用過(guò) Matlab 的讀者都知道, Matlab 中的各項功能幾乎都是些動(dòng)態(tài)鏈入的函數。 iii> 聚合式 顧名思義,就是把插件功能直接寫(xiě)成 EXE 。主程序除了完成自己的職責外,還負責調度這些“插件”。我不喜歡這種形式。這使插件與插件之間,主程序與插件之間(主要是這一點(diǎn))的信息交流困難了許多。巴比倫塔的失敗 [1] 從某種程度上講就是信息交流無(wú)法實(shí)現造成的。 iv> COM 組件 COM [2] 的產(chǎn)生給這個(gè)世界增添了幾分活力。只有接口!我們的插件需要做的只是實(shí)現程序定義的接口。主程序不需要知道插件怎樣實(shí)現預定的功能,它只需要通過(guò)接口訪(fǎng)問(wèn)插件,并提供主程序相關(guān)對象的接口。這樣一來(lái),主程序與各插件之間的信息交流就變得異常簡(jiǎn)單。并且,插件對于主程序來(lái)說(shuō)是完全透明的。 |
3. 決策
C# 是面向對象的程序設計語(yǔ)言。它提供了 interface 關(guān)鍵字來(lái)直接定義接口。同時(shí), System.Reflection 命名空間也提供了訪(fǎng)問(wèn)外部程序集的一系列相關(guān)對象。這就為我們在 C# 中實(shí)現插件構架打下了堅實(shí)的基礎。
下面,我們將以一個(gè)具有插件構架的程序編輯器為例,來(lái)闡述這種構架在 C# 中的實(shí)現。
二、設計過(guò)程
好了,現在我們準備把所有的核心代碼都放在 CSPluginKernel 命名空間中。用VSIDE建立一個(gè)C#類(lèi)庫工程。在命名空間 CSPluginKernel 中開(kāi)始我們的代碼。
1. 接口設計
我們的程序編輯器會(huì )向插件開(kāi)放正在編輯的文檔對象。程序啟動(dòng)后,就枚舉每一個(gè)插件并把它連接到主程序,同時(shí)傳遞主程序對象的接口。插件可以通過(guò)這個(gè)接口來(lái)請求主程序對象或訪(fǎng)問(wèn)主程序功能 。
根據上面的需求,我們首先需要一個(gè)主程序接口:
| public interface IApplicationObject { void Alert( string msg ); // 產(chǎn)生一條信息 void ShowInStatusBar( string msg ); // 將指定的信息顯示在狀態(tài)欄 IDocumentObject QueryCurrentDocument(); // 獲取當前使用的文檔對象 IDocumentObject[] QueryDocuments(); // 獲取所有的文檔對象 // 設置事件處理器 void SetDelegate( Delegates whichOne , EventHandler targer ); } // 目前只需要這一個(gè)事件 public enum Delegates { Delegate_ActiveDocumentChanged , } |
然后是 IDocumentObject 接口。插件通過(guò)這個(gè)接口訪(fǎng)問(wèn)編輯器對象。
| /// /// 編輯器對象必須實(shí)現這個(gè)接口 /// public interface IDocumentObject { // 這些屬性是 RichTextBox 控件的相應的屬性映射 string SelectionText { get ; set ; } Color SelectionColor { get ; set ; } Font SelectionFont { get ; set ; } int SelectionStart { get ; set ; } int SelectionLength { get ; set ; } string SelectionRTF { get ; set ; }
bool HasChanges { get ; }
void Select( int start , int length ); void AppendText( string str );
void SaveFile( string fileName ); void SaveFile(); void OpenFile( string fileName ); void CloseFile(); } |
這個(gè)接口不需要過(guò)多解釋。這里我只實(shí)現了RichTextBox控件少數的幾個(gè)方法,其他可能用得到的,讀者自行添加即可。
再然后,根據插件在其生命周期里的行為,設計插件的接口。
| /// /// 本程序的插件必須實(shí)現這個(gè)接口 /// public interface IPlugin { ConnectionResult Connect( IApplicationObject app ); void OnDestory(); void OnLoad(); void Run(); }
/// /// 表示插件與主程序連接的結果 /// public enum ConnectionResult { Connection_Success , Connection_Failed } |
主程序會(huì )首先調用 Connect() 方法,并傳遞 IApplicationObject 給插件。插件在這個(gè)過(guò)程中做一些初始化工作。然后,插件的 OnLoad() 方法被調用。在這之后,當主程序接收到調用插件的信號時(shí)(鍵盤(pán)、鼠標響應)就會(huì )調用插件的 Run() 方法來(lái)啟動(dòng)這個(gè)插件。程序結束時(shí),調用其 OnDestory() 方法。這樣,插件的生命才宣告結束。
2. 插件信息的存儲與獲取
一個(gè)插件需要有它的名稱(chēng) 、版本等信息。作為設計者的你,也一定要留下你的尊姓大名和個(gè)人網(wǎng)站等用來(lái)宣傳自己。 C# 的新特性——屬性, 就是一個(gè)很好的解決方案。因此我們定義一個(gè)從 System.Attribute 繼承來(lái)的類(lèi) PluginInfoArrtibute :
| /// /// 用來(lái)指定一個(gè)插件的相關(guān)信息 /// public class PluginInfoAttribute : System.Attribute { /// /// Deprecated. Do not use. /// public PluginInfoAttribute() {} public PluginInfoAttribute( string name , string version , string author , string webpage , bool loadWhenStart ) { // 細節已略去 } public string Name { get { return _Name; } } public string Version { get { return _Version; } } public string Author { get { return _Author; } } public string Webpage { get { return _Webpage; } } public bool LoadWhenStart { get { return _LoadWhenStart; } } /// /// 用來(lái)存儲一些有用的信息 /// public object Tag { get { return _Tag; } set { _Tag = value ; } } /// /// 用來(lái)存儲序號 /// public int Index { get { return _Index; } set { _Index = value ; } }
private string _Name = ""; private string _Version = ""; private string _Author = ""; private string _Webpage = ""; private object _Tag = null ; private int _Index = 0; // 暫時(shí)不會(huì )用 private bool _LoadWhenStart = true ; } |
用這個(gè)類(lèi)修飾你的插件,并讓他實(shí)現 IPlugin 接口:
| /// /// My Pluging 1( Just for test ) /// [ PluginInfo( "My Pluging 1( Just for test )" , "1.0" , "Jack H Hansen" , "http://blog.csdn.net/matrix2003b" , true ) ] public class MyPlugin1 : IPlugin { public MyPlugin1() { }
#region IPlugin 成員 // 細節已略去 #endregion
private IApplicationObject _App; private IDocumentObject _CurDoc; } |
3. 加載插件
現在就得用到 System.Refelction 命名空間了。程序在啟動(dòng)時(shí)會(huì )搜索 plugins 目錄下的每一個(gè)文件。對于每一個(gè)文件,如果它是一個(gè)插件,就用 Assembly 對象加載它。然后枚舉程序集中的每一個(gè)對象。判斷一個(gè)程序集是否為我們的插件的方法是判斷它是否直接或間接實(shí)現自 IPlugin。用下面的函數,傳遞從程序集枚舉的對象的System.Type。
| private bool IsValidPlugin( Type t ) { bool ret = false ; Type[] interfaces = t.GetInterfaces(); foreach ( Type theInterface in interfaces ) { if ( theInterface.FullName == "CSPluginKernel.IPlugin" ) { ret = true ; break ; } } return ret; } |
若條件都滿(mǎn)足,IsValidPlugin() 就會(huì )返回 true 。接著(zhù)程序就會(huì )創(chuàng )建這個(gè)對象并把它存于一個(gè) ArrayList 中。
| plugins.Add( pluginAssembly.CreateInstance( plugingType.FullName ) ); |
現在,你就可以撰寫(xiě)測試代碼了。
三、源代碼
由于篇幅所限,完整的源代碼(包含測試用例)請在下面的鏈接下載。下載后請用 VS.NET2003 打開(kāi),重新生成解決方案即可(需要 .NET Framework 1.1)。測試用例是一個(gè)在 RichTextBox 控件里插入紅色文本的插件。很簡(jiǎn)單,只作測試之用。
四、結語(yǔ)
That's all! 有了這種插件構架,可憐的程序員們就再也不用為需求蔓延耗費心機了。另外,歡迎對本文以及本文的附加代碼作出評價(jià)。還有,就是,常去我的 Blog 看看~~ ^_^
聯(lián)系客服