293 lines
11 KiB
Markdown
293 lines
11 KiB
Markdown
# コーディングポリシー
|
||
|
||
速さより丁寧さ、実装の楽さよりコードの正確さを優先する。
|
||
|
||
## 原則
|
||
|
||
| 原則 | 基準 |
|
||
|------|------|
|
||
| Simple > Easy | 書きやすさより読みやすさを優先 |
|
||
| DRY | 本質的な重複は排除する |
|
||
| コメント | Why のみ。What/How は書かない |
|
||
| 関数サイズ | 1関数1責務。30行目安 |
|
||
| ファイルサイズ | 目安として300行。タスクに応じて柔軟に |
|
||
| ボーイスカウト | 触った箇所は少し改善して去る |
|
||
| Fail Fast | エラーは早期に検出。握りつぶさない |
|
||
| プロジェクトスクリプト優先 | ツール実行はプロジェクト定義のスクリプトを使う。直接実行は最後の手段 |
|
||
|
||
## フォールバック・デフォルト引数の禁止
|
||
|
||
値の流れを不明瞭にするコードは書かない。ロジックを追わないと値が分からないのは悪いコード。
|
||
|
||
### 禁止パターン
|
||
|
||
| パターン | 例 | 問題 |
|
||
|---------|-----|------|
|
||
| 必須データへのフォールバック | `user?.id ?? 'unknown'` | エラーになるべき状態で処理が進む |
|
||
| デフォルト引数の濫用 | `function f(x = 'default')` で全呼び出し元が省略 | 値がどこから来るか分からない |
|
||
| null合体で渡す口がない | `options?.cwd ?? process.cwd()` で上位から渡す経路なし | 常にフォールバックになる(意味がない) |
|
||
| try-catch で空値返却 | `catch { return ''; }` | エラーを握りつぶす |
|
||
| 不整合な値のサイレントスキップ | `if (a !== expected) return undefined` | 設定ミスが実行時に黙って無視される |
|
||
|
||
### 正しい実装
|
||
|
||
```typescript
|
||
// ❌ 禁止 - 必須データへのフォールバック
|
||
const userId = user?.id ?? 'unknown'
|
||
processUser(userId) // 'unknown' で処理が進んでしまう
|
||
|
||
// ✅ 正しい - Fail Fast
|
||
if (!user?.id) {
|
||
throw new Error('User ID is required')
|
||
}
|
||
processUser(user.id)
|
||
|
||
// ❌ 禁止 - デフォルト引数で全呼び出し元が省略
|
||
function loadConfig(path = './config.json') { ... }
|
||
// 全呼び出し元: loadConfig() ← path を渡していない
|
||
|
||
// ✅ 正しい - 必須引数にして明示的に渡す
|
||
function loadConfig(path: string) { ... }
|
||
// 呼び出し元: loadConfig('./config.json') ← 明示的
|
||
|
||
// ❌ 禁止 - null合体で渡す口がない
|
||
class Engine {
|
||
constructor(config, options?) {
|
||
this.cwd = options?.cwd ?? process.cwd()
|
||
// 問題: options に cwd を渡す経路がない場合、常に process.cwd() になる
|
||
}
|
||
}
|
||
|
||
// ✅ 正しい - 上位から渡せるようにする
|
||
function createEngine(config, cwd: string) {
|
||
return new Engine(config, { cwd })
|
||
}
|
||
```
|
||
|
||
### 許容されるケース
|
||
|
||
- 外部入力(ユーザー入力、API応答)のバリデーション時のデフォルト値
|
||
- 設定ファイルのオプショナル値(明示的に省略可能と設計されている)
|
||
- 一部の呼び出し元のみがデフォルト引数を使用(全員が省略している場合は禁止)
|
||
|
||
### 判断基準
|
||
|
||
1. **必須データか?** → フォールバックせず、エラーにする
|
||
2. **全呼び出し元が省略しているか?** → デフォルト引数を削除し、必須にする
|
||
3. **上位から値を渡す経路があるか?** → なければ引数・フィールドを追加
|
||
4. **関連する値に不変条件があるか?** → ロード・セットアップ時にクロスバリデーションする
|
||
|
||
## 抽象化
|
||
|
||
### 条件分岐を追加する前に考える
|
||
|
||
- 同じ条件が他にもあるか → あればパターンで抽象化
|
||
- 今後も分岐が増えそうか → Strategy/Mapパターンを使う
|
||
- 型で分岐しているか → ポリモーフィズムで置換
|
||
|
||
```typescript
|
||
// ❌ 条件分岐を増やす
|
||
if (type === 'A') { ... }
|
||
else if (type === 'B') { ... }
|
||
else if (type === 'C') { ... } // また増えた
|
||
|
||
// ✅ Mapで抽象化
|
||
const handlers = { A: handleA, B: handleB, C: handleC };
|
||
handlers[type]?.();
|
||
```
|
||
|
||
### 抽象度を揃える
|
||
|
||
1つの関数内では同じ粒度の処理を並べる。詳細な処理は別関数に切り出す。「何をするか」と「どうやるか」を混ぜない。
|
||
|
||
```typescript
|
||
// ❌ 抽象度が混在
|
||
function processOrder(order) {
|
||
validateOrder(order); // 高レベル
|
||
const conn = pool.getConnection(); // 低レベル詳細
|
||
conn.query('INSERT...'); // 低レベル詳細
|
||
}
|
||
|
||
// ✅ 抽象度を揃える
|
||
function processOrder(order) {
|
||
validateOrder(order);
|
||
saveOrder(order); // 詳細は隠蔽
|
||
}
|
||
```
|
||
|
||
### 言語・フレームワークの作法に従う
|
||
|
||
- Pythonなら Pythonic に、KotlinならKotlinらしく
|
||
- フレームワークの推奨パターンを使う
|
||
- 独自の書き方より標準的な書き方を選ぶ
|
||
- 不明なときはリサーチする。推測で実装しない
|
||
|
||
### インターフェース設計
|
||
|
||
インターフェースは利用側の都合で設計する。実装側の内部構造を露出しない。
|
||
|
||
| 原則 | 基準 |
|
||
|------|------|
|
||
| 利用者視点 | 呼び出し側が必要としないものを押し付けない |
|
||
| 構成と実行の分離 | 「何を使うか」はセットアップ時に決定し、実行APIはシンプルに保つ |
|
||
| メソッド増殖の禁止 | 同じことをする複数メソッドは構成の違いで吸収する |
|
||
|
||
```typescript
|
||
// ❌ メソッド増殖 — 構成の違いを呼び出し側に押し付けている
|
||
interface NotificationService {
|
||
sendEmail(to, subject, body)
|
||
sendSMS(to, message)
|
||
sendPush(to, title, body)
|
||
sendSlack(channel, message)
|
||
}
|
||
|
||
// ✅ 構成と実行の分離
|
||
interface NotificationService {
|
||
setup(config: ChannelConfig): Channel
|
||
}
|
||
interface Channel {
|
||
send(message: Message): Promise<Result>
|
||
}
|
||
```
|
||
|
||
### 抽象化の漏れ
|
||
|
||
特定実装が汎用層に現れたら抽象化が漏れている。汎用層はインターフェースだけを知り、分岐は実装側で吸収する。
|
||
|
||
```typescript
|
||
// ❌ 汎用層に特定実装のインポートと分岐
|
||
import { uploadToS3 } from '../aws/s3.js'
|
||
if (config.storage === 's3') {
|
||
return uploadToS3(config.bucket, file, options)
|
||
}
|
||
|
||
// ✅ 汎用層はインターフェースのみ。非対応は生成時にエラー
|
||
const storage = createStorage(config)
|
||
return storage.upload(file, options)
|
||
```
|
||
|
||
## 構造
|
||
|
||
### 分割の基準
|
||
|
||
- 独自のstateを持つ → 分離
|
||
- 50行超のUI/ロジック → 分離
|
||
- 複数の責務がある → 分離
|
||
|
||
### 依存の方向
|
||
|
||
- 上位層 → 下位層(逆方向禁止)
|
||
- データ取得はルート(View/Controller)で行い、子に渡す
|
||
- 子は親のことを知らない
|
||
|
||
### 状態管理
|
||
|
||
- 状態は使う場所に閉じ込める
|
||
- 子は状態を直接変更しない(イベントを親に通知)
|
||
- 状態の流れは単方向
|
||
|
||
## エラーハンドリング
|
||
|
||
エラーは一元管理する。各所でtry-catchしない。
|
||
|
||
```typescript
|
||
// ❌ 各所でtry-catch
|
||
async function createUser(data) {
|
||
try {
|
||
const user = await userService.create(data)
|
||
return user
|
||
} catch (e) {
|
||
console.error(e)
|
||
throw new Error('ユーザー作成に失敗しました')
|
||
}
|
||
}
|
||
|
||
// ✅ 上位層で一元処理
|
||
// Controller/Handler層でまとめてキャッチ
|
||
// または @ControllerAdvice / ErrorBoundary で処理
|
||
async function createUser(data) {
|
||
return await userService.create(data) // 例外はそのまま上に投げる
|
||
}
|
||
```
|
||
|
||
### エラー処理の配置
|
||
|
||
| 層 | 責務 |
|
||
|----|------|
|
||
| ドメイン/サービス層 | ビジネスルール違反時に例外をスロー |
|
||
| Controller/Handler層 | 例外をキャッチしてレスポンスに変換 |
|
||
| グローバルハンドラ | 共通例外(NotFound, 認証エラー等)を処理 |
|
||
|
||
## 変換処理の配置
|
||
|
||
変換メソッドはDTO側に持たせる。
|
||
|
||
```typescript
|
||
// ✅ Request/Response DTOに変換メソッド
|
||
interface CreateUserRequest {
|
||
name: string
|
||
email: string
|
||
}
|
||
|
||
function toUseCaseInput(req: CreateUserRequest): CreateUserInput {
|
||
return { name: req.name, email: req.email }
|
||
}
|
||
|
||
// Controller
|
||
const input = toUseCaseInput(request)
|
||
const output = await useCase.execute(input)
|
||
return UserResponse.from(output)
|
||
```
|
||
|
||
変換の方向:
|
||
```
|
||
Request → toInput() → UseCase/Service → Output → Response.from()
|
||
```
|
||
|
||
## 共通化の判断
|
||
|
||
基本的に重複は排除する。本質的に同じロジックであり、まとめるべきと判断したら DRY にする。回数で機械的に判断しない。
|
||
|
||
### 共通化すべきもの
|
||
|
||
- 本質的に同じロジックの重複
|
||
- 同じスタイル/UIパターン
|
||
- 同じバリデーションロジック
|
||
- 同じフォーマット処理
|
||
|
||
### 共通化すべきでないもの
|
||
|
||
- ドメインが異なる重複(例: 顧客用バリデーションと管理者用バリデーションは別物)
|
||
- 表面的に似ているが変更理由が異なるコード
|
||
- 「将来使うかも」という予測に基づくもの
|
||
|
||
```typescript
|
||
// ❌ 過度な汎用化
|
||
function formatValue(value, type, options) {
|
||
if (type === 'currency') { ... }
|
||
else if (type === 'date') { ... }
|
||
else if (type === 'percentage') { ... }
|
||
}
|
||
|
||
// ✅ 用途別に関数を分ける
|
||
function formatCurrency(amount: number): string { ... }
|
||
function formatDate(date: Date): string { ... }
|
||
function formatPercentage(value: number): string { ... }
|
||
```
|
||
|
||
## 禁止事項
|
||
|
||
- **フォールバックは原則禁止** - `?? 'unknown'`、`|| 'default'`、`try-catch` で握りつぶすフォールバックを書かない。エラーは上位に伝播させる。どうしても必要な場合はコメントで理由を明記する
|
||
- **説明コメント** - コードで意図を表現する。What/How のコメントは書かない
|
||
- **未使用コード** - 「念のため」のコードは書かない
|
||
- **any型** - 型安全を破壊しない
|
||
- **オブジェクト/配列の直接変更** - スプレッド演算子で新規作成
|
||
- **console.log** - 本番コードに残さない
|
||
- **機密情報のハードコーディング**
|
||
- **各所でのtry-catch** - エラーは上位層で一元処理
|
||
- **後方互換・Legacy対応の自発的追加** - 明示的な指示がない限り不要
|
||
- **内部実装のパブリック API エクスポート** - 公開するのはドメイン操作の関数・型のみ。インフラ層の関数や内部クラスをエクスポートしない
|
||
- **リファクタリング後の旧コード残存** - 置き換えたコード・エクスポートは削除する。明示的に残すよう指示されない限り残さない
|
||
- **安全機構を迂回するワークアラウンド** - 根本修正が正しいなら追加の迂回は不要
|
||
- **プロジェクトスクリプトを迂回するツール直接実行** - `npx tool` 等の直接実行は lockfile を迂回しバージョン不一致を起こす。プロジェクトが定義したスクリプト(npm scripts, Makefile 等)を探して使う。見つからない場合のみ直接実行を検討する
|