在.NET框架下的C#語言,和其他.NET語言一樣提供了很多強大的特性和機制.其中一些是全新的,而有些則是從以前的語言和平臺上照搬過來的。然而,這種巧妙的結(jié)合產(chǎn)生了一些
有趣的方法可以用來解決我們的問題。這篇文章將講述如何利用這些奇妙的特性,用插件(plug-ins)機制建立可擴展的解決方案。后面也將提供一個簡要的例子,你甚至可以用這個東西來替換那些已經(jīng)在很多系統(tǒng)中廣泛使用的獨立的程序。在一個系統(tǒng)中,可能有很多程序經(jīng)常需要進行數(shù)據(jù)處理。可能其中有一個程序用于處理雇員的信息,而另一個用來管理客戶關(guān)系。在大多數(shù)情況下,系統(tǒng)總是被設(shè)計為很多個獨立的程序,他們之間很少有交互,經(jīng)常使用復(fù)制代碼的辦法來共享.而實際上這樣的情況可以把那些程序設(shè)計為插件,再用一個單一的程序來管理這些插件。這種設(shè)計可以讓我們更好的在不同的解決方案中共享公用的方法,提供統(tǒng)一的感觀。
圖片一是一個例子程序的
截圖.用戶界面和其他常見的程序沒有什么不同.整個窗體被垂直的分割為兩塊.左邊的窗格是個樹形菜單,用于顯示插件列表,在每個插件的分支下面,列出了這個插件所管理的數(shù)據(jù).而右邊的窗格則用于編輯左邊被選中的插件的數(shù)據(jù).各個插件提供各自的編輯數(shù)據(jù)的界面.圖片一展示了一個精巧的工作區(qū).
開始
那么,主程序必須能夠加載插件,然后和這些插件進行通信,這樣才能實現(xiàn)我們的設(shè)計.所有這些的實現(xiàn)可以有很多不同的方法,僅取決于開發(fā)者選擇的語言和平臺.如果選擇的是C#和.NET,那么反射(reflection)機制可以用來加載插件,并且其接口和
抽象類可以用于和插件通信.
為了更好的理解主程序和插件之間的通信,可以先了解一下設(shè)計模式.設(shè)計模式最早由Erich Gamma提出,它利用架構(gòu)和對象思想來實現(xiàn)通用的通信模型.不管組件是否具有不同的輸入和輸出,只要他們有相似的結(jié)構(gòu).設(shè)計模式可以幫助開發(fā)者利用廣受證明的面向?qū)ο罄碚搧斫鉀Q問題.事實上它就是描述解決方案的語言,而不用管問題的具體細節(jié)或者編程語言的細節(jié).設(shè)計模式策略的關(guān)鍵點在于如何把整個解決方案根據(jù)功能來分解,這種分解是通過把主程序的不同功能分開執(zhí)行而完成的.這樣主程序和子程序之間的通信可以通過設(shè)計良好的接口來完成.通過這種分解我們立即可以得到這兩個好處:第一,軟件項目被分成較小的不相干的單位,工作流程的設(shè)計可以更容易,而較小的代碼片斷意味著代碼更容易建立和維護.第二個好處在于改變程序行為的時候并不會關(guān)系到主程序的運行,主程序不用關(guān)心子程序如何,他們之間只要有通用的通訊機制就足夠了.
建立接口
在C#程序中,接口是用來定義一個類的功能的.接口定義了預(yù)期的方法,屬性,事件信息.為了使用接口,每個具體的函數(shù)必須嚴格按照接口的定義完成所描述的功能.列表一展示了上面例子程序的接口:IPlug.這個接口定義了四個方法:GetData,GetEditControl,
Save和Print.這四個定義并沒有描述具體是怎么完成的,但是他們保證了這個類支持IPlug接口,也就是保證支持這些方法的調(diào)用.
定制屬性
在查看代碼之前,討論總是先得轉(zhuǎn)移到屬性定制上面.屬性定制是.NET提供的一個非常棒的新特性之一,屬性對于所有的編程語言都是一種通用的結(jié)構(gòu).舉個例子,一個函數(shù)用于標(biāo)識可
訪問權(quán)限的public,private,或者protect標(biāo)志就是這個函數(shù)的一個屬性.屬性定制之所以如此讓人興奮,那是因為編程人員將不再只能從語言本身提供的有限的屬性集中選擇.一個定制的屬性其實也是一個類,它從System.Attribute繼承,它的代碼被允許是自我描述的.屬性定制可以應(yīng)用于絕大多數(shù)結(jié)構(gòu)中,包括C#里面的類,方法,事件,域和屬性等等.示例代碼片斷定義了兩個定制的屬性:PlugDisplayNameAttribute和PlugDescriptionAttribute,所有的插件內(nèi)部的類必須支持這兩個屬性.列表二是用于定義PlugDisplayNameAttribute的類.這個屬性用于顯示插件節(jié)點的內(nèi)容.在程序運行的時候,主程序?qū)⒖梢岳梅瓷?reflection)來取得屬性值.
插件(Plug-Ins)
上面的示例程序包括了兩個插件的執(zhí)行.這些插件在EmployeePlug.cs和CustomerPlug.cs中定義.列表三展示了EmployeePlug類的部分定義.下面是一些關(guān)鍵點.
1.這個類實現(xiàn)了IPlug接口.由于主程序根本不會知道插件內(nèi)部的類是如何定義的,這非常重要,主程序需要使用IPlug接口和各個插件通信.這種設(shè)計利用了面向?qū)ο蟾拍罾锩娴?多態(tài)性".多態(tài)性允許運行時,可以通過指向基類的引用,來調(diào)用實現(xiàn)派生類中的方法.
2.這個類被兩個屬性標(biāo)識,這樣主程序可以判斷這個插件是不是有效的.在C#中,要給一個類標(biāo)識一個屬性,你得在類的定義之前聲明屬性,內(nèi)容附在括號內(nèi).
3.簡明起見,例子只是使用了直接寫入代碼的數(shù)據(jù).而如果這個插件是個正式的產(chǎn)品,那么數(shù)據(jù)總是應(yīng)該放在數(shù)據(jù)庫中或者文件中,各自所有的數(shù)據(jù)都應(yīng)該僅僅由插件本身來管理.EmployeePlug類的數(shù)據(jù)在這里用EmployeeData對象來存儲,那也是一個類型并且實現(xiàn)了IPlugData接口.IPlugData接口在IPlugData.cs中定義,它提供了最基礎(chǔ)的數(shù)據(jù)交換功能,用于主程序和插件之間的通訊.所有支持IPlugData接口的對象在下層數(shù)據(jù)變化的時候?qū)⑻峁┮粋€通知.這個通知實際上就是DataChanged事件的發(fā)生.
4.當(dāng)主程序需要顯示某個插件所含數(shù)據(jù)列表的時候,它會調(diào)用GetData方法.這個方法返回IPlugData對象的一個數(shù)組.這樣主程序就可以對數(shù)組中的每個對象使用ToString方法得到數(shù)據(jù)以建立樹的各個節(jié)點.ToString方法是EmployeeData類的一個重載,用于顯示雇員的名字.
5.IPlug接口也定義了Save和Print方法.定義這兩個方法的目的在于當(dāng)有需要打印或者保存數(shù)據(jù)的時候,要通知一個插件.EmployeePlug類就是用于實現(xiàn)打印和保存數(shù)據(jù)的功能的.在使用Save方法的時候,需要保存數(shù)據(jù)的位置將會在方法調(diào)用的時候提供.這里假設(shè)主程序會向用戶查詢路徑等信息.路徑信息的查詢是主程序提供給各個插件的服務(wù).對于Print方法,主程序?qū)堰x項和內(nèi)容傳遞到System.Drawing.Printing.PrintDocument類的實例.這兩種情況下,和用戶的交互操作都是一致的由主程序提供的.
反射(Reflection)
在一個插件定義好之后,下一步要做的就是查看主程序是怎么加載插件的.為了實現(xiàn)這個目標(biāo),主程序使用了反射機制.反射是.NET中用于運行時查看類型信息的.在反射機制的幫助下,類型信息將被加載和查看.這樣就可以通過檢查這個類型以判斷插件是否有效.如果類型通過了檢查,那么插件就可以被添加到主程序的界面中,就可以被用戶操作.
示例程序使用了.NET框架的三個內(nèi)置類來使用反射:System.Reflection.Assembly,System.Type,和System.Activator.
System.Reflection.Assembly類描述了.NET的程序集.在.NET中,程序集是配置單元.對于一個典型的Windows程序,程序集被配置為單一的Win32可執(zhí)行文件,并且?guī)в刑囟ǖ母郊有畔?使之適應(yīng).NET運行環(huán)境.程序集也可以配置為Win32的DLL(動態(tài)鏈接庫),同樣需要帶有.NET需要的附加信息.System.Reflection.Assembly類可以在運行的時候取得程序集的信息.這些信息包括程序集包含的類型信息.
System.Type類描述了類型定義.一個類型聲明可以是一個類,接口,數(shù)組,結(jié)構(gòu)體,或者枚舉.在加載了一個類之后,System.Type類可以被用于枚舉該類支持的方法,屬性,事件和接口.
System.Activator類用于創(chuàng)建一個類的實例.
加載插件
列表四展示了LoadPlugs方法.LoadPlugs方法在HostForm.cs中定義,是HostForm類的一個private的非靜態(tài)方法.LoadPlugs方法使用.NET的反射機制來加載可用的插件文件,并且驗證它們是否符合被主程序使用的要求,然后把它們添加到主程序的樹形顯示區(qū)中.這個方法包含了下面幾個步驟:
1.通過使用System.IO.Directory類,我們的代碼可以用通配符來查找所有的以.plug為擴展名的文件.而Directory類的靜態(tài)方法GetFiles能夠返回一個System.String類型的數(shù)組,以得到每個符合要求的文件的物理路徑.
2.在得到路徑字符串?dāng)?shù)組之后,就可以開始把文件加載到System.Reflection.Assembly實例中了.建立Asdsembly對象的代碼使用了try/catch代碼塊,這樣如果某個文件并不是一個有效地.NET程序集,就會拋出異常,程序此時將彈出一個MessageBox對話框,告訴用戶無法加載該文件.循環(huán)一直進行直到所有文件都已遍歷完成.
3.在一個程序集加載之后,代碼將遍歷所有可訪問到的類型信息,檢查是否支持了HostCommon.IPlug接口.
4.如果所有類型都支持HostCommon.IPlug接口,那么代碼繼續(xù)驗證這些類型,檢查是否支持那些已預(yù)先為插件定義好的屬性.如果沒有支持,那么一個HostCommon.PlugNotValidException類型的異常將會被拋出,同樣,主程序?qū)棾鲆粋€MessageBox,告訴用戶出錯的具體信息.循環(huán)一直進行直到所有文件都已遍歷完成.
5.最后,如果這些類型支持HostCommon.IPlug接口,也已定義了所有需要定義的屬性,那么它將被包裝為一個PlugTreeNode實例.這個實例就會被添加到主程序的樹形顯示區(qū).
實現(xiàn)
主程序框架被設(shè)計為兩個程序集.第一個程序集是Host.exe,它提供了主程序的Windows窗體界面.第二個程序集是HostCommon.dll,它提供了主程序和插件之間進行通信所需的所有類型定義.比如,IPlug接口就是在HostCommon.dll里面配置的,這樣它可以被主程序和插件等價的訪問.這兩個程序集在一個文件夾內(nèi),同樣的,附加的作為插件的程序集也需要被配置在一起.那些程序集被配置在plugs文件夾內(nèi)(主程序目錄的一個子文件夾).EmployeePlug類在Employee.plug程序集中定義,而CustomerPlug類在Customer.plug程序集中定義.這個例子指定插件文件以.plug為擴展名.事實上這些插件就是個普通的.NET類庫文件,只是通常庫文件使用.dll擴展名,這里用.plug罷了.特殊的擴展名對于程序運行是完全沒有影響的,但是它可以讓用戶更明確的知道這是個插件文件.
設(shè)計的比較
并不是一定要像例子程序這樣設(shè)計才算正確的.比如,在開發(fā)一個帶有插件的C#程序時,并不一定需要使用屬性.例子里使用了兩個自定義的屬性,其實也可以新定義兩個IPlug接口的參數(shù)來實現(xiàn).這里選擇用屬性,是因為插件的名字和它的描述在本質(zhì)上確實就是一個事物的屬性,符合規(guī)范.當(dāng)然了,使用屬性會造成主程序需要更多的關(guān)于反射的代碼.對于不同的需求,設(shè)計者總是需要做出合理的決定.
總結(jié)
示例程序被設(shè)計為盡量的簡單,以幫助理解主程序和插件之間的通信.在實際做產(chǎn)品的時候,可以做很多的改進以滿足實用要求.比如:
1.通過對IPlug接口增加更多的方法,屬性,事件,可以增加主程序和插件之間的通信點.兩者間的更多的交互操作使得插件可以做更多的事情.
2.可以允許用戶主動選擇需要加載的插件.
插件(Plug-Ins)
上面的示例程序包括了兩個插件的執(zhí)行.這些插件在EmployeePlug.cs和CustomerPlug.cs中定義.列表三展示了EmployeePlug類的部分定義.下面是一些關(guān)鍵點.
1.這個類實現(xiàn)了IPlug接口.由于主程序根本不會知道插件內(nèi)部的類是如何定義的,這非常重要,主程序需要使用IPlug接口和各個插件通信.這種設(shè)計利用了面向?qū)ο蟾拍罾锩娴?多態(tài)性".多態(tài)性允許運行時,可以通過指向基類的引用,來調(diào)用實現(xiàn)派生類中的方法.
2.這個類被兩個屬性標(biāo)識,這樣主程序可以判斷這個插件是不是有效的.在C#中,要給一個類標(biāo)識一個屬性,你得在類的定義之前聲明屬性,內(nèi)容附在括號內(nèi).
3.簡明起見,例子只是使用了直接寫入代碼的數(shù)據(jù).而如果這個插件是個正式的產(chǎn)品,那么數(shù)據(jù)總是應(yīng)該放在數(shù)據(jù)庫中或者文件中,各自所有的數(shù)據(jù)都應(yīng)該僅僅由插件本身來管理.EmployeePlug類的數(shù)據(jù)在這里用EmployeeData對象來存儲,那也是一個類型并且實現(xiàn)了IPlugData接口.IPlugData接口在IPlugData.cs中定義,它提供了最基礎(chǔ)的數(shù)據(jù)交換功能,用于主程序和插件之間的通訊.所有支持IPlugData接口的對象在下層數(shù)據(jù)變化的時候?qū)⑻峁┮粋€通知.這個通知實際上就是DataChanged事件的發(fā)生.
4.當(dāng)主程序需要顯示某個插件所含數(shù)據(jù)列表的時候,它會調(diào)用GetData方法.這個方法返回IPlugData對象的一個數(shù)組.這樣主程序就可以對數(shù)組中的每個對象使用ToString方法得到數(shù)據(jù)以建立樹的各個節(jié)點.ToString方法是EmployeeData類的一個重載,用于顯示雇員的名字.
5.IPlug接口也定義了Save和Print方法.定義這兩個方法的目的在于當(dāng)有需要打印或者保存數(shù)據(jù)的時候,要通知一個插件.EmployeePlug類就是用于實現(xiàn)打印和保存數(shù)據(jù)的功能的.在使用Save方法的時候,需要保存數(shù)據(jù)的位置將會在方法調(diào)用的時候提供.這里假設(shè)主程序會向用戶查詢路徑等信息.路徑信息的查詢是主程序提供給各個插件的服務(wù).對于Print方法,主程序?qū)堰x項和內(nèi)容傳遞到System.Drawing.Printing.PrintDocument類的實例.這兩種情況下,和用戶的交互操作都是一致的由主程序提供的.
反射(Reflection)
在一個插件定義好之后,下一步要做的就是查看主程序是怎么加載插件的.為了實現(xiàn)這個目標(biāo),主程序使用了反射機制.反射是.NET中用于運行時查看類型信息的.在反射機制的幫助下,類型信息將被加載和查看.這樣就可以通過檢查這個類型以判斷插件是否有效.如果類型通過了檢查,那么插件就可以被添加到主程序的界面中,就可以被用戶操作.
示例程序使用了.NET框架的三個內(nèi)置類來使用反射:System.Reflection.Assembly,System.Type,和System.Activator.
System.Reflection.Assembly類描述了.NET的程序集.在.NET中,程序集是配置單元.對于一個典型的Windows程序,程序集被配置為單一的Win32可執(zhí)行文件,并且?guī)в刑囟ǖ母郊有畔?使之適應(yīng).NET運行環(huán)境.程序集也可以配置為Win32的DLL(動態(tài)鏈接庫),同樣需要帶有.NET需要的附加信息.System.Reflection.Assembly類可以在運行的時候取得程序集的信息.這些信息包括程序集包含的類型信息.
System.Type類描述了類型定義.一個類型聲明可以是一個類,接口,數(shù)組,結(jié)構(gòu)體,或者枚舉.在加載了一個類之后,System.Type類可以被用于枚舉該類支持的方法,屬性,事件和接口.
System.Activator類用于創(chuàng)建一個類的實例.
加載插件
列表四展示了LoadPlugs方法.LoadPlugs方法在HostForm.cs中定義,是HostForm類的一個private的非靜態(tài)方法.LoadPlugs方法使用.NET的反射機制來加載可用的插件文件,并且驗證它們是否符合被主程序使用的要求,然后把它們添加到主程序的樹形顯示區(qū)中.這個方法包含了下面幾個步驟:
1.通過使用System.IO.Directory類,我們的代碼可以用通配符來查找所有的以.plug為擴展名的文件.而Directory類的靜態(tài)方法GetFiles能夠返回一個System.String類型的數(shù)組,以得到每個符合要求的文件的物理路徑.
2.在得到路徑字符串?dāng)?shù)組之后,就可以開始把文件加載到System.Reflection.Assembly實例中了.建立Asdsembly對象的代碼使用了try/catch代碼塊,這樣如果某個文件并不是一個有效地.NET程序集,就會拋出異常,程序此時將彈出一個MessageBox對話框,告訴用戶無法加載該文件.循環(huán)一直進行直到所有文件都已遍歷完成.
3.在一個程序集加載之后,代碼將遍歷所有可訪問到的類型信息,檢查是否支持了HostCommon.IPlug接口.
4.如果所有類型都支持HostCommon.IPlug接口,那么代碼繼續(xù)驗證這些類型,檢查是否支持那些已預(yù)先為插件定義好的屬性.如果沒有支持,那么一個HostCommon.PlugNotValidException類型的異常將會被拋出,同樣,主程序?qū)棾鲆粋€MessageBox,告訴用戶出錯的具體信息.循環(huán)一直進行直到所有文件都已遍歷完成.
5.最后,如果這些類型支持HostCommon.IPlug接口,也已定義了所有需要定義的屬性,那么它將被包裝為一個PlugTreeNode實例.這個實例就會被添加到主程序的樹形顯示區(qū).
實現(xiàn)
主程序框架被設(shè)計為兩個程序集.第一個程序集是Host.exe,它提供了主程序的Windows窗體界面.第二個程序集是HostCommon.dll,它提供了主程序和插件之間進行通信所需的所有類型定義.比如,IPlug接口就是在HostCommon.dll里面配置的,這樣它可以被主程序和插件等價的訪問.這兩個程序集在一個文件夾內(nèi),同樣的,附加的作為插件的程序集也需要被配置在一起.那些程序集被配置在plugs文件夾內(nèi)(主程序目錄的一個子文件夾).EmployeePlug類在Employee.plug程序集中定義,而CustomerPlug類在Customer.plug程序集中定義.這個例子指定插件文件以.plug為擴展名.事實上這些插件就是個普通的.NET類庫文件,只是通常庫文件使用.dll擴展名,這里用.plug罷了.特殊的擴展名對于程序運行是完全沒有影響的,但是它可以讓用戶更明確的知道這是個插件文件.
設(shè)計的比較
并不是一定要像例子程序這樣設(shè)計才算正確的.比如,在開發(fā)一個帶有插件的C#程序時,并不一定需要使用屬性.例子里使用了兩個自定義的屬性,其實也可以新定義兩個IPlug接口的參數(shù)來實現(xiàn).這里選擇用屬性,是因為插件的名字和它的描述在本質(zhì)上確實就是一個事物的屬性,符合規(guī)范.當(dāng)然了,使用屬性會造成主程序需要更多的關(guān)于反射的代碼.對于不同的需求,設(shè)計者總是需要做出合理的決定.
總結(jié)
示例程序被設(shè)計為盡量的簡單,以幫助理解主程序和插件之間的通信.在實際做產(chǎn)品的時候,可以做很多的改進以滿足實用要求.比如:
1.通過對IPlug接口增加更多的方法,屬性,事件,可以增加主程序和插件之間的通信點.兩者間的更多的交互操作使得插件可以做更多的事情.
2.可以允許用戶主動選擇需要加載的插件.