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容器的详细列表。

相关文章
相关标签/搜索