前言:最近去了趟外地出差,介紹推廣小組開發的框架類產品。推廣對象是本部門在項目上面的同事——1到2年工作經驗的初級程序員。在給他們介紹框架時發現很多框架設計層面的知識他們都沒有接觸過,甚至沒聽說過,這下囧了~~於是乎在想該如何跟他們解釋MEF、AOP、倉儲模式等方面的東東。本來 C#基礎系列 應該還有兩篇關於異步的沒有寫完,奈何現在要推廣這些個東西,博主打算先介紹下項目中目前用到的些技術,異步的往后有時間再做分享。C#進階系列主要圍繞MEF、AOP、倉儲模式、Automapper、WCF等展開。本篇先來介紹下MEF的基礎知識。
1、什么是MEF
先來看msdn上面的解釋:MEF(Managed Extensibility Framework)是一個用於創建可擴展的輕型應用程序的庫。 應用程序開發人員可利用該庫發現並使用擴展,而無需進行配置。 擴展開發人員還可以利用該庫輕松地封裝代碼,避免生成脆弱的硬依賴項。 通過 MEF,不僅可以在應用程序內重用擴展,還可以在應用程序之間重用擴展。
也有人把MEF解釋為“依賴注入”的一種方式,那么什么是“依賴注入”?如果這樣解釋,感覺越陷越深......根據博主的理解,了解MEF只需要抓住以下幾個關鍵點:
(1)字面意思,可擴展的framework,或者叫可擴展的庫。也就是說,使用MEF是為了提高程序的可擴展性。MEF會根據指定的導入導出自動去發現匹配的擴展,不需要進行復雜的程序配置。
(2)在設計層面上來說,為什么要使用MEF?為了“松耦合”!我們知道,程序設計有幾個原則,“高內聚,低耦合”就是其中一個。使用MEF可以幫助我們減少內庫之間的耦合。
當然,如果你之前壓根都沒有聽說過MEF,那么即使看了我上面的解釋,估計也還是雲里霧里。沒關系,如果此刻你還有興趣,看了下面的Demo,相信你會有一個初步的認識。
2、為什么要使用MEF:上面已經解釋過,為了程序的擴展和“松耦合”。
3、MEF的使用:
(1)MEF基礎導入導出的使用:
MEF的使用步驟主要分三步:宿主MEF並組合部件、標記對象的導出、對象的導入使用。
我們先來看一個Demo。
class Program2 {
//導入對象使用 [Import("chinese_hello")] public Person oPerson { set; get; } static void Main(string[] args) { var oProgram = new Program2(); oProgram.MyComposePart(); var strRes = oProgram.oPerson.SayHello("李磊"); Console.WriteLine(strRes); Console.Read(); }
//宿主MEF並組合部件 void MyComposePart() { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); var container = new CompositionContainer(catalog); //將部件(part)和宿主程序添加到組合容器 container.ComposeParts(this); } } public interface Person { string SayHello(string name); }
//聲明對象可以導出 [Export("chinese_hello", typeof(Person))] public class Chinese : Person { public string SayHello(string name) { return "你好:" + name ; } } [Export("american_hello", typeof(Person))] public class American : Person { public string SayHello(string name) { return "Hello:" + name ; } }
得到結果:
我們來分析下這段代碼:
//宿主MEF並組合部件 void MyComposePart() { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); var container = new CompositionContainer(catalog); //將部件(part)和宿主程序添加到組合容器 container.ComposeParts(this); }
這個方法表示添加當前Program2這個類到組合容器,為什么要添加到組合容器?是因為只要添加到組合容器中之后,如果該類里面有Import,MEF才會自動去尋找對應的Export。這也就是為什么使用MEF前必須要組合部件的原因。
[Export("chinese_hello", typeof(Person))] public class Chinese : Person { public string SayHello(string name) { return "你好:" + name ; } }
這里的[Export("chinese_hello", typeof(Person))]這個特性表示標記Chinese類的導出。
將Export轉到定義可以看到:
// // 摘要: // 通過在指定協定名稱下導出指定類型,初始化 System.ComponentModel.Composition.ExportAttribute 類的新實例。 // // 參數: // contractName: // 用於導出使用此特性標記的類型或成員的協定名稱,或 null 或空字符串 ("") 以使用默認協定名稱。 // // contractType: // 要導出的類型。 public ExportAttribute(string contractName, Type contractType);
這里的兩個參數:第一個表示協定名稱,如果找到名稱相同的Import,那么就對應當前的Chinese對象;第二個參數表示要導出的類型。
[Import("chinese_hello")] public Person oPerson { set; get; }
這里的chinese_hello是和Export里面的chinese_hello對應的,由此可知,每一個[Import("chinese_hello")]這種Import一定可以找到一個對應的Export,如果找不到,程序就會報異常。當然如果這里的Import如果改成[Import("american_hello")],那么oPerson肯定就對應一個American對象。
通過上面的程序可以知道,我們使用[Import]這個特性,它的底層其實就是給我們初始化了一個對象。例如上面的[Import("chinese_hello")]等價於Person oPerson=new Chinese();。看到這里可能有人就會說這個Import是多此一舉了,既然我們可以new,為什么非要用這種奇怪的語法呢,怪別扭的。其實如果我們站在架構的層面,它的好處就是可以減少dll之間的引用。這個留在下一篇來講。
(2)MEF導入導出擴展:
按照MEF的約定,任何一個類或者是接口的實現都可以通過[System.ComponentModel.Composition.Export] 屬性將其他定義組合部件(Composable Parts),在任何需要導入組合部件的地方都可以通過在特定的組合部件對象屬性上使用[System.ComponentModel.Composition.Import ]實現部件的組合,兩者之間通過契約(Contracts)進行通信。通過上面的例子我們可以知道,對象是可以通過Import和Export來實現導入和導出的,那么我們進一步擴展,對象的屬性、字段、方法、事件等是否也可以通過[ImportAttribute]進行導入呢?
class Program2 { [Import("TestProperty")] public string ConsoleTest { get; set; } static void Main(string[] args) { var oProgram = new Program2(); oProgram.MyComposePart(); Console.WriteLine(oProgram.ConsoleTest); Console.Read(); } void MyComposePart() { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); var container = new CompositionContainer(catalog); //將部件(part)和宿主程序添加到組合容器 container.ComposeParts(this); } } public class TestPropertyImport { [Export("TestProperty")] public string TestMmport { get { return "測試屬性可以導入導出"; } } }
得到結果:
由此說明,屬性也是可以導入導出的。原理與上類似。既然屬性可以,那么字段就不用演示了,它和屬性應該是類似的。
下面來看看方法是否可以呢?
class Program2 { [Import("chinese_hello")] public Person oPerson { set; get; } [Import("TestProperty")] public string ConsoleTest { get; set; } [Import("helloname")] public Action<string> TestFuncImport { set; get; } static void Main(string[] args) { var oProgram = new Program2(); oProgram.MyComposePart(); oProgram.TestFuncImport("Jim");
//Console.WriteLine(oProgram.ConsoleTest); //var strRes = oProgram.oPerson.SayHello("李磊"); //Console.WriteLine(strRes); Console.Read(); } void MyComposePart() { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); var container = new CompositionContainer(catalog); //將部件(part)和宿主程序添加到組合容器 container.ComposeParts(this); } } public class TestPropertyImport { [Export("TestProperty")] public string TestMmport { get { return "測試屬性可以導入導出"; } } [Export("helloname", typeof(Action<string>))] public void GetHelloName(string name) { Console.WriteLine("Hello:" + name); } }
由此可知,方法的導入和導出是通過匿名委托的方式實現的,那么由此類推,事件應該也是可以的,有興趣的朋友可以一試。原理和上面是一樣一樣的。
既然屬性、字段、方法、事件都可以通過Import和Export實現單一對象或變量的導入和導出,那么如果我們想要一次導入多個對象呢?嘿嘿,微軟總是體貼的,它什么都為我們考慮到了。我們來看看如何實現。
class Program2 { [ImportMany] public IEnumerable<Person> lstPerson { set; get; } static void Main(string[] args) { var oProgram = new Program2(); oProgram.MyComposePart(); Console.WriteLine(oProgram.lstPerson.Count()); Console.Read(); } void MyComposePart() { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); var container = new CompositionContainer(catalog); //將部件(part)和宿主程序添加到組合容器 container.ComposeParts(this); } } public interface Person { string SayHello(string name); } [Export(typeof(Person))] public class Chinese : Person { public string SayHello(string name) { return "你好:" + name ; } } [Export(typeof(Person))] public class American : Person { public string SayHello(string name) { return "Hello:" + name ; } }
得到的結果為2。這里有一點需要注意的,使用ImportMany的時候對應的Export不能有chinese_hello這類string參數,否則lstPerson的Count()為0.
(3)MEF的延遲加載
我們知道,當裝配一個組件的時候,當前組件里面的所有的Import的變量都自動去找到對應的Export而執行了實例化,有些時候,出於程序效率的考慮,不需要立即實例化對象,而是在使用的時候才對它進行實例化。MEF里面也有這種延遲加載的機制。
class Program2 { [Import("chinese_hello")] public Person oPerson { set; get; } [Import("american_hello")] public Lazy<Person> oPerson2 { set; get; }
static void Main(string[] args) { var oProgram = new Program2(); oProgram.MyComposePart(); var strRes = oProgram.oPerson.SayHello("李磊"); var strRes2 = oProgram.oPerson2.Value.SayHello("Lilei"); Console.WriteLine(strRes); Console.Read(); } void MyComposePart() { var catalog = new AssemblyCatalog(Assembly.GetExecutingAssembly()); var container = new CompositionContainer(catalog); //將部件(part)和宿主程序添加到組合容器 container.ComposeParts(this); } } public interface Person { string SayHello(string name); } [Export("chinese_hello", typeof(Person))] public class Chinese : Person { public string SayHello(string name) { return "你好:" + name ; } } [Export("american_hello", typeof(Person))] public class American : Person { public string SayHello(string name) { return "Hello:" + name ; } }
通過調試可知,當程序運行到var strRes = oProgram.oPerson.SayHello("李磊");這一行的時候
oPerson對象已經實例化了,而oPerson2.Value對象沒有實例化,當程序執行var strRes2 = oProgram.oPerson2.Value.SayHello("Lilei")這一句的時候,oPerson2.Value對象才進行實例化。這種需要在某些對程序性能有特殊要求的情況下面有一定的作用。
講到這里,我們再來看前面關於理解MEF的兩個關鍵點:
(1)可擴展的庫:由於MEF允許通過Import的方式直接導入對象、屬性、方法等,試想,有人開發了一個組件,他們事先定義好了一系列的導出(Export),我們只需要將它的組件引進來,使用Import的方式按照他們Export的約定導入對象即可,不用做其他復雜的配置。
(2)能更好的實現“松耦合”:比如我們項目按照面向接口編程的方式這樣分層:UI層、BLL接口層、BLL實現層......UI層只需要引用BLL接口層即可,我們在BLL實現層里面定義好Export的導出規則,然后再UI層里面使用Import導入BLL實現層的對象即可,這樣UI層就不需要添加BLL實現層的引用。減少了dll之間的依賴。
以上就是MEF的一些基礎用法。當然在實際使用中可能不會這么簡單,但是再復雜的用法都是在這些簡單基礎上面擴展起來的。后面還有兩篇會繼續分享MEF在項目設計層面的用法以及帶來的好處。歡迎各位拍磚斧正~~