小引
八年前(2001 年),我曾寫過一篇標題為「DLL 應用 - 設計可抽換的模組」的文章,當時的範例是以 Delphi 實作,之後經過一些修改,也成為自己開發 Windows 應用程式的主要框架。後來轉到 .NET 平台,又將此範例分別改寫成 Delphi.NET 和 C# 版本,並於 .NET Magazine 上發表類似的文章,標題是「設計動態載入的 Plug-in 應用程式」,這時候已經是 2005 年了。如今又過了四年,因為 C# 4.0 的 dynamic 型別,便想把這個範例拿出來改一下,看看有甚麼不一樣的地方。接著會先簡單介紹一下這個框架的基本概念,並示範「動態載入組件,靜態繫結方法呼叫」的寫法。最後再將範例程式改成使用 C# 4.0 動態型別(動態繫結),並比較兩種方式的執行時間。雖然已經知道動態繫結一定比較慢,但結果竟差了十倍之多,還是有點驚訝。
簡介
這次的範例程式雖然和之前的文章採用類似的作法,但去掉了 Windows Forms 的部分,也就是說,僅保留動態載入 DLL 與呼叫 DLL 內含物件的部分,成為更一般化的框架。前面已經有舊文連結,這裡就不重複太多細節,先看一下這個框架的套件圖好了:
圖 1:套件圖
裡面的 MainApp 就是主程式,Plugin1 和 Plugin2 分別代表可動態載入的模組(DLL 組件)。而 PluginInterface(也是 DLL 組件)就是讓主程式和各 DLL 模組「有點黏又不會太黏」的膠水介面。簡單地說,它的功能主要在避免主程式直接參考各 DLL 模組,藉以降低彼此的耦合度。PluginInterface 可說是主程式和 DLL 模組之間的合約。
以此框架來實作可抽換 DLL 模組時,有三項主要的工作:定義膠水介面、建立 DLL 模組、在主程式中載入並呼叫 DLL 模組中的物件。
Part I:定義膠水介面
這個介面定義了主程式與其他擴充模組之間的合約。我通常將此膠水介面命名為 IPlugin,並且編譯成一個獨立的 DLL 組件。這裡將它命名為 PluginInterface.dll。參考以下程式碼:
程式碼列表 1:IPlugin 介面
1: namespace PluginInterface
2: {
3: public interface IPlugin
4: {
5: void Execute();
6: }
7: }
Part II:建立可抽換模組
這裡簡單描述一下建立一個可抽換 DLL 專案的步驟:
- 建立一個新的 Class Library 專案,命名為 Plugin1。
- 加入組件參考:PluginInterface.dll。
- 建立一個類別:PluginClass。此類別必須實作 IPlugin 介面,參考程式碼列表 2。
1: using PluginInterface;
2:
3: namespace Plugin1
4: {
5: public class PluginClass : IPlugin
6: {
7: public void Execute()
8: {
9: Console.WriteLine("Inside Execute(): " + DateTime.Now.ToString());
10: }
11: }
12: }
Execute 方法只有一行程式碼,用來顯示當時的時間,這可以幫助我們觀察組件載入後,動態呼叫物件方法時總共花了多少時間。
Part III:在主程式中動態呼叫 DLL 方法
首先,主程式專案也要加入 PluginInterface.dll 組件參考,然後在程式中利用 Reflection 機制動態載入組件(註1),並建立組件中的物件,然後轉型為 IPlugin 介面參考,再透過此介面參考來呼叫物件的方法。參考程式碼列表 3。
程式碼列表 3:主程式動態載入與呼叫 DLL 方法
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Reflection;
6:
7: namespace Main
8: {
9: class Program
10: {
11: static void Main(string[] args)
12: {
13: Assembly asmb = Assembly.LoadFrom("Plugin1.dll");
14:
15: DateTime beginTime = DateTime.Now;
16: Console.WriteLine("Begin time: " + beginTime.ToString());
17:
18: IPlugin obj = (IPlugin) asmb.CreateInstance("Plugin1.PluginClass");
19: obj.Execute();
20:
21: DateTime endTime = DateTime.Now;
22: Console.WriteLine("End time: " + beginTime.ToString());
23: Console.WriteLine("Total: " + (endTime - beginTime).ToString());
24: }
25: }
26: }
Begin time: 2/6/2009 2:53:50 AM Inside Execute(): 2/6/2009 2:53:50 AM End time: 2/6/2009 2:53:50 AM Total: 00:00:00.1213872
程式計算的執行時間不到 0.2 秒,這並未包含載入組件的時間,而是從建立 plugin 物件開始,直到呼叫的 Execute 方法結束為止。
改用 C# 4.0 動態型別
若使用 C# 4.0 dynamic 型別,在建立物件時就毋需轉型成 IPlugin 介面,故載入 DLL 和呼叫物件方法的部分可改成這樣:
Assembly asmb = Assembly.LoadFrom("Plugin1.dll");
dynamic obj = asmb.CreateInstance("Plugin1.PluginClass");
obj.Execute();
另一個缺點是執行速度較慢。以下是程式改寫後的執行結果:
Begin time: 2/6/2009 2:56:39 AM Inside Execute(): 2/6/2009 2:56:49 AM End time: 2/6/2009 2:56:39 AM Total: 00:00:10.3399824
執行時間和原先靜態繫結的版本竟然差了 10 倍!觀察多次執行的結果,最快也要九秒。我的測試環境是用 Virtual PC 2007 跑 Windows Server 2008(兩個範例程式都是在相同環境上執行)。
靜態繫結 vs. 動態繫結
若採用動態型別,在這個例子當中給我的感覺是主程式和抽換模組之間的耦合更寬鬆,不像膠水介面那樣黏得那麼緊。因為各抽換模組中的類別並不一定要實作 IPlugin 介面,反正只要該類別有提供與 IPlugin 介面相容的方法--更精確地說,只要 .NET runtime 在執行時能夠繫結該方法--主程式就可以順利呼叫它。換言之,PluginInterface 變得有點「僅供參考」的味道了。
若使用靜態繫結的方式,即以膠水介面來銜接主程式和各個擴充模組,三種角色之間的關係當然就緊密一些。即使膠水介面本身非常單純(不包含實作),可是一旦 IPlugin 有變動,例如:增加或移除某個方法,那麼主程式和所有擴充模組就必須重新編譯。此作法除了呼叫方法時比動態繫結還快,另一個明顯的好處是寫程式時有 IntelliSense 的協助,腦袋就不用去記 IPlugin 有哪些方法了。
小結
綜合以上的討論,就動態載入 DLL 模組這個場合,個人還是偏向使用靜態繫結的膠水介面。因為使用 dynamic 型別對此框架所帶來的程式撰寫上的方便並不多,執行速度卻比靜態繫結慢很多。註1:使用此框架時需注意,.NET DLL 組件一旦載入,就會一直留在記憶體中,直到載入它的主程式結束為止。若要更靈活運用記憶體,就必須使用把 DLL 組件載入到不同的 app domain。
你好:
回覆刪除多年前就看過您的文章:DLL 應用 - 設計可抽換的模組,使用delphi去做模組切割。
請教你在.NET 之下,有類似delphi用bpl的方式去作team work、系統等等的切割嗎?
還是一樣只能用dll做呢?
謝謝
在 .NET 之下用 Delphi 的 bpl 啊,印象中好像沒有。後來我用 Delphi.NET 時,也是做成 DLL。由於時間有點遠,且機器上已經沒有裝 Delphi.NET,我也不是很確定它能否邊譯出 .NET bpl。若你有裝 Delphi .NET,不妨看看 .NET 專案模板裡面有沒有這樣的選項。若有的話,應該是 OK 的。
回覆刪除謝謝回應
回覆刪除那請教一下
微軟的Dot NET是用什麼方式去做模組的切割?
在 .NET 平台也同樣可以用 DLL 切割模組,只是它的 DLL 是 .NET 組件,跟傳統的 Win32 DLL 不同。
回覆刪除再次感謝你的回應
回覆刪除再次請教
在.NET之下,3-tier or Multi tier的架構是要用什麼技術?
以前有聽過remoting這個名詞
不知道這個東西跟上述的架構有沒有關係
有關係的。.NET 分散式應用程式架構可以使用的技術包括 Web services、COM+、Remoting 等。這些技術到了 .NET 3.0 之後已經進一步整合成一致的程式設計模型,也就是 Windows Communication Foundation。
回覆刪除