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())
複製代碼

總結

依賴耦合永遠都是咱們在寫業務上必需要解決的問題,不管是服務端的模塊化仍是前端的組件化,下降依賴和讓模塊更穩定,更獨立,實現關注點分離。

相關文章
相關標籤/搜索