这一篇要解决的问题是:当一个能力不再独立存在,而是依赖另一个可能在运行过程中出现、消失和恢复的能力时,系统该如何保持状态一致。为了把这个问题讲清楚,我们会先用最直接的方式手动同步依赖,再看为什么这种同步责任一旦泄漏到外部流程中,inject 就会从“注入一个对象”变成“让 Runtime 接管依赖关系”。
1. 这一篇要解决的问题
现在我们希望问候语能够根据时间变化:
早上 → Good morning
下午 → Good afternoon
晚上 → Good evening这意味着 Greeter 需要依赖一个新的能力:
Clock关系可以写成:
Clock
│
↓
Greeter如果 Clock 永远存在,这件事其实不复杂;但为了更接近真实 Runtime,我们再加一个条件:Clock 不保证一直在线,它可能还没加载,也可能运行一段时间后掉线,之后再恢复。一旦依赖会在运行过程中变化,问题就不再是“构造函数里传一个对象”那么简单了。
这一篇的主线可以先压缩成下面这条路径:
Greeter 开始依赖 Clock
↓
Clock 会动态出现与消失
↓
手写 refresh 很容易漏掉
↓
inject 把依赖关系交给 Runtime2. Vanilla:手动同步依赖状态
先写一个最简单的 Clock:
let clock: Clock | undefined
function loadClock(period: Period = 'morning') {
clock = { period }
}
function unloadClock() {
clock = undefined
}现在 Greeter 是否存在,取决于 Clock 是否存在:
let greeter: Greeter | undefined
function refreshGreeter() {
greeter = clock
? (useEnglish ? makeEnglishGreeter(clock) : makeChineseGreeter(clock))
: undefined
}于是每次 Clock 的状态变化以后,我们都必须显式同步 Greeter:
loadClock()
refreshGreeter()
unloadClock()
refreshGreeter()
loadClock('evening')
refreshGreeter()这里真正值得警惕的不是多写了一次函数调用,而是系统产生了一个新的正确性约束:refreshGreeter() 变成了一个必须调用、却没有机制保证一定会调用的函数。比如我们写出下面这样的代码:
unloadClock()
// 忘记 refreshGreeter()那么系统就会进入一种很危险的状态:Clock 已经消失了,但 Greeter 可能还持有旧的 Clock。这类错误往往不会立刻爆炸,它只是让系统悄悄进入不一致状态,因此比直接报错更难发现。
3. Cordis:用 inject 声明依赖关系
在 Cordis 中,Clock 也是一个 Service:
class Clock extends Service {
constructor(ctx: Context, public period: Period = 'morning') {
super(ctx, 'clock')
}
setPeriod(period: Period) {
this.period = period
}
}Greeter 则显式声明自己的依赖:
abstract class Greeter extends Service {
static inject = ['clock']
constructor(ctx: Context) {
super(ctx, 'greeter')
}
abstract greet(name: string): string
}这句:
static inject = ['clock']表达的不是“启动时把 clock 传给我”这么简单,而是“Greeter 的可用状态依赖 Clock”。也就是说,Runtime 现在知道了一条正式的依赖关系:
Clock
│
↓
Greeter4. Cordis:让 Runtime 接管依赖状态
如果 Greeter 已经安装,但 Clock 还不存在,那么此时:
ctx.greeter unavailable也就是说,依赖没有满足时,Greeter 不会真正变成可用能力。随后当我们加载 Clock:
await ctx.plugin(Clock)Cordis 发现依赖已经满足,Greeter 就会自动可用;如果运行过程中 Clock 被卸载:
await clockFiber.dispose()那么 Greeter 也会一起失效。整个过程不再需要任何手写的:
refreshGreeter()因为 Runtime 会自己根据依赖状态决定 Greeter 是否激活。
5. inject 真正在管理什么
到这里可以看出,inject 管理的不是简单的 constructor injection,而是 dependency lifecycle。它关心的不是“第一次启动时把依赖对象给谁”,而是“依赖什么时候可用、什么时候不可用、不可用以后相关能力是不是也该停掉、恢复以后是不是该重新激活”。
换句话说,inject 的意义不是把一个对象塞进构造函数,而是把一条依赖关系交给 Runtime 管理。正因为有了这个声明,Cordis 才能持续维护下面这条状态链:
Clock unavailable
↓
Greeter inactive
Clock available
↓
Greeter active
Clock removed
↓
Greeter inactive
Clock restored
↓
Greeter active again6. 这一篇的结论
这一篇最关键的结论是:当依赖会在运行过程中变化时,问题就不再是“怎么拿到依赖”,而是“谁来持续维护依赖关系的正确性”。Vanilla 可以通过 refreshGreeter() 勉强工作,但这种同步责任会泄漏到外部调用流程里;Cordis 则通过 inject 把依赖关系本身交给 Runtime 管理。
下一篇会把规模再扩大一点。只要再增加一个同样依赖 Clock 的模块,就能立刻看出手写同步逻辑为什么会从一个函数,迅速长成一片胶水代码。