# Pythonプロトコル設計ガイドライン

**対象プロジェクト**: noveler
**作成日**: 2025-10-24
**バージョン**: 1.0
**責任者**: Development Team

---

## 目次

1. [概要](#概要)
2. [Protocol vs ABC 使い分け基準](#protocol-vs-abc-使い分け基準)
3. [ベストプラクティス](#ベストプラクティス)
4. [アンチパターン集](#アンチパターン集)
5. [Generic Protocol活用法](#generic-protocol活用法)
6. [mypy strict mode対応](#mypy-strict-mode対応)
7. [MCP Tool Registry パターン](#mcp-tool-registry-パターン)
8. [よくある質問（FAQ）](#よくある質問faq)

---

## 概要

### 目的

本ドキュメントは、novelerプロジェクトにおける`typing.Protocol`の正しい使用方法とベストプラクティスを定義します。

Protocol設計の統一により：
- ✅ 型安全性の向上
- ✅ テスト時のモック作成簡素化
- ✅ 設計パターンの明確化
- ✅ チーム内での設計ブレを防止

### 対象読者

- **開発者**: 新しいインターフェース設計時
- **レビュアー**: コードレビュー時の判断基準
- **アーキテクト**: 全体設計時の指針

### 前提知識

本ドキュメントを理解するために必要な知識：
- Python 3.10以上
- `typing`モジュールの基本知識
- DDD（Domain-Driven Design）の基本概念
- ABC（抽象基底クラス）の基本知識

---

## Protocol vs ABC 使い分け基準

### 2.1 基本的な違い

#### Protocol（構造的部分型）
```python
from typing import Protocol

class Drawable(Protocol):
    """描画可能なオブジェクトのプロトコル"""

    def draw(self) -> None: ...

class Circle:
    """Drawableを明示的に継承せず"""
    def draw(self) -> None:
        print("Drawing circle")

class Square:
    """Drawableを明示的に継承せず"""
    def draw(self) -> None:
        print("Drawing square")

# どちらもDrawableとして扱える（ダックタイピング）
def render(drawable: Drawable) -> None:
    drawable.draw()

render(Circle())   # ✅ OK: 共通の draw() メソッドを持つため
render(Square())   # ✅ OK: 共通の draw() メソッドを持つため
```

#### ABC（名目的部分型）
```python
from abc import ABC, abstractmethod

class Drawable(ABC):
    """描画可能なオブジェクトの抽象基底クラス"""

    @abstractmethod
    def draw(self) -> None:
        pass

class Circle(Drawable):  # 明示的な継承が必須
    def draw(self) -> None:
        print("Drawing circle")

class Square(Drawable):  # 明示的な継承が必須
    def draw(self) -> None:
        print("Drawing square")

def render(drawable: Drawable) -> None:
    drawable.draw()

render(Circle())   # ✅ OK
render(Square())   # ✅ OK
```

### 2.2 使い分けフローチャート

```
新しいインターフェースが必要？
│
├─ YES ──→ 共通実装（Helper method/State）が必要？
│          │
│          ├─ YES ──→ Template Methodパターン? ──→ ABC を使用
│          │          (override可能な基本メソッドを定義)
│          │
│          └─ NO  ──→ Protocol を使用（推奨）
│
└─ NO  ──→ 既存インターフェースを拡張？
           │
           ├─ YES ──→ Protocol (構造的部分型 = 拡張性高い)
           │
           └─ NO  ──→ Protocol を使用
```

### 2.3 判定基準表

| 要件 | Protocol | ABC | 選択 |
|------|----------|-----|------|
| インターフェース定義のみ | ✅ | ❌ | **Protocol** |
| 共通実装が必要 | ❌ | ✅ | **ABC** |
| ダックタイピングを許容 | ✅ | ❌ | **Protocol** |
| Template Methodパターン | ❌ | ✅ | **ABC** |
| テストモック作成の簡易性 | ✅ (継承不要) | ❌ (継承必須) | **Protocol** |
| 依存性逆転の実現 | ✅ (構造的) | ✅ (名目的) | **Protocol**推奨 |
| DDD Domain層での使用 | ✅ | △ (限定的) | **Protocol**推奨 |

---

## ベストプラクティス

### 3.1 Protocol定義の基本形

```python
# ✅ Good: シンプルで明確なProtocol定義
from typing import Protocol

class Repository(Protocol):
    """リポジトリのインターフェース

    データストアへのアクセスを抽象化するプロトコル。
    構造的部分型により、任意のリポジトリ実装が使用可能。
    """

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

    def save(self, entity: object) -> None:
        """エンティティを保存"""
        ...
```

### 3.2 Generic Protocolの活用

```python
# ✅ Good: Generic型パラメータを使用した型安全な設計
from typing import Protocol, TypeVar, Generic

T = TypeVar('T')  # エンティティ型
ID = TypeVar('ID')  # ID型

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

    型パラメータにより、具体的なエンティティ型が明確化される。
    """

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

    def save(self, entity: T) -> None:
        """エンティティを保存"""
        ...

    def delete(self, id: ID) -> None:
        """エンティティを削除"""
        ...

# 具体的なリポジトリの型付け
class Episode:
    id: int
    title: str

class EpisodeRepository(Protocol):
    """エピソードリポジトリ（Repository[Episode, int]の具体化）"""

    def find_by_id(self, id: int) -> Episode | None: ...
    def save(self, entity: Episode) -> None: ...
    def delete(self, id: int) -> None: ...
```

### 3.3 docstring の書き方

```python
# ✅ Good: 詳細なdocstringで意図を明確化
from typing import Protocol

class DataLoader(Protocol):
    """データローダーのプロトコル

    ファイルやデータベースからデータを読み込むための
    インターフェースを定義。

    実装者は以下のメソッドを提供する必要があります：
    - load(): データを読み込み、結果を返す
    - validate(): 読み込んだデータを検証する

    用途例：
        ファイルローダーやDBローダー、キャッシュローダーなど、
        データソースの異なる複数の実装が可能。

    Note:
        このプロトコルは構造的部分型なため、
        明示的な継承宣言を不要とします。
    """

    def load(self) -> list[dict]:
        """データを読み込む

        Returns:
            読み込んだデータのリスト

        Raises:
            FileNotFoundError: ファイルが見つからない場合
            ValueError: データが無効な形式の場合
        """
        ...

    def validate(self) -> bool:
        """読み込んだデータを検証する

        Returns:
            検証結果（True=有効、False=無効）
        """
        ...
```

### 3.4 実装クラスでの使用

```python
# ✅ Good: Protocol型アノテーションで型安全性を確保
from pathlib import Path
from typing import Protocol

class DataProcessor:
    """データ処理クラス"""

    def __init__(self, loader: DataLoader) -> None:
        """初期化

        Args:
            loader: DataLoaderプロトコルを満たすローダー
                    実装クラスの種類は問わない（構造的部分型）
        """
        self.loader = loader

    def process(self) -> list[dict]:
        """データを処理"""
        data = self.loader.load()

        if not self.loader.validate():
            raise ValueError("Data validation failed")

        return data

# テスト用の簡単なモック（継承不要）
class MockDataLoader:
    """テスト用モックローダー"""

    def load(self) -> list[dict]:
        return [{"id": 1, "name": "test"}]

    def validate(self) -> bool:
        return True

# 型チェック時にMockDataLoaderがDataLoaderProtocolを
# 満たしているかどうかを自動検証
processor = DataProcessor(MockDataLoader())
result = processor.process()
```

### 3.5 Callable型の活用

```python
# ✅ Good: 関数型をProtocolで定義
from typing import Protocol, Callable

class EventHandler(Protocol):
    """イベントハンドラーのプロトコル"""

    def handle(self, event: dict) -> None:
        """イベントを処理"""
        ...

# 関数もProtocolを満たす実装になり得る
def log_event(event: dict) -> None:
    """イベントをログに出力"""
    print(f"Event: {event}")

def send_notification(event: dict) -> None:
    """イベント通知を送信"""
    # 通知送信ロジック
    pass

class EventBus:
    def __init__(self) -> None:
        self.handlers: list[EventHandler] = []

    def subscribe(self, handler: EventHandler) -> None:
        """ハンドラーを購読"""
        self.handlers.append(handler)

    def emit(self, event: dict) -> None:
        """イベントを発行"""
        for handler in self.handlers:
            handler.handle(event)

# 関数ベースの実装
bus = EventBus()
bus.subscribe(log_event)        # ✅ OK: 関数も可
bus.subscribe(send_notification) # ✅ OK: 関数も可
```

---

## アンチパターン集

### 4.1 ❌ Protocol内での@abstractmethod使用

```python
# ❌ Bad: @abstractmethodはProtocolでは不要
from typing import Protocol
from abc import abstractmethod

class ConfigService(Protocol):
    @abstractmethod  # ← これは不要で、むしろ混乱を招く
    def load_config(self) -> dict:
        ...

# ✅ Good: Protocol定義時は...のみ
class ConfigService(Protocol):
    def load_config(self) -> dict:
        """設定を読み込む"""
        ...
```

**理由**:
- Protocolは構造的部分型なので、継承を強制しない
- `@abstractmethod`は名目的部分型（ABC）の概念
- 混合使用は開発者を混乱させる

### 4.2 ❌ Protocol内でのプロパティ実装

```python
# ❌ Bad: Protocolで実装を含めない
from typing import Protocol

class ConfigService(Protocol):
    @property
    def config(self) -> dict:
        return {"key": "value"}  # ← 実装を含めてはいけない

# ✅ Good: プロトコルは契約のみ
class ConfigService(Protocol):
    @property
    def config(self) -> dict:
        """設定ディクショナリ"""
        ...
```

**理由**: Protocol は契約（インターフェース）を定義するもの

### 4.3 ❌ 不適切な@runtime_checkable使用

```python
# ❌ Bad: すべてのProtocolに@runtime_checkableを付ける
from typing import Protocol, runtime_checkable

@runtime_checkable
class DataProcessor(Protocol):
    def process(self, data: str) -> str: ...

# ✅ Good: ランタイム型チェックが必要な場合のみ使用
@runtime_checkable
class Serializable(Protocol):
    """JSON化可能なオブジェクトのマーカー"""
    def to_json(self) -> str: ...

# 一般的なProtocolは@runtime_checkableなしで良い
class Repository(Protocol):
    def find_by_id(self, id: int) -> object | None: ...
```

**理由**:
- `@runtime_checkable`はランタイムオーバーヘッドを増加させる
- コンパイル時型チェック（mypy）で十分なケースが多い
- ダイナミックディスパッチ時のみ必要

### 4.4 ❌ Protocol内での複雑なロジック

```python
# ❌ Bad: Protocol定義時に複雑なロジックを含める
from typing import Protocol

class DataValidator(Protocol):
    def validate(self, data: dict) -> bool:
        # 複雑な検証ロジック
        if "id" not in data:
            return False
        if not isinstance(data["id"], int):
            return False
        return True  # ← 実装をProtocolに含めてはいけない

# ✅ Good: Protocolは契約のみ、実装はクラスで
class DataValidator(Protocol):
    """データバリデーターのプロトコル"""

    def validate(self, data: dict) -> bool:
        """データを検証"""
        ...

class StrictDataValidator:
    """厳密なバリデーター実装"""

    def validate(self, data: dict) -> bool:
        if "id" not in data:
            return False
        if not isinstance(data["id"], int):
            return False
        return True
```

### 4.5 ❌ インターフェース重複定義

```python
# ❌ Bad: 同じインターフェースが複数ファイルで定義される
# src/noveler/domain/interfaces/logger.py
class ILogger(Protocol):
    def debug(self, msg: str) -> None: ...

# src/noveler/domain/interfaces/logger_service.py
class ILoggerService(Protocol):  # ← ILoggerと同じ定義
    def debug(self, msg: str) -> None: ...

# ✅ Good: 統一された定義を使用
# src/noveler/domain/protocols/infrastructure_protocol.py
class LoggerService(Protocol):
    """統合ログサービスプロトコル"""
    def debug(self, msg: str) -> None: ...
    def info(self, msg: str) -> None: ...
    def warning(self, msg: str) -> None: ...
    def error(self, msg: str) -> None: ...

# 全レイヤーで同一のLoggerServiceを使用
```

---

## Generic Protocol活用法

### 5.1 型変動（Variance）の理解

```python
# ✅ Good: 型変動を正しく使い分ける
from typing import Protocol, TypeVar

# 共変（Covariant）: 読み出し専用
T_co = TypeVar('T_co', covariant=True)

class Producer(Protocol, Generic[T_co]):
    """生産者プロトコル（読み出し）"""
    def produce(self) -> T_co: ...

# 反変（Contravariant）: 書き込み専用
T_contra = TypeVar('T_contra', contravariant=True)

class Consumer(Protocol, Generic[T_contra]):
    """消費者プロトコル（書き込み）"""
    def consume(self, item: T_contra) -> None: ...

# 不変（Invariant）: 読み書き両方
T = TypeVar('T')

class Container(Protocol, Generic[T]):
    """コンテナプロトコル（読み書き）"""
    def get(self) -> T: ...
    def put(self, item: T) -> None: ...
```

### 5.2 複数の型パラメータ

```python
# ✅ Good: 複数の型パラメータで柔軟性を確保
from typing import Protocol, TypeVar, Generic

K = TypeVar('K')  # キー型
V = TypeVar('V')  # 値型

class KeyValueStore(Protocol, Generic[K, V]):
    """キーバリューストアプロトコル"""

    def get(self, key: K) -> V | None:
        """キーに対応する値を取得"""
        ...

    def set(self, key: K, value: V) -> None:
        """キーに値を設定"""
        ...

    def delete(self, key: K) -> None:
        """キーを削除"""
        ...

# 具体的な使用例
class ConfigStore(Protocol):
    """設定ストア（KeyValueStore[str, Any]の具体化）"""

    def get(self, key: str) -> object | None: ...
    def set(self, key: str, value: object) -> None: ...
    def delete(self, key: str) -> None: ...
```

### 5.3 Bound型パラメータ

```python
# ✅ Good: Boundで型パラメータを制限
from typing import Protocol, TypeVar

# Entity型以上のクラスのみ許可
class Entity:
    id: int

E = TypeVar('E', bound=Entity)

class EntityRepository(Protocol, Generic[E]):
    """エンティティリポジトリプロトコル（Entity以上の型のみ受け入れ）"""

    def find_by_id(self, id: int) -> E | None:
        """IDでエンティティを検索"""
        ...

    def save(self, entity: E) -> None:
        """エンティティを保存"""
        ...

# 使用例
class Episode(Entity):
    title: str

class EpisodeRepository(Protocol):
    """エピソードリポジトリ"""
    def find_by_id(self, id: int) -> Episode | None: ...
    def save(self, entity: Episode) -> None: ...
```

---

## mypy strict mode対応

### 6.1 mypy strict modeの設定

```toml
# pyproject.toml
[tool.mypy]
python_version = "3.10"
strict = true  # 厳密モード有効化

# Protocol関連の設定
disallow_any_generics = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
```

### 6.2 Protocol設計時のチェックリスト

```python
# ✅ Good: mypy strictで検証済みのProtocol

from typing import Protocol, TypeVar, Generic
from pathlib import Path

T = TypeVar('T')

class FileRepository(Protocol, Generic[T]):
    """ファイルリポジトリプロトコル

    mypy strict modeで以下の要件を満たします：
    - 全メソッドの型注釈が完全
    - 戻り値の型が明示的
    - 例外がdocstringで明記
    """

    def load(self, path: Path) -> T:
        """ファイルからオブジェクトを読み込む

        Args:
            path: ファイルパス

        Returns:
            読み込んだオブジェクト

        Raises:
            FileNotFoundError: ファイルが見つからない場合
            ValueError: ファイル形式が無効な場合
        """
        ...

    def save(self, obj: T, path: Path) -> None:
        """オブジェクトをファイルに保存

        Args:
            obj: 保存するオブジェクト
            path: 保存先パス

        Raises:
            IOError: ファイル書き込みエラーの場合
        """
        ...
```

### 6.3 mypy strict modeでのエラー回避

```python
# ❌ Bad: mypy strictエラーが出る
from typing import Protocol

class Processor(Protocol):
    def process(self, data):  # ← 型注釈がない（strict error）
        ...  # ← 戻り値の型がない（strict error）

# ✅ Good: 完全な型注釈で対応
from typing import Protocol

class Processor(Protocol):
    def process(self, data: dict) -> dict:  # ← 完全な型注釈
        """データを処理して返す

        Args:
            data: 処理対象データ

        Returns:
            処理済みデータ
        """
        ...
```

---

## MCP Tool Registry パターン

### 7.1 パターンの概要

MCP Tool Registry パターンは、FastMCP サーバーにおけるツール登録を型安全かつ疎結合に行うためのパターンです。Protocolを用いた構造的部分型により、サーバー実装との依存性を最小化します。

**利点:**
- ✅ サーバー実装からツール登録ロジックを分離
- ✅ テスト時のモック作成が容易（継承不要）
- ✅ ツール登録関数の再利用性が高い
- ✅ 複数のサーバー実装で同一の登録ロジック適用可能

### 7.2 コンテキストProtocolの設計

```python
# File: src/noveler/infrastructure/json/mcp/servers/tool_registry.py
# Purpose: Registry pattern with Protocol-based loose coupling
# Context: FastMCP-like servers use this to register conversion/validation tools

from __future__ import annotations
from pathlib import Path
from typing import Any, Protocol

class _FormatterCtx(Protocol):
    """Minimal context required for tool registration.

    Purpose:
        Allow the registry to call back into the host server for conversion,
        logging, and formatting without tight coupling to a specific class.

    Attributes:
        converter: Object exposing `convert(payload: dict) -> dict | None`.
        logger: Logger exposing `.exception(msg)` and `.info(msg, *args)`.
        output_dir: Path root for artefact operations.
        _format_json_result: Callable for pretty-printing conversion results.

    Side Effects:
        None.
    """

    converter: Any
    logger: Any
    output_dir: Path

    def _format_json_result(self, result: dict[str, Any]) -> str:  # noqa: D401
        """Format a JSON conversion result for textual display.

        Purpose:
            Provide a consistent textual representation of conversion output.

        Args:
            result (dict[str, Any]): Conversion result payload.

        Returns:
            str: Formatted lines.

        Side Effects:
            None.
        """
```

**ポイント:**
- Protocolは「サーバーが何を提供すべきか」を定義（構造的契約）
- 具体的なクラス（JSON Conversion Server等）への依存性なし
- テスト時にシンプルなモック実装が可能

### 7.3 レジストリ関数の実装

```python
def register_utility_tools(server: Any, ctx: _FormatterCtx) -> None:
    """Register conversion/validation/file-info tools on the server.

    Purpose:
        Centralise low-level tool registration to keep server classes thin.

    Args:
        server (Any): FastMCP-like server exposing a `.tool(...)` decorator.
        ctx (_FormatterCtx): Host providing converter/logger/output_dir.

    Returns:
        None

    Side Effects:
        Binds decorated functions to the provided server instance.
    """

    @server.tool(
        name="convert_cli_to_json",
        description="CLI実行結果をJSON形式に変換し、95%トークン削減とファイル参照アーキテクチャを適用",
    )
    def convert_cli_to_json(cli_result: dict[str, Any]) -> str:
        """Convert the CLI payload into a compressed JSON response.

        Purpose:
            Transform CLI outputs to JSON and summarise results.

        Args:
            cli_result (dict[str, Any]): Raw CLI result mapping.

        Returns:
            str: Human-readable summary with formatted JSON result.

        Side Effects:
            Writes artefacts through the converter when configured.
        """
        try:
            if not cli_result:
                return "エラー: cli_resultパラメータが必要です"

            json_result = ctx.converter.convert(cli_result)
            return f"変換成功:\n{ctx._format_json_result(json_result)}"
        except Exception as e:  # pragma: no cover
            ctx.logger.exception("CLI→JSON変換エラー")
            return f"変換エラー: {e!s}"
```

**ポイント:**
- `ctx` が `_FormatterCtx` Protocol を満たす限り、どんなクラスでも動作
- サーバー実装の詳細を知らなくても登録ロジック実装可能
- decorator ベースで複数ツール登録を効率化

### 7.4 テスト時のモック実装

```python
# tests/unit/infrastructure/json/mcp/test_tool_registry.py

import pytest
from unittest.mock import Mock, MagicMock
from pathlib import Path
from typing import Any

class MockFormatter:
    """テスト用フォーマッターモック（_FormatterCtxを満たす）"""

    def __init__(self) -> None:
        self.converter = Mock()
        self.logger = Mock()
        self.output_dir = Path("/tmp/test")
        self.format_calls = []

    def _format_json_result(self, result: dict[str, Any]) -> str:
        """モック用フォーマッター"""
        self.format_calls.append(result)
        return "formatted_output"

@pytest.fixture
def mock_ctx() -> MockFormatter:
    """_FormatterCtxプロトコルを満たす簡易モック"""
    return MockFormatter()

def test_convert_cli_to_json(mock_ctx: MockFormatter) -> None:
    """register_utility_toolsの動作確認"""
    # サーバーモック
    server_mock = MagicMock()
    tools_dict = {}

    def tool_decorator(**kwargs: Any) -> Any:
        def wrapper(func: Any) -> Any:
            tools_dict[kwargs["name"]] = func
            return func
        return wrapper

    server_mock.tool = tool_decorator

    # 登録実行
    from mcp_servers.noveler.server.tool_registry import register_utility_tools
    register_utility_tools(server_mock, mock_ctx)

    # ツール呼び出し
    convert_tool = tools_dict["convert_cli_to_json"]
    mock_ctx.converter.convert.return_value = {"status": "ok"}

    result = convert_tool({"some": "data"})

    assert "変換成功" in result
    assert mock_ctx.converter.convert.called
```

**ポイント:**
- Protocolを満たすシンプルなモックで十分
- 継承や複雑な初期化が不要
- テストがシンプルで読みやすい

### 7.5 複数サーバー実装での再利用

```python
# 異なるサーバー実装でも同じ登録ロジック再利用

class JSONConversionServer:
    """JSON変換特化サーバー"""
    def __init__(self) -> None:
        self.converter = CliToJsonConverter()
        self.logger = get_logger("json_server")
        self.output_dir = Path("./outputs/json")

    def _format_json_result(self, result: dict[str, Any]) -> str:
        return json.dumps(result, indent=2, ensure_ascii=False)

    def setup_tools(self, server: Any) -> None:
        """サーバーセットアップ"""
        # 同じ登録関数を再利用
        from mcp_servers.noveler.server.tool_registry import register_utility_tools
        register_utility_tools(server, self)  # selfがProtocolを満たす

class AnotherMCPServer:
    """別のMCP実装"""
    def __init__(self) -> None:
        self.converter = CliToJsonConverter()
        self.logger = get_logger("other_server")
        self.output_dir = Path("./outputs/other")

    def _format_json_result(self, result: dict[str, Any]) -> str:
        return str(result)  # 異なるフォーマッタ実装

    def setup_tools(self, server: Any) -> None:
        """同じ登録関数でセットアップ可能"""
        register_utility_tools(server, self)  # 異なる実装でも使用可
```

**ポイント:**
- Protocol により、複数の異なるサーバー実装で同一の登録関数が再利用可能
- 各サーバーの詳細実装（フォーマッタの違いなど）に依存しない

### 7.6 実装チェックリスト

Protocol ベースの Registry パターン設計時：

- [ ] コンテキスト Protocol が「何を提供するか」を明確に定義
- [ ] レジストリ関数がコンテキスト Protocol のみに依存
- [ ] サーバー実装クラスへの import 不要
- [ ] テスト用モックが Protocol を満たす構造を実装
- [ ] 複数サーバー実装での再利用可能性を確認
- [ ] docstring で Purpose/Args/Side Effects を完全記載
- [ ] mypy strict mode でエラーなし

---

## よくある質問（FAQ）

### Q1. Protocol内で...（エリプシス）を使う理由は？

**A.** Protocolは「何をするか」（契約）を定義し、「どうするか」（実装）は定義しません。`...`（エリプシス）はその意図を表現します。

```python
class Service(Protocol):
    def execute(self, data: dict) -> str:
        ...  # 実装はここには書かない
        # 子クラスで実装される
```

### Q2. Protocol継承は可能ですか？

**A.** はい、Protocolはもう1つのProtocolを継承できます。

```python
class ReadableRepository(Protocol):
    def find_by_id(self, id: int) -> object | None: ...

class WritableRepository(Protocol):
    def save(self, entity: object) -> None: ...

class FullRepository(ReadableRepository, WritableRepository):
    """読み書き両対応リポジトリ"""
    ...
```

### Q3. Protocol実装とABC実装を混在させても大丈夫ですか？

**A.** 可能ですが、推奨されません。統一基準を設けて、新規コードはProtocol中心にしてください。移行期間は非推奨警告を付けて、段階的に移行します。

```python
# 移行期間の対応
import warnings

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

### Q4. RuntimeErrorが出た場合はどうすればいいですか？

**A.** 通常、Protocolは静的型チェック（mypy）で十分です。ランタイムエラーが出る場合は、`@runtime_checkable`の使用を見直してください。

```python
# ❌ @runtime_checkableなしではInstanceCheck不可
isinstance(obj, MyProtocol)  # RuntimeError

# ✅ @runtime_checkableで対応
@runtime_checkable
class MyProtocol(Protocol):
    ...

isinstance(obj, MyProtocol)  # OK
```

### Q5. Protocolとダックタイピングの関係は？

**A.** Protocolは「ダックタイピング」を型チェック（mypy）で検証する仕組みです。

```python
# ダックタイピング（従来）
def render(obj):  # 型情報なし
    obj.draw()    # 「draw()メソッドがあれば良い」という暗黙的な契約

# Protocol（型安全なダックタイピング）
class Drawable(Protocol):
    def draw(self) -> None: ...

def render(obj: Drawable) -> None:  # 型情報あり
    obj.draw()  # 契約が明示的で、myPyが検証
```

---

## チェックリスト：Protocol設計の確認項目

新しいProtocolを定義する際は、以下のチェックリストを参照してください。

- [ ] Protocol定義のみで、実装を含まない
- [ ] `@abstractmethod`デコレータを使用していない
- [ ] 全メソッドの引数・戻り値に型注釈がある
- [ ] docstringで目的・用途・例外を明記している
- [ ] Generic型パラメータ（必要に応じて）を使用している
- [ ] `@runtime_checkable`が本当に必要か確認した
- [ ] mypy strict modeでエラーが出ない
- [ ] 既存Protocol/ABCとの重複がない
- [ ] 実装側がProtocolを満たすことを確認している
- [ ] テストでProtocolを型アノテーションとして使用している

---

## 関連資料

- **PEP 544**: https://peps.python.org/pep-0544/
- **Python公式ドキュメント**: https://docs.python.org/3/library/typing.html#typing.Protocol
- **プロジェクト内参考**:
  - `reports/protocol_redesign_plan_2025_10_24.md`
  - `ARCHITECTURE.md`
  - `AGENTS.md`

