IOC在Nodejs上的初体验

前言

随着项目需求的增长,Node 模块也相继增长,各模块间的依赖耦合度愈来愈严重,很是难维护,有时候改一处代码须要懂好几处代码,项目逐渐达到牵一发而动全身的地步,所谓高内聚、低耦合彻底没得谈,如何作到模块间高度解耦是每一个工程师必要思考的问题。前端

设计模式

为了解决模块间依赖耦合的问题,最好的办法就是要找到一种设计模式来帮助咱们作到模块间解耦。其中作的很好的两个就是 DI 和 IOC 了,下面来探讨一下这两种设计模式。数据库

Dependency Injection

Dependency Injection简称DI,中文是依赖注入的意思。即模块之间的依赖由高层模块在运行期决定,形象的说,当高层模块在运行时肯定须要哪一个底层模块,而且注入进去。来看下面 🌰。设计模式

noDI服务器

这里有两个类,一个是订单模块类,一个是订单控制器类。app

// 订单model类 models/Order.js
class Order {
  constructor() {}
  insert() {
    //......数据库操做
    return true
  }
}

// 订单类 controllers/OrderController.js
const Order = require('./Order.js')

class OrderController {
  constructor() {
    this.order = new Order()
  }
  craeteOrder(...args) {
    this.order.insert(...args)
  }
}

//router/index.js
const OrderController = require('../controllers/OrderController.js')
const orderController = new OrderController()
复制代码

上面是没有依赖注入的状况,能够看出OrderController类严重耦合了Order类。上面的OrderController类依赖了Order类,因此在使用的时候就必须先require Order类,才能够在OrderController类中使用。耦合性过高,假如咱们须要把Order类文件移动到了别的目录,那么全部依赖这个类的文件都须要变化。koa

DI模块化

沿用上面两个类,咱们来看一下依赖注入的状况。函数

// 订单model类 models/Order.js
class Order {
  constructor() {}
  insert() {
    //......数据库操做
    return true
  }
}

// 订单类 controllers/OrderController.js

class OrderController {
  constructor(order) {
    this.order = new order()
  }
  craeteOrder(...args) {
    this.order.insert(...args)
  }
}

//router/index.js
const Order = require('../models/Order.js')
const OrderController = require('../controllers/OrderController.js')
const orderController = new OrderController(new Order())
复制代码

从上面代码来看OrderController类文件中已经不须要手动的引入Order类了,而是经过 constructor 在运行时的时候传进去。在 router 文件中,当实例化OrderController类的时候,同时也实例化Order类,而且做为OrderController构造函数的参数传进去。组件化

小结

能够看出依赖注入已经让咱们模块间解耦,可是仍是有点不足之处,下面总结一下依赖注入的优势与不足。post

优势

经过依赖注入的方式,是咱们高层模块和底层模块间的耦合下降了,所以,当底层模块位置变化的时候,咱们只须要懂 router 中的依赖路径就能够了,高层模块中作了什么咱们都不须要关心了。

不足

能够看出咱们全部的依赖的模块都是在 router 模块中引入的,明显的下降了 router 模块的复杂性。咱们须要一个用来专门管理注入方以及被注入方的容器,让 router 模块和往常同样轻量,没错这个东西就是 IOC。

Inversion of Control

Inversion of Control简称IOC,即控制反转,并非什么技术,而是一种设计模式。上面提到 DI 的不足可使用 IOC 来解决,其实 IOC 也叫 DI。IOC 意味着将依赖模块和被依赖模块都交给容器去管理,当咱们使用模块的时候,由容器动态的将它的依赖关系注入到模块中去。依据上面 DI 的思想,下面就来实现如下 IOC。

首先 IOC 中有个容器的概念,用来管理全部模块。

class IOC {
  constructor() {
    this.container = new Map()
  }
}
复制代码

咱们要有一个方法,用于让咱们往容器中存入模块,例如上面 Order 类,IOC 必需要有这么一个方法。

const Order = require('../models/Order.js')
const OrderController = require('../controllers/OrderController.js')
ioc.bind('order', (...args) => new OrderController(new Order(...args)))
复制代码

bind 方法用于往 IOC 容器中存放模块间的依赖,并此刻肯定高层模块的依赖项。

