# ABC→Protocol マイグレーションガイド

**対象プロジェクト**: noveler
**作成日**: 2025-10-24
**バージョン**: 1.0
**更新予定**: 各Phaseごとに更新

---

## 目次

1. [概要](#概要)
2. [前提条件](#前提条件)
3. [移行手順](#移行手順)
4. [具体的な移行例](#具体的な移行例)
5. [テスト更新方法](#テスト更新方法)
6. [トラブルシューティング](#トラブルシューティング)
7. [ロールバック手順](#ロールバック手順)

---

## 概要

### 目的

本ドキュメントでは、既存のABC（抽象基底クラス）定義をProtocol定義に安全に移行するための手順を説明します。

### 移行対象

- **ABC定義**: `src/noveler/domain/repositories/`, `src/noveler/domain/services/` など計104ファイル
- **Protocol定義先**: `src/noveler/domain/protocols/` 配下に統一化
- **目標**: ABC使用を71%削減（104→30ファイル）、Protocol使用を400%増加

### 移行期間

- **Phase 1**: 基盤整備（1週間）
- **Phase 2**: Core Protocols定義（3日）
- **Phase 3**: Repository層移行（1.5週間）
- **Phase 4**: Service層移行（1週間）
- **Phase 5**: 品質保証（3日）

**総計**: 4-5週間

---

## 前提条件

### 必須環境

- Python 3.10以上
- mypy 1.8.0以上
- pytest 8.0.0以上
- Git（ロールバック用）

### 確認コマンド

```bash
# Python バージョン確認
python --version
# 出力例: Python 3.10.0

# mypy バージョン確認
mypy --version
# 出力例: mypy 1.8.0 (compiled: yes)

# pytest バージョン確認
pytest --version
# 出力例: pytest 8.0.0
```

### 環境セットアップ

```bash
# 仮想環境のアクティベート
source venv/bin/activate  # Linux/Mac
# or
venv\Scripts\activate.bat  # Windows

# 依存パッケージをインストール
pip install -e ".[dev]"
```

---

## 移行手順

### ステップ1: 影響範囲調査（事前準備）

#### 1.1 ABC使用箇所の特定

```bash
# ABC使用ファイルを検索
find src/noveler/domain -name "*.py" -exec grep -l "from abc import ABC" {} \; > /tmp/abc_files.txt

# 個数を確認
wc -l /tmp/abc_files.txt
```

#### 1.2 Protocol使用箇所の確認

```bash
# Protocol使用ファイルを検索
find src/noveler/domain -name "*.py" -exec grep -l "from typing import Protocol" {} \; > /tmp/protocol_files.txt

# 個数を確認
wc -l /tmp/protocol_files.txt
```

#### 1.3 現状レポートの生成

```bash
# 監査スクリプトを実行
python scripts/quality_gates/protocol_validator.py src/noveler/domain > reports/pre_migration_audit.txt

# レポートを確認
cat reports/pre_migration_audit.txt
```

**期待される出力**:
```
Protocol validation report:
- Found 104 files using ABC (HIGH PRIORITY)
- Found 18 files using Protocol (BASELINE)
- Found 12 files with @abstractmethod in Protocol (ANTI-PATTERN)
- Found 8 duplicate interface definitions
- Found 3 inappropriate runtime_checkable usage

Priority list for migration:
1. Episode repositories (5 files)
2. Plot repositories (5 files)
3. Quality repositories (5 files)
...
```

### ステップ2: Protocol定義ファイルの準備

#### 2.1 Protocolディレクトリの確認

```bash
# ディレクトリ構造を確認
ls -la src/noveler/domain/protocols/

# 期待される構成
src/noveler/domain/
└── protocols/
    ├── __init__.py
    ├── repository_protocol.py
    ├── service_protocol.py
    ├── infrastructure_protocol.py
    └── ... (具体的なProtocol定義)
```

#### 2.2 汎用Protocol基盤の確認

新しいProtocolが以下の3ファイルに定義されていることを確認：
- `src/noveler/domain/protocols/repository_protocol.py`
- `src/noveler/domain/protocols/service_protocol.py`
- `src/noveler/domain/protocols/infrastructure_protocol.py`

```bash
# ファイルが存在するか確認
ls -l src/noveler/domain/protocols/{repository,service,infrastructure}_protocol.py
```

### ステップ3: ABC→Protocol移行スクリプトの実行

#### 3.1 自動移行スクリプトの準備

```bash
# スクリプトが存在するか確認
ls -la scripts/migration/abc_to_protocol_migration.py
```

#### 3.2 単一ファイルの移行（テスト）

まず、1つのファイルでドライラン（dry-run）を実施：

```bash
# ドライランモードでテスト
python scripts/migration/abc_to_protocol_migration.py \
    --file src/noveler/domain/repositories/episode_repository.py \
    --dry-run

# 出力を確認
# Expected output:
# [DRY-RUN] Would replace 'from abc import ABC, abstractmethod'
#           with 'from typing import Protocol'
# [DRY-RUN] Would remove '@abstractmethod' decorators
# [DRY-RUN] Would change 'class EpisodeRepository(ABC):'
#           to 'class EpisodeRepository(Protocol):'
```

#### 3.3 実際の移行実行

```bash
# ドライランで問題がなければ、実際に実行
python scripts/migration/abc_to_protocol_migration.py \
    --file src/noveler/domain/repositories/episode_repository.py

# ファイルの変更を確認
git diff src/noveler/domain/repositories/episode_repository.py
```

### ステップ4: Protocol定義ファイルの作成

#### 4.1 Protocol定義ファイルの手動作成

既存のABC定義から新しいProtocol定義ファイルを作成します：

```bash
# 例: Episode系リポジトリのProtocol定義
cp src/noveler/domain/repositories/episode_repository.py \
   src/noveler/domain/protocols/episode_repository_protocol.py

# ファイルの内容を編集（次のステップで説明）
```

#### 4.2 Protocol定義ファイルの内容修正

作成したProtocol定義ファイルを以下のように修正：

```python
# Before
from abc import ABC, abstractmethod

class EpisodeRepository(ABC):
    @abstractmethod
    def find_by_id(self, episode_id: int) -> Episode | None:
        pass

# After
from typing import Protocol

class EpisodeRepositoryProtocol(Protocol):
    """エピソードリポジトリプロトコル"""

    def find_by_id(self, episode_id: int) -> Episode | None:
        """エピソード取得"""
        ...
```

### ステップ5: 既存実装の更新

#### 5.1 旧ABCファイルの非推奨化

```python
# src/noveler/domain/repositories/episode_repository.py
import warnings
from abc import ABC, abstractmethod

class EpisodeRepository(ABC):  # 非推奨
    def __init_subclass__(cls) -> None:
        warnings.warn(
            f"{cls.__name__} inherits deprecated EpisodeRepository ABC. "
            "Use EpisodeRepositoryProtocol instead.",
            DeprecationWarning,
            stacklevel=2
        )
        super().__init_subclass__()

    @abstractmethod
    def find_by_id(self, episode_id: int) -> Episode | None:
        pass
```

#### 5.2 既存実装クラスの確認

```bash
# 既存実装クラスを検索
grep -r "class.*EpisodeRepository" src/noveler/infrastructure/

# 出力例
# src/noveler/infrastructure/repositories/episode_repository_impl.py:
# class EpisodeRepositoryImpl(EpisodeRepository):
```

#### 5.3 実装クラスのimport更新

```python
# Before
from noveler.domain.repositories.episode_repository import EpisodeRepository

class EpisodeRepositoryImpl(EpisodeRepository):
    def find_by_id(self, episode_id: int) -> Episode | None:
        ...

# After
from noveler.domain.protocols.episode_repository_protocol import EpisodeRepositoryProtocol

class EpisodeRepositoryImpl:  # ABC継承を削除
    def find_by_id(self, episode_id: int) -> Episode | None:
        ...

# 型チェック時にEpisodeRepositoryProtocolを満たすかどうかを自動検証
repo: EpisodeRepositoryProtocol = EpisodeRepositoryImpl()
```

### ステップ6: テストの更新と検証

#### 6.1 テストファイルの確認

```bash
# テストファイルを検索
find tests -name "test_episode_repository*" -type f

# 出力例
# tests/unit/domain/repositories/test_episode_repository.py
# tests/unit/infrastructure/repositories/test_episode_repository_impl.py
```

#### 6.2 テストコードの更新

詳細は[テスト更新方法](#テスト更新方法)セクションを参照。

#### 6.3 テスト実行

```bash
# Episode系テストを実行
pytest tests/unit/domain/repositories/test_episode_repository.py -xvs

# 期待される結果
# PASSED tests/unit/domain/repositories/test_episode_repository.py::Test... [100%]
```

### ステップ7: コード品質チェック

#### 7.1 mypy strict mode検証

```bash
# Episode系ファイルをmypy strict modeで検証
mypy --strict src/noveler/domain/protocols/episode_repository_protocol.py

# 期待される結果
# Success: no issues found in 1 source file
```

#### 7.2 importlinter検証

```bash
# importlinter契約を検証
import-linter check

# 期待される結果
# All contracts satisfied!
```

#### 7.3 ruff検証

```bash
# ruff lintを実行
ruff check src/noveler/domain/protocols/

# エラーがないことを確認
```

### ステップ8: git commit

#### 8.1 変更をステージング

```bash
# Episode系ファイルの変更をステージ
git add src/noveler/domain/protocols/episode_repository_protocol.py
git add src/noveler/domain/repositories/episode_repository.py
git add src/noveler/infrastructure/repositories/episode_repository_impl.py
git add tests/unit/domain/repositories/test_episode_repository.py
git add tests/unit/infrastructure/repositories/test_episode_repository_impl.py
```

#### 8.2 コミット

```bash
# コミットメッセージの例
git commit -m "refactor: Migrate EpisodeRepository from ABC to Protocol

- Create EpisodeRepositoryProtocol in domain/protocols/
- Mark EpisodeRepository ABC as deprecated
- Update EpisodeRepositoryImpl to use Protocol
- Update tests to use Protocol
- Verify with mypy strict mode and importlinter"
```

---

## 具体的な移行例

### Example 1: シンプルなRepository移行

#### Before

```python
# src/noveler/domain/repositories/episode_repository.py
from abc import ABC, abstractmethod
from typing import Optional
from noveler.domain.entities.episode import Episode

class EpisodeRepository(ABC):
    """エピソードリポジトリの抽象基底クラス"""

    @abstractmethod
    def find_by_id(self, episode_id: int) -> Optional[Episode]:
        """エピソードをIDで検索"""
        pass

    @abstractmethod
    def save(self, episode: Episode) -> None:
        """エピソードを保存"""
        pass
```

#### After

```python
# src/noveler/domain/protocols/episode_repository_protocol.py
from typing import Protocol
from noveler.domain.entities.episode import Episode

class EpisodeRepositoryProtocol(Protocol):
    """エピソードリポジトリのプロトコル

    エピソード管理に必要な基本操作を定義。
    構造的部分型により、任意のリポジトリ実装が対応可能。
    """

    def find_by_id(self, episode_id: int) -> Episode | None:
        """エピソードをIDで検索

        Args:
            episode_id: エピソードID

        Returns:
            見つかったEpisodeオブジェクト、未検出時はNone
        """
        ...

    def save(self, episode: Episode) -> None:
        """エピソードを保存

        Args:
            episode: 保存するEpisodeオブジェクト
        """
        ...

# 旧ABCは非推奨警告付きで維持（移行期間）
from abc import ABC, abstractmethod
import warnings

class EpisodeRepository(ABC):
    """非推奨: EpisodeRepositoryProtocolを使用してください"""

    def __init_subclass__(cls) -> None:
        warnings.warn(
            f"{cls.__name__} uses deprecated EpisodeRepository ABC. "
            "Use EpisodeRepositoryProtocol instead.",
            DeprecationWarning,
            stacklevel=2
        )
        super().__init_subclass__()

    @abstractmethod
    def find_by_id(self, episode_id: int) -> Episode | None:
        pass

    @abstractmethod
    def save(self, episode: Episode) -> None:
        pass
```

#### 実装クラス

```python
# src/noveler/infrastructure/repositories/episode_repository_impl.py

# Before: ABC継承
class EpisodeRepositoryImpl(EpisodeRepository):
    def find_by_id(self, episode_id: int) -> Episode | None:
        ...

# After: Protocol使用（継承不要）
from noveler.domain.protocols.episode_repository_protocol import EpisodeRepositoryProtocol

class EpisodeRepositoryImpl:
    def find_by_id(self, episode_id: int) -> Episode | None:
        ...

# 型チェック時にProtocolを満たすことを確認
repo: EpisodeRepositoryProtocol = EpisodeRepositoryImpl()
```

### Example 2: Generic Protocolを使用した移行

#### Before

```python
# src/noveler/domain/repositories/repository.py
from abc import ABC, abstractmethod
from typing import Generic, TypeVar, Optional, List

T = TypeVar('T')

class GenericRepository(ABC, Generic[T]):
    """汎用リポジトリの抽象基底クラス"""

    @abstractmethod
    def find_by_id(self, id: int) -> Optional[T]:
        pass

    @abstractmethod
    def save(self, entity: T) -> None:
        pass

    @abstractmethod
    def find_all(self) -> List[T]:
        pass
```

#### After

```python
# src/noveler/domain/protocols/repository_protocol.py
from typing import Protocol, TypeVar, Generic, List

T = TypeVar('T')
ID = TypeVar('ID')

class Repository(Protocol, Generic[T, ID]):
    """汎用リポジトリプロトコル

    任意のエンティティ型Tに対して、
    基本的なCRUD操作を提供するプロトコル。
    """

    def find_by_id(self, id: ID) -> T | None:
        """IDで検索"""
        ...

    def save(self, entity: T) -> None:
        """保存"""
        ...

    def find_all(self) -> list[T]:
        """全件取得"""
        ...

# 旧ABCは非推奨化
from abc import ABC, abstractmethod
import warnings

class GenericRepository(ABC, Generic[T]):
    """非推奨: Repositoryプロトコルを使用してください"""

    def __init_subclass__(cls) -> None:
        warnings.warn(
            "GenericRepository ABC is deprecated. Use Repository protocol.",
            DeprecationWarning,
            stacklevel=2
        )
        super().__init_subclass__()

    @abstractmethod
    def find_by_id(self, id: int) -> T | None:
        pass

    @abstractmethod
    def save(self, entity: T) -> None:
        pass

    @abstractmethod
    def find_all(self) -> list[T]:
        pass
```

---

## テスト更新方法

### 変更前のテストコード

```python
# ❌ ABC継承を前提としたテスト
from unittest.mock import MagicMock
from noveler.domain.repositories.episode_repository import EpisodeRepository
from noveler.domain.entities.episode import Episode

def test_episode_service():
    # ABC継承が必須なため、複雑なセットアップ
    class MockEpisodeRepository(EpisodeRepository):  # 継承強制
        def find_by_id(self, episode_id: int) -> Episode | None:
            return Episode(id=1, title="Test")

        def save(self, episode: Episode) -> None:
            pass

    repo = MockEpisodeRepository()
    # ... テスト続行
```

### 変更後のテストコード

```python
# ✅ Protocol使用で簡単なモック作成
from noveler.domain.protocols.episode_repository_protocol import EpisodeRepositoryProtocol
from noveler.domain.entities.episode import Episode

def test_episode_service():
    # Protocol使用で継承不要、シンプルなモック作成
    class MockEpisodeRepository:  # 継承不要！
        def find_by_id(self, episode_id: int) -> Episode | None:
            return Episode(id=1, title="Test")

        def save(self, episode: Episode) -> None:
            pass

    repo: EpisodeRepositoryProtocol = MockEpisodeRepository()
    # ... テスト続行
```

### テスト実行コマンド

```bash
# 単一テストファイルを実行
pytest tests/unit/domain/repositories/test_episode_repository.py -xvs

# テストスイート全体を実行
pytest tests/ -q

# 特定のテストだけを実行
pytest tests/unit/domain/repositories/test_episode_repository.py::TestEpisodeRepository::test_find_by_id -xvs
```

---

## トラブルシューティング

### エラー1: `ImportError: cannot import name 'EpisodeRepository'`

**原因**: 旧ABCファイルを削除したが、既存コードがまだ参照している

**解決方法**:
```bash
# 参照箇所を検索
grep -r "from noveler.domain.repositories.episode_repository import" src/

# importを更新
# Before: from noveler.domain.repositories.episode_repository import EpisodeRepository
# After: from noveler.domain.protocols.episode_repository_protocol import EpisodeRepositoryProtocol
```

### エラー2: `mypy error: Name 'EpisodeRepository' is not defined`

**原因**: mypy strict modeで型アノテーションが不完全

**解決方法**:
```python
# 型アノテーションを完全にする
# Before
def __init__(self, repo):
    self.repo = repo

# After
from noveler.domain.protocols.episode_repository_protocol import EpisodeRepositoryProtocol

def __init__(self, repo: EpisodeRepositoryProtocol) -> None:
    self.repo = repo
```

### エラー3: `pytest error: AssertionError in test_...`

**原因**: テストが古いABC定義を前提としている

**解決方法**:
```bash
# テストコードのimportを確認
grep -n "from.*EpisodeRepository" tests/

# protocolベースに更新
# Before: from noveler.domain.repositories.episode_repository import EpisodeRepository
# After: from noveler.domain.protocols.episode_repository_protocol import EpisodeRepositoryProtocol
```

### エラー4: `DeprecationWarning: ... uses deprecated ... ABC`

**原因**: 旧ABCを継承しているコードが残っている

**解決方法**: これは警告なので、該当コードを新Protocolに移行してください。
```bash
# 警告を発生させているファイルを特定
grep -r "class.*EpisodeRepository\)" src/

# Protocolベースに更新
```

---

## ロールバック手順

万が一、移行に失敗した場合の対応手順：

### 方法1: Git revertでロールバック

```bash
# 最新のコミットを打ち消す
git revert HEAD

# または、特定のコミットまで戻す
git reset --hard <commit-hash>
```

### 方法2: ファイル単位でのロールバック

```bash
# 特定ファイルを旧版に戻す
git checkout HEAD~1 src/noveler/domain/protocols/episode_repository_protocol.py

# 変更をステージング
git add src/noveler/domain/protocols/episode_repository_protocol.py

# コミット
git commit -m "revert: Rollback Protocol migration for episode_repository"
```

### テストで検証

```bash
# ロールバック後、テストが成功することを確認
pytest tests/ -q

# 期待される結果
# PASSED ... [98.9%]
```

---

## Phase別の進捗確認

### Phase 3: Repository層移行の進捗確認

```bash
# 移行完了したRepository Protocol数を確認
ls -la src/noveler/domain/protocols/*_repository_protocol.py | wc -l

# 期待される結果（各Phase完了時点）
# Phase 3-A: 15ファイル（Episode/Plot/Quality系）
# Phase 3-B: 20ファイル（追加）
# Phase 3-C: 30+ファイル（その他）
```

### 全体進捗の確認

```bash
# ABC使用ファイル数を確認
find src/noveler/domain -name "*.py" -exec grep -l "from abc import ABC" {} \; | wc -l

# 期待される削減
# 開始前: 104ファイル
# Phase 3完了後: 60-70ファイル
# Phase 4完了後: 30ファイル以下
```

---

## 次ステップ

このガイドの各ステップに従うことで、安全にABCからProtocolへの移行が実現されます。

**質問やトラブルが発生した場合**:
1. [トラブルシューティング](#トラブルシューティング)セクションを確認
2. `reports/protocol_migration_progress.md`で既知の問題を確認
3. チーム内で相談（ペアプログラミング推奨）

