程式|架構
原文作者:
Shawn Patrick Walcheske
譯者:
電子科技大學 夏桅
[引言]
在.NET架構下的C#語言,和其他.NET語言一樣提供了很多強大的特性和機制.其中一些是全新的,而有些則是從以前的語言和平台上照搬過來的.然而,這種巧妙的結合產生了一些有趣的方法可以用來解決我們的問題.這篇文章將講述如何利用這些奇妙的特性,用外掛程式(plug-ins)機制建立可擴充的解決方案.後面也將提供一個簡要的例子,你甚至可以用這個東西來替換那些已經在很多系統中廣泛使用的獨立的程式.在一個系統中,可能有很多程式經常需要進行資料處理.可能其中有一個程式用於處理僱員的資訊,而另一個用來管理客戶關係.在大多數情況下,系統總是被設計為很多個獨立的程式,他們之間很少有互動,經常使用複製代碼的辦法來共用.而實際上這樣的情況可以把那些程式設計為外掛程式,再用一個單一的程式來管理這些外掛程式.這種設計可以讓我們更好的在不同的解決方案中共用公用的方法,提供統一的感觀.
圖片一是一個例子程式的截圖.使用者介面和其他常見的程式沒有什麼不同.整個表單被垂直的分割為兩塊.左邊的窗格是個樹形菜單,用於顯示外掛程式列表,在每個外掛程式的分支下面,列出了這個外掛程式所管理的資料.而右邊的窗格則用於編輯左邊被選中的外掛程式的資料.各個外掛程式提供各自的編輯資料的介面.圖片一展示了一個精巧的工作區.
[開始]
那麼,主程式必須能夠載入外掛程式,然後和這些外掛程式進行通訊,這樣才能實現我們的設計.所有這些的實現可以有很多不同的方法,僅取決於開發人員選擇的語言和平台.如果選擇的是C#和.NET,那麼反射(reflection)機制可以用來載入外掛程式,並且其介面和抽象類別可以用於和外掛程式通訊.
為了更好的理解主程式和外掛程式之間的通訊,可以先瞭解一下設計模式.設計模式最早由Erich Gamma提出[1],它利用架構和對象思想來實現通用的通訊模型.不管組件是否具有不同的輸入和輸出,只要他們有相似的結構.設計模式可以協助開發人員利用廣受證明的物件導向理論來解決問題.事實上它就是描述解決方案的語言,而不用管問題的具體細節或者程式設計語言的細節.設計模式策略的關鍵點在於如何把整個解決方案根據功能來分解,這種分解是通過把主程式的不同功能分開執行而完成的.這樣主程式和子程式之間的通訊可以通過設計良好的介面來完成.通過這種分解我們立即可以得到這兩個好處:第一,軟體項目被分成較小的不相干的單位,工作流程的設計可以更容易,而較小的代碼片斷意味著代碼更容易建立和維護.第二個好處在於改變程式行為的時候並不會關係到主程式的運行,主程式不用關心子程式如何,他們之間只要有通用的通訊機制就足夠了.
[建立介面]
在C#程式中,介面是用來定義一個類的功能的.介面定義了預期的方法,屬性,事件資訊.為了使用介面,每個具體的函數必須嚴格按照介面的定義完成所描述的功能.列表一展示了上面例子程式的介面:IPlug.這個介面定義了四個方法:GetData,GetEditControl,Save和Print.這四個定義並沒有描述具體是怎麼完成的,但是他們保證了這個類支援IPlug介面,也就是保證支援這些方法的調用.
[定製屬性]
在查看代碼之前,討論總是先得轉移到屬性定製上面.屬性定製是.NET提供的一個非常棒的新特性之一,屬性對於所有的程式設計語言都是一種通用的結構.舉個例子,一個函數用於標識可存取權限的public,private,或者protect標誌就是這個函數的一個屬性.屬性定製之所以如此讓人興奮,那是因為編程人員將不再只能從語言本身提供的有限的屬性集中選擇.一個定製的屬性其實也是一個類,它從System.Attribute繼承,它的代碼被允許是自我描述的.屬性定製可以應用於絕大多數結構中,包括C#裡面的類,方法,事件,域和屬性等等.範例程式碼片斷定義了兩個定製的屬性:PlugDisplayNameAttribute和PlugDescriptionAttribute,所有的外掛程式內部的類必須支援這兩個屬性.列表二是用於定義PlugDisplayNameAttribute的類.這個屬性用於顯示外掛程式節點的內容.在程式啟動並執行時候,主程式將可以利用反射(reflection)來取得屬性值.
[外掛程式(Plug-Ins)]
上面的樣本程式包括了兩個外掛程式的執行.這些外掛程式在EmployeePlug.cs和CustomerPlug.cs中定義.列表三展示了EmployeePlug類的部分定義.下面是一些關鍵點.
1.這個類實現了IPlug介面.由於主程式根本不會知道外掛程式內部的類是如何定義的,這非常重要,主程式需要使用IPlug介面和各個外掛程式通訊.這種設計利用了物件導向概念裡面的"多態性".多態性允許運行時,可以通過指向基類的引用,來調用實現衍生類別中的方法.
2.這個類被兩個屬性標識,這樣主程式可以判斷這個外掛程式是不是有效.在C#中,要給一個類標識一個屬性,你得在類的定義之前聲明屬性,內容附在括弧內.
3.簡明起見,例子只是使用了直接寫入代碼的資料.而如果這個外掛程式是個正式的產品,那麼資料總是應該放在資料庫中或者檔案中,各自所有的資料都應該僅僅由外掛程式本身來管理.EmployeePlug類的資料在這裡用EmployeeData對象來儲存,那也是一個類型並且實現了IPlugData介面.IPlugData介面在IPlugData.cs中定義,它提供了最基礎的資料交換功能,用於主程式和外掛程式之間的通訊.所有支援IPlugData介面的對象在下層資料變化的時候將提供一個通知.這個通知實際上就是DataChanged事件的發生.
4.當主程式需要顯示某個外掛程式所含資料列表的時候,它會調用GetData方法.這個方法返回IPlugData對象的一個數組.這樣主程式就可以對數組中的每個對象使用ToString方法得到資料以建立樹的各個節點.ToString方法是EmployeeData類的一個重載,用於顯示僱員的名字.
5.IPlug介面也定義了Save和Print方法.定義這兩個方法的目的在於當有需要列印或者儲存資料的時候,要通知一個外掛程式.EmployeePlug類就是用於實現列印和儲存資料的功能的.在使用Save方法的時候,需要儲存資料的位置將會在方法調用的時候提供.這裡假設主程式會向使用者查詢路徑等資訊.路徑資訊的查詢是主程式提供給各個外掛程式的服務.對於Print方法,主程式將把選項和內容傳遞到System.Drawing.Printing.PrintDocument類的執行個體.這兩種情況下,和使用者的互動操作都是一致的由主程式提供的.
[反射(Reflection)]
在一個外掛程式定義好之後,下一步要做的就是查看主程式是怎麼載入外掛程式的.為了實現這個目標,主程式使用了反射機制.反射是.NET中用於運行時查看類型資訊的.在反射機制的協助下,類型資訊將被載入和查看.這樣就可以通過檢查這個類型以判斷外掛程式是否有效.如果類型通過了檢查,那麼外掛程式就可以被添加到主程式的介面中,就可以被使用者操作.
樣本程式使用了.NET架構的三個內建類來使用反射:System.Reflection.Assembly,System.Type,和System.Activator.
System.Reflection.Assembly類描述了.NET的程式集.在.NET中,程式集是登錄區.對於一個典型的Windows程式,程式集被配置為單一的Win32可執行檔,並且帶有特定的附加資訊,使之適應.NET運行環境.程式集也可以配置為Win32的DLL(動態連結程式庫),同樣需要帶有.NET需要的附加資訊.System.Reflection.Assembly類可以在啟動並執行時候取得程式集的資訊.這些資訊包括程式集包含的類型資訊.
System.Type類描述了類型定義.一個型別宣告可以是一個類,介面,數組,結構體,或者枚舉.在載入了一個類之後,System.Type類可以被用於枚舉該類支援的方法,屬性,事件和介面.
System.Activator類用於建立一個類的執行個體.
[載入外掛程式]
列表四展示了LoadPlugs方法.LoadPlugs方法在HostForm.cs中定義,是HostForm類的一個private的非靜態方法.LoadPlugs方法使用.NET的反射機制來載入可用的外掛程式檔案,並且驗證它們是否符合被主程式使用的要求,然後把它們添加到主程式的樹形顯示區中.這個方法包含了下面幾個步驟:
1.通過使用System.IO.Directory類,我們的代碼可以用萬用字元來尋找所有的以.plug為副檔名的檔案.而Directory類的靜態方法GetFiles能夠返回一個System.String類型的數組,以得到每個符合要求的檔案的實體路徑.
2.在得到路徑字串數組之後,就可以開始把檔案載入到System.Reflection.Assembly執行個體中了.建立Asdsembly對象的代碼使用了try/catch代碼塊,這樣如果某個檔案並不是一個有效地.NET程式集,就會拋出異常,程式此時將彈出一個MessageBox對話方塊,告訴使用者無法載入該檔案.迴圈一直進行直到所有檔案都已遍曆完成.
3.在一個程式集載入之後,代碼將遍曆所有可訪問到的類型資訊,檢查是否支援了HostCommon.IPlug介面.
4.如果所有類型都支援HostCommon.IPlug介面,那麼代碼繼續驗證這些類型,檢查是否支援那些已預先為外掛程式定義好的屬性.如果沒有支援,那麼一個HostCommon.PlugNotValidException類型的異常將會被拋出,同樣,主程式將會彈出一個MessageBox,告訴使用者出錯的具體資訊.迴圈一直進行直到所有檔案都已遍曆完成.
5.最後,如果這些類型支援HostCommon.IPlug介面,也已定義了所有需要定義的屬性,那麼它將被封裝為一個PlugTreeNode執行個體.這個執行個體就會被添加到主程式的樹形顯示區.
[實現]
主程式架構被設計為兩個程式集.第一個程式集是Host.exe,它提供了主程式的Windows表單介面.第二個程式集是HostCommon.dll,它提供了主程式和外掛程式之間進行通訊所需的所有類型定義.比如,IPlug介面就是在HostCommon.dll裡面配置的,這樣它可以被主程式和外掛程式等價的訪問.這兩個程式集在一個檔案夾內,同樣的,附加的作為外掛程式的程式集也需要被配置在一起.那些程式集被配置在plugs檔案夾內(主程式目錄的一個子檔案夾).EmployeePlug類在Employee.plug程式集中定義,而CustomerPlug類在Customer.plug程式集中定義.這個例子指定外掛程式檔案以.plug為副檔名.事實上這些外掛程式就是個普通的.NET類庫檔案,只是通常庫檔案使用.dll副檔名,這裡用.plug罷了.特殊的副檔名對於程式運行是完全沒有影響的,但是它可以讓使用者更明確的知道這是個外掛程式檔案.
[設計的比較]
並不是一定要像例子程式這樣設計才算正確的.比如,在開發一個帶有外掛程式的C#程式時,並不一定需要使用屬性.例子裡使用了兩個自訂的屬性,其實也可以新定義兩個IPlug介面的參數來實現.這裡選擇用屬性,是因為外掛程式的名字和它的描述在本質上確實就是一個事物的屬性,符合規範.當然了,使用屬性會造成主程式需要更多的關於反射的代碼.對於不同的需求,設計者總是需要做出旁徵博引的決定.
[總結]
樣本程式被設計為盡量的簡單,以協助理解主程式和外掛程式之間的通訊.在實際做產品的時候,可以做很多的改進以滿足實用要求.比如:
1.通過對IPlug介面增加更多的方法,屬性,事件,可以增加主程式和外掛程式之間的通訊點.兩者間的更多的互動操作使得外掛程式可以做更多的事情.
2.可以允許使用者主動選擇需要載入的外掛程式.
[原始碼]
樣本程式的完整的原始碼可以在這裡下載.
ftp://ftp.cuj.com/pub/2003/2101/walchesk.zip
[備忘]
[1] Erich Gamma et al. Design Patterns (Addison-Wesley, 1995).
圖片一:
列表一:The IPlug interface
public interface IPlug
{
IPlugData[] GetData();
PlugDataEditControl GetEditControl(IPlugData Data);
bool Save(string Path);
bool Print(PrintDocument Document);
}
列表二:The PlugDisplayNameAttribute class definition
[AttributeUsage(AttributeTargets.Class)]
public class PlugDisplayNameAttribute : System.Attribute
{
private string _displayName;
public PlugDisplayNameAttribute(string DisplayName) : base()
{
_displayName=DisplayName;
return;
}
public override string ToString()
{
return _displayName;
}
列表三:A partial listing of the EmployeePlug class definition
[PlugDisplayName("Employees")]
[PlugDescription("This plug is for managing employee data")]
public class EmployeePlug : System.Object, IPlug
{
public IPlugData[] GetData()
{
IPlugData[] data = new EmployeeData[]
{
new EmployeeData("Jerry", "Seinfeld")
,new EmployeeData("Bill", "Cosby")
,new EmployeeData("Martin", "Lawrence")
};
return data;
}
public PlugDataEditControl GetEditControl(IPlugData Data)
{
return new EmployeeControl((EmployeeData)Data);
}
public bool Save(string Path)
{
//implementation not shown
}
public bool Print(PrintDocument Document)
{
//implementation not shown
}
}
列表四:The method LoadPlugs
private void LoadPlugs()
{
string[] files = Directory.GetFiles("Plugs", "*.plug");
foreach(string f in files)
{
try
{
Assembly a = Assembly.LoadFrom(f);
System.Type[] types = a.GetTypes();
foreach(System.Type type in types)
{
if(type.GetInterface("IPlug")!=null)
{
if(type.GetCustomAttributes(typeof(PlugDisplayNameAttribute),
false).Length!=1)
throw new PlugNotValidException(type,
"PlugDisplayNameAttribute is not supported");
if(type.GetCustomAttributes(typeof(PlugDescriptionAttribute),
false).Length!=1)
throw new PlugNotValidException(type,
"PlugDescriptionAttribute is not supported");
_tree.Nodes.Add(new PlugTreeNode(type));
}
}
}
catch(Exception e)
{
MessageBox.Show(e.Message);
}
}
return;
}
[關於作者]
Shawn Patrick Walcheske是美國Arizona州Phoenix市的一名軟體開發工程師.他同時是Microsoft Certified Solution Developer和Sun Certified Programmer for the Java 2 Platform.你可以在這裡聯絡到他, questions@walcheske.com.
[譯者注]
以前就考慮過在.NET裡面如何?外掛程式機制,做來做去總是覺得設計上不夠好.而昨天在網上無意中發現了這篇文章,寫的實在是太棒了,所以看完之後,決定把它翻譯過來,前後一共花了大概10個小時吧.翻譯的可能不太好,請見諒.文中有什麼錯誤,請不吝指正.
我的E-Mail: sunmast@vip.163.com