1. 程式人生 > >在不修改代碼的情況下無限擴展應用項目

在不修改代碼的情況下無限擴展應用項目

blog 自動搜索 自然 如何實現 frame 接口規範 行合並 article 而不是

在許多需要分模塊開發,較為復雜的應用項目(如ERP之類)中,如何做到輕松擴展,往往是一個頭疼的問題。

在傳統條件下,我們會把各個功能分布在不同的類庫中,每添加一個功能就引用一個程序集,而這種方法,我們會發現,當你每添加一個新擴展後,都要對新增的程序集進行引用,這樣也意味著,你每次都要重新編譯一次主應用程序,這一來一往,維護成本也是有的。

到了.NET 3.5時代,你可能會想到Addin,但這個方法也會帶來擴展成本。

而我們所追求的最完美解決方案是:

如果我od 們編寫完應用程序後,可以在原有程序不變的情況下,無限添加擴展就好了。也就是說,我把應用A在用戶的機器上安裝好了,後來我做了一點擴展,這個新功能我已經編譯到fc.dll類庫中了,可我不想每次升級都要把EXE文件和所有組件重新編譯,我只需要把新的fc.dll復制到應用安裝目錄下就可以了。

也許這樣一來,維護成本可以大大降低了,到了.NET 4.0時代,利用MEF框架確實可以做到以上要求,但前提條件是:

1、在開發項目前,對整個項目的結構和規範必須明確,至少整個應用程序是什麽樣子的,你必須在大腦裏面有個底。

2、編寫一個公共類庫,裏面包含所有將來要進行擴展組件的行為規範,也就是在這個公共類庫中定義所有將來可能被實現的接口,後面所有擴展的程序集都必須實現這些接口。

本文涉及的內容可能有些深奧,但願大家可以接受,接受不了也沒關系,畢竟許多東西是需要時間去掌握的。

我弄個簡單的例子吧,比如,我現在要開發一個應用,在一個窗口上點擊按鈕後,顯示對應球類的名字,如“足球”、“皮球”、“排球”等。但是,可能當初我只給出兩個選項——足球和排球,這是我在把程序給客戶前就擴展的兩個程序集,但過了幾天,我突然想起,還有“羽毛球”、“籃球”等,於是,我要為應用程序再加兩個dll,但我希望應用程序擴展後不用修改代碼,無論我將來擴展100個還是10000個dll我都不需要重新生成主程序,我只要把新的dll扔到應用程序中的Ext文件夾中就可以了。

我們來看看如何實現。

1、新建一個公共類庫,寫兩個接口,IBall接口就是與球類信息有關的類,提供擴展時實現該接口。

[csharp] view plain copy
  1. public interface IBall
  2. {
  3. string GetInformation();
  4. }


它有一個公共方法GetInformation,返回對應球類的名字,如“足球”.

另一個接口是用來描述元數據的。

[csharp] view plain copy
  1. public interface IMetaData
  2. {
  3. string BallType { get; }
  4. }

為什麽要定義這個元數據的接口呢?就是為了識別我們應用程序調用了哪個擴展。

比如,FootBall(足球)類擴展實現了IBall接口,VolleyBall(排球)類擴展也實現了IBall接口,BasketBall(籃球)類擴展也實現了IBall接口,可能以後會更多,所有的擴展都實現IBall,那麽,我們怎麽知道我們正在調用的足球?而不是籃球呢?所以,就需要這個IMetaData類,在進行擴展的導出類時,我們為每一個類型定義一下IMetaData的BallType屬性,例如,我在定義足球類時,我定義BallType為“foot ball”,在定義排球類時,把BallType設置為“volley ball”,這樣,在我們的應用程序中,就可以通過這個來判斷我們正在調用哪個擴展,當然,如果你不需要明確知道調用哪個擴展,這個元數據就可以忽略。

2、分別編寫兩個類庫,符合以下兩個條件:

a、實現IBall接口。

b、用ExportAttribute標識為導出類型。

MEF的類都是來自System.ComponentModel.Composition程序集,在需要的地方引用就行了,如何引用程序集,我就不說了,這是基礎知識。這些類分布在以下三個命名空間。

