技术·Cordis 从零开始 · 第 5·2026-08-21·6 分钟

第四篇:当多个模块共享同一依赖时,会发生什么

这一篇要解决的问题是:当多个模块共享同一个动态依赖时,系统复杂度为什么会突然上升。为了把这个问题讲清楚,我们不会一下子把场景做大,而是只再增加一个同样依赖 Clock 的模块,看看依赖图只是多出一条边时,外部同步逻辑会怎样膨胀。

1. 这一篇要解决的问题

现在再加入一个完全独立的能力:

text
Farewell

它也依赖 Clock,于是依赖图变成:

text
        Clock
        /   \
       ↓     ↓
   Greeter  Farewell

需要注意的是,GreeterFarewell 彼此并不互相 import,也不应该知道对方存在。它们只是恰好依赖同一个 Clock

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

text
一个依赖方还勉强能手写同步

两个依赖方开始复制 refresh

依赖图知识泄漏到外部流程

Runtime 统一维护依赖图
多个依赖方的复杂度变化图
多个依赖方的复杂度变化图

2. Vanilla:胶水代码开始复制

先定义 Farewell

ts
let farewell: Farewell | undefined

function refreshFarewell() {
  farewell = clock ? makeFarewell(clock) : undefined
}

现在每次 Clock 改变时,外部流程都必须同时刷新两个依赖方。加载时要写:

ts
loadClock()

refreshGreeter()
refreshFarewell()

卸载时要写:

ts
unloadClock()

refreshGreeter()
refreshFarewell()

恢复时仍然要写:

ts
loadClock('evening')

refreshGreeter()
refreshFarewell()

到这里问题就已经很清楚了。依赖图本身并不复杂,复杂的是调用点开始知道所有依赖方:

text
Clock lifecycle changed

       ├── refreshGreeter()
       └── refreshFarewell()

如果以后再增加 NotificationReminderScheduler 等模块,那么外部流程就会继续膨胀成:

ts
refreshGreeter()
refreshFarewell()
refreshNotification()
refreshReminder()
refreshScheduler()

这时真正失控的不是某一行代码,而是依赖图正在以手写胶水代码的形式散落在系统里。

3. Cordis:新增依赖方时仍然只声明依赖

在 Cordis 中,Farewell 只需要像 Greeter 一样声明自己的依赖:

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

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

  abstract farewell(name: string): string
}

核心仍然只有这一句:

ts
static inject = ['clock']

这意味着依赖关系由每个依赖方自己声明,而不是由 Clock 的加载流程硬编码出来。因此,当我们新增一个 Farewell 实现时,只需要:

ts
await ctx.plugin(EnglishFarewell)

不需要回头修改 Clock 的生命周期代码,也不需要新增一个 refreshFarewell()Clock 并不需要知道到底有多少模块依赖自己,Runtime 会根据依赖图自动处理这些模块的激活和失活。

4. 边际成本为什么开始分叉

如果 Clock 被卸载,那么两个依赖方都会一起失效;如果 Clock 恢复,它们又会一起重新出现。这个差异说明,Vanilla 和 Cordis 真正的分界点不在于某一个 Hello World 谁写得更短,而在于系统规模开始增长时,新增一个依赖方的边际成本是否还稳定。

在 Vanilla 中,新增一个依赖方,就意味着你必须在多个生命周期节点上补更多同步代码;而在 Cordis 中,新增一个依赖方,主要只是让新模块自己声明:

ts
static inject = ['clock']

依赖知识仍然保留在模块内部,而不是泄漏到外部流程中。

5. 这一篇的结论

这一篇真正想说明的是:当系统规模增长以后,问题不再是“能不能工作”,而是“系统的边际复杂度是不是在失控”。Vanilla 当然可以继续写下去,但依赖图会逐渐以 refreshXXX()、状态同步和清理逻辑的形式散落在系统各处;Cordis 则把依赖关系收回到声明式模型里,让新增模块时的额外成本更可控。

下一篇不再继续扩展示例,而是回过头来,把前面一路出现的 ContextPluginServiceEventLifecycleinject 统一收束成一张完整的 Runtime 心智图。