依赖注入 – ASP.NET MVC 4 系列

       从 ASP.NET MVC 3.0 开始就引入了一个新概念:依赖解析器(dependence resolver)极大的加强了应用程序参与依赖注入的能力,更好的在 MVC 使用的服务和建立的一些类(控制器和视图页面)之间创建依赖关系。为更好的理解依赖解析器的工做原理,下面首先定义一些它所用到的通用软件模式。数据库

软件设计模式

       软件设计模式主要用来规范问题及其解决方案的描述,简化开发人员对常见问题及其对应解决方案的标识与交流。设计模式并非新奇的发明,而是为行业中常见的实践给出一个正式的名称和定义。设计模式

设计模式 - 控制反转模式

       几乎每一个人都见过或编写过相似下面的代码:app

public class EmailService
{
    public void SendMessage()
    {
        //...
    }
}
public class NotificationSystem
{
    private EmailService svc;
 
    public NotificationSystem()
    {
        svc = new EmailService();
    }
 
    public void InterestingEventHappened()
    {
        svc.SendMessage();
    }    
}

       上面的代码中,NotificationSystem 类依赖于 EmailService 类。当一个组件依赖于其余组件时,咱们称其为耦合(coupling)。本例中,通知系统(NotificationSystem)在其构造方法内部直接建立 e-mail 服务的一个实例,换言之,通知系统明确地知道建立和使用了哪一种类型的服务。这种耦合表示了代码的内部连接性。一个类知道与其交互的类的大量信息(正如上面的示例),咱们称其为高耦合函数

       在软件设计过程当中,高耦合一般认为是软件设计的责任,当一个类精确的知道另外一个类的设计和实现时,就会增长软件修改的负担,由于修改一个类颇有可能破坏依赖于它的另外一个类。上面的代码还存在一个问题,当感兴趣的事件发生时,通知系统如何发送其余类型的信息?例如系统管理员可能想获得文本信息,或者想把每一个通知都记录到数据库中,为了实现这些功能,咱们必须从新实现 NotificationSystem 类。this

       为下降组件之间的耦合程度,通常采起两个独立但相关的步骤:spa

1. 在两块代码之间引入抽象层设计

       在 .NET 平台中,一般使用接口(或抽象类)来表明两个类之间的抽象层。咱们能够引入一个接口,并确保编写的代码只调用接口中的方法和属性,这样一来,NotificationSystem 类中的私有副本(svc)就变成了一个接口的实例,而再也不是具体类型:rest

public interface IMessagingService
{
    void SendMessage();
}
 
public class EmailService : IMessagingService
{
    public void SendMessage()
    {
        //...
    }
}
 
public class NotificationSystem
{
    private IMessagingService svc;
 
    public NotificationSystem()
    {
        svc = new EmailService();
    }
 
    public void InterestingEventHappened()
    {
        svc.SendMessage();
    }
}

2. 把选择抽象实现的责任移到消费者类的外部code

       须要把 EmailService 类的建立(svc = new EmailService( ))移到 NotificationSystem 类的外面。把依赖的建立移到使用这些依赖的类的外部,这就是控制反转模式!之因此这样命名,是由于反转的是依赖的建立,也正由于如此,才消除了消费者类对依赖建立的控制。控制反转(IoC)模式是抽象的,它只是表述应该从消费者类中移出依赖建立,而没有表述如何实现。下面咱们将探讨用控制反转模式实现责任转移的两种经常使用方法服务定位器依赖注入(重点)对象

设计模式 - 服务定位器

       服务定位器模式是控制反转模式的一种实现方式,它经过一个称为服务定位器的外部组件来为须要依赖的组件提供依赖。服务定位器有时是一个具体的接口,为特定服务提供强类型的请求;有时又多是一个泛型类型,能够提供任意类型的请求服务。

1. 强类型服务定位器

       对应示例程序的强类型服务定位器可能有以下接口:

public interface IServiceLocator
{
    IMessagingService GetMessagingService();
}
       在本例中,当须要一个实现了 IMessagingService 接口的对象时,咱们知道应该调用 GetMessagingService 方法,它会返回一个 IMessagingService 接口对象,所以并不须要转换结果的类型。这里把服务定位器做为一个接口,而不是一个具体的类型。咱们的目标是下降组件之间的耦合程度,其中包括消费者代码和服务定位器之间的耦合。若是消费者代码实现了 IServiceLocator 接口,就能够在运行时环境中选择合适的实现方式。

       要用强类型的服务定位器从新编写 NotificationSystem 类,代码以下:

public class NotificationSystem
{
    private IMessagingService svc;
 
    public NotificationSystem(IServiceLocator locator)
    {
        svc = locator.GetMessagingService();
    }
 
    public void InterestingEventHappened()
    {
        svc.SendMessage();
    }
}

       上面的代码假设建立 NotificationSystem 实例的每一个人都会访问服务定位器。这样作带来的便利是,若是应用程序经过服务定位器建立 NotificationSystem 实例,那么定位器将自身传递到 NotificationSystem 类的构造函数中。

      

2. 弱类型服务定位器

       若是在某个应用中,强类型服务定位器的负面影响超过了它所带来的正面效应,能够考虑改用弱类型服务定位器:

public interface IServiceLocator
{
    object GetService(Type serviceType);
}

       这种变体更加灵活,由于它采用 Type 类型的参数,并返回一个 Object 类型的对象。固然,须要把调用 GetService 方法返回的结果转换为正确类型的对象。

       使用弱类型服务定位器的 NotificationSystem 类的代码以下:

