|
| 1 | +````markdown |
| 2 | +# ContestTableProviderBase へのプロバイダーキー機能追加計画 |
| 3 | + |
| 4 | +**作成日**: 2025-11-12 |
| 5 | + |
| 6 | +**対象ファイル**: |
| 7 | + |
| 8 | +- `src/lib/utils/contest_table_provider.ts` |
| 9 | +- `src/test/lib/utils/contest_table_provider.test.ts` |
| 10 | + |
| 11 | +**優先度**: High |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## 1. 概要 |
| 16 | + |
| 17 | +### 背景 |
| 18 | + |
| 19 | +現在の `ContestTableProviderGroup` では、`ContestType` をキーとして Provider を管理しているため、同じ `ContestType` で複数の異なるテーブル(例:Tessoku Book の例題・応用・力試し)を持つことができません。 |
| 20 | + |
| 21 | +### 目的 |
| 22 | + |
| 23 | +- 同じ `ContestType` で複数のセクション('examples', 'practicals', 'challenges')を区別できる設計に改善 |
| 24 | +- Provider 自身に ID 管理責務を持たせ、関心の分離を徹底 |
| 25 | +- 後方互換性を維持しつつ、拡張性を確保 |
| 26 | + |
| 27 | +### 実装方針 |
| 28 | + |
| 29 | +`static createProviderKey()` メソッドで キー生成ロジックを一元化し、Provider 自身が `getProviderKey()` で自分の識別子を返すようにする。 |
| 30 | + |
| 31 | +--- |
| 32 | + |
| 33 | +## 2. 仕様要件 |
| 34 | + |
| 35 | +| 項目 | 仕様 | 備考 | |
| 36 | +| ----------------------------- | ------------------------------------------------------------------------ | ---------------------------------- | |
| 37 | +| **キー型** | `type ProviderKey = \`${ContestType}\` \| \`${ContestType}::${string}\`` | TypeScript テンプレートリテラル型 | |
| 38 | +| **セクション識別子** | 'examples', 'practicals', 'challenges' | 現在は TessokuBook のみ | |
| 39 | +| **getProviderKey() アクセス** | protected | ContestTableProviderGroup 内部のみ | |
| 40 | +| **後方互換性** | getProvider(contestType) で section 未指定時は複合キーなしで取得 | 既存コード変更なし | |
| 41 | + |
| 42 | +--- |
| 43 | + |
| 44 | +## 3. 変更対象 |
| 45 | + |
| 46 | +### 3.1 ContestTableProviderBase クラス |
| 47 | + |
| 48 | +#### 追加メンバー |
| 49 | + |
| 50 | +```typescript |
| 51 | +// クラスプロパティ |
| 52 | +protected readonly section?: string; |
| 53 | + |
| 54 | +// static メソッド |
| 55 | +static createProviderKey(contestType: ContestType, section?: string): string { |
| 56 | + return section ? `${contestType}::${section}` : `${contestType}`; |
| 57 | +} |
| 58 | + |
| 59 | +// インスタンスメソッド(protected) |
| 60 | +protected getProviderKey(): string { |
| 61 | + return ContestTableProviderBase.createProviderKey(this.contestType, this.section); |
| 62 | +} |
| 63 | +``` |
| 64 | + |
| 65 | +#### コンストラクタ修正 |
| 66 | + |
| 67 | +```typescript |
| 68 | +constructor(contestType: ContestType, section?: string) { |
| 69 | + this.contestType = contestType; |
| 70 | + this.section = section; |
| 71 | +} |
| 72 | +``` |
| 73 | + |
| 74 | +#### 制約 |
| 75 | + |
| 76 | +- `contestType` は `readonly` に変更 |
| 77 | +- `section` は `readonly` に変更 |
| 78 | + |
| 79 | +--- |
| 80 | + |
| 81 | +### 3.2 Tessoku Book プロバイダー修正 |
| 82 | + |
| 83 | +#### TessokuBookForExamplesProvider |
| 84 | + |
| 85 | +```typescript |
| 86 | +constructor(contestType: ContestType) { |
| 87 | + super(contestType, 'examples'); |
| 88 | +} |
| 89 | +``` |
| 90 | + |
| 91 | +#### TessokuBookForPracticalsProvider |
| 92 | + |
| 93 | +```typescript |
| 94 | +constructor(contestType: ContestType) { |
| 95 | + super(contestType, 'practicals'); |
| 96 | +} |
| 97 | +``` |
| 98 | + |
| 99 | +#### TessokuBookForChallengesProvider |
| 100 | + |
| 101 | +```typescript |
| 102 | +constructor(contestType: ContestType) { |
| 103 | + super(contestType, 'challenges'); |
| 104 | +} |
| 105 | +``` |
| 106 | + |
| 107 | +--- |
| 108 | + |
| 109 | +### 3.3 ContestTableProviderGroup クラス |
| 110 | + |
| 111 | +#### Map キー型を変更 |
| 112 | + |
| 113 | +```typescript |
| 114 | +private providers = new Map<string, ContestTableProviderBase>(); |
| 115 | +``` |
| 116 | + |
| 117 | +#### addProvider() メソッド修正 |
| 118 | + |
| 119 | +```typescript |
| 120 | +addProvider(provider: ContestTableProviderBase): this { |
| 121 | + const key = provider['getProviderKey'](); |
| 122 | + this.providers.set(key, provider); |
| 123 | + |
| 124 | + return this; |
| 125 | +} |
| 126 | +``` |
| 127 | + |
| 128 | +**注**: `protected` メソッドへのアクセスのため、角括弧表記を使用 |
| 129 | + |
| 130 | +#### addProviders() メソッド修正 |
| 131 | + |
| 132 | +```typescript |
| 133 | +addProviders(...providers: ContestTableProviderBase[]): this { |
| 134 | + providers.forEach((provider) => { |
| 135 | + const key = provider['getProviderKey'](); |
| 136 | + this.providers.set(key, provider); |
| 137 | + }); |
| 138 | + return this; |
| 139 | +} |
| 140 | +``` |
| 141 | + |
| 142 | +#### getProvider() メソッド修正(後方互換性維持) |
| 143 | + |
| 144 | +```typescript |
| 145 | +getProvider( |
| 146 | + contestType: ContestType, |
| 147 | + section?: string, |
| 148 | +): ContestTableProviderBase | undefined { |
| 149 | + const key = ContestTableProviderBase.createProviderKey(contestType, section); |
| 150 | + return this.providers.get(key); |
| 151 | +} |
| 152 | +``` |
| 153 | + |
| 154 | +--- |
| 155 | + |
| 156 | +### 3.4 prepareContestProviderPresets() 修正 |
| 157 | + |
| 158 | +#### TessokuBook プリセット |
| 159 | + |
| 160 | +```typescript |
| 161 | +TessokuBook: () => |
| 162 | + new ContestTableProviderGroup(`競技プログラミングの鉄則`, { |
| 163 | + buttonLabel: '競技プログラミングの鉄則', |
| 164 | + ariaLabel: 'Filter Tessoku Book', |
| 165 | + }).addProviders( |
| 166 | + new TessokuBookForExamplesProvider(ContestType.TESSOKU_BOOK), |
| 167 | + new TessokuBookForPracticalsProvider(ContestType.TESSOKU_BOOK), |
| 168 | + new TessokuBookForChallengesProvider(ContestType.TESSOKU_BOOK), |
| 169 | + ), |
| 170 | +``` |
| 171 | + |
| 172 | +**変更点**: 引数形式から `ContestTableProviderBase` インスタンスへ直接変更 |
| 173 | + |
| 174 | +--- |
| 175 | + |
| 176 | +## 4. テスト計画 |
| 177 | + |
| 178 | +### 4.1 追加・修正するテスト |
| 179 | + |
| 180 | +**参照**: `docs/dev-notes/2025-11-03/add_tests_for_contest_table_provider/plan.md` |
| 181 | + |
| 182 | +#### TessokuBook 関連テスト |
| 183 | + |
| 184 | +- ✅ TessokuBookForExamplesProvider の getProviderKey() = 'TESSOKU_BOOK::examples' |
| 185 | +- ✅ TessokuBookForPracticalsProvider の getProviderKey() = 'TESSOKU_BOOK::practicals' |
| 186 | +- ✅ TessokuBookForChallengesProvider の getProviderKey() = 'TESSOKU_BOOK::challenges' |
| 187 | +- ✅ 3 つの Provider を同時登録できるか検証 |
| 188 | + |
| 189 | +#### ContestTableProviderGroup 関連テスト |
| 190 | + |
| 191 | +- ✅ addProvider() で Provider 自身の getProviderKey() を使用 |
| 192 | +- ✅ addProviders() で複数 Provider の複合キーを別々に登録 |
| 193 | +- ✅ getProvider(ContestType.TESSOKU_BOOK, 'examples') で正しく取得 |
| 194 | +- ✅ getProvider(ContestType.TESSOKU_BOOK, 'practicals') で正しく取得 |
| 195 | +- ✅ getProvider(ContestType.TESSOKU_BOOK, 'challenges') で正しく取得 |
| 196 | +- ✅ getProvider(ContestType.TESSOKU_BOOK) で section 未指定時は complex key なしで検索(後方互換性) |
| 197 | + |
| 198 | +### 4.2 テスト実行 |
| 199 | + |
| 200 | +```bash |
| 201 | +# 単体テスト実行 |
| 202 | +pnpm test:unit src/test/lib/utils/contest_table_provider.test.ts |
| 203 | + |
| 204 | +# Lint チェック |
| 205 | +pnpm lint src/lib/utils/contest_table_provider.ts |
| 206 | +pnpm lint src/test/lib/utils/contest_table_provider.test.ts |
| 207 | + |
| 208 | +# Format 確認 |
| 209 | +pnpm format src/lib/utils/contest_table_provider.ts |
| 210 | +``` |
| 211 | + |
| 212 | +--- |
| 213 | + |
| 214 | +## 5. 実装手順(Todo リスト) |
| 215 | + |
| 216 | +| # | タスク | 説明 | 依存 | |
| 217 | +| --- | -------------------------------------- | ----------------------------------------------------------------------------------------------------- | ---- | |
| 218 | +| 1 | **型定義追加** | `ProviderKey` 型を定義(テンプレートリテラル型) | | |
| 219 | +| 2 | **ContestTableProviderBase 修正** | `section` プロパティ追加、`readonly` 指定、`static createProviderKey()` 追加、`getProviderKey()` 実装 | 1 | |
| 220 | +| 3 | **Tessoku Book Provider 修正** | 3 つの子クラスの constructor に section を追加 | 2 | |
| 221 | +| 4 | **ContestTableProviderGroup 修正** | Map キー型変更、addProvider/addProviders/getProvider メソッド修正 | 2 | |
| 222 | +| 5 | **prepareContestProviderPresets 修正** | TessokuBook プリセット の引数形式を変更 | 3, 4 | |
| 223 | +| 6 | **既存テスト確認** | 現在のテストが修正後も通るか検証 | 5 | |
| 224 | +| 7 | **新規テスト追加** | 複合キー関連の新テストを 6~8 個追加 | 6 | |
| 225 | +| 8 | **Lint & Format** | ESLint, Prettier で統一 | 7 | |
| 226 | +| 9 | **最終検証** | 全テスト実行、coverage 確認 | 8 | |
| 227 | + |
| 228 | +--- |
| 229 | + |
| 230 | +## 6. 教訓・参照ドキュメント |
| 231 | + |
| 232 | +- **テストパターン**: TessokuBook テスト の構造を参照(`docs/dev-notes/2025-11-03/add_tests_for_contest_table_provider/plan.md`) |
| 233 | + |
| 234 | +--- |
| 235 | + |
| 236 | +## 7. 注意点 |
| 237 | + |
| 238 | +### 実装時の留意事項 |
| 239 | + |
| 240 | +1. **protected メソッドへのアクセス** |
| 241 | + - `getProviderKey()` は protected なため、Group 内では `provider['getProviderKey']()` で呼び出す |
| 242 | + - TypeScript の暗黙の型チェックを通すため、角括弧表記が必須 |
| 243 | + |
| 244 | +2. **後方互換性** |
| 245 | + - `getProvider(contestType)` で section 未指定時、複合キーなしで検索 |
| 246 | + - 既存の `getProvider(ContestType.ABC)` などの呼び出しが動作継続 |
| 247 | + |
| 248 | +3. **Prettier フォーマット** |
| 249 | + - 実装後は必ず `pnpm format` を実行 |
| 250 | + - インデントは Tab、printWidth は 100 |
| 251 | + |
| 252 | +--- |
| 253 | + |
| 254 | +## 8. 完了チェックリスト |
| 255 | + |
| 256 | +- [ ] 型定義 `ProviderKey` 追加完了 |
| 257 | +- [ ] ContestTableProviderBase クラス修正完了 |
| 258 | +- [ ] Tessoku Book 3 プロバイダー修正完了 |
| 259 | +- [ ] ContestTableProviderGroup 修正完了 |
| 260 | +- [ ] prepareContestProviderPresets 修正完了 |
| 261 | +- [ ] 既存テスト全て通過 |
| 262 | +- [ ] 新規テスト 6~8 個追加完了 |
| 263 | +- [ ] Lint エラーなし |
| 264 | +- [ ] Format 統一完了 |
| 265 | +- [ ] 最終テスト実行 & coverage 確認完了 |
| 266 | + |
| 267 | +--- |
| 268 | + |
| 269 | +## 9. 実装完了報告 |
| 270 | + |
| 271 | +**実施日**: 2025-11-12 |
| 272 | + |
| 273 | +### 実施内容 |
| 274 | + |
| 275 | +すべてのタスク(1~9)を完了しました。 |
| 276 | + |
| 277 | +### 主な成果 |
| 278 | + |
| 279 | +1. **ProviderKey 型定義**: テンプレートリテラル型 `ProviderKey = \`${ContestType}\` | \`${ContestType}::${string}\`` を定義 |
| 280 | +2. **ContestTableProviderBase 拡張**: `section` プロパティ、`static createProviderKey()`、`protected getProviderKey()` を追加 |
| 281 | +3. **Tessoku Book プロバイダー強化**: 3 つの子クラスに section パラメータを追加('examples', 'practicals', 'challenges') |
| 282 | +4. **ContestTableProviderGroup 再設計**: Map キーを文字列に変更し、複合キーに対応 |
| 283 | +5. **API 統一化**: `addProvider(provider)`, `addProviders(...providers)`, `getProvider(contestType, section?)` の新シグネチャに統一 |
| 284 | +6. **後方互換性維持**: 既存コード(`getProvider(contestType)` のセクション省略)は引き続き動作 |
| 285 | + |
| 286 | +### テスト結果 |
| 287 | + |
| 288 | +- **全テスト実行結果**: 1,646 テスト合格(1 スキップ) |
| 289 | +- **contest_table_provider.test.ts**: 105 テスト合格 |
| 290 | +- **新規テスト追加**: 複合キー機能に関する 7 個の新テスト追加 |
| 291 | + |
| 292 | +### コード品質 |
| 293 | + |
| 294 | +- **Prettier**: フォーマット完了(Tab インデント、printWidth 100) |
| 295 | +- **ESLint**: 警告なし(設定ファイル互換性警告のみ) |
| 296 | +- **型安全性**: TypeScript strict mode で検証済み |
| 297 | + |
| 298 | +--- |
| 299 | + |
| 300 | +## 10. 教訓 |
| 301 | + |
| 302 | +### 実装パターン |
| 303 | + |
| 304 | +1. **ProviderKey 型の活用**: `type ProviderKey = \`${ContestType}\` | \`${ContestType}::${string}\`` でテンプレートリテラル型を定義し、メソッドの戻り値型として使用することで型安全性を向上 |
| 305 | +2. **保護メソッドの外部アクセス**: Protected メソッドへの外部アクセスは角括弧表記 `provider['getProviderKey']()` で実装 |
| 306 | +3. **複合キー設計**: 単純キー(ContestType)と複合キー(ContestType + section)の併存は後方互換性を損なわず拡張性を確保 |
| 307 | +4. **静的ファクトリメソッド**: `createProviderKey()` を static メソッドで共通化することで、キー生成ロジックの一元管理を実現 |
| 308 | + |
| 309 | +### テスト戦略 |
| 310 | + |
| 311 | +1. **複合キー検証**: 複数 section を持つ Provider の登録・取得を個別テストで検証 |
| 312 | +2. **後方互換性テスト**: セクション省略時の動作も明示的にテスト |
| 313 | +3. **ProviderKey 型確認**: テストでは戻り値の型を runtime で検証(string 型、`::` 区切り文字など) |
| 314 | +4. **新規テスト構成**: 既存テスト 98 個 + 新規テスト 7 個 = 合計 105 テスト |
| 315 | + |
| 316 | +### 開発効率 |
| 317 | + |
| 318 | +- **段階的実装**: 型定義 → ベースクラス → 子クラス → Group クラス → プリセット の順序で実装することで、依存関係を最小化 |
| 319 | +- **型安全性と実用性のバランス**: TypeScript のテンプレートリテラル型で型安全性を確保しつつ、runtime では string として柔軟に操作 |
| 320 | + |
| 321 | +--- |
| 322 | + |
| 323 | +**状態**: ✅ 完了(ProviderKey 型を活用) |
| 324 | +```` |
0 commit comments