一、引言
1. 問題的引入
假設你設計的程序已經部署到用戶的計算機上,並且能夠正常運行了。但是有一天,用戶打來了電話——他們要求增加新的功能。確定了用戶的需求後,你竟然發現原有的軟件架構已經無法勝任新增任務的需求——你需要重新設計這個應用了!但問題是,就算你又用了一個開發周期完成了用戶需要的應用,卻不能保證用戶的需求不會再次變更。也就是說,需求蔓延的可能性依然存在。因此,這種情況下插件構架更能顯示出它的優越性。
2. 幾個解決方案的對比
我總結了一下我所接觸到的插件構架,大致上可分為以下幾類:
i> 腳本式
使用某種語言把插件的程序邏輯寫成腳本代碼。而這種語言可以是 Python ,或是其他現存的已經經過用戶長時間考驗的腳本語言。甚至,你可以自行設計一種腳本語言來配合你程序的特殊需要。當然,用當今最流行的 XML 是再合適不過了。
這種形式的特點在於,稍有點編程知識的用戶就可以自行修改你的腳本( ^_^ 假如你不加密它的話)。我們無法論證這是好處還是壞處。因為,這種情況所造成的後果是不可預知的。
ii> 動態函數庫 DLL
插件功能以動態庫函數的形式存在。主程序通過某種渠道(插件編寫者或某些工具)獲得插件 DLL 中的函數簽名,然後在合適的地方調用它們。用過 Matlab 的讀者都知道, Matlab 中的各項功能幾乎都是些動態鏈入的函數。
iii> 聚合式
顧名思義,就是把插件功能直接寫成 EXE 。主程序除了完成自己的職責外,還負責調度這些“插件”。我不喜歡這種形式。這使插件與插件之間,主程序與插件之間(主要是這一點)的信息交流困難了許多。巴比倫塔的失敗 [1] 從某種程度上講就是信息交流無法實現造成的。
iv> COM 組件
COM [2] 的產生給這個世界增添了幾分活力。只有接口!我們的插件需要做的只是實現程序定義的接口。主程序不需要知道插件怎樣實現預定的功能,它只需要通過接口訪問插件,並提供主程序相關對象的接口。這樣一來,主程序與各插件之間的信息交流就變得異常簡單。並且,插件對於主程序來說是完全透明的。
3. 決策
C# 是面向對象的程序設計語言。它提供了 interface 關鍵字來直接定義接口。同時, System.Reflection 命名空間也提供了訪問外部程序集的一系列相關對象。這就為我們在 C# 中實現插件構架打下了堅實的基礎。
下面,我們將以一個具有插件構架的程序編輯器為例,來闡述這種構架在 C# 中的實現。
二、設計過程
好了,現在我們准備把所有的核心代碼都放在 CSPluginKernel 命名空間中。用VSIDE建立一個C#類庫工程。在命名空間 CSPluginKernel 中開始我們的代碼。
1. 接口設計
我們的程序編輯器會向插件開放正在編輯的文檔對象。程序啟動後,就枚舉每一個插件並把它連接到主程序,同時傳遞主程序對象的接口。插件可以通過這個接口來請求主程序對象或訪問主程序功能 。
根據上面的需求,我們首先需要一個主程序接口:
public interface IApplicationObject {
void Alert( string msg ); // 產生一條信息
void ShowInStatusBar( string msg ); // 將指定的信息顯示在狀態欄
IDocumentObject QueryCurrentDocument(); // 獲取當前使用的文檔對象
IDocumentObject[] QueryDocuments(); // 獲取所有的文檔對象
// 設置事件處理器
void SetDelegate( Delegates whichOne , EventHandler targer );
}
// 目前只需要這一個事件
public enum Delegates {
Delegate_ActiveDocumentChanged ,
}
然後是 IDocumentObject 接口。插件通過這個接口訪問編輯器對象。
///
/// 編輯器對象必須實現這個接口
///
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();
}
這個接口不需要過多解釋。這裡我只實現了RichTextBox控件少數的幾個方法,其他可能用得到的,讀者自行添加即可。
再然後,根據插件在其生命周期裡的行為,設計插件的接口。
///
/// 本程序的插件必須實現這個接口
///
public interface IPlugin {
ConnectionResult Connect( IApplicationObject app );
void OnDestory();
void OnLoad();
void Run();
}
///
/// 表示插件與主程序連接的結果
///
public enum ConnectionResult {
Connection_Success ,
Connection_Failed
}
主程序會首先調用 Connect() 方法,並傳遞 IApplicationObject 給插件。插件在這個過程中做一些初始化工作。然後,插件的 OnLoad() 方法被調用。在這之後,當主程序接收到調用插件的信號時(鍵盤、鼠標響應)就會調用插件的 Run() 方法來啟動這個插件。程序結束時,調用其 OnDestory() 方法。這樣,插件的生命才宣告結束。
2. 插件信息的存儲與獲取
一個插件需要有它的名稱 、版本等信息。作為設計者的你,也一定要留下你的尊姓大名和個人網站等用來宣傳自己。 C# 的新特性——屬性, 就是一個很好的解決方案。因此我們定義一個從 System.Attribute 繼承來的類 PluginInfoArrtibute :
///
/// 用來指定一個插件的相關信息
///
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; } }
///
/// 用來存儲一些有用的信息
///
public object Tag {
get { return _Tag; }
set { _Tag = value ; }
}
///
/// 用來存儲序號
///
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;
// 暫時不會用
private bool _LoadWhenStart = true ;
}
用這個類修飾你的插件,並讓他實現 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 命名空間了。程序在啟動時會搜索 plugins 目錄下的每一個文件。對於每一個文件,如果它是一個插件,就用 Assembly 對象加載它。然後枚舉程序集中的每一個對象。判斷一個程序集是否為我們的插件的方法是判斷它是否直接或間接實現自 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" ) {