control-flow について
control-flowは、複雑な制御フローをリファクタリングする開発者向けスキルです。早期リターン、相互排他的な条件、明確な命名でネストを減らし、失敗処理と正常系を分けます。
主な機能
- 01早期リターンできるケースを先に処理
- 02状態を相互排他的な条件で表現
- 03正常系に主要な処理を集約
- 04自然な言葉のブール変数名を使用
- 05エラー境界と後続処理を分離
活用シーン
- 01重複を含むネスト条件の簡素化
- 02失敗箇所が複数あるハンドラーの整理
- 03危険な呼び出しが1つのtry-catchを再構成
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill control-flowインストール後、Claude Code で/control-flowを入力して読み込まれたか確認します。
詳細
人が読みやすい制御フロー
複雑な制御フローをリファクタリングするときは、人間の自然な推論パターンにならいましょう。
関連スキル:分岐の折りたたみと呼び出し元の根拠については、リファクタリング手法を参照してください。
コアパターン
- まず人間が考える質問をする:「今あるものを使えるか?」→ 成功パスで早期リターンする
- 状況を見極める:「現在の状態はどうなっていて、何をする必要があるか?」→ 明確で相互に排他的な条件を使う
- アクションを起こす:「必要なものを取得する」→ ロジックを最後に集約する
- 自然言語の変数名を使う:
isUsingNavigator、isUsingLocalTranscription、needsOldFileCleanupのように、考えをそのまま読める名前にする - 不自然な構造を避ける:人間が問題を実際に考える方法に合わない、ネストした条件分岐は使わない
これを変換します:ロジックが重複したネスト条件 これに変換します:人間の意思決定を反映した直線的なフロー
例:自然言語の変数名を使った早期リターン
// From apps/whispering/src/routes/(app)/_layout-utils/check-ffmpeg.ts
export async function checkFfmpegRecordingMethodCompatibility() {
if (!window.__TAURI_INTERNALS__) return;
// Only check if FFmpeg recording method is selected
if (settings.value['recording.method'] !== 'ffmpeg') return;
const { data: ffmpegInstalled } =
await rpc.ffmpeg.checkFfmpegInstalled.ensure();
if (ffmpegInstalled) return; // FFmpeg is installed, all good
// FFmpeg recording method selected but not installed
toast.warning('FFmpeg Required for FFmpeg Recording Method', {
// ... toast content
});
}
例:自然言語のブール値
// From apps/whispering/src/routes/(app)/_layout-utils/check-ffmpeg.ts
const isUsingNavigator = settings.value['recording.method'] === 'navigator';
const isUsingLocalTranscription =
settings.value['transcription.selectedTranscriptionService'] ===
'whispercpp' ||
settings.value['transcription.selectedTranscriptionService'] === 'parakeet';
return isUsingNavigator && isUsingLocalTranscription && !isFFmpegInstalled;
例:コメント付きのクリーンアップ判定
// From packages/epicenter/src/indexes/markdown/markdown-index.ts
/**
* This is checking if there's an old filename AND if it's different
* from the new one. It's essentially checking: "has the filename
* changed?" and "do we need to clean up the old file?"
*/
const needsOldFileCleanup = oldFilename && oldFilename !== filename;
if (needsOldFileCleanup) {
const oldFilePath = path.join(tableConfig.directory, oldFilename);
await deleteMarkdownFile({ filePath: oldFilePath });
tracking[table.name]!.deleteByFilename({ filename: oldFilename });
}
例:try-catchをガード+成功パスに直線化する
try-catchブロックは、try本体とcatch本体という2つの分岐からなるネスト構造を作ります。try内で実際に例外をスローする可能性がある呼び出しが1つだけの場合は、try-catchをガード付きの呼び出し+早期リターンに置き換えると、コードを上から下へ読み進められます。
変換前(ネストしており、例外とリターンが混在):
async ({ body, status }) => {
const adapter = createAdapter(body.provider);
try {
const stream = chat({ adapter, messages: body.messages });
return toServerSentEventsResponse(stream);
} catch (error) {
if (error instanceof Error && error.name === 'AbortError') {
throw status(499, 'Client closed request');
}
const message = error instanceof Error ? error.message : 'Unknown error';
throw status('Bad Gateway', `Provider error: ${message}`);
}
};
変換後(直線的で、リターンが一貫):
async ({ body, status }) => {
const adapter = createAdapter(body.provider);
const { data: stream, error: chatError } = trySync({
try: () => chat({ adapter, messages: body.messages }),
catch: (e) => Err(e instanceof Error ? e : new Error(String(e))),
});
if (chatError) {
if (chatError.name === 'AbortError') {
return status(499, 'Client closed request');
}
return status('Bad Gateway', `Provider error: ${chatError.message}`);
}
return toServerSentEventsResponse(stream);
};
この変換も、同じ人間の推論パターンに従います。
- リスクのある処理を試す:失敗する可能性がある箇所だけを囲む
- 失敗したかどうかを確認する:適切なエラーで早期リターンする
- 成功パスを続ける:関数の残りの部分では成功を前提とする
これによりネストがなくなり、returnとthrowの使い分けに一貫性が生まれ、エラー処理の境界と、その後に続く安全なコードを分離できます。
例:ハンドラー内での逐次ガード
ハンドラーに失敗する可能性のある箇所が複数ある場合は、それぞれのガードで同じパターンに従います。処理を実行し、結果を確認して、早期リターンするか続行します。
async ({ body, status }) => {
// Guard 1: validate input
if (!isSupportedProvider(body.provider)) {
return status('Bad Request', `Unsupported provider: ${body.provider}`);
}
// Guard 2: resolve dependency
const apiKey = resolveApiKey(body.provider, headers['x-api-key']);
if (!apiKey) {
return status('Unauthorized', 'Missing API key');
}
// Guard 3: risky operation
const { data: stream, error } = trySync({
try: () => chat({ adapter: createAdapter(body.provider, apiKey) }),
catch: (e) => Err(e instanceof Error ? e : new Error(String(e))),
});
if (error) return status('Bad Gateway', error.message);
// Happy path: all guards passed
return toServerSentEventsResponse(stream);
};
すべてのガードは同じ形です。確認し、失敗していれば早期リターンします。成功パスは最後に集約されます。上から下へ読むと、成功ケースが現れる前に、関数が失敗するあらゆる方法が分かります。