概要(「インターフェース的設計」は“共通の窓口”で差し替え可能にすること)
インターフェースは、異なる実装を“同じやり方で使えるようにする共通の約束”です。PythonにはJavaのようなinterface構文はありませんが、抽象基底クラス(ABC)、ダックタイピング、typing.Protocolで同じ概念を実現できます。目的は、多態性でコードの依存を“やり方(メソッド名・引数)”に揃え、内部の差を外へ漏らさないことです。
基本の考え方(「同じメソッド名で呼べる」ことが設計の核)
インターフェースの要点(統一窓口で多態性を成立させる)
インターフェース的設計は「複数の種類のオブジェクトを統一的に扱う」ための共通規格(メソッド群)を決めることです。たとえば“図形は面積を返せる”“通知は送れる”といった約束を形にすると、呼び手は実装を意識せず同じ手順で使えます Qiita。
Pythonでの実現手段(ABC・Protocol・ダックタイピング)
- ABCは継承で“必須メソッドの実装”を強制できます。
- Protocolは“構造的型”として、継承なしでも「そのメソッドを持っていればOK」を表現できます。
- ダックタイピングは型宣言を省き、実際に動くメソッドがあれば受け入れるPython的アプローチです。
ABCで作る“守るべき約束”(強制力のある設計図)
代替実装を安全に差し替える(共通操作を抽象化)
from abc import ABC, abstractmethod
class Notifier(ABC):
@abstractmethod
def send(self, to: str, message: str) -> None:
"""通知は必ず送れる(実装必須)"""
pass
class EmailNotifier(Notifier):
def send(self, to: str, message: str) -> None:
print(f"[EMAIL] to={to} msg={message}")
class SlackNotifier(Notifier):
def send(self, to: str, message: str) -> None:
print(f"[SLACK] to={to} msg={message}")
def alert(n: Notifier, user: str, text: str) -> None:
n.send(user, text) # 共通の窓口で呼べる
Python- “send”という共通インターフェースで束ねると、EmailでもSlackでも同じ呼び方で動きます。ABCは未実装をエラーにするため、実装漏れを防げます。
テンプレートメソッドで“流れ”を固定し差分だけ抽象化
from abc import ABC, abstractmethod
class Exporter(ABC):
def export(self, data: dict) -> str:
self._validate(data)
return self._format(data)
def _validate(self, data: dict) -> None:
if "id" not in data: raise ValueError("id必須")
@abstractmethod
def _format(self, data: dict) -> str:
pass
class JsonExporter(Exporter):
def _format(self, data: dict) -> str:
import json
return json.dumps(data, ensure_ascii=False)
Python- 共通の検証・流れは親で提供し、フォーマットだけ抽象化すると、拡張が安全かつ一貫します。
Protocolとダックタイピング(柔軟で拡張に強い“構造的”設計)
Protocolで“継承なしでもインターフェース”
from typing import Protocol
class HasArea(Protocol):
def area(self) -> float: ...
def is_same_area(a: HasArea, b: HasArea) -> bool:
return abs(a.area() - b.area()) < 1e-9
class Circle:
def __init__(self, r: float): self.r = r
def area(self) -> float: return 3.14159 * self.r * self.r
class Rect:
def __init__(self, w: float, h: float): self.w, self.h = w, h
def area(self) -> float: return self.w * self.h
print(is_same_area(Circle(1), Rect(3.14159, 1))) # Trueに近い
Python- Protocolは“このメソッドを持っていれば受け入れる”という構造的インターフェース。ライブラリ間の継承を強制せず、後付け拡張にも強いです。
ダックタイピングで“動けばOK”をシンプルに
def notify_all(notifiers, user: str, text: str) -> None:
for n in notifiers:
n.send(user, text) # sendを持っていれば動く(ダックタイピング)
Python- 実運用で軽快ですが、型チェックは弱いので、外部境界ではProtocolやABCでガードすると安心です。
インターフェース的設計の実務パターン(戦略・工場・依存逆転)
戦略パターン(アルゴリズム差し替えを窓口で統一)
from abc import ABC, abstractmethod
class PriceRule(ABC):
@abstractmethod
def calc(self, amount: float) -> float: ...
class NormalRule(PriceRule):
def calc(self, amount: float) -> float: return amount
class DiscountRule(PriceRule):
def calc(self, amount: float) -> float: return amount * 0.9
class Checkout:
def __init__(self, rule: PriceRule): self.rule = rule
def total(self, amount: float) -> float: return self.rule.calc(amount)
Python- “calc”というインターフェースでアルゴリズムを差し替え可能にすると、拡張が安全になります。
ファクトリー・メソッドで「作り方」をインターフェース化
from abc import ABC, abstractmethod
class ClientFactory(ABC):
@abstractmethod
def create(self, base_url: str) -> object: ...
class RequestsFactory(ClientFactory):
def create(self, base_url: str) -> object:
import requests
s = requests.Session(); s.headers.update({"User-Agent": "MyApp"})
return {"base_url": base_url, "session": s}
Python- 生成過程を統一の窓口にまとめると、変更が局所化されます。
依存逆転(上位はインターフェースに依存し、下位が具体化)
上位(サービス)は具体クラスではなくインターフェース(ABC/Protocol)に依存します。これで実装差し替え・テストダブルの注入が容易になり、保守性が向上します。
設計のコツ(契約の明確化・最小公開面・テスト容易性)
契約を“名前・引数・戻り値の意味”まで固定する
名前だけでなく、引数の型と意味、戻り値の構造を明記します。変更は“拡張”で行い、互換性を壊さない(追加引数にデフォルト、戻り値にフィールド追加など)方針が安全です。
公開面(インターフェース)は最小限に
中間状態や内部詳細までインターフェース化すると、依存が増えて壊れやすくなります。利用者が本当に必要な最小のメソッド集合に絞ります。
テストを先に書くと“良いインターフェース”が浮かび上がる
テストダブル(モック・スタブ)を作りやすいインターフェースは、利用者視点で無駄がない証拠です。TDDは“窓口の質”を磨く近道になります。
例題(データエクスポート機能をインターフェースで差し替え可能に)
エクスポータの窓口で形式を切り替える
from abc import ABC, abstractmethod
import json, csv, io
class Exporter(ABC):
@abstractmethod
def export(self, rows: list[dict]) -> str: ...
class JsonExporter(Exporter):
def export(self, rows: list[dict]) -> str:
return json.dumps(rows, ensure_ascii=False, indent=2)
class CsvExporter(Exporter):
def export(self, rows: list[dict]) -> str:
buf = io.StringIO()
if not rows: return ""
w = csv.DictWriter(buf, fieldnames=list(rows[0].keys()))
w.writeheader(); w.writerows(rows)
return buf.getvalue()
class ExportService:
def __init__(self, exporter: Exporter): self.exporter = exporter
def run(self, rows: list[dict]) -> str: return self.exporter.export(rows)
rows = [{"id": 1, "name": "太郎"}, {"id": 2, "name": "花子"}]
print(ExportService(JsonExporter()).run(rows))
print(ExportService(CsvExporter()).run(rows))
Python- “export”のインターフェースだけを上位へ公開。形式の追加・差し替えは下位で完結し、呼び手は変更不要です。
まとめ(Python流の“インターフェース”はABC・Protocol・ダックタイピングで形にする)
インターフェース的設計は“同じ窓口で異なる実装を扱う”ための共通約束です。PythonではABCで強制力を持たせ、Protocolで継承なしの柔軟さを確保し、ダックタイピングで軽快に運用できます。契約(名前・シグネチャ・意味)を明確にし、公開面は最小に、上位はインターフェースへ依存。これを型として徹底すれば、初心者でも“差し替えに強く、壊れにくく、テストしやすい”設計を自然に作れるようになります。
