美文网首页Java 核心技术Java
详解学习依赖注入(DI)与控制反转(Ioc)

详解学习依赖注入(DI)与控制反转(Ioc)

作者: cbw100 | 来源:发表于2020-03-11 13:23 被阅读0次

    1. 前言

    在学习nest.js的时候,我们都知道,这个框架采用的angular的思想,依赖注入,对于前端来说,这有点后端概念,所以我们现在来学习认识这些设计模式。

    2. 什么是依赖注入

    2.1 它是设计模式

    首先,依赖注入是一个设计模式,因为它解决的是一类问题。这类问题是什么呢?这类问题和依赖有关系。

    2.2 依赖倒转原则

    依赖倒转原则(Dependence Inversion Priciple, DIP)提倡:

    • 高层模块不应该依赖低层模块。两个都应该依赖抽象
    • 抽象不应该依赖细节,细节应该依赖抽象
    • 针对接口编程,不要针对实现编程

    要想讲明白这个设计模式得先给大家说一个现实场景的案例:主板和内存条 大家都知道内存条依赖主板,内存条坏了和主板无关,主板坏了也和内存条无关,可以把电脑理解成是大的软件系统,任何部件相互依赖,但又彼此独立,即这些部件就是电脑中封装的类或程序集,在电脑里这叫易插拔,在编程中这叫强内聚低耦合。 内存模块(高层模块)不依赖主板模块(低层模块),它们依赖的是被抽象的接口(模块依赖都应该依赖抽象),抽象不应该依赖细节,细节应该依赖抽象这句话说白了,就是要针对借口编程,不要对实现编程。无论主板、cpu、内存都是针对接口设计的,都是标准的接口,如果针对实现来设计,内存要对应到具体的厂商主板,内存坏了主板也得换。

    这也就是说:在编程时,我们对系统进行模块化,它们之间有依赖,比如模块A依赖模块B 那么依据依赖倒转原则,模块A应该依赖模块B的接口,而不应该依赖模块B的实现。

    虽然模块A只依赖接口编程,但在运行的时候,它还是需要有一个具体的模块来负责模块A需要的功能的,所以模块A在【运行时】是需要一个【真的】模块B,而不是它的接口。即模块A在【运行时】需要有一个接口的实现模块作为它的属性。 那么这个实现模块怎么来?它是怎么初始化,然后怎么传给模块A的? 解决这个问题的就是依赖注入。

    2.3 前端的依赖注入

    依赖注入更多的是后端的概念,对于前端来说,很少有抽象,更别说有接口了。但是,依赖注入却是一直都存在,只是许多人没有认出来而已。

    比如用过vue的都应该知道main.js,其实他就是一个依赖注入,我们见过有这样一段代码。

    import ElementUI from 'element-ui' // vue的ui组件-(饿了么-ui)element-ui
    Vue.use(ElementUI)
    
    

    其实就是我们的需要的模块依赖vue模块,main.js就是vue模块抽象出来的接口,这里使用Vue.use(),把我们需要的模块vue注入进来,然后我们就可以用它了。 这是个很普通的代码,太正常了,我们每天都会写这些代码。 其实依赖注入它只做两件事:

    • 初始化被依赖的模块
    • 注入到依赖模块中

    这个时候应该知道了,import ElementUI from 'element-ui',初始化了被依赖的模块;而Vue.use(ElementUI)是把我们依赖的模块注入到依赖模块中。 我们不依赖element-ui的具体实现,我们只是使用他这个库的抽象接口而已,比如它的button组件。

    2.4 依赖注入的作用

    看了上面我们常用的引入是依赖注入,是不是有点吃惊?不用觉得这是什么很屌的是,关于这个我们仔细深究她的作用:

    初始化被依赖的模块 注入到依赖模块中

    我们为什么需要依赖注入呢? 1. 初始化被依赖的模块

    如果不通过依赖注入模式来初始化被依赖的模块,那么就要依赖模块自己去初始化了 那么问题来了:依赖模块就耦合了被依赖模块的初始化信息了 2. 注入到依赖模块中

    被依赖模块已经被其他管理器初始化了,那么依赖模块要怎么获取这个模块呢? 有两种方式:

    • 自己去问
    • 别人主动给你

    没用依赖注入模式的话是1,用了之后就是2 想想,你需要某个东西的时候,你去找别人要,你需要提供别人什么信息?最简单的就是那个东西叫什么,即你需要提供一个名称。 所以,方式1的问题是:依赖模块耦合了被依赖模块的【名称】还有那个【别人】 而方式2解决了这个问题,让依赖模块只依赖需要的模块的接口。

    3. 前端代码实例

    1. 我们首先得为模块依赖提供抽象的接口
    2. 下来应该能够注册依赖关系
    3. 在注册这个依赖关系后有地方存储它
    4. 存储后,我们应该把被依赖的模块注入依赖模块中
    5. 注入应该保持被传递函数的作用域
    6. 被传递的函数应该能够接受自定义参数,而不仅仅是依赖描述

    基于Injector、dependencies和函数参数名的依赖注入

    设想我们有1个模块Student,它依赖3个模块Notebook、Pencil、School

    function Notebook() {}
    Notebook.prototype.notebookName = function () {
      return 'this is a notebook'
    }
    
    function Pencil() {}
    Pencil.prototype.printName = function () {
      return 'this is a pencil'
    }
    
    function School() {}
    School.prototype.schoolName = function () {
      return '广工大'
    }
    
    function Student() {}
    Student.prototype.write = function (notebook, pencil, school) {
      if (!notebook || !pencil || !school) {
        throw new Error('Dependencies not provided!')
      }
      console.log('writing...')
      console.log(notebook)
      console.log(pencil)
      console.log(school)
      return '我拥有School、Pencil和Notebook'
    }
    
    

    我们需要在Student中用到,它依赖的3个模块Notebook、Pencil、School,记得依赖注入的功能:初始化被依赖的模块,注入到依赖模块中。

    知道这些根据我们的目标,下面开始我们的injector接口,第一种方法:从方法中解析出参数的依赖注入

    var injector = { // 依赖注入的抽象接口
      dependencies: {}, // 存储被依赖的模块
      register: function (key, value) { // 注册初始化被依赖的模块
        this.dependencies[key] = value
      },
      resolve: function (deps, func, scope) { // 注入到依赖的模块中,注入应该接受一个函数,并返回一个我们需要的函数
        var paramNames = this.getParamNames(func) // 取得参数名
        var params = []
        for (var i = 0; i < paramNames.length; i++) { // 通过参数名在dependencies中取出相应的依赖
          let d = paramNames[i]
          let depen = this.dependencies[d] || deps[i]
          if (depen) {
            params.push(depen)
          } else {
            throw new Error('缺失的依赖:' + d)
          }
        }
        // 注入依赖,执行,并返回一个我们需要的函数
        return func.apply(scope || {}, params) // 将func作用域中的this关键字绑定到bind对象上,bind对象可以为空
      },
      getParamNames: function (func) { // 获取方法的参数名字
        var paramNames = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1]
        paramNames = paramNames.replace(/ /g, '')
        paramNames = paramNames.split(',')
        return paramNames // Array
      }
    }
    
    

    这里我们使用register方法来注册初始化被依赖的模块,使用dependencies来存储被依赖的模块,resolve来进行注入,getParamNames来获取我们的传递方法的参数,注入依赖,执行,并返回一个我们需要的函数。

    调用代码:

    injector.register('notebook', new Notebook()) // 注册notebook
    injector.register('pencil', new Pencil()) // 注册pencil
    var school = new School()
    var student = new Student()
    // 以参数的形式传入school
    var studentWrite = injector.resolve([, , school], student.write, student)
    console.log(studentWrite) // "我拥有School、Pencil和Notebook"
    
    

    执行结果:

    2019090701.png

    上面我们通过injector(注入器、注射器)向write方法提供它所需要的依赖。通过依赖注入,函数的执行和其所依赖对象的创建逻辑就被解耦开来了。这里我们初始化的方式有两种:一种是通过injector.register来注册,一种是直接使用injector.resolve中的deps来注册其依赖的。

    这种方法我们是从方法中解析出参数她的参数得知他所依赖的模块。下面来说说另外一种方法:从方法中解析出参数及参数声明的依赖注入。

    var injector = { // 依赖注入的抽象接口
      dependencies: {}, // 存储被依赖的模块
      isDeclare: false, // 是否为参数声明
      param: [], // 声明参数存储
      paramDeclare: function (param) { // 依赖注入参数声明
        if (Object.prototype.toString.call(param) !== '[object Array]') {
          try {
            throw new Error('接受的是一个数组,但是却得到一个' + typeof filterArray)
          } catch (e) {
            console.error(e)
          }
        }
        this.param = param.concat()
        console.log(this.param)
        this.isDeclare = true
      },
      register: function (key, value) { // 注册初始化被依赖的模块
        this.dependencies[key] = value
      },
      resolve: function (deps, func, scope) { // 注入到依赖的模块中,注入应该接受一个函数,并返回一个我们需要的函数
        console.log(deps)
        var paramNames = this.isDeclare ? this.param : this.getParamNames(func) // 取得参数名
        if (paramNames.length === 0 && this.isDeclare) {
          throw new Error('缺失的参数声明')
        }
        if (paramNames.length === 0 && !this.isDeclare) {
          throw new Error('该方法没有参数依赖')
        }
        var params = []
        for (var i = 0; i < paramNames.length; i++) { // 通过参数名在dependencies中取出相应的依赖
          let d = paramNames[i]
          let depen = this.dependencies[d] || deps[i]
          if (depen) {
            params.push(depen)
          } else {
            throw new Error('缺失的依赖:' + d)
          }
        }
        // 注入依赖,执行,并返回一个我们需要的函数
        return func.apply(scope || {}, params) // 将func作用域中的this关键字绑定到bind对象上,bind对象可以为空
      },
      getParamNames: function (func) { // 获取方法的参数名字
        var paramNames = func.toString().match(/^function\s*[^\(]*\(\s*([^\)]*)\)/m)[1]
        paramNames = paramNames.replace(/ /g, '')
        paramNames = paramNames.split(',')
        return paramNames // Array
      }
    }
    
    

    调用代码1:

    injector.register('notebook', new Notebook()) // 注册notebook
    injector.register('pencil', new Pencil()) // 注册pencil
    var school = new School()
    var student = new Student()
    // 以参数的形式传入school
    var studentWrite = injector.resolve([, , school], student.write, student)
    console.log(studentWrite) // "我拥有School、Pencil和Notebook"
    
    

    调用代码2:

    injector.register('notebook', new Notebook())
    injector.register('pencil', new Pencil())
    injector.paramDeclare(['notebook', 'pencil', 'school']) // injector.paramDeclare-依赖注入参数声明
    var school = new School()
    var student = new Student()
    var studentWrite = injector.resolve([, , school], student.write, student)
    console.log(studentWrite) // "我拥有School、Pencil和Notebook"
    
    

    4. 什么是IoC

    Ioc - Inversion of Control , 即"控制反转"。在开发中, IoC 意味着你设计好的对象交给容器控制,而不是使用传统的方式,在对象内部直接控制。

    如何理解好 IoC 呢?理解好 IoC的关键是要明确"谁控制谁,控制什么,为何是反转(有反转就应该有正转),哪些方面反转了",我们来深入分析一下。

    谁控制谁,控制什么: 在传统的程序设计中,我们直接在对象内部通过 new 的方式创建对象,是程序主动创建依赖对象;而 IoC 是有专门一个容器来创建这些对象,即由 IoC 容器控制对象的创建;谁控制谁?当然是 IoC 容器控制了对象;控制什么?主要是控制外部资源获取。

    为何是反转了,哪些方面反转了: 有反转就有正转,传统应用程序是由我们自己在对象中主动控制去获取依赖对象,也就是正转;而反转则是由容器来帮忙创建及注入依赖对象;为何是反转?因为由容器帮我们查找及注入依赖对象,对象只是被动的接受依赖对象,所以是反转了;哪些方面反转了?依赖对象的获取被反转了。

    5. IoC能做什么

    Ioc 不是一种技术,只是一种思想,一个重要的面向对象编程法则,它能指导我们如何设计松耦合、更优良的系统。传统应用程序都是由我们在类内部主动创建依赖对象,从而导致类与类之间高耦合,难于测试;有了 IoC 容器后,把创建和查找依赖对象的控制权交给了容器,由容器注入组合对象,所以对象之间是松散耦合,这样也便于测试,利于功能复用,更重要的是使得程序的整个体系结构变得非常灵活。

    其实 IoC 对编程带来的最大改变不是从代码上,而是思想上,发生了"主从换位"的变化。应用程序本来是老大,要获取什么资源都是主动出击,但在 IoC思想中,应用程序就变成被动了,被动的等待 IoC 容器来创建并注入它所需的资源了。

    6. IoC 和 DI

    DI - Dependency Injection,即"依赖注入":组件之间的依赖关系由容器在运行期决定,形象的说,即由容器动态的将某个依赖关系注入到组件之中。依赖注入的目的并非为软件系统带来更多功能,而是为了提升组件重用的频率,并为系统搭建一个灵活、可扩展的平台。通过依赖注入机制,我们只需要通过简单的配置,而无需任何代码就可指定目标需要的资源,完成自身的业务逻辑,而不需要关心具体的资源来自何处,由谁实现。

    理解 DI 的关键是:"谁依赖了谁,为什么需要依赖,谁注入了谁,注入了什么",那我们来深入分析一下:

    • 谁依赖了谁:当然是应用程序依赖 IoC 容器

    • 为什么需要依赖:应用程序需要 IoC 容器来提供对象需要的外部资源

    • 谁注入谁:很明显是 IoC 容器注入应用程序依赖的对象

    • 注入了什么:注入某个对象所需的外部资源(包括对象、资源、常量数据)

    IoC 和 DI 有什么关系?其实它们是同一个概念的不同角度描述,由于控制反转的概念比较含糊(可能只是理解为容器控制对象这一个层面,很难让人想到谁来维护依赖关系),所以 2004 年大师级人物 Martin Fowler 又给出了一个新的名字:"依赖注入",相对 IoC 而言,"依赖注入" 明确描述了被注入对象依赖 IoC 容器配置依赖对象。

    总的来说, 控制反转(Inversion of Control)是说创建对象的控制权发生转移,以前创建对象的主动权和创建时机由应用程序把控,而现在这种权利转交给 IoC 容器,它就是一个专门用来创建对象的工厂,你需要什么对象,它就给你什么对象。有了 IoC 容器,依赖关系就改变了,原先的依赖关系就没了,它们都依赖 IoC容器了,通过 IoC 容器来建立它们之间的关系。

    控制反转:创建对象实例的控制权从代码控制剥离到IOC容器控制,实际就是你在xml文件控制,侧重于原理。 依赖注入:创建对象实例时,为这个对象注入属性值或其它对象实例,侧重于实现。

    依赖注入和控制反转是同一概念,是对同一件事情的不同描述,它们描述的角度不同。

    依赖注入是从应用程序的角度在描述:应用程序依赖容器创建并注入它所需要的外部资源;

    而控制反转是从容器的角度在描述:容器控制应用程序,由容器反向的向应用程序注入应用程序所需要的外部资源(对象、文件等)。

    相关文章:

    相关文章

      网友评论

        本文标题:详解学习依赖注入(DI)与控制反转(Ioc)

        本文链接:https://www.haomeiwen.com/subject/uftcjhtx.html