AI 開発でコード品質を保つために開発原則を使おう
こんにちは。Gaji-Labo でフロントエンドエンジニアをしているクロジです。
Claude Code などのコーディングエージェントツールを使用していて、それらが出力するコードの品質に悩んでいませんか。「動くけど読みにくい」「過剰に抽象化されている」「レビューしづらい」といった問題を聞くことがありますが、みなさんはいかがでしょうか。
そこで今回は、AI に「人間がレビューしやすい今必要な最小限だけを実装」させるために、私がシステムプロンプトに記述して使用している様々な開発原則を、その適用方法とともに紹介したいと思います。
なぜ AI が出力するコードは無駄が多いのか
まず最初に、なぜ AI は過度に抽象化した読みにくいコードを出力してしまうのかを考えてみたいと思います。
これは私の仮説ですが、いくつかの要因が重なっているのではないかと考えています。(実際は異なるかもしれませんが、原因のわからない現象を変えるためには、仮定と検証を積み重ねるしかありません……)
- 訓練データのバイアス
- オープンソースのコードには「拡張性の高い設計」が多く含まれており、そこから学習している
- その上で、優れたオープンソースプロジェクトにおける抽象化されたコードが「良いコード」として学習されている
- 結果、単純な処理でも抽象化を提案することが増えてしまう
- コンテキストの限界
- LLM には見えているのは直近前後のコンテキストと「今の要件」
- しかし、訓練データから「将来の拡張性」を考慮することが優れていると学んでいる
- この「「今の要件」と「将来の拡張性」の境界が曖昧になるため抽象化が行われる
- 推論という本質
- LLM は「次に何が来るか」を予測する推論を行う性質上、先回りしてしまう
- 明示的な制約がないと、可能性を広く考えてしまう
こういった問題を解消するために、先人の知恵を借りて開発原則を活用することにしました。
原則の階層構造
私が使用している開発原則は、その優先度を AI に伝えやすくするために3つの階層に分けています。上に行くほど頻繁に使い、下に行くほど状況に応じて使用する扱いとなっています
最も重要な原則:オッカムの剃刀
↓
デフォルトで使用する原則:プログレッシブエンハンスメント、リーキーアブストラクション、リーダブルコード、DRY原則
↓
状況に応じて使用する原則:TDD、ベイビーステップ、デメテルの法則、コンテナ・プレゼンテーションパターン、Tidying
最も重要な原則
オッカムの剃刀
AIはとにかく「賢く見えるコード」を書きたがります。
単純な処理でも拡張性を予測して実装してしまうのですが、そのほとんどが要求に対して過剰です。
オッカムの剃刀(Occam’s Razor)は、「ある事柄を説明するために、必要以上に多くを仮定するべきでない」という哲学的な指針です。
これを開発のための原則として「ある実装のために、必要以上の拡張性を予測するべきではない」と AI に捉えてもらう事で、シンプルな実装を出力させます。
これを利用しているすべての原則の頂点に置き、常に優先させています。
実際の例
Claude Code に「ユーザーカードコンポーネント」を頼んだところ、こんなコードが生成されました。
interface UserCardProps {
user: User
}
interface UserCardRenderer {
render(props: UserCardProps): JSX.Element
}
class DefaultUserCardRenderer implements UserCardRenderer {
render({ user }: UserCardProps): JSX.Element {
return (
<div className="user-card">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)
}
}
export const UserCard = ({ user }: UserCardProps) => {
const renderer = new DefaultUserCardRenderer()
return renderer.render({ user })
}
これは問題なく動作し、テストも通りそうです。しかし、決して読みやすいとは言えません。最初の実装としては不要な記述が多く、人間がレビューしづらいコードになっています。
これを「オッカムの剃刀」を適用することで以下のような出力に変化させられました。
export const UserCard = ({ user }: { user: User }) => {
return (
<div className="user-card">
<h2>{user.name}</h2>
<p>{user.email}</p>
</div>
)
}
人間が普通に書くとしたらこうなるよね、という出力です。これがなかなか出てこなかったりするんですよね……。
オッカムの剃刀は私にとって、AI に最初から適切なレベルの実装を提案させるために必要不可欠な原則となっています。
# オッカムの剃刀 - シンプルさの原則
## 核心哲学
**「必要なしに存在を増やすべきではない」** - オッカムのウィリアム
問題を解決する最もシンプルな解決策が、通常最良の解決策です。
## 別名と表現
### KISS - Keep It Simple, Stupid
この原則はソフトウェア開発界では**KISS**として表現されることがよくあります。この覚えやすい頭字語は実践的なリマインダーとして機能します:
- **Keep It Simple** - 目標
- **Stupid** - 賢い解決策がしばしば裏目に出ることをユーモラスに思い出させる
オッカムの剃刀もKISSも同じ基本的な真理を表現しています:**設計においてシンプルさは美徳である**。
#### なぜ「Stupid」なのか?
「stupid」という言葉は侮辱ではありません - リマインダーです:
- **Stupid simple** = 明白に見えるほどシンプル
- 賢いコードはしばしばメンテナンスの悪夢になる
- 今日素晴らしく見えるものが明日みんなを混乱させる(あなたを含めて)
#### 実践でのKISS
```typescript
// ❌ 「賢い」 - 不必要に高度な技術を使用
const isEven = n => !(n & 1); // ビット演算
// ✅ KISS - 明らかに正しい
const isEven = n => n % 2 === 0; // 明確な意図
```
### その他の表現
- **節約の原則** - 学術用語
- **節約の法則** - 科学的文脈
- **Lex Parsimoniae** - ラテン語形式
- **最小十分原則** - 日本語表現
すべて同じアイデアを表現:シンプルな説明と解決策を好む。
## ソフトウェア開発における原則
### 意味すること
複数の解決策に直面した時:
1. **要件を満たす最もシンプルなものを選ぶ**
2. **必要性が証明されるまで不必要な複雑さを避ける**
3. **すべての抽象化を疑う** - 本当に必要か?
### なぜ重要か
- **保守性**:シンプルなコードは理解と修正が容易
- **デバッグ**:動く部分が少ないとバグの場所も少ない
- **オンボーディング**:新しい開発者がシンプルな解決策を速く理解
- **パフォーマンス**:複雑さが少ないと性能も良い場合が多い
## 実践的な適用
### コードの例
```typescript
// ❌ 不必要に複雑
class UserAuthenticationManager {
private strategies: Map<string, AuthStrategy>;
private validators: ValidatorChain[];
private observers: AuthObserver[];
authenticate(credentials: Credentials): AuthResult {
// ... 50行の抽象化
}
}
// ✅ シンプルで十分
function authenticate(username: string, password: string): boolean {
const user = findUser(username);
return user && verifyPassword(password, user.passwordHash);
}
```
### アーキテクチャの例
```markdown
❌ 過度な設計:
- 単一実装のための抽象ファクトリー
- 2コンポーネントのためのイベントバス
- 1日10ユーザーのためのマイクロサービス
✅ シンプル:
- 直接の関数呼び出し
- スケールが要求するまではモノリス
- キャッシュが測定可能に必要になるまでデータベース
```
### タスクスコープ別アプローチ
想像上の未来ではなく、実際のタスクスコープに基づいて実装アプローチを選択:
```typescript
// 単一関数タスク → 直接的な手続き型
async function uploadFile(file: File): Promise<string> {
const data = await file.arrayBuffer()
return await storage.put(data)
}
// ファイルレベルのロジック → 最小限の抽象化を持つ関数
function validateUser(user: User): ValidationError[] { }
function saveUser(user: User): Promise<void> { }
function notifyUser(user: User): void { }
// モジュールレベルの複雑さ → クラス/インターフェースを検討
class UserRepository {
// 複数の関連操作
// 共有状態(接続、キャッシュ)
// 明確な責任境界
}
// システムレベルのアーキテクチャ → SOLID原則を適用
// 以下の場合のみ:複数チーム、プラグインシステム、パブリックAPI
```
**決定ルール:**
1. 現在のスコープに対して最もシンプルなアプローチから始める
2. 以下の場合のみ次のレベルにリファクタリング:
- 現在のアプローチが保守困難になる
- 複数の類似実装が現れる
- 複雑さが**測定され**、想像ではない
**覚えておくこと:** 手続き型 vs OOPは好みの問題ではなく、解決策の複雑さを問題の複雑さに一致させることです。
## 他の原則との関連
### Baby Steps (TDD_RGRC)
- 各ステップは**可能な限り最小の変更**
- テストが要求した時のみ複雑さが現れる
### プログレッシブエンハンスメント
- シンプルに始め、必要な時に強化
- 「シンプル > 複雑」はオッカムの剃刀の実践
### YAGNI (You Aren't Gonna Need It)
- 想像上の将来のニーズのために複雑さを追加しない
- 機能開発に適用されたオッカムの剃刀
## 判断フレームワーク
解決策を評価する時:
```markdown
1. **実行可能な解決策をリスト**
- 解決策A:10行、依存関係1つ
- 解決策B:100行、抽象化3つ
2. **最もシンプルなものを選ぶ**
- 現在の要件をすべて満たす?
- 最も理解しやすい?
→ これが答え
```
## 警告サイン
### コードの臭い
- **「念のため」の抽象化** - 単一実装のインターフェース
- **早すぎる最適化** - 測定前のキャッシュ
- **パターンの過度な使用** - シンプルなオブジェクト作成のためのファクトリー
- **深い継承** - シンプルなドメインに4レベル以上
### アーキテクチャの臭い
- **小さなアプリのマイクロサービス** - 分散複雑性
- **複数のデータベース** - 1つで十分な時
- **メッセージキュー** - 同期操作用
- **Kubernetes** - 単一サーバーデプロイメント用
## 複雑さの予算
複雑さを限られた予算として考える:
```markdown
プロジェクト複雑さ予算:100ポイント
使用済み:
- 認証:20ポイント ✓(必要)
- データベースORM:15ポイント ✓(より多くの複雑さを節約)
- イベントソーシング:40ポイント ✗(まだ不要)
残り:65ポイント
→ 実際に必要になった時のために保存
```
## 実践的ガイドライン
### 適用する時
- **すべてのアーキテクチャ決定** - 最もシンプルなものから始める
- **すべての抽象化** - その必要性を疑う
- **すべてのパターン** - 実際の問題を解決するか?
- **すべての依存関係** - 複雑さに見合うか?
### 適用しない時
- **セキュリティ** - 安全性を犠牲にしてシンプルにしない
- **データ整合性** - 正確性がシンプルさより優先
- **法的要件** - コンプライアンスは交渉不可
- **証明されたスケーリングニーズ** - メトリクスが必要性を示す時
## オッカムのように考える
自問すべき質問:
- 「この複雑さはどんな問題を解決するのか?」
- 「その問題は現実か想像か?」
- 「動作する可能性のある最もシンプルなものは何か?」
- 「コードを追加する代わりに削除できるか?」
- 「ジュニア開発者はこれを理解できるか?」
## 覚えておくこと
> 「完璧とは、付け加えるものがなくなった時ではなく、取り去るものがなくなった時に達成される」 - アントワーヌ・ド・サン=テグジュペリ
最良のコードはしばしば:
- **退屈** - 賢いトリックなし
- **明白** - 意図が明確
- **短い** - 必要なことだけを言う
- **直接的** - 最小限の間接性
## 関連する原則
### 核心原則(同レベル)
- [@./SOLID.md](./SOLID.md) - SRPは集中したシンプルさと一致
- [@./DRY.md](./DRY.md) - 重複を排除、シンプルに保つ
### 実践での適用
- [@../development/PROGRESSIVE_ENHANCEMENT.md](../development/PROGRESSIVE_ENHANCEMENT.md) - シンプルに構築、段階的に強化
- [@../development/TDD_RGRC.md](../development/TDD_RGRC.md) - Baby Stepsはオッカムの剃刀を体現
- [@../development/READABLE_CODE.md](../development/READABLE_CODE.md) - 理解のためのシンプルさ
- [@../development/LEAKY_ABSTRACTION.md](../development/LEAKY_ABSTRACTION.md) - 複雑な「完璧な」ものよりシンプルな漏れのある抽象化を受け入れる
デフォルトで使う原則
プログレッシブエンハンスメント(段階的な拡張)
「もし〜だったら」で始まる機能は実装しない。これがプログレッシブエンハンスメントの核心です。
AI が出力してしまうのが、次のような React コンポーネントです。数が少なく、増える予定がないデータを扱う場合であっても、過剰に拡張性を考慮して実装してしまうことがあります。
// 「もし大量データだったら」を先回りして実装
export const ProductList = ({ products }: { products: Product[] }) => {
const [page, setPage] = useState(1) // ページネーション
const [cache, setCache] = useState<Map>() // キャッシュ
// 仮想スクロール、無限スクロール、検索フィルター...
return (
<ul>
{products.map(product => (
<li key={product.id}>{product.name}</li>
))}
</ul>
)
}
機能は必要になってから段階的に実装するべきですし、そもそも要求していない実装を勝手に追加しないことの方が重要です。
特に、スタイル指定で解決できる課題に対して過剰に JavaScript を実装するパターンが気になっていたので、私は以下のようなルールを指定しています。
# プログレッシブエンハンスメント
**デフォルトアプローチ**: シンプルに構築 → 段階的に強化
## 核心哲学
- **根本原因**: 「なぜ?」であって「どう修正する?」ではない
- **予防 > パッチ**: 最良の解決策は問題を防ぐこと
- **シンプル > 複雑**: エレガンス = 正しい問題を解決すること
## 優先順位
1. **HTML** - 構造
2. **CSS** - ビジュアル/レイアウト
3. **JavaScript** - 必要な場合のみ
## CSS優先ソリューション
- **レイアウト**: Grid/Flexbox
- **位置**: Transform(リフローなし)
- **表示/非表示**: visibility/opacity
- **レスポンシブ**: Media/Container queries
- **状態**: :target、:checked、:has()
## 例
```css
/* ✅ CSS Gridオーバーレイ */
.container { display: grid; }
.panel { grid-column: 1 / -1; grid-row: 1 / -1; }
```
## 適用対象
レイアウト、ポジショニング、表示/非表示、レスポンシブ、アニメーション、ビジュアル状態
## 覚えておくこと
「最良のコードはコードがないこと」 - CSSで解決できるならJSをスキップ
## 関連する原則
### 開発実践
- [@./LEAKY_ABSTRACTION.md] - 漏れを想定して抽象化を段階的に構築
- [@./LAW_OF_DEMETER.md] - 段階的な結合度削減
- [@./READABLE_CODE.md] - シンプルに始め、徐々に可読性を向上
### 核心原則
- [@../reference/OCCAMS_RAZOR.md] - シンプルさ優先はプログレッシブエンハンスメントと一致
このように、オッカムの剃刀を補足するためにプログレッシブエンハンスメントの原則を利用しています。
リーキーアブストラクション(漏れのある抽象化)
これは Joel Spolsky による法則で、「完璧な抽象化は幻想である」という考え方です。
シンプルさのための原則ではありませんが、拡張性よりも優先したく、かつ必ずカバーしたい概念なので原則としてシステムプロンプトに含めています。
例えば、React Query を使うことで「取得・ローディング・エラー・再試行」などを抽象化して、単純なデータフェッチングを楽にできます。しかし、複雑な依存関係や楽観的更新が必要になると、結局キャッシュの仕組みを理解する必要があります。
export const useUsersWithCache = () => {
const queryClient = useQueryClient()
return {
...useQuery(['users'], fetchUsers),
// キャッシュ操作が必要な時の逃げ道
invalidateCache: () => queryClient.invalidateQueries(['users']),
updateCache: (newData: User[]) =>
queryClient.setQueryData(['users'], newData)
}
}
つまり、完璧な抽象化は存在せず、必ず考慮事項が生まれる……。この前提を持って実装させるために、こちらのリーキーアブストラクションをシステムプロンプトに含めています。
# 漏れのある抽象化の法則
**核心原則**: 「すべての重要な抽象化は、ある程度、漏れている」 - Joel Spolsky
## 核心哲学
抽象化は簡素化を意図していますが、必然的に根底にある複雑さを露呈します。**この現実を受け入れ、それに応じて設計しましょう。**
目標は完璧な抽象化を作ることではなく:
- **漏れが発生する場所を認識する**
- **漏れが起きた時の計画を立てる**
- **抽象化をできるだけシンプルに保つ**
- **いつ抽象化を突破すべきか知る**
## 完璧な抽象化の問題
```typescript
// ❌ 幻想:「SQLを知る必要はない」
const users = await User.findAll({
where: { active: true },
include: ['posts', 'comments']
})
// ✅ 現実:パフォーマンスがSQLの知識を強制
const users = await db.raw(`
SELECT u.*, COUNT(p.id) as posts
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
WHERE u.active = true
GROUP BY u.id
`)
```
抽象化が漏れました。結局SQLの知識が必要でした。
## 一般的な漏れのある抽象化
### 1. ORM
```typescript
// ❌ 抽象化が完全だと信じる
async getActiveUsers() {
return User.findAll({ where: { active: true }})
// 隠蔽:N+1クエリ、SQL制御なし
}
// ✅ 漏れを認識
async getActiveUsers() {
return simpleQuery
? User.findAll({ where: { active: true }})
: this.db.raw(optimizedQuery)
}
```
### 2. ネットワーク呼び出し
```typescript
// ❌ ネットワーク呼び出しをローカルのように扱う
async function getUser(id: string) {
return api.users.get(id)
}
// ✅ ネットワークの現実を認識
async function getUser(id: string) {
try {
return await withTimeout(api.users.get(id), 5000)
} catch (error) {
if (isNetworkError(error)) {
return retry(() => api.users.get(id))
}
throw error
}
}
```
### 3. クロスプラットフォームコード
```typescript
// ❌ プラットフォームの違いを無視
const filePath = `${dir}/${filename}`
// ✅ プラットフォームを意識
import path from 'path'
const filePath = path.join(dir, filename)
```
## 漏れを考慮した設計
### 1. プログレッシブな抽象化
```typescript
class DataService {
// レベル1:シンプルなケース(80%)
async findUsers(criteria: Simple) {
return this.orm.findAll(criteria)
}
// レベル2:複雑なケース
async findUsersRaw(sql: string) {
return this.db.raw(sql) // 脱出ハッチ
}
}
```
### 2. 脱出ハッチの提供
```typescript
class CacheLayer {
async get(key: string) {
return this.redis.get(key)
}
// 抽象化が壊れた時の脱出ハッチ
get rawClient() {
return this.redis
}
}
```
### 3. 境界の文書化
```typescript
/**
* 抽象化の限界:
* - 最大レスポンス:5MB
* - タイムアウト:30秒
* バルク操作にはbulkFetchUsers()を使用
*/
async function fetchUser(id: string) {
// 実装
}
```
## 抽象化を突破すべき時
### パフォーマンス要件
```typescript
// 遅すぎる:5秒
await orm.findAll({ include: ['author', 'tags'] })
// 突破:100ms
await db.raw('SELECT * FROM posts...')
```
## 漏れのある抽象化の管理ガイドライン
### 抽象化を層にする
```markdown
高レベル(ビジネスロジック)
↓
中レベル(サービス層)← ほとんどのコードはここ
↓
低レベル(直接アクセス)← 脱出ハッチ
```
### 抽象化の健全性を監視
抽象化が漏れすぎている兆候:
- 常に脱出ハッチを使用
- 全員が実装の詳細を知る必要がある
- 通常のケースよりエッジケースが多い
### スタックを知る
- **1レベル下を理解**:Reactを使うならDOMを理解
- **失敗モードを学ぶ**:ORMがどう失敗するか知る
- **エッジケースを研究**:フレームワークを壊すものは何か
## 他の原則との統合
### プログレッシブエンハンスメント
- シンプルな抽象化から始める
- 漏れが現れたときだけ複雑さを追加
### オッカムの剃刀
- シンプルな漏れのある抽象化 > 複雑な「完璧な」抽象化
### YAGNI
- 想像上のニーズのために抽象化層を追加しない
- 実際に漏れが起きるまで待つ
## 実践的な適用
### コンポーネントライブラリ
```tsx
// ✅ 脱出ハッチを提供
function Button({
children,
onClick,
className, // スタイル用脱出ハッチ
...restProps // DOMプロップス用脱出ハッチ
}) {
return (
<button
className={cn('btn-default', className)}
onClick={onClick}
{...restProps}
>
{children}
</button>
)
}
```
## 覚えておくこと
> 「漏れのある抽象化の法則は、誰かが素晴らしい新しいコード生成ツールを思いつくたびに、多くの人が『まず手動でやり方を学び、それから時間を節約するために素晴らしいツールを使え』と言うのを聞くことを意味する。」 - Joel Spolsky
**重要なポイント**:
- すべての抽象化は漏れる - それを計画する
- 抽象化の1レベル下を知る
- 脱出ハッチを提供する
- 境界を文書化する
- プログレッシブエンハンスメントは抽象化にも適用される
## 関連する原則
### 開発実践
- [@./PROGRESSIVE_ENHANCEMENT.md] - 抽象化を段階的に構築
- [@./LAW_OF_DEMETER.md] - 抽象化の境界を管理
### 核心原則
- [@../reference/OCCAMS_RAZOR.md] - シンプルな漏れのある抽象化 > 複雑な「完璧な」もの
- [@../reference/SOLID.md] - DIPは漏れのある抽象化を考慮すべき</code></pre></div>
<!-- /wp:loos-hcb/code-block -->
<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">リーダブルコード</h3>
<!-- /wp:heading -->
<!-- wp:paragraph -->
<p>こちらは、Dustin Boswell と Trevor Foucher の著書より引用しています。</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>ミラーの法則(人が短期記憶で扱える情報はだいたい7±2)という認知科学の知見に基づいて、<strong>新しいコードを見て1分経っても理解できなければ、リファクタが必要</strong>というルールを原則として置きました。</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>ネストが深い、変数名が抽象的、コンポーネントが長いなどなど……これらはすべて認知負荷を上げてしまいます。</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>次で説明する DRY 原則と衝突することもありますが、その場合は<strong>「少しの重複は、複雑な抽象化より遥かに良い」</strong>という考え方で、読みやすさを優先するようにしています。</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>これも人間がレビューしやすくするという観点で重要なので、システムプロンプトに含めています。</p>
<!-- /wp:paragraph -->
<!-- wp:loos-hcb/code-block {"langType":"md","langName":"Markdown"} -->
<div class="hcb_wrap"><pre class="prism line-numbers lang-md" data-lang="Markdown"><code># The Art of Readable Code
**デフォルトマインドセット**: コードは理解しやすくあるべき - Dustin Boswell & Trevor Foucher
## 核心哲学
**「コードは他の誰かがそれを理解するのにかかる時間を最小限にするように書かれるべきである」**
- その「他の誰か」は6ヶ月後のあなたかもしれない
- 理解時間 > 書く時間
## 科学的基盤
Millerの法則(7±2)は、人間の認知能力が限られていることを示しています。コードがこれらの限界を超えると:
- 理解時間が指数関数的に増加
- エラー率が倍増
- 精神的疲労が加速
この科学的裏付けは、なぜ可読性のあるコードが重要かを説明します:私たちの脳は文字通り、一度に多くの複雑さを処理できないのです。
→ 可読性のあるコードの背後にある認知科学については [@../reference/MILLERS_LAW.md] を参照
## 主要な実践
### 1. コードを理解しやすくする
#### 誤解されない名前
```typescript
// ❌ 曖昧
results.filter(x => x > LIMIT) // 以上?より大きい?
// ✅ 明確な意図
results.filter(x => x >= MIN_ITEMS_TO_DISPLAY)
```
#### 具体的 > 抽象的
```typescript
// ❌ 抽象的
processData(data)
// ✅ 具体的
validateUserRegistration(formData)
```
### 2. ループとロジックの簡素化
#### 制御フローを明確に
```typescript
// ❌ 複雑なネスト
if (user) {
if (user.isActive) {
if (user.hasPermission) {
// 何かする
}
}
}
// ✅ 早期リターン
if (!user) return
if (!user.isActive) return
if (!user.hasPermission) return
// 何かする
```
#### ネストを最小限に
- ガード句を使用
- 複雑な条件を名前付き関数に抽出
- 早期リターンを優先
### 3. コードの再編成
#### 無関係なサブ問題を抽出
```typescript
// ✅ 各関数が1つのことをする
function getActiveUsers(users: User[]) {
return users.filter(isActiveUser)
}
function isActiveUser(user: User): boolean {
return user.status === 'active' && user.lastLogin > thirtyDaysAgo()
}
```
#### 一度に1つのタスク
- 関数は1つのことをすべき
- 説明に「と」が必要なら分割する
- 「何を」と「どうやって」を分離
### 4. コードは「明らかに正しい」べき
#### コードを意図のように見せる
```typescript
// ❌ 意図が不明確
const p = products.filter(p => p.price > 0 && p.stock)
// ✅ 意図が明白
const availableProducts = products.filter(product =>
product.price > 0 &&
product.stock > 0
)
```
### 5. 書く前の重要な質問
自問すること:
1. 「これを理解する最も簡単な方法は何か?」
2. 「これを読む人を混乱させるものは何か?」
3. 「意図をもっと明確にできるか?」
## 実践的な適用
### 変数命名
- **具体的 > 一般的**: `data`ではなく`userEmail`
- **検索可能**: `7`ではなく`MAX_RETRY_COUNT`
- **発音可能**: `cstmr`ではなく`customer`
### 関数設計
- **小さく集中**: 5-10行が理想的
- **説明的な名前**: `calc()`ではなく`calculateTotalPrice()`
- **一貫したレベル**: 高レベルと低レベルの操作を混在させない
### コメント
- **何ではなくなぜ**: 仕組みではなく決定を説明
- **更新または削除**: 古いコメントはないよりも悪い
- **コードファースト**: 何を説明する必要があるなら、コードを書き直す
## 最終テスト
**「新しいチームメンバーは1分以内にこれを理解できるか?」**
できなければ、さらに簡素化する。
## AIコードのクセ
AI生成コード(自分のものを含む)をレビューする際、これらの一般的な過度な設計パターンに注意:
### 早すぎる抽象化
```typescript
// ❌ AI生成:単一実装のためのインターフェース
interface UserProcessor {
process(user: User): ProcessedUser
}
class DefaultUserProcessor implements UserProcessor {
process(user: User): ProcessedUser {
return { ...user, processed: true }
}
}
// ✅ 直接的アプローチ:具体的に始める
function processUser(user: User): ProcessedUser {
return { ...user, processed: true }
}
// 2番目の実装が現れた時のみインターフェースを追加
```
### シンプルなタスクに対する不要なクラス
```typescript
// ❌ AI生成:手続き的タスクのためのOOP
class CSVReader {
async read(path: string): Promise<string[][]> {
const text = await fs.readFile(path, 'utf-8')
return this.parse(text)
}
private parse(text: string): string[][] {
return text.split('\n').map(line => line.split(','))
}
}
// ✅ 手続き的アプローチ:一度きりのタスクにはよりシンプル
async function readCSV(path: string): Promise<string[][]> {
const text = await fs.readFile(path, 'utf-8')
return text.split('\n').map(line => line.split(','))
}
```
### 想像上の拡張性
```typescript
// ❌ AI生成:シンプルな検証のためのプラグインシステム
class ValidationEngine {
private validators: Map<string, Validator>
registerValidator(name: string, validator: Validator) { }
validate(data: unknown, rules: string[]): Result { }
}
// ✅ 直接的な検証:今必要なものを構築
function validateUser(user: User): ValidationError[] {
const errors = []
if (!user.email) errors.push({ field: 'email', message: 'Required' })
if (user.age < 0) errors.push({ field: 'age', message: 'Invalid' })
return errors
}
```
### 検出と修正
**危険信号:**
- 具体的なユースケースのない「将来性のある」抽象化
- 単一の関数をラップするクラス
- 解決しない問題に適用されたパターン
- 正確に1回だけ使用されるヘルパー関数
**修正戦略:**
1. オッカムの剃刀を適用 - 抽象化を削除
2. 最も直接的な解決策から始める
3. 以下の場合のみ複雑さを追加:
- 同じパターンが3回以上現れる(DRY)
- 複数の実装が**実際に**必要
- 現在のアプローチが**測定可能に**失敗
## 覚えておくこと
- 明確さは賢さに勝る
- 未来のあなたは別人
- 読むことは書くことよりも多く起こる
## 関連する原則
### 科学的基盤
- [@../reference/MILLERS_LAW.md] - 認知限界がなぜ可読性が重要かを説明
### 補完的な原則
- [@./LAW_OF_DEMETER.md] - シンプルなインターフェースが可読性を向上
- [@./CONTAINER_PRESENTATIONAL.md] - 明確な分離が理解を向上
- [@../reference/OCCAMS_RAZOR.md] - シンプルさが可読性を強化
DRY原則
DRY原則は「同じ知識を複数箇所に持たない」ことを目指す設計思想です。
保守性を高めるために必要ですが、過度な抽象化をしがちな AI にとっては毒にもなりえます。
しかし、AI が独自に行っている抽象化をあえて DRY原則で上書きして縛る事で意味を持たせ、さらにルール・オブ・スリー(3回現れたらリファクタリング)で抽象化のタイミングを決めることでコントロールしやすくしています。
# DRY原則 - Andy Hunt & Dave Thomasのように
プラグマティックプログラマーのようにDon't Repeat Yourself原則を適用 - コードだけでなく、知識の重複を排除する。
## 核心哲学
**「システム内のあらゆる知識の断片は、単一で、曖昧でない、権威ある表現を持つべきである」**
これは単なるコードの重複ではなく、知識の重複に関するものです。
## 重複の種類
### 1. リテラルコードの重複
```javascript
// ❌ 悪い:コピーペースト
function validateEmail(email) { /* 検証 */ }
function checkEmail(email) { /* 同じ検証 */ }
// ✅ 良い:単一のソース
function validateEmail(email) { /* 検証 */ }
```
### 2. 構造的重複
```javascript
// ❌ 悪い:構造の繰り返し
if (user.age > 18 && user.hasConsent) { allow() }
if (post.age > 18 && post.hasConsent) { allow() }
// ✅ 良い:パターンを抽出
function canAccess(entity) {
return entity.age > 18 && entity.hasConsent
}
```
### 3. 知識の重複
```javascript
// ❌ 悪い:ビジネスルールが複数の場所に
// 検証で:maxLength = 100
// データベースで:VARCHAR(100)
// UIで:maxlength="100"
// ✅ 良い:真実の単一ソース
const LIMITS = { username: 100 }
// どこでもLIMITSを使用
```
## DRYを適用する時
**適用すべき対象**:
- ビジネスルール/ロジック
- データスキーマ
- 設定値
- アルゴリズム
- 複雑な条件
**過度に適用しない対象**:
- 偶然の類似性
- 異なるコンテキスト
- テストデータ
- シンプルなワンライナー
## 3の法則
2回重複を見たら?記録する。
3回見たら?リファクタリングする。
## 一般的な違反と修正
```javascript
// ❌ マジックナンバー
if (items.length > 10)
// ✅ 名前付き定数
if (items.length > MAX_ITEMS)
```
```javascript
// ❌ 繰り返される条件
if (user && user.isActive && user.hasPermission)
// ✅ カプセル化されたロジック
if (user.canPerform(action))
```
## DRYテクニック
1. **関数を抽出**:繰り返されるロジック用
2. **設定オブジェクト**:繰り返される値用
3. **継承/コンポジション**:繰り返される構造用
4. **コード生成**:繰り返しパターン用
5. **テンプレート**:類似の実装用
## 他の原則との統合
- **SOLIDと**:DRYは良い抽象化を促進(DIP)
- **TDDと**:テストが早期に重複を明らかに
- **Tidyingsと**:重複を段階的に削除
## 警告サイン
- ショットガン手術(多くの場所での変更)
- 時間と共に発散する変更
- 「これ、さっき書かなかった?」
- グローバル検索置換の必要性
## 覚えておくこと
「DRYは知識の重複、意図の重複についてです。同じことを2つ以上の場所で、おそらく2つ以上の全く異なる方法で表現することについてです。」- The Pragmatic Programmer
状況に応じて使う原則
TDD/ベイビーステップ
XP 原則において「一度に少しずつ、ごく小さな変更だけを行う」のがベイビーステップの核心です。
大きな機能を一気に実装すると、どこでバグが入ったか分からなくなります。小さく刻むことで、エラーの原因を直前の作業に絞れるため、人間によるレビューがしやすくなるのです。
TDD(テスト駆動開発)において、次のようなサイクルを小さく回すために原則として使用しています。
Red(失敗するテストを書く)
↓
Green(最小の実装で通す)
↓
Refactor(整理)
↓
Commit(保存)
# t_wadaのようにテスト駆動開発
## 核心哲学
新機能実装やバグ修正時は、t_wadaのように考え行動する - 厳格なRed-Green-Refactor-Commit(RGRC)サイクルを使い、各ステップがなぜ重要かを深く理解する。
**究極の目標**: 「動作するきれいなコード」(Clean code that works) - Ron Jeffries
## TDDプロセス概要
1. **テストシナリオリストの作成** - 小さなテスト可能な単位に分解し、TodoWriteで追跡
2. **RGRCサイクルの実行** - 一度に1つのシナリオ、最小ステップ、素早く繰り返す
## Baby Steps - TDDの基礎
### 核心概念
**各ステップで可能な限り最小の変更を行う** - これがTDDの成功の鍵
### Baby Stepsが重要な理由
- **即座のエラー特定**: テストが失敗したら、原因は直前の小さな変更にある
- **継続的な動作状態**: コードは常に数秒でグリーンに戻せる
- **高速フィードバック**: 各ステップは最大1-2分
- **自信の構築**: 小さな成功が複合して大きな機能になる
### Baby Stepsの実践例
```typescript
// ❌ 大きなステップ - 複数の変更を一度に
function calculateTotal(items, tax, discount) {
const subtotal = items.reduce((sum, item) => sum + item.price, 0);
const afterTax = subtotal * (1 + tax);
const afterDiscount = afterTax * (1 - discount);
return afterDiscount;
}
// ✅ Baby Steps - 一度に1つの変更
// ステップ1: ゼロを返す(最小限でテストを通す)
function calculateTotal(items) {
return 0;
}
// ステップ2: 基本的な合計(次のテストがこれを駆動)
function calculateTotal(items) {
return items.reduce((sum, item) => sum + item.price, 0);
}
// ステップ3: 税金サポートを追加(テストが要求したときのみ)
// ... 小さな増分で継続
```
### Baby Stepsのリズム
1. **最小の失敗テストを書く** (30秒)
2. **最小限のコードで通す** (1分)
3. **テストを実行** (10秒)
4. **必要なら小さなリファクタリング** (30秒)
5. **グリーンならコミット** (20秒)
合計サイクル: 約2分
## RGRCサイクルの実装
1. **Redフェーズ - 失敗するテストを最初に書く**
```bash
npm test
```
テストを書く → 正しく失敗することを確認 → 具体的で焦点を絞る
2. **Greenフェーズ - 最小限の実装**
```bash
npm test
```
パスするのに十分なコードのみ → 「罪を犯してもよい」 → 余分な機能は我慢
3. **Refactorフェーズ - コード品質の改善**
```bash
npm test
```
重複を除去 → 構造/可読性を改善 → 常にテストを緑に保つ
4. **Commitフェーズ - 進捗を保存**
```bash
git add -A && git commit -m "feat: [説明] (RGRC完了)"
```
テスト + 実装を含める → メッセージでRGRCを参照
## t_wadaのように考える
- **小さなステップ**: 「なぜステップを小さくするのか?」 - 各ステップが特定のことを教えてくれるから
- **素早いイテレーション**: 「このサイクルをもっと速くできるか?」 - スピードが設計の問題を早期に明らかにする
- **テスト失敗の理由**: 「正しい理由で失敗しているか?」 - 間違った失敗は間違った理解を意味する
- **実践を通じた学習**: 「このサイクルから何を学んだか?」 - 各サイクルは単なる進捗ではなく学習機会
## TodoWriteとの統合
ワークフロー例:
```markdown
# テストシナリオリスト
1. ⏳ ユーザーはメールとパスワードで登録できる
2. ⏳ 無効なメールで登録に失敗する
3. ⏳ 弱いパスワードで登録に失敗する
4. ⏳ 既存のメールでは登録できない
# 現在のRGRCサイクル(シナリオ1用)
1.1 ❌ Red: ユーザー登録の失敗するテストを書く
1.2 ⏳ Green: 最小限の登録ロジックを実装
1.3 ⏳ Refactor: バリデーションロジックを抽出
1.4 ⏳ Commit: 登録実装を保存
```
## TDDをスキップする場合
スキップ: プロトタイプ、外部API(モックを使用)、使い捨てスクリプト
## Claude Codeでの利点
フェーズごとの明確な境界、過度な設計の防止、自己文書化されたテスト、自然なチェックポイント
## 関連する原則
- [@../reference/OCCAMS_RAZOR.md] - Baby Stepsはシンプルさの原則を体現
デメテルの法則
デメテルの法則は「直接の友達」とだけと話すこと、React で言えば、あるコンポーネントが構造やプロパティに対して持っている仮定を最小限にすべきという原則です。
親コンポーネントから受け取った props を子コンポーネント内で深くアクセスしないようにし、保守性とテスト容易性を向上させる目的で含めている原則です。
// 必要なデータだけを受け取る
export const UserProfile = ({
displayName,
email,
theme
}: {
displayName: string
email: string
theme: string
}) => {
return (
<div>
<h2>{displayName}</h2>
<p>{email}</p>
<span>{theme}</span>
</div>
)
}
// 親コンポーネント側でデータを展開
export const UserProfileContainer = ({ user }) => {
return (
<UserProfile
displayName={user.getDisplayName()}
email={user.getPrimaryEmail()}
theme={user.getTheme()}
/>
)
}
これがコンテナ・プレゼンテーションパターンの基礎にもなります。
# デメテルの法則 - 最小知識の原則
**核心原則**: 「直接の友達とだけ話す」
## 核心哲学
オブジェクトは以下とのみ相互作用すべき:
- **自分自身** - 自身のメソッドとプロパティ
- **パラメータ** - 引数として渡されたオブジェクト
- **作成したオブジェクト** - 自身が作成したオブジェクト
- **直接のコンポーネント** - 直接のプロパティ
- **グローバルオブジェクト** - 限定的で明確に定義された場合
## 解決する問題
```typescript
// ❌ Train wreck - デメテルの法則違反
const street = user.getAddress().getCity().getStreet().getName()
if (order.getCustomer().getPaymentMethod().isValid()) {
order.getCustomer().getPaymentMethod().charge(amount)
}
// ✅ 直接の相互作用のみ
const street = user.getStreetName()
if (order.canCharge()) {
order.charge(amount)
}
```
解決される問題:
- **高い結合度** - コードが知りすぎる
- **脆いコード** - 変更が連鎖する
- **テストが困難** - 複雑なモックが必要
- **カプセル化の欠如** - 内部が露出
## 主要テクニック
### 1. Tell, Don't Ask
```typescript
// ❌ データを聞いて判断
if (employee.getDepartment().getBudget() > amount) {
employee.getDepartment().getBudget().subtract(amount)
}
// ✅ オブジェクトに何をすべきか伝える
if (employee.canExpense(amount)) {
employee.expense(amount)
}
```
### 2. 委譲を隠す
```typescript
// ❌ 内部構造を露出
class Order {
getCustomer(): Customer { return this.customer }
}
// 使用:order.getCustomer().getName()
// ✅ 委譲を隠す
class Order {
getCustomerName(): string {
return this.customer.getName()
}
}
// 使用:order.getCustomerName()
```
## 実践的な適用
### Reactコンポーネント
```tsx
// ❌ コンポーネントが知りすぎる
function UserCard({ user }) {
return <h2>{user.profile.info.name.displayName}</h2>
}
// ✅ コンポーネントは必要なものだけ受け取る
function UserCard({ displayName }) {
return <h2>{displayName}</h2>
}
// 親が抽出を処理
function UserCardContainer({ user }) {
return <UserCard displayName={user.getDisplayName()} />
}
```
### API設計
```typescript
// ❌ 内部構造を露出
class BankAccount {
getTransactions(): Transaction[] {
return this.transactions
}
}
// ✅ 特定のメソッドを提供
class BankAccount {
getTransactionsAbove(amount: number): Transaction[] {
return this.transactions.filter(t => t.amount > amount)
}
}
```
### テストの利点
```typescript
// ❌ 複雑なモッキング
const mockCity = { getTaxRate: jest.fn(() => 0.08) }
const mockAddress = { getCity: jest.fn(() => mockCity) }
const mockCustomer = { getAddress: jest.fn(() => mockAddress) }
// ✅ シンプルなインターフェース
const mockCustomer = { getTaxRate: jest.fn(() => 0.08) }
```
## 例外
### 違反してもOKな場合
**流暢インターフェース** - チェーン用に設計:
```typescript
query.select('*').from('users').where('active', true).limit(10)
```
また許容される:
- ビルダーパターン
- データ変換パイプライン(map/filter/reduce)
## 適用のガイドライン
### コードレビューチェックリスト
- [ ] メソッドチェーンが2-3呼び出し以下
- [ ] コンポーネントは特定のプロップスを受け取り、オブジェクト全体ではない
- [ ] オブジェクトを通してデータを取得していない
- [ ] Tell, don't askの原則が適用されている
- [ ] テストセットアップがシンプルで、深くネストしていない
### リファクタリングのシグナル
探すべきもの:
- `getX().getY().getZ()` パターン
- テストでの複雑なモックセットアップ
- チェーンでの頻繁なnullチェック
- 無関係なコードを壊す変更
## 覚えておくこと
> 「各ユニットは他のユニットについて限定的な知識のみを持つべき:現在のユニットと『密接に』関連するユニットのみ」
目標は**低結合**と**高凝集**:
- オブジェクトは互いについて知ることが少ない
- 変更がシステム全体に連鎖しない
- テストがシンプルになる
- コードが保守しやすくなる
## 関連する原則
### 開発実践
- [@./CONTAINER_PRESENTATIONAL.md] - Reactでのデメテルの自然な適用
- [@./READABLE_CODE.md] - シンプルなインターフェースが可読性を向上
### 核心原則
- [@../reference/SOLID.md] - SOLID原則を補完
- [@../reference/OCCAMS_RAZOR.md] - シンプルな相互作用が複雑さを削減
Tidying
Kent Beck が書籍 Tidy First? のなかで提唱したごく小さなリファクタリングの手法が Tidying です。
レガシーコードを全部リファクタするのは現実的ではありません。しかし、触った部分だけ改善すれば、コードベースは徐々に良くなっていきます。
重要なのは、触った範囲だけ改善すること。同じファイル内の他のコンポーネントまで手を出すと、レビュー負荷が高まってしまうので、それを避けるために採用しています。
# Kent Beckのように整理整頓
小さなコード改善を行う際は、Kent Beckのように考え行動する - 機能を壊すリスクのない微小改善を蓄積する。
## 実装方針
### タイミング
メインタスクの後、コミット前、編集したファイルのみ
### Kent Beckのように考える
- 「これは動作を変更するか?」 - はいの場合、それは整理整頓ではない
- 「この変更に不安を感じるか?」 - はいの場合、もっと小さくする
- 「もっと小さくできるか?」 - 変更が小さいほど安全
- 「次の小さな改善は何か?」 - 常に次の微小ステップを探す
## 具体的な基準
1. **空白文字**: 末尾のスペース、連続空白→単一、EOF整理、インデント修正
2. **インポート**: 未使用を削除、アルファベット順、同一パッケージをまとめる、タイプ別にグループ化
3. **変数**: 未使用を削除、単一使用をインライン化(読みやすい場合)、let→const
4. **コメント**: 解決済みTODOを削除、冗長な説明、デッドコード
5. **型(TS)**: 推論可能な注釈を削除、any→具体的、重複を統合
6. **フォーマット**: 一貫したセミコロン/引用符/カンマ(プロジェクトの慣例に従う)
7. **命名**: タイポ修正、大文字小文字の不一致を修正
## やってはいけないこと
**絶対にしない**: ロジック変更、構造変更、新機能、パフォーマンス最適化、公開API変更、テスト修正
## 報告フォーマット
`🧹 整理整頓: 空白文字 ✓ インポート ✓ 未使用コード ✓ その他 ✓`
Claude Code での活用
私は Claude Code を利用しているので、CLAUDE.md
に原則をまとめています。
Claude.md には主要な原則を短く書き、詳細は/rules/
以下のファイルに分けて参照させるようにしました。
.claude/
├── CLAUDE.md
└── rules/
├── reference/
│ ├── OCCAMS_RAZOR.md
│ ├── DRY.md
│ └── SOLID.md
└── development/
├── PROGRESSIVE_ENHANCEMENT.md
├── READABLE_CODE.md
├── TDD_RGRC.md
└── TIDYINGS.md
こうすることで、AIは毎回主要な原則を参照しつつ、必要に応じて詳細も確認できます。
AI にどう書かせるか
AI 時代のコード品質管理は、「何を書かせるか」より「どう書かせるか」が重要です。
今回紹介したような原則を活用することで、小さなコンテキストで AI が一貫した品質のコードを生成してくれるようになりました。
皆さんのプロジェクトでも、ぜひ試していただいて、プロジェクトのルールやコーディングスタイルに合わせてカスタマイズしていくのが良いかなと思います。
チーム全体で同じ原則を共有できれば、コードレビューの時間も減り、議論も建設的になります。プロジェクトのあり方や進め方を決めるための最初の議論として、原則の整理から始めるのもありかもしれません。
ぜひ試してみてください!
Gaji-Labo は フロントエンドのAI開発の実績と知見があります
急速に進化するAI技術、進まないUIとの統合…。 ユーザー体験を損なわずにAIを導入したいと考えながら、実装や設計に悩み、開発が停滞している。 そんな課題を抱えるプロダクトや開発チームを、私たちは数多く支援してきました。
フロントエンド開発の専門企業である Gaji-Labo は、AIチャットや自然言語処理UIなどの設計・実装において、AIの特性を踏まえた体験設計・UI開発・運用まで、フェーズに応じたサポートが可能です。