原文作者:
shawn patrick walcheske
譯者:
電子科技大學(xué) 夏桅
[引言]
在.net框架下的c#語(yǔ)言,和其他.net語(yǔ)言一樣提供了很多強(qiáng)大的特性和機(jī)制.其中一些是全新的,而有些則是從以前的語(yǔ)言和平臺(tái)上照搬過(guò)來(lái)的.然而,這種巧妙的結(jié)合產(chǎn)生了一些有趣的方法可以用來(lái)解決我們的問(wèn)題.這篇文章將講述如何利用這些奇妙的特性,用插件(plug-ins)機(jī)制建立可擴(kuò)展的解決方案.后面也將提供一個(gè)簡(jiǎn)要的例子,你甚至可以用這個(gè)東西來(lái)替換那些已經(jīng)在很多系統(tǒng)中廣泛使用的獨(dú)立的程序.在一個(gè)系統(tǒng)中,可能有很多程序經(jīng)常需要進(jìn)行數(shù)據(jù)處理.可能其中有一個(gè)程序用于處理雇員的信息,而另一個(gè)用來(lái)管理客戶(hù)關(guān)系.在大多數(shù)情況下,系統(tǒng)總是被設(shè)計(jì)為很多個(gè)獨(dú)立的程序,他們之間很少有交互,經(jīng)常使用復(fù)制代碼的辦法來(lái)共享.而實(shí)際上這樣的情況可以把那些程序設(shè)計(jì)為插件,再用一個(gè)單一的程序來(lái)管理這些插件.這種設(shè)計(jì)可以讓我們更好的在不同的解決方案中共享公用的方法,提供統(tǒng)一的感觀(guān).
圖片一是一個(gè)例子程序的截圖.用戶(hù)界面和其他常見(jiàn)的程序沒(méi)有什么不同.整個(gè)窗體被垂直的分割為兩塊.左邊的窗格是個(gè)樹(shù)形菜單,用于顯示插件列表,在每個(gè)插件的分支下面,列出了這個(gè)插件所管理的數(shù)據(jù).而右邊的窗格則用于編輯左邊被選中的插件的數(shù)據(jù).各個(gè)插件提供各自的編輯數(shù)據(jù)的界面.圖片一展示了一個(gè)精巧的工作區(qū).
[開(kāi)始]
那么,主程序必須能夠加載插件,然后和這些插件進(jìn)行通信,這樣才能實(shí)現(xiàn)我們的設(shè)計(jì).所有這些的實(shí)現(xiàn)可以有很多不同的方法,僅取決于開(kāi)發(fā)者選擇的語(yǔ)言和平臺(tái).如果選擇的是c#和.net,那么反射(reflection)機(jī)制可以用來(lái)加載插件,并且其接口和抽象類(lèi)可以用于和插件通信.
為了更好的理解主程序和插件之間的通信,可以先了解一下設(shè)計(jì)模式.設(shè)計(jì)模式最早由erich gamma提出[1],它利用架構(gòu)和對(duì)象思想來(lái)實(shí)現(xiàn)通用的通信模型.不管組件是否具有不同的輸入和輸出,只要他們有相似的結(jié)構(gòu).設(shè)計(jì)模式可以幫助開(kāi)發(fā)者利用廣受證明的面向?qū)ο罄碚搧?lái)解決問(wèn)題.事實(shí)上它就是描述解決方案的語(yǔ)言,而不用管問(wèn)題的具體細(xì)節(jié)或者編程語(yǔ)言的細(xì)節(jié).設(shè)計(jì)模式策略的關(guān)鍵點(diǎn)在于如何把整個(gè)解決方案根據(jù)功能來(lái)分解,這種分解是通過(guò)把主程序的不同功能分開(kāi)執(zhí)行而完成的.這樣主程序和子程序之間的通信可以通過(guò)設(shè)計(jì)良好的接口來(lái)完成.通過(guò)這種分解我們立即可以得到這兩個(gè)好處:第一,軟件項(xiàng)目被分成較小的不相干的單位,工作流程的設(shè)計(jì)可以更容易,而較小的代碼片斷意味著代碼更容易建立和維護(hù).第二個(gè)好處在于改變程序行為的時(shí)候并不會(huì)關(guān)系到主程序的運(yùn)行,主程序不用關(guān)心子程序如何,他們之間只要有通用的通訊機(jī)制就足夠了.
[建立接口]
在c#程序中,接口是用來(lái)定義一個(gè)類(lèi)的功能的.接口定義了預(yù)期的方法,屬性,事件信息.為了使用接口,每個(gè)具體的函數(shù)必須嚴(yán)格按照接口的定義完成所描述的功能.列表一展示了上面例子程序的接口:iplug.這個(gè)接口定義了四個(gè)方法:getdata,geteditcontrol,save和print.這四個(gè)定義并沒(méi)有描述具體是怎么完成的,但是他們保證了這個(gè)類(lèi)支持iplug接口,也就是保證支持這些方法的調(diào)用.
[定制屬性]
在查看代碼之前,討論總是先得轉(zhuǎn)移到屬性定制上面.屬性定制是.net提供的一個(gè)非常棒的新特性之一,屬性對(duì)于所有的編程語(yǔ)言都是一種通用的結(jié)構(gòu).舉個(gè)例子,一個(gè)函數(shù)用于標(biāo)識(shí)可訪(fǎng)問(wèn)權(quán)限的public,private,或者protect標(biāo)志就是這個(gè)函數(shù)的一個(gè)屬性.屬性定制之所以如此讓人興奮,那是因?yàn)榫幊倘藛T將不再只能從語(yǔ)言本身提供的有限的屬性集中選擇.一個(gè)定制的屬性其實(shí)也是一個(gè)類(lèi),它從system.attribute繼承,它的代碼被允許是自我描述的.屬性定制可以應(yīng)用于絕大多數(shù)結(jié)構(gòu)中,包括c#里面的類(lèi),方法,事件,域和屬性等等.示例代碼片斷定義了兩個(gè)定制的屬性:plugdisplaynameattribute和plugdescriptionattribute,所有的插件內(nèi)部的類(lèi)必須支持這兩個(gè)屬性.列表二是用于定義plugdisplaynameattribute的類(lèi).這個(gè)屬性用于顯示插件節(jié)點(diǎn)的內(nèi)容.在程序運(yùn)行的時(shí)候,主程序?qū)⒖梢岳梅瓷?reflection)來(lái)取得屬性值.
[插件(plug-ins)]
上面的示例程序包括了兩個(gè)插件的執(zhí)行.這些插件在employeeplug.cs和customerplug.cs中定義.列表三展示了employeeplug類(lèi)的部分定義.下面是一些關(guān)鍵點(diǎn).
1.這個(gè)類(lèi)實(shí)現(xiàn)了iplug接口.由于主程序根本不會(huì)知道插件內(nèi)部的類(lèi)是如何定義的,這非常重要,主程序需要使用iplug接口和各個(gè)插件通信.這種設(shè)計(jì)利用了面向?qū)ο蟾拍罾锩娴?多態(tài)性".多態(tài)性允許運(yùn)行時(shí),可以通過(guò)指向基類(lèi)的引用,來(lái)調(diào)用實(shí)現(xiàn)派生類(lèi)中的方法.
2.這個(gè)類(lèi)被兩個(gè)屬性標(biāo)識(shí),這樣主程序可以判斷這個(gè)插件是不是有效的.在c#中,要給一個(gè)類(lèi)標(biāo)識(shí)一個(gè)屬性,你得在類(lèi)的定義之前聲明屬性,內(nèi)容附在括號(hào)內(nèi).
3.簡(jiǎn)明起見(jiàn),例子只是使用了直接寫(xiě)入代碼的數(shù)據(jù).而如果這個(gè)插件是個(gè)正式的產(chǎn)品,那么數(shù)據(jù)總是應(yīng)該放在數(shù)據(jù)庫(kù)中或者文件中,各自所有的數(shù)據(jù)都應(yīng)該僅僅由插件本身來(lái)管理.employeeplug類(lèi)的數(shù)據(jù)在這里用employeedata對(duì)象來(lái)存儲(chǔ),那也是一個(gè)類(lèi)型并且實(shí)現(xiàn)了iplugdata接口.iplugdata接口在iplugdata.cs中定義,它提供了最基礎(chǔ)的數(shù)據(jù)交換功能,用于主程序和插件之間的通訊.所有支持iplugdata接口的對(duì)象在下層數(shù)據(jù)變化的時(shí)候?qū)⑻峁┮粋€(gè)通知.這個(gè)通知實(shí)際上就是datachanged事件的發(fā)生.
4.當(dāng)主程序需要顯示某個(gè)插件所含數(shù)據(jù)列表的時(shí)候,它會(huì)調(diào)用getdata方法.這個(gè)方法返回iplugdata對(duì)象的一個(gè)數(shù)組.這樣主程序就可以對(duì)數(shù)組中的每個(gè)對(duì)象使用tostring方法得到數(shù)據(jù)以建立樹(shù)的各個(gè)節(jié)點(diǎn).tostring方法是employeedata類(lèi)的一個(gè)重載,用于顯示雇員的名字.
5.iplug接口也定義了save和print方法.定義這兩個(gè)方法的目的在于當(dāng)有需要打印或者保存數(shù)據(jù)的時(shí)候,要通知一個(gè)插件.employeeplug類(lèi)就是用于實(shí)現(xiàn)打印和保存數(shù)據(jù)的功能的.在使用save方法的時(shí)候,需要保存數(shù)據(jù)的位置將會(huì)在方法調(diào)用的時(shí)候提供.這里假設(shè)主程序會(huì)向用戶(hù)查詢(xún)路徑等信息.路徑信息的查詢(xún)是主程序提供給各個(gè)插件的服務(wù).對(duì)于print方法,主程序?qū)堰x項(xiàng)和內(nèi)容傳遞到system.drawing.printing.printdocument類(lèi)的實(shí)例.這兩種情況下,和用戶(hù)的交互操作都是一致的由主程序提供的.
[反射(reflection)]
在一個(gè)插件定義好之后,下一步要做的就是查看主程序是怎么加載插件的.為了實(shí)現(xiàn)這個(gè)目標(biāo),主程序使用了反射機(jī)制.反射是.net中用于運(yùn)行時(shí)查看類(lèi)型信息的.在反射機(jī)制的幫助下,類(lèi)型信息將被加載和查看.這樣就可以通過(guò)檢查這個(gè)類(lèi)型以判斷插件是否有效.如果類(lèi)型通過(guò)了檢查,那么插件就可以被添加到主程序的界面中,就可以被用戶(hù)操作.
示例程序使用了.net框架的三個(gè)內(nèi)置類(lèi)來(lái)使用反射:system.reflection.assembly,system.type,和system.activator.
system.reflection.assembly類(lèi)描述了.net的程序集.在.net中,程序集是配置單元.對(duì)于一個(gè)典型的windows程序,程序集被配置為單一的win32可執(zhí)行文件,并且?guī)в刑囟ǖ母郊有畔?使之適應(yīng).net運(yùn)行環(huán)境.程序集也可以配置為win32的dll(動(dòng)態(tài)鏈接庫(kù)),同樣需要帶有.net需要的附加信息.system.reflection.assembly類(lèi)可以在運(yùn)行的時(shí)候取得程序集的信息.這些信息包括程序集包含的類(lèi)型信息.
system.type類(lèi)描述了類(lèi)型定義.一個(gè)類(lèi)型聲明可以是一個(gè)類(lèi),接口,數(shù)組,結(jié)構(gòu)體,或者枚舉.在加載了一個(gè)類(lèi)之后,system.type類(lèi)可以被用于枚舉該類(lèi)支持的方法,屬性,事件和接口.
system.activator類(lèi)用于創(chuàng)建一個(gè)類(lèi)的實(shí)例.
[加載插件]
列表四展示了loadplugs方法.loadplugs方法在hostform.cs中定義,是hostform類(lèi)的一個(gè)private的非靜態(tài)方法.loadplugs方法使用.net的反射機(jī)制來(lái)加載可用的插件文件,并且驗(yàn)證它們是否符合被主程序使用的要求,然后把它們添加到主程序的樹(shù)形顯示區(qū)中.這個(gè)方法包含了下面幾個(gè)步驟:
1.通過(guò)使用system.io.directory類(lèi),我們的代碼可以用通配符來(lái)查找所有的以.plug為擴(kuò)展名的文件.而directory類(lèi)的靜態(tài)方法getfiles能夠返回一個(gè)system.string類(lèi)型的數(shù)組,以得到每個(gè)符合要求的文件的物理路徑.
2.在得到路徑字符串?dāng)?shù)組之后,就可以開(kāi)始把文件加載到system.reflection.assembly實(shí)例中了.建立asdsembly對(duì)象的代碼使用了try/catch代碼塊,這樣如果某個(gè)文件并不是一個(gè)有效地.net程序集,就會(huì)拋出異常,程序此時(shí)將彈出一個(gè)messagebox對(duì)話(huà)框,告訴用戶(hù)無(wú)法加載該文件.循環(huán)一直進(jìn)行直到所有文件都已遍歷完成.
3.在一個(gè)程序集加載之后,代碼將遍歷所有可訪(fǎng)問(wèn)到的類(lèi)型信息,檢查是否支持了hostcommon.iplug接口.
4.如果所有類(lèi)型都支持hostcommon.iplug接口,那么代碼繼續(xù)驗(yàn)證這些類(lèi)型,檢查是否支持那些已預(yù)先為插件定義好的屬性.如果沒(méi)有支持,那么一個(gè)hostcommon.plugnotvalidexception類(lèi)型的異常將會(huì)被拋出,同樣,主程序?qū)?huì)彈出一個(gè)messagebox,告訴用戶(hù)出錯(cuò)的具體信息.循環(huán)一直進(jìn)行直到所有文件都已遍歷完成.
5.最后,如果這些類(lèi)型支持hostcommon.iplug接口,也已定義了所有需要定義的屬性,那么它將被包裝為一個(gè)plugtreenode實(shí)例.這個(gè)實(shí)例就會(huì)被添加到主程序的樹(shù)形顯示區(qū).
[實(shí)現(xiàn)]
主程序框架被設(shè)計(jì)為兩個(gè)程序集.第一個(gè)程序集是host.exe,它提供了主程序的windows窗體界面.第二個(gè)程序集是hostcommon.dll,它提供了主程序和插件之間進(jìn)行通信所需的所有類(lèi)型定義.比如,iplug接口就是在hostcommon.dll里面配置的,這樣它可以被主程序和插件等價(jià)的訪(fǎng)問(wèn).這兩個(gè)程序集在一個(gè)文件夾內(nèi),同樣的,附加的作為插件的程序集也需要被配置在一起.那些程序集被配置在plugs文件夾內(nèi)(主程序目錄的一個(gè)子文件夾).employeeplug類(lèi)在employee.plug程序集中定義,而customerplug類(lèi)在customer.plug程序集中定義.這個(gè)例子指定插件文件以.plug為擴(kuò)展名.事實(shí)上這些插件就是個(gè)普通的.net類(lèi)庫(kù)文件,只是通常庫(kù)文件使用.dll擴(kuò)展名,這里用.plug罷了.特殊的擴(kuò)展名對(duì)于程序運(yùn)行是完全沒(méi)有影響的,但是它可以讓用戶(hù)更明確的知道這是個(gè)插件文件.
[設(shè)計(jì)的比較]
并不是一定要像例子程序這樣設(shè)計(jì)才算正確的.比如,在開(kāi)發(fā)一個(gè)帶有插件的c#程序時(shí),并不一定需要使用屬性.例子里使用了兩個(gè)自定義的屬性,其實(shí)也可以新定義兩個(gè)iplug接口的參數(shù)來(lái)實(shí)現(xiàn).這里選擇用屬性,是因?yàn)椴寮拿趾退拿枋鲈诒举|(zhì)上確實(shí)就是一個(gè)事物的屬性,符合規(guī)范.當(dāng)然了,使用屬性會(huì)造成主程序需要更多的關(guān)于反射的代碼.對(duì)于不同的需求,設(shè)計(jì)者總是需要做出合理的決定.
[總結(jié)]
示例程序被設(shè)計(jì)為盡量的簡(jiǎn)單,以幫助理解主程序和插件之間的通信.在實(shí)際做產(chǎn)品的時(shí)候,可以做很多的改進(jìn)以滿(mǎn)足實(shí)用要求.比如:
1.通過(guò)對(duì)iplug接口增加更多的方法,屬性,事件,可以增加主程序和插件之間的通信點(diǎn).兩者間的更多的交互操作使得插件可以做更多的事情.
2.可以允許用戶(hù)主動(dòng)選擇需要加載的插件.
[源代碼]
示例程序的完整的源代碼可以在這里下載.
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;
}
[關(guān)于作者]
shawn patrick walcheske是美國(guó)arizona州phoenix市的一名軟件開(kāi)發(fā)工程師.他同時(shí)是microsoft certified solution developer和sun certified programmer for the java 2 platform.你可以在這里聯(lián)系到他, [email protected]
[譯者注]
以前就考慮過(guò)在.net里面如何實(shí)現(xiàn)插件機(jī)制,做來(lái)做去總是覺(jué)得設(shè)計(jì)上不夠好.而昨天在網(wǎng)上無(wú)意中發(fā)現(xiàn)了這篇文章,寫(xiě)的實(shí)在是太棒了,所以看完之后,決定把它翻譯過(guò)來(lái),前后一共花了大概10個(gè)小時(shí)吧.翻譯的可能不太好,請(qǐng)見(jiàn)諒.文中有什么錯(cuò)誤,請(qǐng)不吝指正.
我的e-mail: [email protected]
新聞熱點(diǎn)
疑難解答
圖片精選