依賴注入是一個過程,就是當一個類需要調用另一個類來完成某項任務的時候,在調用類里面不要去new被調用的類的對象,而是通過注入的方式來獲取這樣一個對象。具體的實現就是在調用類里面有一個被調用類的接口,然后通過調用接口的函數來完成任務。比如A調用B,而B實現了接口C,那么在A里面用C定義一個變量D,這個變量的實例不在A里面創建,而是通過A的上下文來獲取。這樣做的好處就是將類A和B分開了,他們之間靠接口C來聯系,從而實現對接口編程。
依賴注入最常用的兩種方式是setter注入和構造函數注入。
setter注入:
就是在類A里面定義一個C接口的屬性D,在A的上下文通過B實例化一個對象,然后將這個對象賦值給屬性D。主要就是set 與 get
構造函數注入:
就是在創建A的對象的時候,通過參數將B的對象傳入到A中。
還有常用的注入方式就是工廠模式的應用了,這些都可以將B的實例化放到A外面,從而讓A和B沒有關系。還有一個接口注入,就是在客戶類(A)的接口中有一個服務類(B)的屬性。在實例化了這個接口的子類后,對這個屬性賦值,這和setter注入一樣。
MEF:
(一)、
下面重點介紹C#中實現依賴注入的一種組件MEF。先看一個簡單的例子
創建一個控制台項目,添加一個接口IBookService:
然后創建一個類MusicBookService來實現這個接口。下面的這個Export的作用后面再說。
創建一個客戶類,在客戶類中要調用MusicBookService中的函數來完成任務
namespace DependencyInjection.MEF { class MusicBookClient { [Import] public IBookService Service { get; set; } public static void Mef() { MusicBookClient pro = new MusicBookClient(); pro.Compose(); if (pro.Service != null) { Console.WriteLine(pro.Service.GetBookName()); } Console.Read(); } private void Compose() { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());//反射 CompositionContainer container = new CompositionContainer(catalog); container.ComposeParts(this); } } }
然后在main函數中調用這個MusicBookClient.Mef();運行程序就會看到音樂書籍這幾個字了。
在MusicBookClient中,按照以前的做法有3種:在Mef中實例化Service;定義一個參數為IBookService的構造函數,在創建MusicBookClient對象的時候將Service實例化;在main函數中實例化一個MusicBookService,然后賦值給MusicBookClient的service屬性。
但是看上面的這幾段代碼,沒有發現實例化MusicBookService的地方,但是確實在MusicBookClient中調用了MusicBookService的函數。這就是MEF組件來實現依賴注入的特殊之處。這個應該也是用的反射技術,但是通過MEF用起來要簡單的多。
現在再來看看上面的[Export(typeof(IBookService))],這句的作用是將類MusicBookService按照類型IBookService導出,如果沒有指定類型,那么將按照object導出。導出之后,看MusicBookClient類中,有個[Import],這句的作用是將剛剛導出的MusicBookService導入,下面的Compose方法,實例化CompositionContainer來實現組合。這整個過程都是MEF組件來完成,我們不用去關心它怎么做到的。但是有一點要注意,實現接口的類,必須有無參數的構造函數,否則會報錯
通過上面的代碼可以對MEF有個初步的認識。但是如果有多個類實現了IBookService,也和上面一樣用[Export(typeof(IBookService))],那么再運行代碼的時候就會報錯,因為系統不知道你要導入的是哪個具體的類。下面就來介紹一下這種情況的處理。
(二)、
接口還是那個接口,不變,現在重新創建接口的實現類和客戶類:
[Export("MathBookService", typeof(IBookService))] class MathBookService : IBookService { public string GetBookName() { return "數學書籍"; } } [Export("ChineseBookService", typeof(IBookService))] class ChineseBookService : IBookService { public string GetBookName() { return "語文書籍"; } }
現在創建了兩個類來實現接口,但是在export屬性的構造函數就必須要指定一個名稱,這個名稱可以隨意指定,而且可以重復,但最好還是別亂起。
客戶類BookClient1:這里可以看到,import也用了上面取的名字了,在main函數中調用Mef1,輸出的是語文書籍。這里的Compose函數和上面的是一樣的。
[Import("ChineseBookService")] public IBookService Service { set; get; } public static void Mef1() { BookClient1 pro = new BookClient1(); pro.Compose(); Console.WriteLine( pro.Service.GetBookName()); Console.Read(); }
剛才說了,export屬性的構造函數里面取的名字可以重復,那么現在我們來看看這種情況,再創建一個類,實現接口IBookService:
看到這里的export的第一個參數和MathBookService類的一樣,名字重復了。
[Export("MathBookService", typeof(IBookService))] class MyMathBookService : IBookService { public string GetBookName() { return "數學書籍1"; } }
在客戶類BookClient1中添加如下代碼:
[ImportMany("MathBookService")] public IEnumerable<IBookService> Services { get; set; } public static void Mef() { BookClient1 pro = new BookClient1(); pro.Compose(); if (pro.Services != null) { foreach (var s in pro.Services) { Console.WriteLine(s.GetBookName()); } } Console.Read(); }
注意,這里不是用 的import,而是ImportMany,並且service也不是原來的那樣了,而是一個集合。這個機會包含了所有取名為MathBookService的類的對象。
在main函數中調用Mef函數,會輸出兩行文字。
注意:IEnumerable<T>中的類型必須和類的導出類型匹配,如類上面標注的是[Exprot(typeof(object))],那么就必須聲明為IEnumerable<object>才能匹配到導出的類。如果不指定類型,默認是object
(三)、
前面導出的都是類,那么方法和屬性能不能導出呢???答案是肯定的,下面就來說下MEF是如何導出方法和屬性的。
接口還是不變,重新定義接口的實現類和客戶類:
class HistoryBookService : IBookService { //導出私有屬性 [Export(typeof(string))] private string _privateBookName = "Private History BookName"; //導出公有屬性 [Export(typeof(string))] public string _publicBookName = "Public History BookName"; //導出公有方法 [Export(typeof(Func<string>))] public string GetBookName() { return "歷史書籍"; } //導出私有方法 [Export(typeof(Func<int, string>))] private string GetBookPrice(int price) { return "$" + price; } }
客戶類:
class BookClient2 { //導入屬性,這里不區分public還是private [ImportMany] public List<string> InputString { get; set; } //導入無參數方法 [Import] public Func<string> methodWithoutPara { get; set; } //導入有參數方法 [Import] public Func<int, string> methodWithPara { get; set; } public static void Mef() { BookClient2 c2 = new BookClient2(); c2.Compose(); foreach (var str in c2.InputString) { Console.WriteLine(str); } //調用無參數方法 if (c2.methodWithoutPara != null) { Console.WriteLine(c2.methodWithoutPara()); } //調用有參數方法 if (c2.methodWithPara != null) { Console.WriteLine(c2.methodWithPara(3000)); } Console.Read(); } private void Compose() { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly());//反射 CompositionContainer container = new CompositionContainer(catalog); container.ComposeParts(this); } }
在main函數中調用BookClient2.Mef();,運行后:
至此,MEF組件的用法基本介紹完了,下面看看MEF在項目中如何使用。
重新建一個控制台項目,項目結構如下:
BankInterface是接口項目,BankOfChina是一個類庫項目,MEFDemo是主項目,后兩者需要引用接口項目。
接口項目中定義一個接口:
public interface ICard { //賬戶金額 double Money { get; set; } //獲取賬戶信息 string GetCountInfo(); //存錢 void SaveMoney(double money); //取錢 void CheckOutMoney(double money); }
BankOfChina項目中定義一個類ZHCard,實現ICard接口:
namespace BankOfChina { [Export(typeof(ICard))] public class ZHCard : ICard { public string GetCountInfo() { return "中國銀行"; } public void SaveMoney(double money) { this.Money += money; } public void CheckOutMoney(double money) { this.Money -= money; } public double Money { get; set; } } }
主項目:
class Program { [ImportMany(typeof(ICard))] public IEnumerable<ICard> cards { get; set; } static void Main(string[] args) { Program pro = new Program(); pro.Compose(); foreach (var c in pro.cards) { Console.WriteLine(c.GetCountInfo()); } Console.Read(); } private void Compose() { var catalog = new DirectoryCatalog("Cards"); var container = new CompositionContainer(catalog); container.ComposeParts(this); } }
注意到Compose函數,這里的和上面的有點不一樣,在上面的代碼里面獲取的是當前項目所在的程序集,而這里呢是獲取指定目錄中的所有dll文件,其目的都是為了用反射創建對象。
然后先編譯一遍項目,在主項目的Debug文件夾下面創建一個cards文件夾,為什么是cards呢,因為代碼里面指定的是這個名字。然后將BankOfChina項目編譯的dll放到里面。然后運行才可以正確輸出信息(畢竟我們沒有引用那個項目)
運行后看到輸出的內容是中國銀行。
整個項目到此應該是完整了,現在的問題是,我們需要對項目進行擴展,需要添加一個工商銀行。怎么擴展呢,如果不用MEF組件,按照原來的方式,肯定是要重新編譯主項目的,因為要修改主項目嘛。但是現在用了MEF組件的依賴注入功能,就不用了。
新建一個項目BankOfICBC,這個項目和BankOfChina基本是一樣的。
namespace BankOfICBC { [Export(typeof(ICard))] public class ICBCCard : ICard { public string GetCountInfo() { return "工商銀行"; } public void SaveMoney(double money) { this.Money += money; } public void CheckOutMoney(double money) { this.Money -= money; } public double Money { get; set; } } }
項目寫完之后,這里可以只編譯這一個項目,然后將編譯好的BankOfICBC.dll放到cards文件夾。然后運行程序,會輸出:中國銀行,工商銀行。這兩行文字。如果要擴展其他的銀行的,都可以按照這樣的方式。這就完美的實現了只擴展,不修改的原則。
但是這里還有一個問題,就是在主項目MEFDemo的main函數中,我們無法知道pro.cards中的每個對象具體是哪個,也就無法分別作出處理。這就需要重新定義export特性了:
在接口項目中添加特性類ExportCardAttribute: 注意,這里的構造函數用的是無參的,然后調用了父類的構造函數,但是卻傳遞了一個參數,這里寫死了,本來打算寫一個有參的構造函數,像注釋的那樣,但是好像不行。
namespace BankInterface { /// <summary> /// AllowMultiple = false,代表一個類不允許多次使用此屬性 /// </summary> [MetadataAttribute] [AttributeUsage(AttributeTargets.Class, AllowMultiple = false)] public class ExportCardAttribute : ExportAttribute { public ExportCardAttribute() : base(typeof(ICard)) { } //public ExportCardAttribute(Type t) : base(t) //{ //} public string CardType { get; set; } } }
在這個自定義的特性中,添加了一個屬性CardType,用來當作一個區分標記。
添加一個接口:
public interface IMetaData { string CardType { get; } }
然后修改BankOfChina項目:
namespace BankOfChina { //[Export(typeof(ICard))] [ExportCard( CardType = "BankOfChina")] public class ZHCard : ICard { public string GetCountInfo() { return "中國銀行"; } public void SaveMoney(double money) { this.Money += money; } public void CheckOutMoney(double money) { this.Money -= money; } public double Money { get; set; } } }
主項目:
class Program { //[ImportMany(typeof(ICard))] //public IEnumerable<ICard> cards { get; set; } //其中AllowRecomposition=true參數就表示運行在有新的部件被裝配成功后進行部件集的重組. [ImportMany(AllowRecomposition = true)] public IEnumerable<Lazy<ICard, IMetaData>> cards { get; set; } static void Main(string[] args) { Program pro = new Program(); pro.Compose(); foreach (var c in pro.cards) { if (c.Metadata.CardType == "BankOfChina") { Console.WriteLine("這是中國銀行卡"); Console.WriteLine(c.Value.GetCountInfo()); } else if (c.Metadata.CardType == "NongHang") { Console.WriteLine("這是農行卡"); Console.WriteLine(c.Value.GetCountInfo()); } } Console.Read(); } private void Compose() { var catalog = new DirectoryCatalog("Cards"); var container = new CompositionContainer(catalog); container.ComposeParts(this); } }
記得要重新編譯BankOfChina項目,然后將dll放到cards文件夾。