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