|
| 1 | +# getMergedTasksMap のリファクタリング教訓 |
| 2 | + |
| 3 | +## 学習内容 |
| 4 | + |
| 5 | +### 1. **参照 vs コピー** |
| 6 | + |
| 7 | +TypeScript(JavaScript)の `const newTask = task;` は参照をコピーするため、`newTask` を変更すると元の `task` も変更されます。 |
| 8 | + |
| 9 | +- **浅いコピー**: `const newTask = { ...task };` |
| 10 | +- **深いコピー**: `const newTask = JSON.parse(JSON.stringify(task));` |
| 11 | + |
| 12 | +### 2. **TypeScript らしいコード書き方** |
| 13 | + |
| 14 | +- `map()` で初期化: `new Map(tasks.map(task => [task.task_id, task]))` |
| 15 | +- ループではなく関数型メソッド (`filter()`, `map()`, `flatMap()`) |
| 16 | +- スプレッド演算子で Map をマージ: `new Map([...map1, ...map2])` |
| 17 | + |
| 18 | +### 3. **`flatMap()` vs `map()`** |
| 19 | + |
| 20 | +`flatMap()` は返した配列を1段階フラット化するため、条件付きの可変長結果に最適: |
| 21 | + |
| 22 | +```typescript |
| 23 | +// flatMap で条件分岐を自然に表現 |
| 24 | +.flatMap((pair) => { |
| 25 | + if (!task || !contestType) return []; |
| 26 | + return [createMergedTask(...)]; |
| 27 | +}); |
| 28 | +// 結果: 該当する要素だけが含まれる |
| 29 | +``` |
| 30 | + |
| 31 | +### 4. **読みやすさ > 1行でまとめる** |
| 32 | + |
| 33 | +無理やり `return` や1行で書く必要はない: |
| 34 | + |
| 35 | +- 複雑な条件は `if` 文で早期リターン |
| 36 | +- オブジェクト生成は `key` と `value` を分けて記述 |
| 37 | +- 難しいロジックはヘルパー関数に抽出 |
| 38 | + |
| 39 | +### 5. **計算量の分析** |
| 40 | + |
| 41 | +- 全体: **O(N + M)** (N: タスク数、M: ペア数) |
| 42 | +- `Map.has()`, `Map.get()` は **O(1)** なのでループ内で複数回呼んでもOK |
| 43 | + |
| 44 | +### 6. **ドキュメント化のポイント** |
| 45 | + |
| 46 | +- 関数の目的と副作用を明確に |
| 47 | +- **計算量と根拠** を記載 |
| 48 | +- **使用例** を `@example` で示す |
| 49 | +- 戻り値の構造を詳しく説明 |
| 50 | + |
| 51 | +## コード例(改善版) |
| 52 | + |
| 53 | +src/lib/services/tasks.ts を参照 |
| 54 | + |
| 55 | +## キーポイント |
| 56 | + |
| 57 | +- ✅ 非破壊的な変更(スプレッド演算子) |
| 58 | +- ✅ 関数型パラダイム(`filter()`, `flatMap()` 使用) |
| 59 | +- ✅ 早期リターンで複雑さを減らす |
| 60 | +- ✅ ヘルパー関数で責任分離 |
| 61 | +- ✅ 明確なドキュメント化 |
| 62 | + |
| 63 | +--- |
| 64 | + |
| 65 | +# createContestTaskPairKey のテスト設計教訓 |
| 66 | + |
| 67 | +## テスト作成で学んだこと |
| 68 | + |
| 69 | +### 1. **ヘルパー関数で重複削減** |
| 70 | + |
| 71 | +同じパターンのテストコードは **ヘルパー関数** に抽出: |
| 72 | + |
| 73 | +```typescript |
| 74 | +// ❌ Before: 重複が多い |
| 75 | +const key1 = createTestKey(pair); |
| 76 | +const key2 = createTestKey(pair); |
| 77 | +expect(key1).toBe(key2); |
| 78 | + |
| 79 | +// ✅ After: ヘルパー関数化 |
| 80 | +const expectKeysToBeConsistent = (pair: TestPair): void => { |
| 81 | + const key1 = createTestKey(pair); |
| 82 | + const key2 = createTestKey(pair); |
| 83 | + expect(key1).toBe(key2); |
| 84 | +}; |
| 85 | +expectKeysToBeConsistent(pair); |
| 86 | +``` |
| 87 | + |
| 88 | +### 2. **パラメタライズテスト(test.each)で 4 個 → 1 個に** |
| 89 | + |
| 90 | +4 つの似たテストは `test.each()` で 1 つにまとめる: |
| 91 | + |
| 92 | +```typescript |
| 93 | +// ❌ Before: 4 つのテスト関数 |
| 94 | +test('expects empty contest_id to throw an error', () => { ... }); |
| 95 | +test('expects empty task_id to throw an error', () => { ... }); |
| 96 | +test('expects whitespace-only contest_id to throw an error', () => { ... }); |
| 97 | +test('expects whitespace-only task_id to throw an error', () => { ... }); |
| 98 | + |
| 99 | +// ✅ After: 1 つのパラメタライズテスト |
| 100 | +test.each<[string, string, string]>([ |
| 101 | + ['', 'abc123_a', 'contestId must be a non-empty string'], |
| 102 | + [' ', 'abc123_a', 'contestId must be a non-empty string'], |
| 103 | + ['abc123', '', 'taskId must be a non-empty string'], |
| 104 | + ['abc123', ' ', 'taskId must be a non-empty string'], |
| 105 | +])('expects error when contest_id="%s" and task_id="%s"', (contestId, taskId, expectedError) => { |
| 106 | + expect(() => createContestTaskPairKey(contestId, taskId)).toThrow(expectedError); |
| 107 | +}); |
| 108 | +``` |
| 109 | + |
| 110 | +### 3. **テストデータを集約して保守性向上** |
| 111 | + |
| 112 | +テストデータを `pairs` オブジェクトで一元管理: |
| 113 | + |
| 114 | +```typescript |
| 115 | +const pairs = { |
| 116 | + normal: [...], // 正常系ケース |
| 117 | + edge: [...], // エッジケース |
| 118 | + anomaly: [...], // 異常系ケース |
| 119 | +}; |
| 120 | +``` |
| 121 | + |
| 122 | +### 4. **テストカバレッジの考え方** |
| 123 | + |
| 124 | +- **正常系**: 期待通りに動くか |
| 125 | +- **エッジケース**: 空文字列、ホワイトスペース、長い文字列 |
| 126 | +- **異常系**: 特殊文字、Unicode、改行、タブ |
| 127 | +- **キー検証**: フォーマット、一意性、可逆性 |
| 128 | + |
| 129 | +### 5. **べストプラクティス** |
| 130 | + |
| 131 | +| 改善内容 | 効果 | |
| 132 | +| -------------------- | -------------------- | |
| 133 | +| ヘルパー関数化 | コード重複 -40% | |
| 134 | +| パラメタライズテスト | テスト関数数 削減 | |
| 135 | +| テストデータ集約 | 保守性向上 | |
| 136 | +| beforeEach で初期化 | テスト間の独立性確保 | |
| 137 | + |
| 138 | +### 6. **コード レビュー フィードバック対応** |
| 139 | + |
| 140 | +#### 指摘事項 |
| 141 | + |
| 142 | +| 項目 | 内容 | 対応 | |
| 143 | +| ------------------ | ------------------------------------ | ------------------ | |
| 144 | +| 弱い Assertion | `toContain()` では不正確 | `toBe()` に変更 | |
| 145 | +| 冗長テスト | O(n²) の全ペア比較テストは不要 | O(n) Set検証に統一 | |
| 146 | +| beforeEach削減 | イミュータブルデータの不要なコピー | 削除 | |
| 147 | +| 特殊文字カバレッジ | **デリミタ文字(コロン)が未テスト** | 3ケース追加 | |
| 148 | + |
| 149 | +#### 学んだことと根拠 |
| 150 | + |
| 151 | +- **アサーション強度**: `toContain()` は部分一致で誤検知の可能性 → `toBe()` で完全一致を保証 |
| 152 | +- **テスト効率**: 冗長な検証は実装と同じ複雑さ → 代表的パターンだけ実装 |
| 153 | +- **パーサビリティ脆弱性**: デリミタ文字(`:`)が ID に含まれると `split(':')` で分割失敗 → **デリミタ自体のテストケースが必須** |
| 154 | + |
| 155 | +#### 対応結果 |
| 156 | + |
| 157 | +- ✅ Assertion を 4 個改善(`toContain()` → `toBe()` 統一) |
| 158 | +- ✅ 冗長テスト 1 個削除(O(n²) → O(n)) |
| 159 | +- ✅ コロン文字テスト 3 個追加(`contestId` のみ、`taskId` のみ、両方) |
| 160 | +- ✅ **テスト総数: 26 → 29 個**(全成功 ✅) |
| 161 | + |
| 162 | +#### 重要な発見 |
| 163 | + |
| 164 | +**コロン文字は関数内で保存されるが、デリミタと同じため使用時に注意が必要** |
| 165 | + |
| 166 | +```typescript |
| 167 | +// 実装例:コロンを含む ID |
| 168 | +const key = createContestTaskPairKey('abc:123', 'task_a'); |
| 169 | +// 結果: "abc:123:task_a" (コロン3個) |
| 170 | + |
| 171 | +// ⚠️ split(':') での分割に注意 |
| 172 | +const [contestId, ...taskIdParts] = key.split(':'); |
| 173 | +// contestId = 'abc', taskIdParts = ['123', 'task_a'] ❌ 失敗! |
| 174 | +``` |
| 175 | + |
| 176 | +**教訓**: デリミタ文字も含めてテストし、実装側で検証ルールを明確にすべき。 |
| 177 | + |
| 178 | +## テスト統計 |
| 179 | + |
| 180 | +- **総テスト数**: 29 個(全成功 ✅) |
| 181 | +- **パラメタライズテスト**: 2 グループ(合計 11 ケース) |
| 182 | +- **ヘルパー関数**: 5 個 |
| 183 | +- **テストデータセット**: 3 グループ(normal, edge, anomaly) |
| 184 | +- **特殊文字カバレッジ**: パイプ 4 ケース + コロン 3 ケース + その他 8 ケース |
0 commit comments