丹尼大叔

数学专业毕业,爱上编程的大叔,兴趣广泛。使用博客园这个平台分享我工作和业余的学习内容,以编程交友。有朋自远方来,不亦乐乎。

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

摘要:

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

posted on 2016-08-02 14:57  丹尼大叔  阅读(2426)  评论(4编辑  收藏  举报