Ninject之旅之一:理解DI


摘要:

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM