依赖注入
简单来说,依赖注入由以下两部分组成:
- 参数化函数或类内部硬编码的依赖,这样我们能有更高程度的控制,并且这种方式会带来更好的测试性与可维护性(本质)
- 创建一个已经注入依赖的版本,这样我们可以分发给那些不需要关心内部构建的用户
让我们以一个生成随机数的函数的实现为例子进行说明。
假设我们有以下函数:
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 container1ts
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 initializationts
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 0ts
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)
}