Python|Protocol型を使ったinterface設計の考え方
この記事のポイント
- Protocol型を用いたinterface設計の正しい適用場面を理解できる
- 実装と型定義の責務分離をレビューで評価できる
- 構造的サブタイピングの設計ミスを発見できる
そもそもProtocolとは
Pythonでは静的型付け拡張(PEP 544)でProtocol型が導入されています。
これはJavaやGoのinterfaceに相当する役割を担います。
from typing import Protocol
class JsonSerializable(Protocol):
def to_json(self) -> str: ...
特徴は以下です。
- 構造的サブタイピング(ダックタイピング型安全版)
- 実装側はProtocolを継承不要
- 型ヒントのみでinterfaceを表現
柔軟性と型安全を両立可能ですが、設計意図が曖昧になる失敗も多発します。
なぜこれをレビューするのか
現場で頻発する失敗例は以下です。
- なんでもProtocol化 → 汎用型肥大
- 実装詳細に踏み込みすぎたProtocol定義
- 継承型interfaceとの混在運用崩壊
- 呼び出し側での型矛盾混在
レビューアーは「Protocolの目的は呼び出し契約の明示」と理解し、設計責務を読む必要があります。
レビューアー視点
- Protocol化理由が設計意図に一致しているか
- メソッド署名だけが定義され実装詳細が漏洩していないか
- 継承型との役割分離が整理されているか
- 呼び出し側が自然に使えるAPI契約になっているか
- 汎用Protocol濫用になっていないか
開発者視点
- 呼び出し契約の表現にProtocolを活用
- 実装型・データ属性はProtocolに含めない
- 継承型との役割線引きを徹底
- 汎用Protocol定義は最小限に抑制
- Protocolはサービス境界線・抽象依存制御に限定
良い実装例
なぜこの実装が良いのか
- 呼び出し側が依存する契約責務だけ定義
- 実装側はProtocol非依存で開発可能
- DIP原則(依存関係逆転)を自然に表現
- 型安全と柔軟性のバランス良好
# protocols.py
from typing import Protocol
class Logger(Protocol):
def log(self, message: str) -> None: ...
# implementations.py
class ConsoleLogger:
def log(self, message: str) -> None:
print(message)
# services.py
def process_data(logger: Logger):
logger.log("Start processing")
# 処理本体
logger.log("End processing")
補足
Protocolは契約表現のみを責務とする。実装側はinterface意識不要という設計思想が実現できています。
レビュー観点
- Protocolは呼び出し契約のみ表現されているか
- 実装詳細を含めていないか
- 継承型interfaceと役割混在していないか
- 呼び出し側の型利用が自然か
- 過剰抽象化になっていないか
良くない実装例: ケース1(属性まで含める設計崩壊)
# bad_protocol_with_attributes.py
from typing import Protocol
class RequestLog(Protocol):
request_id: int
endpoint: str
client_ip: str
def save(self) -> None: ...
@Reviewer属性までProtocol定義に含めると実装拘束が強まり、役割分離が崩壊します。契約メソッドのみ表現してください。
問題点
- データ属性まで含めたinterface化
- 実装側が内部属性制約を受けすぎる
- DIP違反に近づく
改善例
# good_protocol_method_only.py
from typing import Protocol
class RequestLogSaver(Protocol):
def save(self) -> None: ...
Protocolは「呼び出し契約」だけ表現するが原則です。属性依存はデータモデル側で表現します。
良くない実装例: ケース2(汎用Protocol濫用)
# bad_overgeneric_protocol.py
from typing import Protocol, Any
class CallableProcessor(Protocol):
def __call__(self, *args: Any, **kwargs: Any) -> Any: ...
@ReviewerAnyによる汎用型定義は型安全を完全に放棄しています。具体契約をProtocolで表現してください。
問題点
- Any多用による型安全性崩壊
- 汎用性追求が実質ダックタイピングに逆戻り
- Protocol利用の意味消失
改善例
# good_protocol_precise.py
from typing import Protocol
class DataProcessor(Protocol):
def __call__(self, data: str) -> int: ...
汎用性より契約粒度を優先設計が原則です。レビューではAny利用有無を重点確認します。
良くない実装例: ケース3(継承型とProtocol型の混在崩壊)
# bad_protocol_inheritance_mix.py
from typing import Protocol
class BaseLogger:
def log(self, message: str) -> None:
print(message)
class FileLogger(BaseLogger, Protocol):
def rotate(self) -> None: ...
@ReviewerProtocolと通常継承型を混在させると実装設計が崩壊します。責務分離を整理してください。
問題点
- Protocolと継承型を混在継承
- 設計上の多重責務混濁
- 読み手が型の意図を理解不能化
改善例
# good_protocol_inheritance_separation.py
from typing import Protocol
class Logger(Protocol):
def log(self, message: str) -> None: ...
class Rotatable(Protocol):
def rotate(self) -> None: ...
class FileLogger:
def log(self, message: str) -> None:
print(message)
def rotate(self) -> None:
print("Rotated")
継承型は実装責務、Protocolは契約責務と完全分離して設計します。レビューではこの線引きを読む力が重要です。
良くない実装例: ケース4(Protocolのinterface肥大化)
# bad_protocol_bloating.py
from typing import Protocol
class DataManager(Protocol):
def save(self, data: str) -> None: ...
def load(self, key: str) -> str: ...
def delete(self, key: str) -> None: ...
def update(self, key: str, data: str) -> None: ...
@Reviewer単一責務から逸脱し肥大化しています。Protocolは契約単位ごとに分割設計してください。
問題点
- 多機能interface化による肥大
- SRP(単一責務原則)違反
- 実装側に過剰負担強制
改善例
# good_protocol_split.py
from typing import Protocol
class Saver(Protocol):
def save(self, data: str) -> None: ...
class Loader(Protocol):
def load(self, key: str) -> str: ...
契約単位ごとに小さく分割するのがProtocol設計の原則です。レビューでは肥大化を即座に指摘します。
観点チェックリスト
まとめ
Protocol型は「型安全なinterface表現」であり、設計者の責務整理力が如実に現れる領域です。
レビューアーは
- なぜProtocolか?
- 契約粒度は妥当か?
- 実装依存が混入していないか?
を読み取り、「呼び出し契約を綺麗に表現できているか」という一点に集中して指摘していくことが重要です。
Protocolレビューは現場設計品質の底上げ教材として極めて実戦効果が高い領域になります。