factory-function-composition について
TypeScript のファクトリー関数で依存性注入と関数合成を行うためのスキルです。引数や設定の分離、関数内部の構成、戻り値の型に関する規約を示します。
主な機能
- 01依存関係を先頭に置くシグネチャ
- 02クライアント・サービス・メソッド設定の分離
- 03メソッドを持つオブジェクトの返却
- 04ファクトリー関数の4ゾーン構成
- 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 でクリーンな依存性注入と関数合成を実現するためのファクトリー関数パターンの適用を支援します。
関連スキル:呼び出し元の数え方や、1 回しか使われない抽出処理のインライン化については、リファクタリング手法を参照してください。
共通のシグネチャ
すべてのファクトリー関数は、次のシグネチャに従います:
function createSomething(dependencies, options?) {
return {
/* methods */
};
}
- 第 1 引数:常にリソースです。単一のクライアント、または複数の依存関係を分割代入したオブジェクトのいずれかです。
- 第 2 引数:このファクトリー固有の任意の設定です。クライアントの設定ではありません。クライアントの設定は、クライアントの作成時に指定します。
引数は最大 2 つです。第 1 引数はリソース、第 2 引数は設定です。例外はありません。
基本パターン
// 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 引数にします
- メソッドを持つオブジェクトを返します:リソースを渡す必要がある単独の関数は返しません
アンチパターンを見分ける
アンチパターン 1:関数がクライアントを第 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 } を説明しました。このセクションでは、関数本体の内部構造を説明します。すべてのファクトリー関数は、次の 4 つのゾーンをこの順序で配置します:
// 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 の名前にバインドされていることです。4 つのゾーンは次のとおりです:
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 から導出する
エクスポートする型が、1 つのファクトリーから返されるハンドルだけを表す場合は、ファクトリーに型注釈を付けるのではなく、ファクトリーから型を導出してください。
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 です。手動で戻り値の型を重複して記述すると、情報源が二重になり、エディターでの移動先も変わります。「定義へ移動」は、返されるメンバーではなく、型エイリアスに移動しがちです。公開 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を安全に使用できる場合。