编码最佳实践——依赖注入原则

咱们在这个系列的前四篇文章中分别介绍了SOLID原则中的前四个原则,今天来介绍最后一个原则——依赖注入原则。依赖注入(DI)是一个很简单的概念,实现起来也很简单。可是简单却掩盖不了它的重要性,若是没有依赖注入,前面的介绍的SOLID技术原则都不可能实际应用。算法

控制反转(IoC)

人们在谈论依赖注入的时候,常常也会谈到另外一个概念——控制反转(IoC)。按照大内老A的解释:“IoC主要体现了这样一种设计思想:经过将一组通用流程的控制权从应用转移到框架中以实现对流程的复用,并按照“好莱坞法则”实现应用程序的代码与框架之间的交互“。概念比较抽象,咱们拆开解读一下。编程

咱们要作的任何一件事情,不管大小,均可以分解为相应的步骤。因此任何一件事情都有其固定的流程。与现实问题域同样,解决方案域(程序实现)也是这样。因此IoC控制能够理解为“对流程的控制”。以HTTP请求处理的流程为例,在传统面向类库编程的时代,针对HTTP请求处理的流程紧紧控制在应用程序手中。在引入框架以后,请求处理的控制权转移到了框架手上。类库(Library)和框架(Framework)的不一样之处在于,前者每每只是提供实现某种单一功能的API,然后者则针对一个目标任务对这些单一功能进行编排造成一个完整的流程,这个流程在一个引擎的驱动下自动执行。如此,全部使用此框架的程序均可以复用关于HTTP请求处理的流程。c#

在好莱坞,把简历递交给演艺公司后就只有回家等待。由演艺公司对整个娱乐项目的彻底控制,演员只能被动式的接受电影公司的工做,在须要的环节中,完成本身的演出。“不要给咱们打电话,咱们会给你打电话(don‘t call us, we‘ll call you)”这是著名的好莱坞法则。设计模式

mark

IoC完美地体现了这一法则,对于ASP.NET MVC应用开发来讲,咱们只须要按照约定规则(好比目录结构和命名等)定义相应的Controller类型和View文件就能够了,这就是所谓的“约定大于配置”。当ASP.NET MVC框架在进行处理请求的过程当中,它会根据解析生成的路由参数定义为对应的Controller类型,并按照预约义的规则找到咱们定义的Controller,而后自动建立并执行它。若是定义在当前Action方法须要呈现一个View,框架自身会根据预约义的目录约定找到咱们定义的View文件,并对它实施动态编译和执行。整个流程到处体现了“框架Call应用”的好莱坞法则。微信

简单的说,控制反转(IoC)的过程就是一组通用流程的控制权从应用程序转移到框架中的过程,为的是实现流程的复用。可是有一个问题,被反转的仅仅是一个泛化的流程,在特定场景可能会有一些特殊的流程或者流程节点,此时就须要进行流程定制。定制通常是经过框架预留的扩展点进行的,好比ASP.NET中的HttpHandler和HttpModule,ASP.NET Core中的Middleware。架构

前面提到控制反转(IoC)是一种设计思想。因此控制反转(IoC)并不能解决某一类具体的问题。可是基于控制反转(IoC)思想的设计模式却能够,最简单直观的就是模板方法模式。该模式主张将一个可复用的工做流程或者由多个步骤组成的算法定义成模板方法,组成这个流程或者算法的步骤实如今相应的虚方法之中,模板方法根据按照预先编排的流程去调用这些虚方法。全部这些方法均定义在同一个类中,咱们能够经过派生该类并重写相应的虚方法达到对流程定制的目的。框架

public class TemplateMethod
{
    //流程编排
    public void ABCD()
    {
        A();
        B();
        C();
        D();
    }
    //步骤A
    protected virtual void A() { }
    //步骤B
    protected virtual void B() { }
    //步骤C
    protected virtual void C() { }
    //步骤D
    protected virtual void D() { }
}

依赖注入(DI)

依赖注入(DI)也是架构在控制反转思想上的一种模式。在这里咱们将提供的对象统称为“服务”、“服务对象”或者“服务实例”。在一个采用DI的应用中,在定义某个服务类型的时候,咱们直接将依赖的服务采用相应的方式注入进来。按照“面向接口编程”的原则,被注入的最好是依赖服务的接口而非实现。正确的依赖注入对于项目的绝大多数代码都是不可见的,它们(注册代码)被局限在一个很小的代码范围内,一般是一个独立的程序集。函数

在应用启动的时候,会对所需的服务进行全局注册。服务通常都是针对接口进行注册的,服务注册信息的核心目的是为了在后续消费过程当中可以根据接口建立或者提供对应的服务实例。按照“好莱坞法则”,应用只须要定义好所需的服务,服务实例的激活和调用则彻底交给框架来完成,而框架则会采用一个独立的“容器(Container)”来提供所需的每个服务实例。咱们将这个被框架用来提供服务的容器称为“DI容器”,也由不少人将其称为“IoC容器”。全部的DI容器都符合注册、解析、释放模式post

依赖注入的三种注入方式

1.构造函数注入学习

public class TaskService
{
    private ITaskOneRepository taskOneRepository;
    private ITaskTwoRepository taskTwoRepository;
    public TaskService(
        ITaskOneRepository taskOneRepository,
        ITaskTwoRepository taskTwoRepository)
        {
            this.taskOneRepository = taskOneRepository;
            this.taskTwoRepository = taskTwoRepository;
        }
}

