摘要:

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. OpenGL角轴

    概述 轴旋转 角轴 概述 OpenGL旋转矩阵 旋转角度直接影响OpenGL GL_MODELVIEW矩阵的前三列,准确地说是向左.向上与向前三轴元素.例如,如果一沿X轴的单位向量(1,0,0)与任一 ...

  2. 如何提高Linux操作系统的安全性 转自https://yq.aliyun.com/articles/24251?spm=5176.100239.blogcont24250.7.CfBYE9

    摘要: Linux系统不论在功能上.价格上或性能上都有很多优点,但作为开放式操作系统,它不可避免地存在一些安全隐患.关于如何解决这些隐患,为应用提供一个安全的操作平台,本文会告诉你一些最基本.最常用, ...

  3. pc wap 判断浏览器ua属性

    var ua = navigator.userAgent.toLowerCase(); var Android = String(ua.match(/android/i)) == "andr ...

  4. hdu3294 girl‘s research

    题目大意:有多组数据,每组数据给出一个字符和一个字符串.该字符将变成’a‘,表示字符串中的所有该字符将变成’a‘,同时其他字符也将做相同的偏移.具体来说,如果该字符为’b‘,表示字符串中的’b‘都将变 ...

  5. RHEL7 修改SSH默认端口及修改SELinux运行状态

    RHEL7安装后,默认开启SSH服务以便远程配置,但默认端口22并不安全,一般不建议使用默认端口,那就修改SSH默认端口.在sshd_config里面的修改RHEL7.0上修改和7.0以下类似,但要注 ...

  6. 移动应用开发测试工具Bugtags集成和使用教程

    前段时间,有很多APP突然走红,最终却都是樱花一现.作为一个创业团队,突然爆红是非常难得的机会.然并卵,由于没有经过充分的测试,再加上用户的激增,APP闪退.服务器数据异常等问题就被暴露出来,用户的流 ...

  7. linux mint 崩溃

    换完linux mint 今天突然崩溃了.出现如下错误 因为是双屏.一个屏幕显示一般,这么不重要.搜了一下,找到解决方案 解决办法 ctrl+atl+f1 login sudo apt-get ins ...

  8. Microsoft Capicom 2.1 On 64bit OS

    第一步下载capicom.dll http://files.cnblogs.com/files/chen110xi/DLL.7z 第二步注册capicom.dll至SysWow64 第三步VS中设置 ...

  9. ORACLE10gRAC数据库迁移至10gRAC

    1.数据库备份RUN {ALLOCATE CHANNEL ch00 DEVICE TYPE disk;ALLOCATE CHANNEL ch01 DEVICE TYPE disk;ALLOCATE C ...

  10. jQuery 事件探秘

    jQuery 事件中为 dom 元素动态绑定事件的方式: 绑定事件到元素上 $(selector).bind('eventName', function(){}); 一次绑定多个事件时,事件名用单个空 ...