1 引言
在資訊系統開發,使用者業務功能變化預先不可知,故要提高系統後期的業務擴充。一般情況下使用者需求發生變化,要重新編寫代碼,編譯,生產部署套件,然後再更新使用者程式。這樣的過程比較繁瑣。
本文討論產生後的應用系統與外部編譯的業務庫實現動態綁定,應用程式在運行過程中動態綁定要實現的外部業務,當業務發生變化,也只是替換這些外部的動態庫,不用重新對應用程式進行修改和編譯,實現了耦合綁定。同時,業務執行個體對象可以在程式運行時實現執行個體化,達到了封裝效果。並且降低了調用代碼和具體實作類別代碼的耦合,增強靈活性和可複用性,增加了軟體的可維護性。
C#提供的反射機制,再結合自適應資料參數的傳遞,通過這個技術,我們可以將應用程式框架中的擴充點以外掛程式式程式集的方式來動態載入、構建,從而實現可動態擴充的應用程式。
2 反射機制的基礎知識
反射[1][2]是.NET中重要機制,通過反射,可以在運行時獲得.NET中每一個類型(包括類、結構、委託、介面和枚舉等)的成員,包括方法、屬性、事件,以及建構函式等。還可以獲得每個成員的名稱、限定符和參數等。.NET的應用程式結構分為應用程式定義域、程式集、模組、類型和成員幾個層次,公用語言運行庫載入器管理應用程式域,這種管理組件括將每個程式集載入到相應的應用程式定義域以及控制每個程式集中類型階層的記憶體布局。程式集包含模組,而模組包含類型,類型又包含成員,反射則提供了封裝程式集、模組和類型的對象。我們可以使用反射動態地建立類型的執行個體,將類型綁定到現有對象或從現有對象中擷取類型,然後調用類型的方法或訪問其欄位和屬性。
反射通常具有以下用途:① 使用Assembly定義和載入程式集,載入在資訊清單中列出的模組,以及從此程式集中尋找類型並建立該類型的執行個體;② 使用Module瞭解如下的類似資訊,如模組的程式集以及模組中的類等:③ 使用CoustructorInfo瞭解如下的類似資訊,如建構函式的名稱、參數、存取修飾詞(如public或private)和實現詳細資料(如abstract或virtua1)等;④ 使用MethodInfo來瞭解如下的類似資訊,如方法的名稱、傳回型別、參數、存取修飾詞(如public或private)和實現詳細資料(如abstract或virtua1)等;⑤ 使用FieldInfo來瞭解如下的類似資訊,如欄位的名稱、存取修飾詞(如public或private)和實現詳細資料(如static)等,並擷取或設定欄位值;⑥ 使用EventInfo來瞭解如下的類似資訊,如事件的名稱、事件處理常式資料類型、自訂屬性、宣告類型和反射類型等,並添加或移除事件處理常式:⑦ 使用PropertyInfo來瞭解如下的類似資訊,如屬性的名稱、資料類型、宣告類型、反射類型和唯讀或可寫狀態等,並擷取或設定屬性值;⑧ 使用ParameterInfo來瞭解如下的類似資訊,如參數的名稱、資料類型、參數是輸入參數還是輸出參數,以及參數在方法簽名中的位置等。
3.總體設計思路
外掛程式是一種遵循一定規範的應用程式介面編寫出來的程式模組。當應用程式已經部署,但業務發生了變化,這樣可以通過讀取外掛程式配置資訊,載入新的應用構件,實現變化的業務。
對於應用系統的架構而言,擴充點是架構中預先定義的一些“點”。 在架構複用中應用構件的組裝需要基於擴充點進行。構造性和演化性是軟體的兩個本質特徵,作為一類重要的可複用軟體製品。而基於擴充點可以組裝不同的應用構件以適應領域的變化性。則體現了架構對於軟體演化特徵的支援[3]。
本文涉及到幾個概念,外掛程式配置定義,介面定義,方法定義和調用參數定義和返回參數定義。在本外掛程式平台中,設定檔描述外掛程式配置定義,介面定義,方法定義。對於調用參數定義和返回參數定義則採用通用對象和動態對象組[4]來實現傳入和返回參數。
外掛程式平台的實現過程1所示。當平台運行初始化時,通過讀取XML配置資訊,裝載DLL,通過C#的反射機制分析DLL裡的全部實作類別和方法。外部構件可以在平台容器中被執行個體化,並執行外掛程式點的方法。實現的演算法不再是編碼硬綁定。
圖1 PlugPlatform整個過程圖
這樣,應用程式在運行過程中動態綁定要實現的外部業務,當業務發生變化,也只是替換這些外部的動態庫,不用重新對應用程式進行修改和編譯,實現了耦合綁定。
4.具體實現
PlugPlatform平台包括四個部分:① 設定檔的擷取和解析;② 通用參數和動態參數組處理;③ 外掛程式平台裝載DLL並執行外部方法;④ 異常處理。
4.1 設定檔的擷取和解析
設定檔以XML Schema為基礎,分為兩種類型,一種是類設定檔,主要描述關於外部DLL中的類以及方法的內容。第二種設定檔是介面設定檔,主要描述關於外部DLL中的介面以及方法的內容。
類設定檔的XSD2所示。
圖片看不清楚?請點擊這裡查看原圖(大圖)。
圖2 類設定檔的schema圖
按照此XSD形成的配置XML3所示。
圖3 類設定檔的XML樹
同理可以介面設定檔的XSD內容(4)和XML樹(5)。
圖片看不清楚?請點擊這裡查看原圖(大圖)。
圖4 介面設定檔的schema圖
圖5 介面設定檔的XML樹
在實現將XML樹狀結構的資料轉換為二維MethodObject雜湊表,MethodObject雜湊表是一key/value的索引值對,其中key通常可用來快速尋找,value用於儲存對應於key的值。MethodObject類資料結構如下:
public class MethodObject {
private ClassObject classobject = null;
private InterfaceObject interfaceobject = null;
private DllFileObject dllfileobject = null;
private string name = string.Empty;
private string simplename = string.Empty;
private string implementname = string.Empty;
public string MethodName {get { return this.name; } set { this.name = value; } }
public string SimpleName { get { return this.simplename; }set { this.simplename = value; } }
public string ImplementName {get { return this.implementname; }set { this.implementname = value; }}
public ClassObject ClassObject {get { return this.classobject; }set { this.classobject = value; }}
public InterfaceObject InterfaceObject {get {return this.interfaceobject; }set {this.interfaceobject = value; }}
public DllFileObject DLLFileObject{ get { return this.dllfileobject; } set { this.dllfileobject = value; }}
}
MethodObject雜湊表中key為保證內容的唯一性而採用方法的全名。可以,這樣形成的主鍵可以進行快速尋找。value用來儲存MethodObject對象。同時MethodObject對象與ClassObject對象,InterfaceObject對象和DLLFile對象都是多對一的關係,所以,一旦獲得了MethodObject對象,就可以反推出ClassObject對象,InterfaceObject對象和DLLFile對象。
圖6 配置XML轉MethodObject雜湊表
根據XML Schema可以構建XML文檔樹,對XML文檔樹的節點進行分層遍曆,然後採用遞迴演算法,依次把XML文檔樹上最邊上的葉子轉化為方法對象雜湊表,實現方式6所示。
4.2通用參數和動態參數組處理
對於外部的方法,要傳入參數,同時也獲得結果。這些都要用一些通用的資料結構來描述。參數必須可以支援任何類型,是一個通用性的參數。通過建立一個資料的通用類,可以保證支援任何資料類型。
由於傳入和傳出的參數有多有少,這就要求參數組能實現隨意的自動成長和減少。通過設計一個動態自增長的參數數組就可以實現。設計模型7表示。
圖7 DataValueObject類和DynamicArrayObject類的設計模型
動態參數組的增加數組對象方法如下:
public DynamicArrayObject addObject(DataValueObject obj) {
if (Objects == null) {
Objects = new DataValueObject[1];
Objects[0] = obj;
return this; }
else {
DataValueObject[] objectList = new DataValueObject[Length + 1];
for (int i = 0; i < Length; i++) { objectList[i] = Objects[i]; }
objectList[Length] = obj;
Objects = objectList;
return this;}
}
動態參數組的刪除數組對象方法如下:
public DynamicArrayObject deleteObject(int idx) {
if (Objects == null) return this;
else {
if (idx >= 0 && idx < Length && Length > 1) {
DataValueObject[] objectList = new DataValueObject[Length - 1];
for (int i = 0; i < idx; i++) {objectList[i] = Objects[i];}
for (int i = idx + 1; i < Length; i++) {objectList[i - 1] = Objects[i]; }
Objects = objectList;
return this; }
else {
if (idx == 0 && Length <= 1) {
Objects = null;
return this; }
else return this; } }
}
4.3外掛程式平台動態裝載DLL並執行
PlugFramework是整個PlugPlatform平台的核心內容。主要實現檢查和裝載DLL檔案,動態建立執行個體化對象,驗證並執行外部方法。
圖片看不清楚?請點擊這裡查看原圖(大圖)。
圖8 PlugPlatform動態裝載的全過程
PlugPlatform依據設定檔可以瞭解裝載的外部DLL檔案和要求執行的類或介面方法。整個裝載和的調用過程8說明。
PlugFramework包含有類和介面,這些類和介面之間有繼承、實現、關聯關係。其類圖9所示。
圖9 PlugPlatform類圖
PlugFramework各個類的具體詳細描述如下:
序號
名稱
實現功能
備忘
1
Plus.PlusConfig
外掛程式平台的配置資訊類,可建立工廠類
類
2
Plus.PlugFactory
外掛程式平台的工廠類,可以建立方法實作類別
類
3
Plus.IAction
方法實現的介面
介面
4
Plus.Framework.PlugAction
方法實現的抽象祖先類
類
5
Plus.Framework.ClassAction
類對象的實作類別
類
6
Plus.Framework.InterfaceAction
介面對象的實作類別
類
7
Plus.Framework.AssemblyManager
Assembly的管理類,產生Assembly。
類
8
Plus.Framework.TypeManager
Type的管理類,可實現對Type、Object的產生和檢查。包括動態方法的調用。
類
動態調用外部方法的核心代碼如下所示:
public object InvokeClassMethod(String className, object[] objectArgs, String methodName, object[] methodArgs) {
Object[] newArgs = new Object[methodArgs.Length];
Object thisObject = new Object();
Type type = CreateType(className);
MethodInfo[] methods = type.GetMethods();
Object instance = CreateObject(type, objectArgs);
foreach (MethodInfo m in methods) {
if (m.Name == methodName) {
newArgs = ConvertArgsType(m, methodArgs);
try {
if (!m.IsStatic) thisObject = m.Invoke(instance, newArgs); //非靜態方法,使用類執行個體調用
else thisObject = m.Invoke(null, newArgs);
return thisObject;}
catch (Exception e){throw new PlusException("不能動態調用方法,原因:" + e.Message, e); }}
}
return thisObject;
}
圖10表示PlugPlatform實現全過程。下面分別對每個步驟做一個詳細描述:
① 外部應用請求動態調用。
② PlusConfig類根據設定檔建立PlugFactory對象。
③ PlugFactory對象建立一個Action對象。
④ Action對象獲得MethodObject對象組,逆向產生DllFileObject對象。
⑤ 根據DllFileObject對象中的DLL檔案資訊,Action對象通過AssemblyManager類獲得Assembly對象。
⑥ Action對象使用Assembly對象建立TypeManager對象。
⑦ Action對象傳遞MethodObject對象給TypeManager對象。
⑧ TypeManager對象可依據MethodObject對象獲得ClassObject對象。並使用ClassObject對象資訊動態建立一個外部ClassObject對象的執行個體instance。
⑨ TypeManager對象使用instance和MethodObject對象資訊調用instance的動態方法。instance把執行結果返回給Action對象。
⑩ Action對象把執行結果返回給外部應用。
圖片看不清楚?請點擊這裡查看原圖(大圖)。
圖10 PlugPlatform實現的順序圖
其中Plus原廠模式採用了Factory模式。對於Assembly的產生採用了Singleton模式。
4.4 異常處理
由於應用程式中有很多不可預料的問題,本平台在很多地方都有可能出現人為錯誤,如找不到設定檔;設定檔的格式不對,不能解析設定檔;類或介面名稱寫錯了,不能執行個體化類;方法名稱寫錯了,不能執行方法等等。增加異常處理主要是增強其容錯性,在這裡就不做更多的說明。
5.應用執行個體
本例子程式主要有三個方面組成:XML設定檔、外部DLL檔案和PlusPlatform調用代碼。
5.1 XML設定檔
採用的XML設定檔有兩個,一個是針對類對象的XML設定檔,一個是針對介面對象的設定檔。
其中類對象的XML設定檔:
<?xml version="1.0" encoding="utf-8" ?>
<PlugPlatformResource>
<DllFile name="UserLibrary.dll" filepath ="/" objectType ="class">
<classobject name="UserLibrary.UserTest1" >
<Methodobject>testAction01</Methodobject>
<Methodobject>testAction02</Methodobject>
<Methodobject>testAction03</Methodobject>
</classobject>
<classobject name="UserLibrary.UserTest2" >
<Methodobject>testAction01</Methodobject>
</classobject>
</DllFile>
</PlugPlatformResource>
介面對象的設定檔與類對象設定檔基本相同,只不過配置資訊中由類換成了介面:
<?xml version="1.0" encoding="utf-8" ?>
<PlugPlatformResource>
<DllFile name="UserLibrary.dll" filepath ="/" objectType ="interface">
<interfaceobject name="InterfaceTest1" implement="UserLibrary.UserTest2" >
<Methodobject>testAction01</Methodobject>
</interfaceobject>
</DllFile>
</PlugPlatformResource>
5.2 DLL檔案內容
其編譯的DLL檔案為UserLibrary.dll,該dll檔案包括兩個類和一個介面,其內部代碼為: public class UserTest1 {
public DynamicArrayObject testAction01(DynamicArrayObject outObject) {
DynamicArrayObject thisObject = new DynamicArrayObject();
//分解DynamicArrayObject
DataValueObject do1 = null;
string ls = null;
for (int i = 0; i < outObject.Length; i++) {
do1 = outObject.getObject(i);
ls += (String)do1.getDataValue(); }
DataValueObject do2 = new DataValueObject();
do2.setDataType(do1.getDataType()).setDataValue(ls);
//組裝DynamicArrayObject,返回DynamicArrayObject
thisObject.addObject(do2);
return thisObject;
}
public DynamicArrayObject testAction02(DynamicArrayObject outObject) {
return outObject; }
}
public class UserTest2 : InterfaceTest1 {
public DynamicArrayObject testAction01(DynamicArrayObject outObject) {
DynamicArrayObject thisObject = new DynamicArrayObject();
//分解DynamicArrayObject
DataValueObject do1 = null;
string ls = null;
for (int i = 0; i < outObject.Length; i++) {
do1 = outObject.getObject(i);
ls += (String)do1.getDataValue(); }
DataValueObject do2 = new DataValueObject();
do2.setDataType(do1.getDataType()).setDataValue(ls);
//組裝DynamicArrayObject,返回DynamicArrayObject
thisObject.addObject(do2);
return thisObject;
}
public DynamicArrayObject testAction02(DynamicArrayObject outObject){
return outObject;}
}
public interface InterfaceTest1 {
DynamicArrayObject testAction01(DynamicArrayObject outObject);
}
5.3 調用外掛程式平台代碼
調用代碼也分為兩類,一類是針對類對象處理的,代碼如下:
DynamicArrayObject thisObject = new DynamicArrayObject();
DataValueObject do1 = new DataValueObject();
DataValueObject do2 = new DataValueObject();
do1.setDataType("string").setDataValue("類測試:第一個對象值.");
do2.setDataType("string").setDataValue("第二個對象值.");
thisObject.addObject(do1).addObject(do2);
string dllFile = Application.StartupPath + "\DllClassFile.xml";
PlugFactory factory = PlusConfig.BuildFactory(dllFile);
IAction action = factory.CreatAction();
DynamicArrayObject outputObject = action.Execute("UserLibrary.UserTest1.testAction01", thisObject);
另一類是針對介面對象處理,代碼如下:
DynamicArrayObject thisObject = new DynamicArrayObject();
DataValueObject do1 = new DataValueObject();
DataValueObject do2 = new DataValueObject();
do1.setDataType("string").setDataValue("介面測試:第一個對象值.");
do2.setDataType("string").setDataValue("第二個對象值.");
thisObject.addObject(do1).addObject(do2);
string dllFile = Application.StartupPath + "\DllInterfaceFile.xml";
PlugFactory factory = PlusConfig.BuildFactory(dllFile);
IAction action = factory.CreatAction();
DynamicArrayObject outputObject = action.Execute("InterfaceTest1.testAction01", thisObject);
可以對返回的DynamicArrayObject做分解查看,滿足設計要求。
6.結束語
反射機制結合動態數組很好地解決了應用軟體的後期維護和升級。對於應用軟體的變化,可不改動任何現有的程式,只要修改XML設定檔的相應對象名稱和載入新的對象即可,程式不需要任何的重編、重啟和硬性改動,並保證了原應用系統的可複用性從而實現降低耦合度,實現複用的目標。
本模型在層與層之間藉助類調用和介面實現,利用反射機制把調用者與實現者在編譯期分離。運行期通過讀設定檔動態載入實作類別,並通過介面將實現者強制轉型,使其為調用者所用,完成調用者和實現者的解耦。但是,這個功能並不是完全完善,對於外掛程式平台也有很多的改進性,如果能對類和介面設定檔更加豐富,把外掛程式平台升級為一個架構容器,該容器能把對象之間的依賴關係先行剝離,然後在適當時候由容器負責產生具體的執行個體再注射到調用者中,即控制權由應用代碼中轉到了外部容器,控制權發生了轉移。即所謂的控制反轉模式,這種模式在java中已經有比較成熟的架構,如Spring等。相信憑藉Microsoft.Net龐大的技術架構平台,在C#上也會有這樣的控制反轉架構出現。
參考文獻
[1](美)Karli Watson Christian Nagel等.康博,譯.C#入門經典.北京:清華大學出版社,2006.
[2] MSDN Library .NET Framework開發員指南:在運行時瞭解類型資訊.2003.
[3] 劉瑜 張世琨 王立福 楊芙清. 基於構件的軟體架構與角色擴充形態研究. 軟體學報,2004.14(8):1364-1370
[4] 段春筍 杜立新. C#動態數組設計原理. 電腦編程技巧與維護,2005.(7):24-25
[5] 何文海 謝建剛. 基於.NET平台的外掛程式式應用程式框架開發. 電腦知識與技術:學術交流,2007.(8):755-756
[6] 冷山述 陸倜 武裝. 用C#構造可複用軟體體繫結構. 航空計算技術,2003.(4):88-90,93
[7] 殷凱 謝文威. 在Factory 方法模式中.NET反射技術應用的研究. 常州工學院學報,2006.(4):28-34
[8] 姚明 李家蘭. 基於.NET的通用軟體開發平台的研究與實現. 電腦知識與技術:學術交流,2007.(8):797-798