public class NotificationSystem
{
    private IMessagingService svc;
 
    public NotificationSystem(IServiceLocator locator)
    {
        svc = (IMessagingService)locator.GetService(typeof(IMessagingService));
    }
 
    public void InterestingEventHappened()
    {
        svc.SendMessage();
    }
}

       这样的代码并不简洁,这主要是由于调用 GetService 后须要转换类型。自从 .NET 2.0 引入泛型以来,就能够包含 GetService 方法的泛型版本:

public interface IServiceLocator
{
    object GetService(Type serviceType);
    TService GetService<TService>();
}

       按照泛型方法的约定,它将返回一个已经转换为正确类型的对象,注意,返回的类型是 TService 而不是 Object,这使得 NotificationSystem 类的代码变得简洁一些:

public class NotificationSystem
{
    private IMessagingService svc;
 
    public NotificationSystem(IServiceLocator locator)
    {
        svc = locator.GetService<IMessagingService>();
    }
 
    public void InterestingEventHappened()
    {
        svc.SendMessage();
    }
}

       Object 版本的 GetService 仍然存在的意义,是由于并不是每个调用 API 的消费者在编译时都精确地知道它们将要调用的类型。如今这样作的负面影响是,它强制 IServiceLocator 接口必须实现两个几乎相同的方法!这些无谓的努力在 .NET 3.5 中被移除,由于一个新特性:扩展方法

       把扩展方法做为静态类的静态方法来编写,在它的第一个参数中利用特殊的 this 关键字来指定扩展方法要附加到的类型。

       把 GetService 泛型方法分割成为扩展方法后,代码以下:

public interface IServiceLocator
{
    object GetService(Type serviceType);       
}
 
public static class ServiceLocatorExtensions
{
    public static TService GetService<TService>(this IServiceLocator locator)
    {
        return (TService)locator.GetService(typeof(TService));
    }
}

3. 服务定位器的利弊

       服务定位器的用法比较简单:首先,咱们从某个地方获得服务定位器,而后利用定位器查找依赖。可能在一个已知的(全局)位置找到服务定位器,或者经过咱们的建立者得到服务定位器。尽管依赖关系有时会发生改变,但签名不会变,由于查找依赖惟一须要的就是定位器。持久签名带来好处的同时,也带来了弊端。它致使了组件需求的不透明性:使用组件的开发人员经过查看构造函数的签名不能知道服务要求的是什么。

       需求的不透明性促使咱们选择下一个反转控制模式:依赖注入

设计模式 - 依赖注入

       依赖注入(Dependency Injection,DI)是另外一种控制反转模式的形式,它没有像服务定位器同样的中间对象。相反,组件以一种容许依赖的方式来编写,一般由构造函数参数或属性设置器来显式表示。

       若是采用构造函数注入,NotificationSystem 类的代码将以下所示:

public class NotificationSystem
{
    private IMessagingService svc;
 
    public NotificationSystem(IMessagingService service)
    {
        this.svc = service;
    }
 
    public void InterestingEventHappened()
    {
        svc.SendMessage();
    }
}

       这段代码有 3 个显著优势:

  1. 极大的简化了构造函数的实现,组件老是指望建立它的类可以传递须要的依赖,而它只需存储 IMessagingService 接口的实例以便以后使用!(IMessagingService 的实现能够有各类版本,好比发送 email、发送文本消息,这就是消费者类调用接口而不是只调用 Email 类的好处)
  2. 这段代码减小了 NotificationSystem 类须要知道的信息量。在之前,NotificationSystem 既须要知道服务定位器,也须要知道它本身的依赖项,而如今只须要知道它本身的依赖项就好了
  3. 需求的透明性。任何想建立 NotificationSystem 类实例的代码都能查看构造函数,并精确知道哪些内容是使用 NotificationSystem 类必需的。

      

       属性注入(property injection)是一种不太常见的依赖注入方式,该方式经过设置对象上的公有属性而不是经过构造函数传递参数来注入依赖

       若是采用属性注入,NotificationSystem 类的代码将以下所示:

public class NotificationSystem
{
    public IMessagingService MessagingService { get; set; }
 
    public void InterestingEventHappened()
    {
        MessagingService.SendMessage();
    }
}

       如今,NotificationSystem 类但愿任何消费者类都经过属性来提供依赖。如今的 InterestingEventHappened 方法可能会产生一个空引用对象的异常,若是它被调用时并无提供服务依赖(没有对属性来赋值)。应作如下完善以确保使用服务以前已提供了服务依赖:

public void InterestingEventHappened()
{
    if (this.MessagingService == null)
    {
        throw new InvalidOperationException("Please set MessagingService before calling InterestingEventHappened().");
    }
    MessagingService.SendMessage();
}

       属性注入下降了透明性(没法经过构造函数知晓依赖),也比构造函数注入更容易产生错误,那么开发人员为什么还要这样使用呢?究其缘由,主要有 2 点:

  1. 若是依赖在某种意义上是真正可选的,即在消费者类不提供依赖时,也有相应的处理(好比说,在 MessagingService 的 get 中有一个默认的初始化动做)。
  2. 类的实例可能须要在咱们尚未控制调用的构造函数的状况下被建立,这个缘由并不太明显。

       一般状况下,开发人员更倾向于使用构造函数注入!

相关文章
相关标签/搜索