JavaScript 中的依赖注入容器

2025-06-09

JavaScript 中的依赖注入容器

在Medium上找到我
加入我的时事通讯

JavaScript 因其灵活性而具备多种技术。在本文中,我们将讨论依赖注入容器。

这种模式实际上提供了与依赖注入相同的目标,但是以更灵活和强大的方式充当容器,在需要时(例如在初始化阶段)容纳需要它们的函数(或类)的依赖项。

无需容器的依赖注入

让我们快速回顾一下依赖注入是什么、它在代码中是什么样子、它解决了什么问题以及它存在什么问题。

依赖注入是一种模式,它有助于避免模块中硬编码的依赖关系,使调用者能够在一个地方更改它们并提供自己的依赖关系(如果他们愿意的话)。

这些依赖项可以注入构造函数(实例化)阶段,也可以稍后通过某些setter 方法设置

class Frog {
  constructor(name, gender) {
    this.name = name
    this.gender = gender
  }

  jump() {
    console.log('jumped')
  }
}

class Toad {
  constructor(habitat, name, gender) {
    this.habitat = habitat
    this.frog = new Frog(name, gender)
  }
}

const mikeTheToad = new Toad('land', 'mike', 'male')
Enter fullscreen mode Exit fullscreen mode

这其中存在一些问题:

问题#1:如果我们需要改变Toad构造方式,并且它需要一些脆弱的东西,比如参数的定位或数据结构,我们就必须手动更改代码,因为它被硬编码到他们的代码块中。

这种场景的一个例子是当课堂上发生重大变化时Frog

首先,如果在其构造函数中Frog 添加了第三个参数weight,例如:

class Frog {
  constructor(name, gender, weight) {
    this.name = name
    this.gender = gender
    this.weight = weight
  }

  jump() {
    console.log('jumped')
  }
}
Enter fullscreen mode Exit fullscreen mode

然后我们Toad 必须更新,因为这个新的依赖项被添加到我们的Frog实例中:

class Toad {
  constructor(habitat, name, gender, weight) {
    this.habitat = habitat
    this.frog = new Frog(name, gender, weight)
  }
}
Enter fullscreen mode Exit fullscreen mode

Toad因此,如果我们保持这种方式,如果你在某个青蛙创业公司,并且这是你开始使用的第一批代码之一,你认为你最终需要改变多少次?

问题 #2:您必须知道Toad每次要使用什么依赖项。

我们必须知道,现在需要 4 个完全相同顺序的Toad参数才能正确启动一个实例,甚至它们的数据类型也必须相同,否则很容易出现错误。Frog

Toad 如果你知道 a 本质上是一只青蛙,你可能会误以为Toad会延伸,这看起来会很尴尬。然后你意识到,实际上是在内部创建了Frog一个 的实例,现在你完全糊涂了,因为你是一个聪明的人,代码把你搞糊涂了——意识到代码与现实世界不符。FrogToad

问题 3:不必要地涉及更多代码

使用依赖注入模式,通过反转依赖项实例化方式的控制来解决这些问题:

class Frog {
  constructor({ name, gender, weight }) {
    this.name = name
    this.gender = gender
    this.weight = weight
  }

  jump() {
    console.log('jumped')
  }
}

class Toad {
  constructor(habitat, frog) {
    this.habitat = habitat
    this.frog = frog
  }
}
Enter fullscreen mode Exit fullscreen mode

好吧,这很简单。现在,当 发生另一个重大更改时Frog(例如将参数放入 JavaScript对象中),我们甚至不必触及Toad或浪费脑细胞去阅读Toad,然后Frog,然后再回到Toad,等等。