class IOC {
  constructor() {
    this.container = new Map()
  }
  bind(key, callback) {
    this.container.set(key, { callback, single: false })
  }
}
复制代码

上面就是 bind 方法很简单,你会发现我在容器的每一项上添加了 single 属性,这是用来标识有些类时单例的。下面来实现如下单例的写法。

class IOC {
  constructor() {
    this.container = new Map()
  }
  bind(key, callback) {
    this.container.set(key, { callback, single: false })
  }
  singleton(key, callback) {
    this.container.set(key, { callback, single: true })
  }
}
复制代码

当模块放入容器中的时候,咱们须要用的时候怎么办呢?因此必需要有一个方法用于获取到容器中的模块。

//router.js
const ioc = require('ioc')
const orderController = ioc.use('order')
复制代码

上面经过 use 方法就能够获取到了。下面是 use 方法的实现

class IOC {
  constructor() {
    this.container = new Map()
  }
  bind(key, callback) {
    this.container.set(key, { callback, single: false })
  }
  singleton(key, callback) {
    this.container.set(key, { callback, single: true })
  }
  use(key) {
    const item = this.container.get(key)
    if (!item) {
      throw new Error('error')
    }
    if (item.single && !item.instance) {
      item.instance = item.callback()
    }

    return item.single ? item.instance : item.callback()
  }
}
复制代码

上面代码就是 use 方法的实现,首先经过 key 值在容器中找到对应的模块,判断若是模块不存在则报错,而后判断是不是单例,若是是单例判断是否已经被实例化,已经实例化就不须要再进行,最后若是是单例的话返回单例实例,则执行 callback 实例化。

以上的代码彻底能够作到 IOC 的功能。

添油加醋

运行在服务器上的代码,当咱们去作测试的时候,确定不能直接使用运行时的代码。因此咱们给 IOC 添油加醋,作一个测试所用的容器。

class IOC {
  constructor() {
    this.container = new Map()
    this.fakes = new Map()
  }
  bind(key, callback) {
    this.container.set(key, { callback, single: false })
  }
  singleton(key, callback) {
    this.container.set(key, { callback, single: true })
  }
  fake(key, callback) {
    this.fakes.set(key, { callback, single: false })
  }
  restore(key) {
    this.fakes.delete(key)
  }
  findInContainer(key) {
    if (this.fakes.has(key)) {
      return this.fakes.get(key)
    }
    return this.container.get(key)
  }
  use(key) {
    const item = this.findInContainer(key)
    if (!item) {
      throw new Error('error')
    }
    if (item.single && !item.instance) {
      item.instance = item.callback()
    }

    return item.single ? item.instance : item.callback()
  }
}
复制代码

上面添加了三种方法:fakerestorefindInContainer

  1. fake 方法的做用是往测试容器中添加模块
  2. restore 方法的做用是删除测试容器中的模块
  3. findInContainer 的做用是统一 use 方法中生产容器和测试容器中获取模块的方法,当测试容器中存在的时候就取测试容器中的,不然取生产容器中的。(这里注意:测试容器中的模块用完即删)

最后咱们经过订单类 🌰 来实践一下使用 IOC 的状况。

首先定义一个 constants 文件用于存在全部的 key 值

const TYPES = {
  order: Symbol.for('order')
}
module.exports = {
  TYPES
}
复制代码

而后建立一个 orderIOC 文件作 IOC 的注册中心

const IOC = require('ioc')
const Order = require('../models/Order.js')
const OrderController = require('../controllers/OrderController.js')
const { TYPES } = require('../constants')
const ioc = new IOC()

ioc.bind(TYPES.order, (...args) => new OrderController(new Order(...args)))

module.exports = ioc
复制代码

在 router 文件中经过 IOC 来获取到 OrderController 类的实例,以 koa 为例

const Router = require('koa-router')
const ioc = require('../ioc')
const { TYPES } = require('../constants')
const router = new Router()
const orderController = ioc.use(TYPES.order)

router.post('/create', orderController.create)

module.exports = app => app.use(router.routes()).use(router.allowedMethods())
复制代码

总结

依赖耦合永远都是咱们在写业务上必需要解决的问题,不管是服务端的模块化仍是前端的组件化,下降依赖和让模块更稳定,更独立,实现关注点分离。

相关文章
相关标签/搜索