摘要:
DI(IoC)是當前軟件架構設計中比較時髦的技術。DI(IoC)可以使代碼耦合性更低,更容易維護,更容易測試。現在有很多開源的依賴反轉的框架,Ninject是其中一個輕量級開源的.net DI(IoC)框架。目前已經非常成熟,已經在很多項目中使用。這篇文章講DI概念以及使用它的優勢。使用一個簡單的例子,重構這個例子讓他逐步符合DI設計原則。
思考和設計代碼的方法遠比如何使用工具和技術更重要。– Mark Seemann
1、什么是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就是:“不要找我們,我們來找你”。
2、 DI是如何工作的
每一個軟件都不可避免地改變。當新的需求到來的時候,你修改你的代碼導致代碼量增加。維護你的代碼的重要性變得很明顯,一個可維護性差的軟件系統是不可能進行下去的。一個指導設計可維護性代碼的設計原則叫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接口的類對象作為他的助手。
通過對接口編程和分離關注點,我們得到了一個可維護性的代碼。
3、第一個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 }
4、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. |
5、為什么使用Ninject
Ninject是一個輕量級的.NET應用程序DI框架。他幫助你將你的應用程序分解成松耦合高內聚的片段集合,然后將他們靈活地連接在一起。在你的軟件架構中使用Ninject,你的代碼將變得更容易容易寫、更容易重用、測試和修改。不依賴於引用反射,Ninject利用CLR的輕量級代碼生成技術。可以在很多情況下大幅度提高反應效率。Ninject包含很多先進的特征。例如,Ninject是第一個提供環境綁定依賴注入的。根據請求的上下文注入不同的具體實現。Ninject提供幾乎所有其他框架能提供的所有重要功能(許多功能都是通過在核心類上擴展插件實現的)。可以訪問Ninject官方wiki https://github.com/ninject/ninject/wiki 獲得更多Ninject成為最好的DI容器的詳細列表。