Claude Codeのスキルを再設計した話
この記事は以前の記事の大幅アップデートだ。けれど読まなくてもいい。面倒だろうし、この記事から読んでも理解できるように前提から整理する。
Claude Codeにこう指示したことがある。「Polymorphism over Switch。switch文ではなくポリモーフィズムを使え」と。
結果はどうだったか。AIは相変わらずswitch文を書いてきた。
なぜか。「ポリモーフィズムを使え」という指示が抽象的すぎるからだ。人間のシニアエンジニアでも、この一文だけでは「どういう場面で」「どういうパターンで」ポリモーフィズムを適用すべきかわからない。AIならなおさらだ。
ぼくは気づいた。AIに良いコードを書かせたければ、「良いコードとは何か」を徹底的に言語化するしかない。抽象的なルールは、人間にもAIにも機能しない。
500行のスキルが機能しなかった理由
ぼくは約500行のClaude Codeスキルを運用していた。「単一責任原則を守れ」「小さいクラスを作れ」「副作用を分離しろ」。どれも正しい。正しいが、役に立たなかった。
たとえば「単一責任原則」。これは「クラスは一つの理由でのみ変更されるべき」という原則だ。けれども、何が「一つの理由」なのかは、文脈によって変わる。ユーザー管理クラスは「ユーザーに関することを担当する」という意味では単一責任かもしれない。しかし「認証」「プロフィール更新」「権限管理」を全部持っていたら、それは単一責任だろうか。
AIはこの曖昧さの中で迷う。そして迷った結果、最も「安全」な選択肢を取る。つまり、学習データに多く含まれている手続き的なコードを生成する。
Command/Query/ReadModelという分類
ぼくがたどり着いた解決策の一つが、操作の3層分類だ。
-
Command: 状態を変更し、何も返さない。
-
Query: 状態を変更せず、値を返す。
-
ReadModel: 複数の集約をまたぐ、読み取り専用のビュー。
// Command: 状態を変更し、voidを返す
interface ApproveExpenseCommand {
execute(expenseId: ExpenseId): Promise<void>
}
// Query: 状態を変更せず、値を返す
interface GetExpenseQuery {
execute(expenseId: ExpenseId): Promise<ExpenseDto>
}
// ReadModel: 複数集約をまたぐ読み取り専用ビュー
interface ExpenseReportReadModel {
getMonthlyReport(userId: UserId, month: YearMonth): Promise<MonthlyExpenseReport>
}
この分類があれば、AIは「この操作はCommandかQueryかReadModelか」と判断できる。「単一責任」という抽象的な概念より、はるかに明確だ。
Pending Object Patternで状態遷移を型にする
もう一つの発見は、状態遷移の扱い方だった。
経費精算を例に考えよう。経費には「下書き」「申請中」「承認済み」「却下」といった状態がある。これをswitch文で管理すると、こうなる。
// 悪い例: switch文による状態管理
function processExpense(expense: Expense, action: string): Expense {
switch (expense.status) {
case 'draft':
if (action === 'submit') return { ...expense, status: 'pending' }
break
case 'pending':
if (action === 'approve') return { ...expense, status: 'approved' }
if (action === 'reject') return { ...expense, status: 'rejected' }
break
}
throw new Error('Invalid transition')
}
この何が問題か。状態が増えるたびにcaseを追加する必要がある。そして、「下書きから直接承認」のような不正な遷移は、ランタイムエラーになる。コンパイル時にはわからない。
Pending Object Patternは、この問題を型で解決する。
// Pending Object Pattern: 型による状態遷移の表現
class DraftExpense {
constructor(
readonly id: ExpenseId,
readonly amount: Money,
readonly description: string
) {}
submit(submittedBy: UserId, submittedAt: Date): PendingExpense {
return new PendingExpense(
this.id,
this.amount,
this.description,
submittedBy,
submittedAt
)
}
}
class PendingExpense {
constructor(
readonly id: ExpenseId,
readonly amount: Money,
readonly description: string,
readonly submittedBy: UserId,
readonly submittedAt: Date
) {}
approve(approvedBy: UserId, approvedAt: Date): ApprovedExpense {
return new ApprovedExpense(
this.id,
this.amount,
this.description,
this.submittedBy,
this.submittedAt,
approvedBy,
approvedAt
)
}
reject(rejectedBy: UserId, rejectedAt: Date, reason: string): RejectedExpense {
return new RejectedExpense(
this.id,
this.amount,
this.description,
this.submittedBy,
this.submittedAt,
rejectedBy,
rejectedAt,
reason
)
}
}
DraftExpenseにはapprove()メソッドがない。つまり、下書きから直接承認しようとすると、コンパイルエラーになる。不正な状態遷移が、型システムによって防がれる。
AIにこのパターンを見せれば、同じ構造のコードを生成できる。「ポリモーフィズムを使え」という抽象的な指示より、はるかに明確だ。
憲法と施行細則
ここでひとつ問題がある。Pending Object PatternやCommand/Query分離は普遍的な原則だが、プロジェクトごとに異なる設定もある。使用するフレームワーク、ディレクトリ構造、命名規則。
ぼくは2層構造で解決した。
/strict-refactoringは「憲法」だ。プロジェクトを問わず適用される普遍的な原則を定義する。Command/Query分離、Pending Object Pattern、完全コンストラクタ。これらはどんなプロジェクトでも有効な設計原則だ。
CLAUDE.mdは「施行細則」だ。プロジェクト固有の設定を記述する。「このプロジェクトではNext.jsを使う」「ディレクトリ構造はこう」「このライブラリのこの関数を使え」。
そして/generate-claude-mdは、施行細則を自動生成するツールだ。プロジェクトのREADMEや仕様書を読み取り、CLAUDE.mdを生成する。
/strict-refactoring(憲法)
↑ 参照
CLAUDE.md(施行細則)
↑ 自動生成
/generate-claude-md(ツール)
この構造により、普遍的な原則は一度書けば済む。プロジェクト固有の設定だけを、プロジェクトごとに生成すればいい。
日本語ドメインという壁
もう一つ、日本語圏特有の問題がある。業務システムでは「稟議」「経費精算」「勤怠」といった日本語の概念が頻出する。これをどう英語のクラス名にマッピングするか。
AIに任せると、毎回違う命名になる。ある時はRingi、ある時はApprovalRequest、また別の時はApprovalFlow。一貫性がない。
ぼくはスキルに明示的なマッピング表を追加した。
| 日本語 | 英語クラス名 | 説明 |
|---|---|---|
| 稟議 | Approval / ApprovalRequest | 承認フロー |
| 経費精算 | ExpenseReport / Reimbursement | 立替経費の精算 |
| 勤怠 | Attendance / TimeRecord | 出退勤記録 |
| 有給休暇 | PaidLeave / AnnualLeave | 年次有給休暇 |
これでAIは「稟議」を見たとき、即座にApprovalRequestにマッピングできる。他の日本語が来ても命名の一貫性が保証される可能性が高い。少なくともあまりにも変な命名規則は提案されない。
長すぎないか
1,755行のスキルは長すぎないか。そう思われるかもしれない。
けれども、長さではなく構造が重要だ。スキルは明確なセクションに分かれており、AIは必要な部分だけを参照できる。むしろ、500行の曖昧なルールより、1,755行の具体的なルールのほうがAIにとって扱いやすい。
これは人間のドキュメントと同じだ。「短くて曖昧」より「長くて明確」のほうが実用的なのだ。
言語化という仕事
AIに良いコードを書かせることは、「良いコードとは何か」を言語化する仕事だ。
ぼくたちは普段、暗黙知に頼っている。「なんとなくこう書くべき」「経験的にこのパターンがいい」。けれどもAIには暗黙知がない。すべてを明示的に教える必要がある。
その過程で気づいたことがある。言語化できないルールは、実は自分でも理解していないのだ。「ポリモーフィズムを使え」と言いながら、どういう場面でどう使うかを説明できなければ、それは理解ではなく信仰だ。
AIに教えることは、自分の理解を試すことでもある。500行が1,755行になったのは、ぼく自身の理解が深まった証拠でもある。
スキルの設計は続く。まだ言語化できていないパターンがある。まだAIに伝わっていない意図がある。けれども、その作業自体が、ソフトウェア設計への理解を深めてくれる。AIとの協働は、ぼくたちに言語化を強いる。それは面倒であると同時に、貴重な学びの機会でもある。
改修したスキルはGitHubで公開している: https://github.com/NAM-MAN/strict-refactoring-plugin