摘要:git
DI(IoC)是當前軟件架構設計中比較時髦的技術。DI(IoC)可使代碼耦合性更低,更容易維護,更容易測試。如今有不少開源的依賴反轉的框架,Ninject是其中一個輕量級開源的.net DI(IoC)框架。目前已經很是成熟,已經在不少項目中使用。這篇文章講DI概念以及使用它的優點。使用一個簡單的例子,重構這個例子讓他逐步符合DI設計原則。github
思考和設計代碼的方法遠好比何使用工具和技術更重要。– Mark Seemann編程
一、什麼是DI(依賴反轉)架構
DI(依賴反轉)是一個軟件設計方面的技術,經過管理依賴組件,提升軟件應用程序的可維護性。用一個實際的例子來描述什麼是DI以及DI的要素。框架
定義一個木匠類Carpenter,木匠對象(手裏)有工具Saw對象,木匠有製造椅子MakeChair方法。MakeChair方法使用saw對象的Cut方法來製做椅子。函數
1 class Carpenter 2 { 3 Saw saw = new Saw(); 4 void MakeChair() 5 { 6 saw.Cut(); 7 // ... 8 } 9 }
定義一個手術醫生類,手術醫生對象有手術鉗Forceps對象,手術醫生作手術方法Operate。Operate方法使用手術鉗對象的Grab方法來作手術。手術醫生不須要知道他用的手術鉗去哪裏找,這是他助理的任務。他只須要關注作手術這一個關注點就好了。工具
1 class Surgeon 2 { 3 private Forceps forceps; 4 5 // The forceps object will be injected into the constructor 6 // method by a third party while the class is being created. 7 public Surgeon(Forceps forceps) 8 { 9 this.forceps = forceps; 10 } 11 12 public void Operate() 13 { 14 forceps.Grab(); 15 //... 16 } 17 }
上面兩個例子木匠和醫生都依賴於一個工具類,他們須要的工具是他們的依賴組件。依賴反轉是指如何得到他們須要的工具的過程。第一個例子,木匠和鋸子強依賴。第二個例子,醫生的構造函數將他跟手術鉗產生了依賴。測試
Martin Fowler給控制反轉(IoC)下的定義是:Ioc是一種編程方式,這種編程方式使用框架來控制流程而不是經過你本身寫的代碼。比較處理事件和調用函數來理解IoC。當你本身寫代碼調用框架裏的函數時,你在控制流程,由於你本身決定調用函數的順序。可是使用事件時,你將函數綁定到事件上,而後觸發事件,經過框架反過來調用函數。這時候控制反轉到由框架來定義而不是你本身手寫代碼。DI是一個具體的IoC類型。組件不須要關心它本身的依賴項,依賴關係由框架來提供。實際上,根據Mark Seemann所說,DI in .NET,IoC是一個很寬的概念,不侷限於DI,儘管他們兩個概念常常互相通用。用好萊塢一句著名的臺詞來描述IoC就是:「不要找咱們,咱們來找你」。ui
二、 DI是如何工做的this
每個軟件都不可避免地改變。當新的需求到來的時候,你修改你的代碼致使代碼量增長。維護你的代碼的重要性變得很明顯,一個可維護性差的軟件系統是不可能進行下去的。一個指導設計可維護性代碼的設計原則叫Separation of Concerns(SoC)【中文:分離關注點】。SoC是一個寬泛的概念而不只限於軟件設計。在軟件組件設計方面,SoC設計一些不一樣的類,這些類各自有本身單獨的責任。在上一個手術醫生例子中,找工具和作手術是兩個不一樣的關注點,分離他們爲兩個不一樣的關注點是開發可維護性的代碼的一個前提。
SoC不能必然產生一個可維護性的代碼,若是這些關注點相互之間的代碼很緊密的耦合在一塊兒。
儘管手術醫生在作手術的過程當中須要不少不一樣類型的手術鉗,可是他不必說具體哪種是他須要的。他只須要說他要手術鉗,他的助理來決定哪一個手術鉗是他最須要的。若是醫生說的具體的那個手術鉗暫時沒有,助手能夠給他提供另外一個合適的,由於助手知道只要手術鉗合適醫生並不關心是哪一種類型的。換句話說,手術醫生不是跟手術鉗緊密耦合在一塊兒的。
對接口編程,而不是對具體實現編程。
咱們用抽象元素(接口或類)來實現依賴,而不用具體類。咱們就可以很容易地替換具體的依賴類而不影響上層的調用組件。
1 class Surgeon 2 { 3 private IForceps forceps; 4 5 public Surgeon(IForceps forceps) 6 { 7 this.forceps = forceps; 8 } 9 10 public void Operate() 11 { 12 forceps.Grab(); 13 //... 14 } 15 }
類Surgeon如今依賴於接口IForceps,而不用關心在構造函數中注入的對象具體的類型。C#編譯器可以保證傳入構造函數的對象的類型實現了IForceps接口而且有Grab方法。下面的代碼是上層調用。
1 var forceps = assistant.Get<IForceps>(); 2 var surgeon = new Surgeon (forceps);
由於Surgeon類依賴IForceps接口而不是具體的類,咱們可以自由地初始化任何實現了IForceps接口的類對象做爲他的助手。
經過對接口編程和分離關注點,咱們獲得了一個可維護性的代碼。
三、第一個DI應用程序
首先建立一個服務類,在這個服務類裏關注點沒有被分離。而後,一步一步改進程序的可維護性。第一步分離關注點,而後面向接口編程,使程序鬆耦合。最後,獲得第一個DI應用程序。
服務類主要的責任是使用提供的信息發送郵件。
1 using System.Net.Mail; 2 3 namespace Demo.Ninject 4 { 5 public class MailService 6 { 7 public void SendEmail(string address, string subject, string body) 8 { 9 var mail = new MailMessage(); 10 mail.To.Add(address); 11 mail.Subject = subject; 12 mail.Body = body; 13 var client = new SmtpClient(); 14 // Setup client with smtp server address and port here 15 client.Send(mail); 16 } 17 } 18 }
而後給程序添加日誌功能。
1 using System; 2 using System.Net.Mail; 3 4 namespace Demo.Ninject 5 { 6 public class MailService 7 { 8 public void SendEmail(string address, string subject, string body) 9 { 10 Console.WriteLine("Creating mail message..."); 11 var mail = new MailMessage(); 12 mail.To.Add(address); 13 mail.Subject = subject; 14 mail.Body = body; 15 var client = new SmtpClient(); 16 // Setup client with smtp server address and port here 17 Console.WriteLine("Sending message..."); 18 client.Send(mail); 19 Console.WriteLine("Message sent successfully."); 20 } 21 } 22 }
過了一會後,咱們發現給日誌信息添加時間信息頗有用。在這個例子裏,發送郵件和記錄日誌是兩個不一樣的關注點,這兩個關注點同時寫在了同一個類裏面。若是要修改日誌功能必需要修改MailService類。所以,爲了給日誌添加時間,須要修改MailService類。因此,讓咱們重構這個類分離添加日誌和發送郵件這兩個關注點。
1 using System; 2 using System.Net.Mail; 3 4 namespace Demo.Ninject 5 { 6 public class MailService 7 { 8 private ConsoleLogger logger; 9 public MailService() 10 { 11 logger = new ConsoleLogger(); 12 } 13 14 public void SendMail(string address, string subject, string body) 15 { 16 logger.Log("Creating mail message..."); 17 var mail = new MailMessage(); 18 mail.To.Add(address); 19 mail.Subject = subject; 20 mail.Body = body; 21 var client = new SmtpClient(); 22 // Setup client with smtp server address and port here 23 logger.Log("Sending message..."); 24 client.Send(mail); 25 logger.Log("Message sent successfully."); 26 } 27 } 28 29 class ConsoleLogger 30 { 31 public void Log(string message) 32 { 33 Console.WriteLine("{0}: {1}", DateTime.Now, message); 34 } 35 } 36 }
類ConsoleLogger只負責記錄日誌,將記錄日誌的關注點從MailService類中移除了。如今,就能夠在不影響MailService的條件下修改日誌功能了。
如今,新需求來了。須要將日誌寫在Windows Event Log裏,而不寫在控制檯。看起來須要添加一個EventLog類。
1 class EventLogger 2 { 3 public void Log(string message) 4 { 5 System.Diagnostics.EventLog.WriteEntry("MailService", message);6 } 7 }
儘管發送郵件和記錄日誌分離到兩個不一樣的類,MailService仍是跟ConsoleLogger類緊密耦合,若是要換一種日誌方式必需要修改MailService類。咱們離打破MailService和Logger的耦合僅一步之遙。須要引入依賴接口而不是具體類。
1 public interface ILogger 2 { 3 void Log(string message); 4 }
ConsoleLogger和EventLogger都繼承ILogger接口。
1 class ConsoleLogger : ILogger 2 { 3 public void Log(string message) 4 { 5 Console.WriteLine("{0}: {1}", DateTime.Now, message); 6 } 7 } 8 9 class EventLogger : ILogger 10 { 11 public void Log(string message) 12 { 13 System.Diagnostics.EventLog.WriteEntry("MailService", message); 14 } 15 }
如今能夠移除對具體類ConsoleLogger的引用,而是使用ILogger接口。
1 private ILogger logger; 2 public MailService(ILogger logger) 3 { 4 this.logger = logger; 5 }
在此時,咱們的類是鬆耦合的,能夠自由地修改日誌類而不影響MailService類。使用DI,將建立新的Logger類對象的關注點(建立具體哪個日誌類對象)和MailService的主要責任發送郵件分開。
修改Main函數,調用MailService。
1 namespace Demo.Ninject 2 { 3 class Program 4 { 5 static void Main(string[] args) 6 { 7 var mailService = new MailService(new EventLogger()); 8 mailService.SendMail("someone@somewhere.com", "My first DI App", "Hello World!"); 9 } 10 } 11 }
四、DI容器
DI容器是一個注入對象,用來向對象注入依賴項。上一個例子中咱們看到,實現DI並不必定須要DI容器。然而,在更復雜的狀況下,DI容器自動完成這些工做比咱們手寫代碼節省不少的時間。在現實的應用程序中,一個簡單的類可能有許多的依賴項,每個依賴項有有各自的其餘的依賴項,這些依賴組成一個龐大的依賴圖。DI容器就是用來解決這個依賴的複雜性問題的,在DI容器裏決定抽象類須要選擇哪個具體類實例化對象。這個決定依賴於一個映射表,映射表能夠用配置文件定義也能夠用代碼定義。來看一個例子:
<bind service="ILogger" to="ConsoleLogger" />
也能夠用代碼定義。
Bind<ILogger>().To<ConsoleLogger>();
也能夠用條件規則定義映射,而不是這樣一個一個具體類型進行分開定義。
容器負責管理建立對象的生命週期,他應當知道他建立的對象要保持活躍狀態多長時間,何時處理,何時返回已經存在的實例,何時建立一個新的實例。
除了Ninject,還有其餘的DI容器能夠選擇。能夠看Scott Hanselman's博客(http://www.hanselman.com/blog/ListOfNETDependencyInjectionContainersIOC.aspx)。有Unity, Castle Windsor, StructureMap, Spring.NET和Autofac
Unity |
Castle Windsor |
StructureMap |
Spring.NET |
Autofac |
|
---|---|---|---|---|---|
License |
MS-PL |
Apache 2 |
Apache 2 |
Apache 2 |
MIT |
Description |
Build on the "kernel" of ObjectBuilder. |
Well documented and used by many. |
Written by Jeremy D. Miller. |
Written by Mark Pollack. |
Written by Nicholas Blumhardt and Rinat Abdullin. |
五、爲何使用Ninject
Ninject是一個輕量級的.NET應用程序DI框架。他幫助你將你的應用程序分解成鬆耦合高內聚的片斷集合,而後將他們靈活地鏈接在一塊兒。在你的軟件架構中使用Ninject,你的代碼將變得更容易容易寫、更容易重用、測試和修改。不依賴於引用反射,Ninject利用CLR的輕量級代碼生成技術。能夠在不少狀況下大幅度提升反應效率。Ninject包含不少先進的特徵。例如,Ninject是第一個提供環境綁定依賴注入的。根據請求的上下文注入不一樣的具體實現。Ninject提供幾乎全部其餘框架能提供的全部重要功能(許多功能都是經過在覈心類上擴展插件實現的)。能夠訪問Ninject官方wiki https://github.com/ninject/ninject/wiki 得到更多Ninject成爲最好的DI容器的詳細列表。