System.ComponentModel.Composition

System.ComponentModel.Composition.Hosting

System.ComponentModel.Composition.Primitives

[csharp] view plain copy
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using System.ComponentModel.Composition;
  7. using System.ComponentModel.Composition.Hosting;
  8. namespace BallLibA
  9. {
  10. /// <summary>
  11. /// 足球
  12. /// </summary>
  13. [Export(typeof(CommonLib.IBall))]
  14. [ExportMetadata("BallType","Foot Ball")]
  15. public class FootBall:CommonLib.IBall
  16. {
  17. public string GetInformation()
  18. {
  19. return "足球";
  20. }
  21. }
  22. }


在MEF中,我們不需要實現提供元數據的接口,只需要在ExportMetadata特性中直接為屬性設置值就行,運行時會自動生成實現元數據(本例是IMetaData接口)的類。

接照同樣的方法,我們再做一個類庫。

[csharp] view plain copy
  1. /// <summary>
  2. /// 排球
  3. /// </summary>
  4. [Export(typeof(CommonLib.IBall))]
  5. [ExportMetadata("BallType", "Volley Ball")]
  6. public class VolleyBall : CommonLib.IBall
  7. {
  8. public string GetInformation()
  9. {
  10. return "排球";
  11. }
  12. }

現在,我們的項目已經有兩個擴展了。

3、我們來實現我們的主應用程序,我們只需引用我們前面編寫的公共類庫中的接口即可,而擴展的dll我們不需要引用,MEF會自動尋找。因此,把所有擴展的程序集都生成dll文件,然後統一扔到與exe文件同一位置的Ext文件夾中就行了,你有1000個dll就全部扔到文件夾裏就行,MEF會自動尋找。

我們用一個WinForm程序作為主程序,如下圖所示。

技術分享圖片

在程序運行時,會根據Ext目錄下的所有擴展的dll自動發現所有程序集,然後顯示在ComboBox中,我們選擇對應的球類,然後點擊按鈕,這樣在文本框中就會對應地顯示球類的名稱。

[csharp] view plain copy
  1. using System;
  2. using System.Collections.Generic;
  3. using System.ComponentModel;
  4. using System.Data;
  5. using System.Drawing;
  6. using System.Linq;
  7. using System.Text;
  8. using System.Threading.Tasks;
  9. using System.Windows.Forms;
  10. using System.ComponentModel.Composition;
  11. using System.ComponentModel.Composition.Hosting;
  12. namespace TestApp
  13. {
  14. public partial class Form1 : Form
  15. {
  16. CompositionContainer myContainer = null;
  17. // 引入的組合類型
  18. [ImportMany]
  19. IEnumerable<Lazy<CommonLib.IBall, CommonLib.IMetaData>> mBalls;
  20. public Form1()
  21. {
  22. InitializeComponent();
  23. DirectoryCatalog catl = new DirectoryCatalog("Ext");
  24. myContainer = new CompositionContainer(catl);
  25. try
  26. {
  27. myContainer.ComposeParts(this);//組合組件
  28. }
  29. catch (Exception ex)
  30. {
  31. MessageBox.Show(ex.Message);
  32. }
  33. var resBalls = (from t in mBalls
  34. select t.Metadata.BallType).ToArray();
  35. this.comboBox1.DataSource = resBalls;
  36. }
  37. private void button1_Click(object sender, EventArgs e)
  38. {
  39. if (this.comboBox1.SelectedIndex == -1)
  40. {
  41. MessageBox.Show("請選擇一個擴展。"); return;
  42. }
  43. string ballName = this.comboBox1.SelectedItem.ToString();
  44. // 取出要執行哪個擴展程序集
  45. var ballInstance = mBalls.FirstOrDefault(x => x.Metadata.BallType == ballName);
  46. if (ballInstance != null)
  47. {
  48. this.txtResult.Text = ballInstance.Value.GetInformation();
  49. }
  50. }
  51. }
  52. }


從上面的代碼中,可以總結出MEF的用法,這方法你有興趣的話可以背下來,因為無論你用到什麽項目,思路都是一樣的。

