关于 factory-function-composition
面向 TypeScript 开发者的工厂函数模式指南,介绍依赖注入与函数组合。内容涵盖参数约定、配置分层、函数内部组织和返回类型。
主要功能
- 01依赖优先的工厂签名
- 02区分客户端、服务和方法选项
- 03返回包含方法的对象
- 04工厂函数的四区组织
- 05结构化契约兼容
- 06适配器与 `this` 使用指南
适用场景
- 01重构函数以注入客户端
- 02在多个服务间共享客户端
- 03分离各层配置
- 04让工厂返回值符合外部契约
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill factory-function-composition安装后在 Claude Code 中输入/factory-function-composition确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/method-documentation-and-sibling-calls.mdsingle-or-array-pattern.mdsync-construction-render-gate.md详细介绍
工厂函数组合
这项技能可帮助你在 TypeScript 中应用工厂函数模式,实现简洁的依赖注入和函数组合。
相关技能:有关调用方计数和内联仅使用一次的提取内容,请参阅重构方法。
通用签名
每个工厂函数都遵循此签名:
function createSomething(dependencies, options?) {
return {
/* methods */
};
}
- 第一个参数:始终是资源。可以是单个客户端,也可以是由多个依赖项解构而成的对象。
- 第二个参数:此工厂专属的可选配置。绝不能是客户端配置;客户端配置应在创建客户端时设置。
最多两个参数。第一个是资源,第二个是配置。没有例外。
核心模式
// Single dependency
function createService(client, options = {}) {
return {
method(methodOptions) {
// Uses client, options, and methodOptions
},
};
}
// Multiple dependencies
function createService({ db, cache }, options = {}) {
return {
method(methodOptions) {
// Uses db, cache, options, and methodOptions
},
};
}
// Usage
const client = createClient(clientOptions);
const service = createService(client, serviceOptions);
service.method(methodOptions);
关键原则
- 客户端配置应在创建客户端时设置:不要通过工厂传递 clientOptions
- 每一层都有自己的选项:客户端、服务和方法的选项应彼此分开
- 依赖项放在前面:工厂函数将依赖项作为第一个参数
- 返回带有方法的对象:不要返回需要传入资源的独立函数
识别反模式
反模式 1:函数将客户端作为第一个参数
// Bad
function doSomething(client, options) { ... }
doSomething(client, options);
// Good
const service = createService(client);
service.doSomething(options);
反模式 2:在内部隐藏客户端创建过程
// Bad
function doSomething(clientOptions, methodOptions) {
const client = createClient(clientOptions); // Hidden!
// ...
}
// Good
const client = createClient(clientOptions);
const service = createService(client);
service.doSomething(methodOptions);
反模式 3:混杂的选项对象
// Bad
doSomething({
timeout: 5000, // Client option
retries: 3, // Client option
endpoint: '/users', // Method option
payload: data, // Method option
});
// Good
const client = createClient({ timeout: 5000, retries: 3 });
const service = createService(client);
service.doSomething({ endpoint: '/users', payload: data });
反模式 4:隐藏多个层级
// Bad
function doSomething(clientOptions, serviceOptions, methodOptions) {
const client = createClient(clientOptions);
const service = createService(client, serviceOptions);
return service.method(methodOptions);
}
// Good: each layer visible and configurable
const client = createClient(clientOptions);
const service = createService(client, serviceOptions);
service.method(methodOptions);
多个依赖项
当服务需要多个客户端时:
function createService(
{ db, cache, http }, // Dependencies as destructured object
options = {}, // Service options
) {
return {
method(methodOptions) {
// Uses db, cache, http
},
};
}
// Usage
const db = createDbConnection(dbOptions);
const cache = createCacheClient(cacheOptions);
const http = createHttpClient(httpOptions);
const service = createService({ db, cache, http }, serviceOptions);
service.method(methodOptions);
规范的内部结构
前面的章节介绍了外部签名:(deps, options?) → return { methods }。本节介绍函数体内部的结构。每个工厂函数都遵循四个区域的顺序:
// Option A: destructure in the signature (preferred for small dep lists)
function createSomething({ db, cache }: Deps, options?) {
const maxRetries = options?.maxRetries ?? 3;
// ...
}
// Option B: destructure in zone 1 (fine when you also need the deps object itself)
function createSomething(deps: Deps, options?) {
const { db, cache } = deps;
const maxRetries = options?.maxRetries ?? 3;
// ...
}
两种写法都有效。关键在于,到达区域 2 时,所有依赖项和配置都已绑定到 const 名称。四个区域如下:
function createSomething({ db, cache }, options?) {
// Zone 1: Immutable state (const from deps/options)
const maxRetries = options?.maxRetries ?? 3;
// Zone 2: Mutable state (let declarations)
let connectionCount = 0;
let lastError: Error | null = null;
// Zone 3: Private helpers
function resetState() {
connectionCount = 0;
lastError = null;
}
// Zone 4: Public API (always last)
return {
connect() { ... },
disconnect() { ... },
get errorCount() { return connectionCount; },
};
}
状态较少时,可以合并区域 1 和 2。简单的工厂可以没有区域 3。但返回对象始终放在最后:它就是完整的公共 API。
公共返回类型由区域 4 推导
当导出的类型只是某个工厂返回的句柄时,应从工厂推导类型,而不是用该类型标注工厂。
export type RemoteClient = ReturnType<typeof createRemoteClient>;
export function createRemoteClient(options: RemoteClientOptions) {
return {
actions<T>(peerId: string): RemoteActionProxy<T> {
// ...
},
describe(peerId: string): Promise<ActionManifest> {
// ...
},
};
}
区域 4 已经是公共 API。在手动返回类型中重复定义它会形成第二个事实来源,并改变编辑器导航行为:转到定义通常会跳转到类型别名,而不是返回的成员。当方法参数和返回值的注解能让公共接口更清晰时,请将它们保留在区域 4 中。
这是一个更广泛原则的体现:在整理类型和导出项时,始终考虑“转到定义”的行为。适配器/代理/包装器工厂若没有改变行为,也属于同一类回退问题:转到定义会落在包装器上,而非事实来源。“折叠适配器”规则是针对这一问题的具体解决方法。有关需要注意的所有回退问题,请参阅 typescript 中的“转到定义意识”。
不要将此模式用于由多个工厂实现的共享服务契约。这些契约是词汇表。当工厂需要证明自己符合外部契约,同时保留具体的返回结构时,请在返回对象上使用 satisfies。
this 决策规则
在返回对象内部,公共方法有时需要调用其他公共方法。如果是在返回对象上调用方法,请使用 this.method();简写方法会将该对象作为自己的 this。如果调用方提取该方法,它就会失去这个接收对象。关于文档编写和提取方法的权衡,请参阅方法文档和同级调用。
如果某个函数既由返回对象的方法调用,又由返回前的初始化逻辑调用,它就属于区域 3(私有辅助函数)。直接按名称调用它;不需要 this。
| 函数所在位置 | 调用方式 |
|---|---|
| 返回对象(区域 4) | 从同级方法调用时使用 this.method() |
| 私有辅助函数(区域 3) | 按名称直接调用:helperFn() |
| 两个区域都需要 | 保留在区域 3,所有地方都按名称调用 |
完整理由和实际代码库示例,请参阅闭包比关键字更能保护隐私。
结构契约:满足外部接口的工厂
工厂的返回对象可以设计为在结构上满足外部契约,因此无需适配器即可直接传递给与平台无关的核心。
典型示例是 createPersistedState / createStorageState,其返回形状在结构上满足 @epicenter/auth 的 createAuth 所使用的 SessionStore 契约:
// The contract (in a platform-agnostic core):
type SessionStore = {
get(): T | null;
set(value: T | null): void;
watch(fn: (next: T | null) => void): () => void;
};
// The factory exposes BOTH a reactive accessor AND the contract methods:
function createPersistedState(opts) {
let value = $state(readFromStorage());
// ...
return {
get current() { return value }, // reactive read for templates
set current(v) { setAndPersist(v) },
get(): T { return value }, // contract: sync read
set: setAndPersist, // contract: fire-and-forget write
watch(fn) { /* ... */ }, // contract: change notification
};
}
// Consumer: pass the factory result directly, no adapter:
const session = createPersistedState({ key, schema, defaultValue });
const auth = createAuth({ baseURL, session });
“折叠适配器”规则
如果你发现自己在编写一个 fromX 转换器,而它只重命名字段或重新投影字段,请删除它,改为扩展工厂的返回形状。这个适配器纯属形式;工厂已经持有状态,因此只需同时暴露两种接口即可。
适配器应当折叠的迹象:
- 它与工厂一一对应(每个调用方都会包装工厂结果)。
- 它只重命名方法或添加简单的透传层。
- 工厂与契约的形状不一致,但语义一致(
.current和.get():值相同,API 不同)。
适配器应当保留的迹象:
- 它在衔接处确实完成了工作(例如,协调同步读取与异步获取,或在底层
watch只会在外部更改时触发的情况下,分发本地写入)。 - 多个使用方使用不同契约包装同一个工厂。
拿不准时:先不加适配器。只有当衔接处确实需要它时才添加。
为什么这样可行
TypeScript 的结构类型系统意味着工厂不必 implements SessionStore,也不必导入契约类型。只要返回形状匹配,就可以赋值。这样可以让工厂包不依赖使用方的依赖项:createPersistedState 位于 @epicenter/svelte 中,完全不了解 @epicenter/auth。
思维模型
可以把它想成一条链,每个环节都会:
- 接收上一个环节提供的资源
- 添加自己的配置
- 为下一个环节生成内容
createClient(...) → createService(client, ...) → service.method(...)
↑ ↑ ↑
clientOptions serviceOptions methodOptions
优势
- 可测试性:轻松注入模拟客户端
- 可复用性:在多个服务之间共享客户端
- 灵活性:独立配置每一层
- 清晰性:明确每一层的配置归属
参考资料
如需了解更多详情,请参阅完整文章:
- 通用工厂函数签名:深入讲解签名
- 停止将客户端作为参数传递:实践指南
- 工厂函数模式:详细说明
- 工厂方法模式:分离选项与方法模式
- 闭包比关键字更能保护隐私:内部结构,以及闭包为何胜过类关键字
按需加载:
- references/single-or-array-pattern.md:工厂或 CRUD 入口何时应同时接受单个项目或数组。
- references/sync-construction-render-gate.md:当客户端的同步方法依赖异步初始化的状态,且 UI 必须等到就绪后才能渲染时。
- references/method-documentation-and-sibling-calls.md:返回的方法应在哪里编写 JSDoc,以及何时可以安全地使用
this调用同级方法。