技术·Cordis 从零开始 · 第 4·2026-08-20·7 分钟

第三篇:从 Greeter 依赖 Clock 出发,理解 inject

这一篇要解决的问题是:当一个能力不再独立存在,而是依赖另一个可能在运行过程中出现、消失和恢复的能力时,系统该如何保持状态一致。为了把这个问题讲清楚,我们会先用最直接的方式手动同步依赖,再看为什么这种同步责任一旦泄漏到外部流程中,inject 就会从“注入一个对象”变成“让 Runtime 接管依赖关系”。

1. 这一篇要解决的问题

现在我们希望问候语能够根据时间变化:

text
早上 → Good morning
下午 → Good afternoon
晚上 → Good evening

这意味着 Greeter 需要依赖一个新的能力:

text
Clock

关系可以写成:

text
Clock


Greeter

如果 Clock 永远存在,这件事其实不复杂;但为了更接近真实 Runtime,我们再加一个条件:Clock 不保证一直在线,它可能还没加载,也可能运行一段时间后掉线,之后再恢复。一旦依赖会在运行过程中变化,问题就不再是“构造函数里传一个对象”那么简单了。

这一篇的主线可以先压缩成下面这条路径:

text
Greeter 开始依赖 Clock

Clock 会动态出现与消失

手写 refresh 很容易漏掉

inject 把依赖关系交给 Runtime
inject 与动态依赖关系图
inject 与动态依赖关系图

2. Vanilla:手动同步依赖状态

先写一个最简单的 Clock

ts
let clock: Clock | undefined

function loadClock(period: Period = 'morning') {
  clock = { period }
}

function unloadClock() {
  clock = undefined
}

现在 Greeter 是否存在,取决于 Clock 是否存在:

ts
let greeter: Greeter | undefined

function refreshGreeter() {
  greeter = clock
    ? (useEnglish ? makeEnglishGreeter(clock) : makeChineseGreeter(clock))
    : undefined
}

于是每次 Clock 的状态变化以后,我们都必须显式同步 Greeter

ts
loadClock()
refreshGreeter()

unloadClock()
refreshGreeter()

loadClock('evening')
refreshGreeter()

这里真正值得警惕的不是多写了一次函数调用,而是系统产生了一个新的正确性约束:refreshGreeter() 变成了一个必须调用、却没有机制保证一定会调用的函数。比如我们写出下面这样的代码:

ts
unloadClock()

// 忘记 refreshGreeter()

那么系统就会进入一种很危险的状态:Clock 已经消失了,但 Greeter 可能还持有旧的 Clock。这类错误往往不会立刻爆炸,它只是让系统悄悄进入不一致状态,因此比直接报错更难发现。

3. Cordis:用 inject 声明依赖关系

在 Cordis 中,Clock 也是一个 Service

ts
class Clock extends Service {
  constructor(ctx: Context, public period: Period = 'morning') {
    super(ctx, 'clock')
  }

  setPeriod(period: Period) {
    this.period = period
  }
}

Greeter 则显式声明自己的依赖:

ts
abstract class Greeter extends Service {
  static inject = ['clock']

  constructor(ctx: Context) {
    super(ctx, 'greeter')
  }

  abstract greet(name: string): string
}

这句:

ts
static inject = ['clock']

表达的不是“启动时把 clock 传给我”这么简单,而是“Greeter 的可用状态依赖 Clock”。也就是说,Runtime 现在知道了一条正式的依赖关系:

text
Clock


Greeter

4. Cordis:让 Runtime 接管依赖状态

如果 Greeter 已经安装,但 Clock 还不存在,那么此时:

text
ctx.greeter unavailable

也就是说,依赖没有满足时,Greeter 不会真正变成可用能力。随后当我们加载 Clock

ts
await ctx.plugin(Clock)

Cordis 发现依赖已经满足,Greeter 就会自动可用;如果运行过程中 Clock 被卸载:

ts
await clockFiber.dispose()

那么 Greeter 也会一起失效。整个过程不再需要任何手写的:

ts
refreshGreeter()

因为 Runtime 会自己根据依赖状态决定 Greeter 是否激活。

5. inject 真正在管理什么

到这里可以看出,inject 管理的不是简单的 constructor injection,而是 dependency lifecycle。它关心的不是“第一次启动时把依赖对象给谁”,而是“依赖什么时候可用、什么时候不可用、不可用以后相关能力是不是也该停掉、恢复以后是不是该重新激活”。

换句话说,inject 的意义不是把一个对象塞进构造函数,而是把一条依赖关系交给 Runtime 管理。正因为有了这个声明,Cordis 才能持续维护下面这条状态链:

text
Clock unavailable

Greeter inactive

Clock available

Greeter active

Clock removed

Greeter inactive

Clock restored

Greeter active again

6. 这一篇的结论

这一篇最关键的结论是:当依赖会在运行过程中变化时,问题就不再是“怎么拿到依赖”,而是“谁来持续维护依赖关系的正确性”。Vanilla 可以通过 refreshGreeter() 勉强工作,但这种同步责任会泄漏到外部调用流程里;Cordis 则通过 inject 把依赖关系本身交给 Runtime 管理。

下一篇会把规模再扩大一点。只要再增加一个同样依赖 Clock 的模块,就能立刻看出手写同步逻辑为什么会从一个函数,迅速长成一片胶水代码。