摘要:

DI(IoC)是当前软件架构设计中比较时髦的技术。DI(IoC)可以使代码耦合性更低,更容易维护,更容易测试。现在有很多开源的依赖反转的框架,Ninject是其中一个轻量级开源的.net DI(IoC)框架。目前已经非常成熟,已经在很多项目中使用。这篇文章讲DI概念以及使用它的优势。使用一个简单的例子,重构这个例子让他逐步符合DI设计原则。

思考和设计代码的方法远比如何使用工具和技术更重要。– Mark Seemann

1、什么是DI(依赖反转)

DI(依赖反转)是一个软件设计方面的技术,通过管理依赖组件,提高软件应用程序的可维护性。用一个实际的例子来描述什么是DI以及DI的要素。

定义一个木匠类Carpenter,木匠对象(手里)有工具Saw对象,木匠有制造椅子MakeChair方法。MakeChair方法使用saw对象的Cut方法来制作椅子。

 class Carpenter
{
Saw saw = new Saw();
void MakeChair()
{
saw.Cut();
// ...
}
}

定义一个手术医生类,手术医生对象有手术钳Forceps对象,手术医生做手术方法Operate。Operate方法使用手术钳对象的Grab方法来做手术。手术医生不需要知道他用的手术钳去哪里找,这是他助理的任务。他只需要关注做手术这一个关注点就行了。

 class Surgeon
{
private Forceps forceps; // The forceps object will be injected into the constructor
// method by a third party while the class is being created.
public Surgeon(Forceps forceps)
{
this.forceps = forceps;
} public void Operate()
{
forceps.Grab();
//...
}
}

上面两个例子木匠和医生都依赖于一个工具类,他们需要的工具是他们的依赖组件。依赖反转是指如何获得他们需要的工具的过程。第一个例子,木匠和锯子强依赖。第二个例子,医生的构造函数将他跟手术钳产生了依赖。

Martin Fowler给控制反转(IoC)下的定义是:Ioc是一种编程方式,这种编程方式使用框架来控制流程而不是通过你自己写的代码。比较处理事件和调用函数来理解IoC。当你自己写代码调用框架里的函数时,你在控制流程,因为你自己决定调用函数的顺序。但是使用事件时,你将函数绑定到事件上,然后触发事件,通过框架反过来调用函数。这时候控制反转到由框架来定义而不是你自己手写代码。DI是一个具体的IoC类型。组件不需要关心它自己的依赖项,依赖关系由框架来提供。实际上,根据Mark Seemann所说,DI in .NET,IoC是一个很宽的概念,不局限于DI,尽管他们两个概念经常互相通用。用好莱坞一句著名的台词来描述IoC就是:“不要找我们,我们来找你”。

2、 DI是如何工作的

每一个软件都不可避免地改变。当新的需求到来的时候,你修改你的代码导致代码量增加。维护你的代码的重要性变得很明显,一个可维护性差的软件系统是不可能进行下去的。一个指导设计可维护性代码的设计原则叫Separation of Concerns(SoC)【中文:分离关注点】。SoC是一个宽泛的概念而不仅限于软件设计。在软件组件设计方面,SoC设计一些不同的类,这些类各自有自己单独的责任。在上一个手术医生例子中,找工具和做手术是两个不同的关注点,分离他们为两个不同的关注点是开发可维护性的代码的一个前提。

SoC不能必然产生一个可维护性的代码,如果这些关注点相互之间的代码很紧密的耦合在一起。

尽管手术医生在做手术的过程中需要很多不同类型的手术钳,但是他没必要说具体哪一种是他需要的。他只需要说他要手术钳,他的助理来决定哪个手术钳是他最需要的。如果医生说的具体的那个手术钳暂时没有,助手可以给他提供另一个合适的,因为助手知道只要手术钳合适医生并不关心是哪种类型的。换句话说,手术医生不是跟手术钳紧密耦合在一起的。

对接口编程,而不是对具体实现编程。

我们用抽象元素(接口或类)来实现依赖,而不用具体类。我们就能够很容易地替换具体的依赖类而不影响上层的调用组件。

 class Surgeon
{
private IForceps forceps; public Surgeon(IForceps forceps)
{
this.forceps = forceps;
} public void Operate()
{
forceps.Grab();
//...
}
}

类Surgeon现在依赖于接口IForceps,而不用关心在构造函数中注入的对象具体的类型。C#编译器能够保证传入构造函数的对象的类型实现了IForceps接口并且有Grab方法。下面的代码是上层调用。

 var forceps = assistant.Get<IForceps>();
var surgeon = new Surgeon (forceps);

因为Surgeon类依赖IForceps接口而不是具体的类,我们能够自由地初始化任何实现了IForceps接口的类对象作为他的助手。

