mcpField
F
factory-function-composition

factory-function-composition

github.com/epicenterhq开发4.8k69 次安装2 周前更新
代码仓库

关于 factory-function-composition

面向 TypeScript 开发者的工厂函数模式指南,介绍依赖注入与函数组合。内容涵盖参数约定、配置分层、函数内部组织和返回类型。

主要功能

  1. 01依赖优先的工厂签名
  2. 02区分客户端、服务和方法选项
  3. 03返回包含方法的对象
  4. 04工厂函数的四区组织
  5. 05结构化契约兼容
  6. 06适配器与 `this` 使用指南

适用场景

  1. 01重构函数以注入客户端
  2. 02在多个服务间共享客户端
  3. 03分离各层配置
  4. 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.md
single-or-array-pattern.md
sync-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);

关键原则

  1. 客户端配置应在创建客户端时设置:不要通过工厂传递 clientOptions
  2. 每一层都有自己的选项:客户端、服务和方法的选项应彼此分开
  3. 依赖项放在前面:工厂函数将依赖项作为第一个参数
  4. 返回带有方法的对象:不要返回需要传入资源的独立函数

识别反模式

反模式 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

优势

  • 可测试性:轻松注入模拟客户端
  • 可复用性:在多个服务之间共享客户端
  • 灵活性:独立配置每一层
  • 清晰性:明确每一层的配置归属

参考资料

如需了解更多详情,请参阅完整文章:

按需加载:

评论

支持 Markdown