优势:

  • 在构造方法中体现出对其余类的依赖,一眼就能看出这个类须要其余那些类才能工做。
  • 脱离了IOC框架,这个类仍然能够工做(穷人的依赖注入)。
  • 一旦对象初始化成功了,这个对象的状态确定是正确的。

缺点:

  • 构造函数会有不少参数。
  • 有些类是须要默认构造函数的,好比MVC框架的Controller类,一旦使用构造函数注入,就没法使用默认构造函数。

2.属性注入

public class TaskService
{
    private ITaskRepository taskRepository;
    private ISettings settings;
    public TaskService(
        ITaskRepository taskRepository,
        ISettings settings)
        {
            this.taskRepository = taskRepository;
            this.settings = settings;
        }
    public void OnLoad()
    {
        taskRepository.settings = settings;
    }
}

优势:

  • 在对象的整个生命周期内,能够随时动态的改变依赖。
  • 很是灵活。

缺点:

  • 对象在建立后,被设置依赖对象以前这段时间状态是不对的(从构造函数注入的依赖实例在类的整个生命周期内均可以使用,而从属性注入的依赖实例还能从类生命周期的某个中间点开始起做用)。
  • 不直观,没法清晰地表示哪些属性是必须的。

3.方法注入

public class TaskRepository
{
    private ISettings settings;

    public void PrePare(ISettings settings)
    {
        this.settings = settings;
    }
}

优势:

  • 比较灵活。

缺点:

  • 新加入依赖时会破坏原有的方法签名,若是这个方法已经被其余不少模块用到就很麻烦。
  • 与构造方法注入同样,会有不少参数。

在这三种注入方式中,推荐使用构造函数注入。最重要的缘由是服务应该是独立自治的,即便脱离了DI框架,这个服务应该仍然能够工做。构造函数注入就符合这一要求,即便脱离了DI框架,仍然能够手动注入依赖的服务。

依赖注入反模式 —— Service Locator

假设咱们须要定义一个服务类型C,它依赖于另外两个服务A和B,后者对应的服务接口分别为IA和IB。若是当前应用中具备一个DI容器(Container),那么咱们能够采用以下两种方式来定义这个服务类型C。

public class C : IC
{
    public IA A { get; }
    public IB B { get; }
    public C(IA a, IB b)
    {
        A = a;
        B = b;
    }
    public void Invoke()
    {
        a.Invoke();
        b.Invoke();
    }
}

public class C : IC
{
    public Container Container { get; }
    public C(Container container)
    {
        Container = container;
    }
    public void Invoke()
    {
        Container.GetService<IA>().Invoke();
        Container.GetService<IB>().Invoke();
    }
}

从表面上看,这两种方式并无什么太大的区别。都解决了针对依赖服务的耦合问题,将针对服务实现依赖变成针对接口的依赖。可是,其实后一种方式并非依赖注入模式,而是服务定位器反模式。由于看起来和依赖注入模式很类似,人们常常会忽视它给代码带来的破坏。

咱们能够从“DI容器”和“Service Locator”被谁使用的角度来区分这两种设计模式的差异。DI容器的使用者是框架而不是应用程序,Service Locator的使用者是应用程序,应用程序利用它来提供服务实例。有时候,它是惟一能提供依赖注入钩子的方式。

那么Service Locator(服务定位器反模式)对代码形成了哪些破坏呢?

  1. 由于容器中的服务是全局注册的,因此DI容器是静态的,这会致使出现静态类或者服务中出现静态变量和字段。
  2. 服务定位器暴露了容器存在的信息。缘由是服务定位器容许类检索任何对象,不管是否合适。这样违背了依赖注入的“好莱坞准则”,不要调用咱们,咱们会调用你。
  3. 服务定位器会直接委托Container实例来解析实例对象,这样会形成服务没有依赖的假象。可是服务确定是有依赖的,否则为何要从服务定位器获取它们呢。

虽然咱们对服务定位器反模式提出了这么多批判,可是它仍是很是常见。由于有时候根本没有从构造函数注入的任何机会,惟一的选择就是服务定位器。毕竟它确定比不注入依赖要好,也比手动构造注入依赖要好。

总结

依赖注入(DI)是架构在控制反转(IoC)思想上的一种模式,全部的DI容器都符合注册、解析、释放模式。注入代码一般在一个独立的程序集,注入的最好是依赖服务的接口而非实现,服务实例的激活和调用则彻底交给框架来完成。在依赖注入的三种注入方式中,推荐使用构造函数注入。另外在没有从构造函数注入的机会时,能够考虑选择服务定位器反模式。选择模式的原则是:依赖注入模式优于服务定位器反模式,优于手动构造注入依赖,优于不注入依赖

参考

依赖注入1: 控制反转

依赖注入2: 基于IoC的设计模式

依赖注入3: 依赖注入模式

《C#敏捷开发实践》

做者:CoderFocus

微信公众号:

声明:本文为博主学习感悟总结,水平有限,若是不当,欢迎指正。若是您认为还不错,不妨点击一下下方的推荐按钮,谢谢支持。转载与引用请注明做者及出处。

相关文章
相关标签/搜索