Ninject之旅之一:理解DI

摘要: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容器的詳細列表。

相關文章
相關標籤/搜索