1、聲明一個CompositionContainer變量是必須的,因為它可以用來指示當前應用程序與哪些擴展程序集進行合並。

2、在實例化CompositionContainer時,我使用DirectoryCatalog類,為什麽?因為這個類好用,你只需要告訴它你擴展的dll放在哪個文件夾就行了。它會在你指定的文件夾裏面自動找到導出的擴展類。

3、有導出類,自然就有導入類,因為我們的所有擴展都是實現IBall接口的,所以,擴展的類的導出類型應使用IBall,這樣,凡是聲明為導出類的都會被MEF發現並自動加載。

所以,導出是針對擴展的程序集而言的,那導入就好理解了,就是針對我們的主應用程序而言,像本例,WinForm應用作為主程序,所有擴展都是在這個WinForm中使用的,所以這個WinForm就必須對類型進行導入。因此才有了以下代碼。

[csharp] view plain copy
  1. // 引入的組合類型
  2. [ImportMany]
  3. IEnumerable<Lazy<CommonLib.IBall, CommonLib.IMetaData>> mBalls;

使用Lazy來延遲實例化的好處是提高性能,記住,加了Import的導入類型是不用new的,因為DirectoryCatalog在Ext文件夾下找到所有的dll都會自動實例化,這就是要用延遲實便化的原因,只有在用的時候才new,不然,如果我的擴展目錄下有100000個dll,350000000個類,那你一運行就全部實例化,那這性能估計要把內存用爆。

前面我說過,IMetaData用於標識元數據,我們不必自己去實現,而我們也不必指事實上哪個接口,因為上面代碼中,Lazy<T, TMetadata>就有兩個泛型參數,看到沒?

T是我們要導入的類型,本例中是IBall,註意,我們這裏的類型一定要是公共的接口,不是擴展的具體類,不然就實現不了無限擴展的目的,接口用途就是它有通用性。

TMetadata就是用來標識元數的類型,本例是IMetaData接口,所以,前面我為什麽不用指定IMetaData的原因,因為這裏會指定,MEF會沿著這個線索自動搜索它的屬性BallType。

在實例化CompositionContainer容器後,要記得調用擴展方法ComposeParts,告訴MEF,所有擴展的程序集將和當前實例進行組合,不然你將無法調用。

現在,你運行一個這個WinForm,你就明白了。

技術分享圖片

技術分享圖片

看到了吧,FootBall和VolleyBall類所在的兩個程序集我並沒有在項目中,引用,只是把它們扔到Ext目錄下,應用程序就自動識別了。

我們的WinForm程序不用修改一行代碼。

如果你還不信的話,我們接下來再增加一個dll,定義一個BasketyBall(籃球類),然後,把這個籃球類庫也生成一個dll,同樣扔到Ext目錄下,而WinForm程序我根本不需要改動。

[csharp] view plain copy
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. using System.ComponentModel.Composition;
  7. using System.ComponentModel.Composition.Hosting;
  8. namespace BallLibC
  9. {
  10. /// <summary>
  11. /// 籃球
  12. /// </summary>
  13. [Export(typeof(CommonLib.IBall))]
  14. [ExportMetadata("BallType","Basket Ball")]
  15. public class BasketBall:CommonLib.IBall
  16. {
  17. public string GetInformation()
  18. {
  19. return "籃球";
  20. }
  21. }
  22. }

同樣道理,把這個類庫編譯成dll,然後扔到Ext文件下,然後你再運行一下WinForm程序看看。

技術分享圖片

看到了吧,我沒有對WinForm做任何修改,只是在Ext目錄下多放了一個dll而已,運行後,程序就自動識別並找到對應的類型了。下拉列表框中就自動多了一個Basket Ball的選項了。選擇它,並單擊按鈕,這個BadketBall類就被執行了,輸出“籃球”。

以此類推,你再添加一千個一萬個dll,只要它符合IBall接口規範並設置導出,然後把這一千個一萬個dll全放到Ext目錄下,應用程序不需要做任何修改,運行後就會自動找到一千個一萬個擴展類了。

這樣一來,是不是節約了不少維護和升級成本了?MEF(Managed Extensibility Framework)強大吧?

在不修改代碼的情況下無限擴展應用項目