这是因为我们现在只需更改创建实例的部分Toad(这比进入内部并在Toad实现中更改内容要好 - 这是不好的做法!它不必担心如何构造青蛙 - 它只应该知道它将青蛙作为参数并将其存储在其.frog属性中以供以后使用。现在负责它的依赖关系。

const mikeTheToad = new Toad(
  'land',
  new Frog({
    name: 'mike',
    gender: 'male',
    weight: 12.5,
  }),
)
Enter fullscreen mode Exit fullscreen mode

Frog所以,我们只是通过从构造函数中抽象出实现细节来实践了一些简洁的代码实践Toad。这很合理:真的Toad需要关心它是如何 Frog构造的吗?如果有的话,它应该只是扩展它而已!

依赖注入容器(DIC)模式

现在我们已经重新认识了依赖注入,让我们来讨论依赖注入容器!

那么为什么我们需要 DIC 模式,以及为什么在困难的情况下没有容器的依赖注入是不够

问题在于:它根本不具备可扩展性。项目越大,你就越容易对长期维护代码失去信心,因为随着时间的推移,代码会变得一团糟。此外,你还必须确保依赖注入的顺序正确,以免undefined在实例化某个对象时出现问题。

因此从本质上讲,6 个月后我们的代码可以演变成如下形式:

class Frog {
  constructor({ name, gender, weight }) {
    this.name = name
    this.gender = gender
    this.weight = weight
  }

  jump() {
    console.log('jumped')
  }

  setHabitat(habitat) {
    this.habitat = habitat
  }
}

class Toad extends Frog {
  constructor(options) {
    super(options)
  }

  leap() {
    console.log('leaped')
  }
}

class Person {
  constructor() {
    this.id = createId()
  }
  setName(name) {
    this.name = name
    return this
  }
  setGender(gender) {
    this.gender = gender
    return this
  }
  setAge(age) {
    this.age = age
    return this
  }
}

function createId() {
  var idStrLen = 32
  var idStr = (Math.floor(Math.random() * 25) + 10).toString(36) + '_'
  idStr += new Date().getTime().toString(36) + '_'
  do {
    idStr += Math.floor(Math.random() * 35).toString(36)
  } while (idStr.length < idStrLen)

  return idStr
}

class FrogAdoptionFacility {
  constructor(name, description, location) {
    this.name = name
    this.description = description
    this.location = location
    this.contracts = {}
    this.adoptions = {}
  }

  createContract(employee, client) {
    const contractId = createId()
    this.contracts[contractId] = {
      id: contractId,
      preparer: employee,
      client,
      signed: false,
    }
    return this.contracts[contractId]
  }

  signContract(id, signee) {
    this.contracts[id].signed = true
  }

  setAdoption(frogOwner, frogOwnerLicense, frog, contract) {
    const adoption = {
      [frogOwner.id]: {
        owner: {
          firstName: frogOwner.owner.name.split(' ')[0],
          lastName: frogOwner.owner.name.split(' ')[1],
          id: frogOwner.id,
        },
        frog,
        contract,
        license: {
          id: frogOwnerLicense.id,
        },
      },
    }
    this.adoptions[contract.id] = adoption
  }

  getAdoption(id) {
    return this.adoptions[id]
  }
}

class FrogParadiseLicense {
  constructor(frogOwner, licensePreparer, frog, location) {
    this.id = createId()
    this.client = {
      firstName: frogOwner.name.split(' ')[0],
      lastName: frogOwner.name.split(' ')[1],
      id: frogOwner.id,
    }
    this.preparer = {
      firstName: licensePreparer.name.split(' ')[0],
      lastName: licensePreparer.name.split(' ')[1],
      id: licensePreparer.id,
    }
    this.frog = frog
    this.location = `${location.street} ${location.city} ${location.state} ${location.zip}`
  }
}

class FrogParadiseOwner {
  constructor(frogOwner, frogOwnerLicense, frog) {
    this.id = createId()
    this.owner = {
      id: frogOwner.id,
      firstName: frogOwner.name.split(' ')[0],
      lastName: frogOwner.name.split(' ')[1],
    }
    this.license = frogOwnerLicense
    this.frog = frog
  }

  createDocument() {
    return JSON.stringify(this, null, 2)
  }
}
Enter fullscreen mode Exit fullscreen mode

我们有一个很棒的应用程序——一个青蛙领养机构,顾客可以来领养一只青蛙。但领养过程并非简单的金钱交易。我们假设有一条法律要求,每个青蛙领养机构在将青蛙交给新主人时都必须进行这一流程。

setAdoption当fromFrogAdoptionFacility被调用时,整个收养过程就结束了。

假设您开始使用这些类开发代码并最终得到如下工作版本:

const facilityTitle = 'Frog Paradise'
const facilityDescription =
  'Your new one-stop location for fresh frogs from the sea! ' +
  'Our frogs are housed with great care from the best professionals all over the world. ' +
  'Our frogs make great companionship from a wide variety of age groups, from toddlers to ' +
  'senior adults! What are you waiting for? ' +
  'Buy a frog today and begin an unforgettable adventure with a companion you dreamed for!'
const facilityLocation = {
  address: '1104 Bodger St',
  suite: '#203',
  state: 'NY',
  country: 'USA',
  zip: 92804,
}

const frogParadise = new FrogAdoptionFacility(
  facilityTitle,
  facilityDescription,
  facilityLocation,
)

const mikeTheToad = new Toad({
  name: 'mike',
  gender: 'male',
  weight: 12.5,
})

const sally = new Person()
sally
  .setName('sally tran')
  .setGender('female')
  .setAge(27)

const richardTheEmployee = new Person()
richardTheEmployee
  .setName('richard rodriguez')
  .setGender('male')
  .setAge(77)

const contract = frogParadise.createContract(richardTheEmployee, sally)

frogParadise.signContract(contract.id, sally)

const sallysLicense = new FrogParadiseLicense(
  sally,
  richardTheEmployee,
  mikeTheToad,
  facilityLocation,
)

const sallyAsPetOwner = new FrogParadiseOwner(sally, sallysLicense, mikeTheToad)

frogParadise.setAdoption(sallyAsPetOwner, sallysLicense, mikeTheToad, contract)

const adoption = frogParadise.getAdoption(contract.id)
console.log(JSON.stringify(adoption, null, 2))
Enter fullscreen mode Exit fullscreen mode

如果我们运行代码,它将起作用并为我们创建一个新的采用对象,如下所示:

{
  "t_k8pgj8gh_k4ofadkj2x4yluemfgvmm": {
    "owner": {
      "firstName": "sally",
      "lastName": "tran",
      "id": "t_k8pgj8gh_k4ofadkj2x4yluemfgvmm"
    },
    "frog": {
      "name": "mike",
      "gender": "male",
      "weight": 12.5
    },
    "contract": {
      "id": "m_k8pgj8gh_kdfr55oui28c88lisswak",
      "preparer": {
        "id": "n_k8pgj8gh_uxlbmbflwjrj4cqgjyvyw",
        "name": "richard rodriguez",
        "gender": "male",
        "age": 77
      },
      "client": {
        "id": "h_k8pgj8gh_hkqvp4f3uids8uj00i47d",
        "name": "sally tran",
        "gender": "female",
        "age": 27
      },
      "signed": true
    },
    "license": {
      "id": "y_k8pgj8gh_0qnwm9po0cj7p3vgsedu3"
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

我们有一个很棒的应用程序——一个青蛙领养机构,顾客可以来领养一只青蛙。但领养过程并非简单的金钱交易。我们假设有一条法律要求,每个青蛙领养机构在将青蛙交给新主人时都必须进行这一流程。

因此, Frog Paradise需要与客户签订一份合同,并由客户签字。之后,Frog Paradise 还会现场签发许可证,客户需要随身携带,以提供法律保障。最后,一切完成后,领养就完成了。

看一下这个FrogOwner类:

class FrogParadiseOwner {
  constructor(frogOwner, frogOwnerLicense, frog) {
    this.id = createId()
    this.owner = frogOwner
    this.license = frogOwnerLicense
    this.frog = frog
  }

  createDocument() {
    return JSON.stringify(this, null, 2)
  }
}
Enter fullscreen mode Exit fullscreen mode

它有三个依赖项:frogOwner、、frogOwnerLicensefrog

frogOwner假设(的实例)进行了更新,Person并且它变为了的实例Client

class Client extends Person {
  setName(name) {
    this.name = name
  }
}
Enter fullscreen mode Exit fullscreen mode

FrogParadiseOwner现在需要更新初始化调用。

但是如果我们FrogParadiseOwner在代码的多个位置都进行了初始化会怎么样呢?如果我们的代码变得更长,并且这些实例的数量增加,那么维护起来就会变得更加困难。

这就是依赖注入容器可以发挥作用的地方,因为您只需要在一处更改代码。

依赖注入容器如下所示:

import parseFunction from 'parse-function'

const app = parseFunction({
  ecmaVersion: 2017,
})

class DIC {
  constructor() {
    this.dependencies = {}
    this.factories = {}
  }

  register(name, dependency) {
    this.dependencies[name] = dependency
  }

  factory(name, factory) {
    this.factories[name] = factory
  }

  get(name) {
    if (!this.dependencies[name]) {
      const factory = this.factories[name]
      if (factory) {
        this.dependencies[name] = this.inject(factory)
      } else {
        throw new Error('No module found for: ' + name)
      }
    }
    return this.dependencies[name]
  }

  inject(factory) {
    const fnArgs = app.parse(factory).args.map((arg) => this.get(arg))
    return new factory(...fnArgs)
  }
}
Enter fullscreen mode Exit fullscreen mode

有了这个,更新更改就变得非常简单:

class Client extends Person {
  setName(name) {
    this.name = name
  }
}

const dic = new DIC()
dic.register('frogOwner', Client)
dic.register('frogOwnerLicense', sallysLicense)
dic.register('frog', mikeTheToad)

dic.factory('frog-owner', FrogParadiseOwner)
const frogOwner = dic.get('frog-owner')
Enter fullscreen mode Exit fullscreen mode

现在,我们不再像以前那样直接初始化它,而是必须更改代码的所有其他实例:

const frogOwner = new FrogParadiseOwner(Client, sallysLicense, mikeTheToad)
// some other location
const frogOwner2 = new FrogParadiseOwner(...)
// some other location
const frogOwner3 = new FrogParadiseOwner(...)
// some other location
const frogOwner4 = new FrogParadiseOwner(...)
// some other location
const frogOwner5 = new FrogParadiseOwner(...)
Enter fullscreen mode Exit fullscreen mode

您可以改为使用 DIC 来更新一次,而不需要更改代码的任何其他部分,因为我们将其流向容器的方向反转了:

// Update here only by passing the dependency to the DIC
const dic = new DIC()
dic.register('frogOwner', Client)
dic.register('frogOwnerLicense', sallysLicense)
dic.register('frog', mikeTheToad)
dic.factory('frog-owner', FrogParadiseOwner)

const frogOwner = dic.get('frog-owner')
Enter fullscreen mode Exit fullscreen mode

让我们解释一下 DIC 正在做什么:

您可以通过将任何想要由 DIC 解析的类或函数传递到.factory()存储在.factory属性中的方法中来插入它。

JavaScript 中的依赖注入容器工厂方法

对于传入的每个函数,.factory您都必须使用 来注册它们的参数,.register()以便在容器初始化请求的函数时获取它们。它们从.dependencies属性中获取。您可以使用 方法来将内容添加到依赖项中.dependencies()

JavaScript 中的依赖注入容器依赖项

当你想要检索某些内容时,可以使用.getwith some key。它会使用key来查找 its dependencies,如果找到内容,就会返回它。否则,它会继续查找 its factories,如果找到内容,就会将其视为你希望它解析的函数。

依赖注入容器在 JavaScript 中的获取方法

然后,它将调用传递给函数,.inject在其中读取函数依赖项(参数)的名称并从其.dependencies属性中获取它们,调用函数并注入其参数,返回结果。

依赖注入容器注入 JavaScript

在我们的代码示例中,我曾经parse-function允许该inject方法获取函数参数的名称。

要在不使用库的情况下执行此操作,您可以添加一个额外的参数.get并将其传递给它,.inject如下所示:

class DIC {
  constructor() {
    this.dependencies = {}
    this.factories = {}
  }

  register(name, dependency) {
    this.dependencies[name] = dependency
  }

  factory(name, factory) {
    this.factories[name] = factory
  }

  get(name, args) {
    if (!this.dependencies[name]) {
      const factory = this.factories[name]
      if (factory) {
        this.dependencies[name] = this.inject(factory, args)
      } else {
        throw new Error('No module found for: ' + name)
      }
    }
    return this.dependencies[name]
  }

  inject(factory, args = []) {
    const fnArgs = args.map((arg) => this.get(arg))
    return new factory(...fnArgs)
  }
}
Enter fullscreen mode Exit fullscreen mode
const dic = new DIC()
dic.register('frogOwner', Client)
dic.register('frogOwnerLicense', sallysLicense)
dic.register('frog', mikeTheToad)

dic.factory('frog-owner', FrogParadiseOwner)
const frogOwner = dic.get('frog-owner', [
  'frogOwner',
  'frogOwnerLicense',
  'frog',
])
console.log('frog-owner', JSON.stringify(frogOwner, null, 2))
Enter fullscreen mode Exit fullscreen mode

尽管如此,我们仍然得到相同的结果:

{
  "id": "u_k8q16rjx_fgrw6b0yb528unp3trokb",
  "license": {
    "id": "m_k8q16rjk_jipoch164dsbpnwi23xin",
    "client": {
      "firstName": "sally",
      "lastName": "tran",
      "id": "b_k8q16rjk_0xfqodlst2wqh0pxcl91j"
    },
    "preparer": {
      "firstName": "richard",
      "lastName": "rodriguez",
      "id": "g_k8q16rjk_f13fbvga6j2bjfmriir63"
    },
    "frog": {
      "name": "mike",
      "gender": "male",
      "weight": 12.5
    },
    "location": "undefined undefined NY 92804"
  },
  "frog": {
    "name": "mike",
    "gender": "male",
    "weight": 12.5
  }
}
Enter fullscreen mode Exit fullscreen mode

在Medium上找到我
加入我的时事通讯

鏂囩珷鏉ユ簮锛�https://dev.to/jsmanifest/dependency-injection-containers-in-javascript-12ng
PREV
如何最大化 React 组件的可重用性
NEXT
2020 年 React 中操作和使用组件的 9 种方法