通过对接口编程和分离关注点,我们得到了一个可维护性的代码。

3、第一个DI应用程序

首先创建一个服务类,在这个服务类里关注点没有被分离。然后,一步一步改进程序的可维护性。第一步分离关注点,然后面向接口编程,使程序松耦合。最后,得到第一个DI应用程序。

服务类主要的责任是使用提供的信息发送邮件。

 using System.Net.Mail;

 namespace Demo.Ninject
{
public class MailService
{
public void SendEmail(string address, string subject, string body)
{
var mail = new MailMessage();
mail.To.Add(address);
mail.Subject = subject;
mail.Body = body;
var client = new SmtpClient();
// Setup client with smtp server address and port here
client.Send(mail);
}
}
}

然后给程序添加日志功能。

 using System;
using System.Net.Mail; namespace Demo.Ninject
{
public class MailService
{
public void SendEmail(string address, string subject, string body)
{
Console.WriteLine("Creating mail message...");
var mail = new MailMessage();
mail.To.Add(address);
mail.Subject = subject;
mail.Body = body;
var client = new SmtpClient();
// Setup client with smtp server address and port here
Console.WriteLine("Sending message...");
client.Send(mail);
Console.WriteLine("Message sent successfully.");
}
}
}

过了一会后,我们发现给日志信息添加时间信息很有用。在这个例子里,发送邮件和记录日志是两个不同的关注点,这两个关注点同时写在了同一个类里面。如果要修改日志功能必须要修改MailService类。因此,为了给日志添加时间,需要修改MailService类。所以,让我们重构这个类分离添加日志和发送邮件这两个关注点。

 using System;
using System.Net.Mail; namespace Demo.Ninject
{
public class MailService
{
private ConsoleLogger logger;
public MailService()
{
logger = new ConsoleLogger();
} public void SendMail(string address, string subject, string body)
{
logger.Log("Creating mail message...");
var mail = new MailMessage();
mail.To.Add(address);
mail.Subject = subject;
mail.Body = body;
var client = new SmtpClient();
// Setup client with smtp server address and port here
logger.Log("Sending message...");
client.Send(mail);
logger.Log("Message sent successfully.");
}
} class ConsoleLogger
{
public void Log(string message)
{
Console.WriteLine("{0}: {1}", DateTime.Now, message);
}
}
}

类ConsoleLogger只负责记录日志,将记录日志的关注点从MailService类中移除了。现在,就可以在不影响MailService的条件下修改日志功能了。

现在,新需求来了。需要将日志写在Windows Event Log里,而不写在控制台。看起来需要添加一个EventLog类。

 class EventLogger
{
public void Log(string message)
{
System.Diagnostics.EventLog.WriteEntry("MailService", message); }
}

尽管发送邮件和记录日志分离到两个不同的类,MailService还是跟ConsoleLogger类紧密耦合,如果要换一种日志方式必须要修改MailService类。我们离打破MailService和Logger的耦合仅一步之遥。需要引入依赖接口而不是具体类。

     public interface ILogger
{
void Log(string message);
}

ConsoleLogger和EventLogger都继承ILogger接口。

     class ConsoleLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine("{0}: {1}", DateTime.Now, message);
}
} class EventLogger : ILogger
{
public void Log(string message)
{
System.Diagnostics.EventLog.WriteEntry("MailService", message);
}
}

现在可以移除对具体类ConsoleLogger的引用,而是使用ILogger接口。

         private ILogger logger;
public MailService(ILogger logger)
{
this.logger = logger;
}

在此时,我们的类是松耦合的,可以自由地修改日志类而不影响MailService类。使用DI,将创建新的Logger类对象的关注点(创建具体哪一个日志类对象)和MailService的主要责任发送邮件分开。

修改Main函数,调用MailService。

 namespace Demo.Ninject
{
class Program
{
static void Main(string[] args)
{
var mailService = new MailService(new EventLogger());
mailService.SendMail("someone@somewhere.com", "My first DI App", "Hello World!");
}
}
}

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

