【声明】本博客所有内容均为个人业余时间创作,所述技术案例均来自公开开源项目(如Github,Apache基金会),不涉及任何企业机密或未公开技术,如有侵权请联系删除
标题
164、【Agent】【OpenCode】TuiThreadCmd(工厂设计对比)
背景
上篇 blog
【Agent】【OpenCode】TuiThreadCmd(原生 fetch)
分析了原生 fetch 既是具体的函数实现,也有对应的类型声明,在具体实现上,fetch 是浏览器/Node.js/Deno 等宿主环境用 C++ / Rust / V8 等底层语言写死的真实函数,通常无法看到其源码,而在类型声明时,TypeScript 本身不提供任何运行时功能,它只负责描述形状,在之前的 createWorkerFetch 工厂函数的例子里,用了 typeof fetch 作为返回值类型,只借用了它的类型签名,而没有借用它的底层 C++ 实现,createWorkerFetch 的返回值就是要替代原生 fetch,下面继续分析
OpenCode
最后再对比下它俩,它们都用了工厂模式,但解决的问题和类型系统的玩法完全不同

简单来说:RPC 泛型推导是"造数据/造类型 ",而 createWorkerFetch 是"造函数/造行为"。
下面是它们的深度对比:
🔗 联系:为什么都用工厂模式?
- 依赖注入 / 闭包绑定 :两者都需要在创建时"记住 "一个外部依赖(RpcClient)。工厂函数把
client关进闭包,返回的东西就能随时使用它,而不需要调用方每次都传参。 - 关注点分离:把"配置/初始化"和"实际使用"分成两步。先配置好通道,再专心发请求/调方法。
- 运行时与编译时的桥梁 :都在运行时 生成具体的对象/函数,同时在编译时通过 TS 类型系统约束生成的产物。
⚔️ 区别:核心差异在哪?
| 维度 | RPC 泛型推导工厂 | createWorkerFetch 工厂 |
|---|---|---|
| 产出物 | 带类型的代理对象 / 方法映射 | 一个符合标准签名的函数 |
| 类型来源 | 动态推导:从 Schema/接口定义中提取,每个方法的参数和返回值类型都不同 | 静态借用:直接复用原生 typeof fetch,签名固定不变 |
| 泛型的角色 | 核心主角:T 决定了返回对象的每一个字段和方法的类型 |
几乎不用泛型:重点是结构兼容,而非类型变换 |
| 设计目标 | 让 IDE 能智能提示出所有 RPC 方法名、参数、返回值 | 让假 fetch 能无缝替换真 fetch,对第三方库透明 |
| 类型复杂度 | 高:涉及映射类型、条件类型、索引签名等高级技巧 | 低:只需保证函数签名一致即可 |
📌 在实际项目中,它们经常组合使用
在一个完整的 Worker RPC 架构里,这两者往往是配合关系:
javascript
// 1. RPC 泛型工厂:生成带类型的业务 API
const api = createRpcClient<MyServiceSchema>(client)
await api.getUser({ id: 1 }) // ✅ 有完整类型提示
// 2. Fetch 工厂:生成标准的网络层
const workerFetch = createWorkerFetch(client)
// 3. 组合:把假 fetch 传给依赖标准 fetch 的库
const queryClient = new QueryClient({
defaultOptions: { queryFn: ({ queryKey }) => workerFetch(`/api/${queryKey}`) }
})
一句话总结
两者都是工厂,但 RPC 泛型工厂的核心价值是 "类型推导 "------让 TS 知道有什么方法;createWorkerFetch 的核心价值是 "接口兼容 "------让代码感觉不到底层被换了。前者解决的是"开发体验",后者解决的是"架构透明性"。
下面来对比下 C 语言对这种设计模式的支持,先说结论:C 语言能实现"工厂返回函数"这个运行时行为,但完全无法实现 typeof fetch 这种编译时契约。
✅ C 语言能做到的部分(运行时)
C 有函数指针,完全可以写一个工厂返回函数:
javascript
// 1. 定义函数类型(类似 typeof fetch)
typedef Response* (*FetchFn)(const char* url, const RequestInit* init);
// 2. 工厂函数:闭包用结构体模拟
typedef struct { RpcClient* client; } FetchContext;
FetchFn createWorkerFetch(RpcClient* client) {
// ⚠️ C 没有闭包!必须手动分配上下文
FetchContext* ctx = malloc(sizeof(FetchContext));
ctx->client = client;
// 返回一个知道如何使用 ctx 的函数指针
// 但这里有个致命问题 ↓
}
❌ C 语言做不到的部分(也是核心差异)
| 能力 | TypeScript | C 语言 |
|---|---|---|
| 闭包 | (input, init?) => { ... } 自动捕获 client |
❌ 没有闭包,必须手动 malloc + 传上下文 |
| 返回的函数携带上下文 | 函数本身就是闭包,直接调用即可 | ❌ 函数指针是纯地址,不绑定任何数据 |
| 编译期签名校验 | : typeof fetch 强制检查参数/返回值 |
⚠️ typedef 只检查函数指针赋值,不检查实现内部 |
| 异步 Promise | Promise<Response> 是一等公民 |
❌ 没有原生异步,需回调或手写事件循环 |
| 结构化类型兼容 | 只要形状对就能赋值 | ❌ 必须精确匹配 typedef,无鸭子类型 |
🔑 最致命的障碍:C 没有闭包
TS 的 createWorkerFetch 之所以优雅,是因为箭头函数自动把 client 关进闭包,返回的函数可以直接当 fetch 用:
javascript
// TS:返回的函数自带上下文,调用方无感知
const myFetch = createWorkerFetch(client)
await myFetch("/api/data") // ✅ 不需要额外传 client
而 C 的函数指针是裸地址,不携带任何状态。要实现同样效果,必须二选一:
c
// 方案A:全局变量(丑、不可重入、线程不安全)
static RpcClient* g_client;
Response* workerFetch(const char* url, const RequestInit* init) {
rpc_call(g_client, url, init); // 依赖全局状态
}
// 方案B:改签名,加 context 参数(破坏了 fetch 接口兼容性!)
typedef Response* (*FetchFn)(void* ctx, const char* url, const RequestInit* init);
// ^^^^^^^^ 多了一个参数,不再是标准 fetch 签名
💡 核心矛盾
createWorkerFetch 的设计精髓是返回的东西和原生 fetch 签名一模一样 ,C 因为没有闭包,要么破坏签名(加 ctx 参数),要么引入全局状态 。两条路都违背了"透明替换"的设计目标。
📌 一句话总结
C 能实现"工厂返回函数指针"这个骨架,但因为没有闭包和结构化类型系统,它无法实现 createWorkerFetch 的灵魂------返回一个"自带上下文、且与原生 fetch 签名完全兼容、编译期可验证"的函数。这不是语法糖的差异,是语言抽象能力的代际差距。
OK,本篇先到这里,如有疑问,欢迎评论区留言讨论,祝各位功力大涨,技术更上一层楼!!!更多内容见下篇 blog