Skip to content

依赖注入

简单来说,依赖注入由以下两部分组成:

  • 参数化函数或类内部硬编码的依赖,这样我们能有更高程度的控制,并且这种方式会带来更好的测试性与可维护性(本质)
  • 创建一个已经注入依赖的版本,这样我们可以分发给那些不需要关心内部构建的用户

让我们以一个生成随机数的函数的实现为例子进行说明。

假设我们有以下函数:

ts
export const randomNumber1 = (max: number) =>
  Math.floor(Math.random() * (max + 1))

由于函数返回值是随机的,显然无法做单测。想要对上述函数做单测,我们就需要修改该函数,下面提供几种方法。

参数化

ts
export type RandomGenerator = () => number

export const randomNumber2 = (randomGenerator: RandomGenerator, max: number) =>
  Math.floor(randomGenerator() * (max + 1))
ts
export const randomNumberList1 = (max: number, length: number) =>
  Array(length)
    .fill(null)
    .map(() => randomNumber2(Math.random, max))

缺点

破坏了函数本身接口,降低了易用性,而且如果已经有人使用这个函数,这将会是一个 BREAKING CHANGES,我们可以看一下 randomNumberList1,之前使用 randomNumber 的用户不需要关心 randomGenerator 实现,甚至不会注意到它的存在,因为实现细节被有意封装在 randomNumber 内部。现在,因为 randomGenerator 被暴露出来,用户需要负责提供 randomGenerator,这意味着他们更有可能被 randomNumber 内部的更改所影响。例如,我们决定更改 randomGenerator 的接口。此时,用户就有可能需要更改他们提供的函数以适配新的接口。

区分构建与使用

ts
export const randomNumberImplementation = (
  randomGenerator: RandomGenerator,
  max: number
) => Math.floor(randomGenerator() * (max + 1))

export const randomNumber3 = (max: number) =>
  randomNumberImplementation(Math.random, max)
ts
export const randomNumberList2 = (max: number, length: number) =>
  Array(length)
    .fill(null)
    .map(() => randomNumber3(max))

缺点

依赖与原始参数混合一起,导致 randomNumber 的参数不得不传递给内部构建的 randomNumber 版本。

工厂函数模式

ts
export const makeRandomNumber =
  (randomGenerator: RandomGenerator) => (max: number) =>
    Math.floor(randomGenerator() * (max + 1))

export const randomNumber4 = makeRandomNumber(Math.random)

缺点

代码中同时存在构建和使用,这样职责不清晰,而且因为每个文件都要提前引用依赖,依赖间容易形成循环引用,即便从具体函数层面看,并没有发生函数间的循环引用。

统一依赖注入入口

ts
import { secureRandomNumber } from './secure-random-number'
import { makeFastRandomNumber1 } from './fast-random-number'
import { makeRandomNumberList1 } from './random-number-list'

const randomGenerator = Math.random
const fastRandomNumber = makeFastRandomNumber1(randomGenerator)
const randomNumber =
  process.env.NODE_ENV === 'production' ? secureRandomNumber : fastRandomNumber
const randomNumberList = makeRandomNumberList1(randomNumber)

export const container1 = {
  randomNumber,
  randomNumberList,
}

export type Container1 = typeof container1
ts
export const secureRandomNumber = (max: number) =>
  Math.floor(Math.random() * (max + 1))
ts
export type RandomGenerator = () => number

export const makeFastRandomNumber1 =
  (randomGenerator: RandomGenerator) => (max: number) =>
    Math.floor(randomGenerator() * (max + 1))
ts
export type RandomNumber = (max: number) => number

export const makeRandomNumberList1 =
  (randomNumber: RandomNumber) => (max: number, length: number) =>
    Array(length)
      .fill(null)
      .map(() => randomNumber(max))

缺点

统一注入的入口代码要随着业务文件的变化而变化,同时,如果构造函数之间存在复杂的依赖链条,手动维护起顺序将是一件越来越复杂的事情:比如 A 依赖 B,B 依赖 C,那么想要初始化 C 的构造函数,就要先初始化 A 再初始化 B,最后初始化 C。

自动依赖注入容器,保证依赖顺序正确

ts
import { makeFastRandomNumber } from './fast-random-number'
import { makeRandomNumberList } from './random-number-list'

const dependenciesFactories = {
  randomNumber:
    process.env.NODE_ENV === 'production'
      ? () => secureRandomNumber
      : makeFastRandomNumber,
  randomNumberList: makeRandomNumberList,
  randomGenerator: () => Math.random,
}

type DependenciesFactories = typeof dependenciesFactories

export type Container = {
  [p in keyof DependenciesFactories]: ReturnType<DependenciesFactories[p]>
}

export const container = {} as Container

Object.entries(dependenciesFactories).forEach(([dependencyName, factory]) => {
  Object.defineProperty(container, dependencyName, {
    // 这里使用 getter 避免立即执行
    // 工厂函数, 不然执行的时候如果有
    // 依赖为 undefined 会报错
    // 这种方式, 工厂函数只能在整个 container
    // 设置好的时候被调用, 如果 factory 里
    // 调用了其它依赖, 将会递归执行该步骤,
    // 这样所有的依赖就被创建了
    get: () => factory(container),
  })
})
ts
export const secureRandomNumber = (max: number) =>
  Math.floor(Math.random() * (max + 1))
ts
export interface Dependencies {
  randomGenerator: () => number
}

export const makeFastRandomNumber =
  ({ randomGenerator }: Dependencies) =>
  (max: number) =>
    Math.floor(randomGenerator() * (max + 1))
ts
export interface Dependencies {
  randomNumber: (max: number) => number
}

export const makeRandomNumberList =
  ({ randomNumber }: Dependencies) =>
  (max: number, length: number) =>
    Array(length)
      .fill(null)
      .map(() => randomNumber(max))

缺点

需要解决循环依赖。下面使用两个例子说明这个问题。

使用依赖注入的情况下:

ts
import { a } from './a'

a(2)
// ReferenceError: Cannot access 'a' before initialization
ts
import { makeA } from './a-impl'
import { b } from './b'

export type A = (value: number) => void

export const a = makeA({ b })
ts
import { makeB } from './b-impl'
import { a } from './a'

export type B = (value: number) => void

export const b = makeB({ a })
ts
import { B } from './b'

export interface Dependencies {
  b: B
}

export const makeA =
  ({ b }: Dependencies) =>
  (value: number) => {
    console.log('a', value)
    if (!value) return
    b(value - 1)
  }
ts
import { A } from './a'

export interface Dependencies {
  a: A
}

export const makeB =
  ({ a }: Dependencies) =>
  (value: number) => {
    console.log('b', value)
    if (!value) return
    a(value - 1)
  }

没有使用依赖注入的情况下:

ts
import { a } from './a'

a(2)
// a 2
// b 1
// a 0
ts
import { b } from './b'

export const a = (value: number) => {
  console.log('a', value)
  if (!value) return
  b(value - 1)
}
ts
import { a } from './a'

export const b = (value: number) => {
  console.log('b', value)
  if (!value) return
  a(value - 1)
}

参考