Ninject之旅之一:理解DI的更多相关文章

  1. Ninject之旅目录

    第一章:理解依赖注入 Ninject之旅之一:理解DI 第二章:开始使用Ninject Ninject之旅之二:开始使用Ninject(附程序下载) Ninject之旅之三:Ninject对象生命周期 ...

  2. ninject 的 实现 的 理解

    mvc 用ninject 好像 有 的. 加上 ClassDiagram  .ClassDiagram1.rar Represents a site on a type where a value c ...

  3. Ninject之旅之十一:Ninject动态工厂(附程序下载)

    摘要 如果我们已经知道了一个类所有的依赖项,在我们只需要依赖项的一个实例的场景中,在类的构造函数中引入一系列的依赖项是容易的.但是有些情况,我们需要在一个类里创建依赖项的多个实例,这时候Ninject ...

  4. Ninject之旅之十:Ninject自定义提供者

    摘要 提供者是特殊的工厂类,Ninject使用它来实例化解析类型.任何时候我们绑定一个服务类型到一个组件,我们都隐式地关联那个服务类型到一个可以实例化那个组件的提供者.这个隐藏的提供者被称为Stand ...

  5. Ninject之旅之七:Ninject依赖注入

    摘要 可以使用不同的模式向消费者类注入依赖项,向构造器里注入依赖项是其中一种.有一些遵循的模式用来注册依赖项,同时有一些需要避免的模式,因为他们经常导致不合乎需要的结果.这篇文章讲述那些跟Ninjec ...

  6. Ninject之旅之三:Ninject对象生命周期

    摘要 DI容器的一个责任是管理他创建的对象的生命周期.他应该决定什么时候创建一个给定类型的对象,什么时候使用已经存在的对象.他还需要在对象不需要的时候处理对象.Ninject在不同的情况下管理对象的生 ...

  7. Ninject之旅之二:开始使用Ninject(附程序下载)

    摘要 这篇文章介绍怎样将Ninject添加到实际的项目中,使用Ninject框架最基本的功能.首先用一个Hello World例子介绍怎么添加和使用Ninject.然后用一个更复杂的例子,介绍Ninj ...

  8. Ninject之旅之十二:Ninject在Windows Form程序上的应用(附程序下载)

    摘要: 下面的几篇文章介绍如何使用Ninject创建不同类型的应用系统.包括: Windows Form应用系统 ASP.NET MVC应用系统 ASP.NET Web Form应用系统 尽管对于不同 ...

  9. Ninject之旅之十三:Ninject在ASP.NET MVC程序上的应用(附程序下载)

    摘要: 在Windows客户端程序(WPF和Windows Forms)中使用Ninject和在控制台应用程序中使用Ninject没什么不同.在这些应用程序里我们不需要某些配置用来安装Ninject, ...

随机推荐

  1. NewQuant的设计(一)——整体的领域设计

    NewQuant的设计思路——整体的领域分析 “领域驱动设计(DDD)”是著名软件工程建模专家Eric Evans提出的一个重要概念,是“面向对象分析设计(OOAD)”的深化.当业务逻辑变得复杂,系统 ...

  2. OpenGL法向量变换

    OpenGL光照开启时,法向量用于决定特定顶点或面上接受到光照的多少.光照处理过程作用于观察坐标空间,因此,模型对象坐标系的法向量也需要使用GL_MODELVIEW矩阵变换到观察坐标系. 然而,法向量 ...

  3. WPF 打印

    1. System.Windows.Controls.PrintDialog printDialog = new System.Windows.Controls.PrintDialog(); if ( ...

  4. openacs与easycwmp的对接

    原创作品,转载请注明出处 copyright:weishusheng   2015.3.18 email:642613208@qq.com 平台: Linux version 2.6.32-279.e ...

  5. 转-decorators.xml的用法-http://blog.csdn.net/gavinloo/article/details/7458062

    今天改前人做的项目,用struts2,spring,hibernate框架做的,对了,还有jQuery.我用jquery做异步请求到后台,生成json数据返回前台生成下拉输入框,请求到后台以后,成功生 ...

  6. Ubuntu14.04+Beanstalkd1.9最佳实践

    目录 [TOC] 1.基本概念 1.1.什么是Beanstalkd?   Beanstalkd 是一个轻量级消息中间件,它最大特点是将自己定位为基于管道 (tube) 和任务 (job) 的工作队列. ...

  7. Linux LVM硬盘管理之二:创建逻辑卷步骤

    创建逻辑卷(LV)的顺序:Linux分区---物理卷(PV)---卷组(VG)---逻辑卷(LV)---挂载到文件系统 删除逻辑卷(LV)的顺序:卸载文件系统----逻辑卷(LV)---卷组(VG)- ...

  8. [nginx] connect() failed (111: Connection refused) while connecting to upstream, client: 101.18.123.107, server: localhost,

    nginx一直报错, 2016/12/02 10:23:19 [error] 1472#0: *31 connect() failed (111: Connection refused)while c ...

  9. 高通AR增强现实Unity3D

    AR: 增强现实,台湾翻译叫做扩张实境 1.注册.然后下载sdk(注册账号主要是为了第3步中制作识别图而用的) 下载地址:https://developer.vuforia.com/resources ...

  10. 如何用Matplotlib绘制三元函数

    #!/usr/bin/env python #coding=GBK from mpl_toolkits.mplot3d import Axes3D from matplotlib import cm ...