概要(依存性注入は「必要な相手を外から渡す」だけでコードが劇的に扱いやすくなる)
依存性注入(DI)は、クラスが必要とする相手(例:メール送信、DB接続、HTTPクライアント)を自分の中で作らず、外から渡してもらう設計です。これだけで結合が弱まり、差し替え・テスト・拡張が一気に楽になります。初心者は「コンストラクタで渡す」「インターフェースで窓口を固定」「テストではモックを注ぐ」という3点をまず身につけましょう。
何が問題で、DIでどう解くか(密結合→疎結合へ)
問題のある設計(自分で相手を作る)
class NotificationService:
def __init__(self):
self.sender = GmailSender() # ← ここで型が固定
def send(self, to: str, message: str) -> None:
self.sender.send_email(to, "お知らせ", message)
Python自分でGmailSenderを決め打ちすると、YahooSenderに差し替えたい時も本体を改修しなければなりません。テストでネットワークを避けることも困難です。
DIで解決(外から渡す)
class NotificationService:
def __init__(self, sender):
self.sender = sender # 何を使うかは外が決める
def send(self, to: str, message: str) -> None:
self.sender.send_email(to, "お知らせ", message)
# 本番
service = NotificationService(GmailSender())
# テスト
service_test = NotificationService(FakeSender()) # モックに差し替え
Pythonこれで「どう送るか」は注入対象に委ねられ、利用側は同じ手順で使えます。差し替えとテストの自由度が飛躍的に向上します。
基本パターン(コンストラクタ注入・メソッド注入・デフォルト差し替え)
コンストラクタ注入(最も標準的で安全)
class ApiClient:
def __init__(self, session, base_url: str, timeout: float = 5.0):
self.session = session # 依存(requests.Session相当)
self.base_url = base_url
self.timeout = timeout
def get_user(self, uid: str) -> dict:
r = self.session.get(f"{self.base_url}/users/{uid}", timeout=self.timeout)
r.raise_for_status()
data = r.json()
return {"id": str(data.get("id", "")), "name": data.get("name") or "unknown"}
Python必須の相手をコンストラクタで受け取ります。所有関係が明確で、テスト時にモックを渡しやすい形です。
メソッド注入(単発の相手を都度渡す)
class ExportService:
def run(self, rows: list[dict], exporter) -> str:
return exporter.export(rows) # 毎回異なる挙動に差し替え可
Python一回だけ使う戦略の切替に適します。長期的な所有が不要な場面で有効です。
デフォルト実装+差し替え可能(初心者にも運用しやすい)
class Logger:
def __init__(self, writer=None):
self.writer = writer or PrintWriter() # デフォルト
def log(self, msg: str) -> None:
self.writer.write(msg)
# 差し替え例
Logger(writer=FileWriter("app.log"))
Python「何も渡さなければ標準の挙動」「必要なら注いで差し替え」ができ、移行コストが低いのが利点です。
インターフェースで窓口を固定する(ABC・Protocolで多態性を安全に)
ABCで“必須メソッド”を強制
from abc import ABC, abstractmethod
class Sender(ABC):
@abstractmethod
def send_email(self, to: str, subject: str, body: str) -> None:
pass
class GmailSender(Sender):
def send_email(self, to, subject, body): ...
class FakeSender(Sender):
def send_email(self, to, subject, body):
print(f"[FAKE] to={to} subject={subject} body={body}")
class NotificationService:
def __init__(self, sender: Sender):
self.sender = sender
def send(self, to: str, message: str) -> None:
self.sender.send_email(to, "お知らせ", message)
Python未実装を避けて「同じ名前・同じ意味」で呼べる窓口を守れます。
Protocolで“継承なしでも受け入れる”
from typing import Protocol
class EmailSender(Protocol):
def send_email(self, to: str, subject: str, body: str) -> None: ...
def notify(sender: EmailSender, to: str, msg: str):
sender.send_email(to, "お知らせ", msg)
Pythonサードパーティや既存クラスをそのまま受け入れやすい柔軟なインターフェースです。
実務例(通知サービス・APIクライアント・リポジトリ)
通知サービスの差し替え(メール→Slack→SMS)
class SlackSender:
def send_email(self, to, subject, body):
print(f"[SLACK] to={to} title={subject} text={body}") # 仮: 同じ窓口名に合わせる
service = NotificationService(SlackSender())
service.send("user123", "こんにちは")
Python窓口を“send_email”で揃えれば、内部は何で送っても同じ手順で扱えます。
APIクライアントのセッション注入
import requests
real = requests.Session()
real.headers.update({"Authorization": "Bearer TOKEN"})
fake = type("FakeSession", (), {"get": lambda self, url, timeout: FakeResponse(url)})
client_real = ApiClient(real, "https://api.example.com")
client_fake = ApiClient(fake(), "https://api.example.com")
Pythonネットワークを触らずにテストでき、実運用では本物のセッションに切り替えるだけです。
リポジトリ(DB/メモリ)の切替
class UserRepo:
def get(self, uid: str) -> dict: ...
class MemoryUserRepo(UserRepo):
def __init__(self): self.store = {}
def get(self, uid): return self.store.get(uid, {"id": uid, "name": "unknown"})
class Service:
def __init__(self, repo: UserRepo): self.repo = repo
def profile(self, uid: str) -> dict: return self.repo.get(uid)
svc = Service(MemoryUserRepo()) # 本番はDB版に差し替え
Pythonビジネスロジックはリポジトリに依存し、実体は外から注入して切り替えます。
テストでの効果(モック・スタブを注入して速く・確実に)
ネットワークやI/Oを止める
class StubSession:
def get(self, url: str, timeout: float):
class R:
def raise_for_status(self): pass
def json(self): return {"id": 1, "name": "太郎"}
return R()
client = ApiClient(StubSession(), "https://api.example.com")
assert client.get_user("1")["name"] == "太郎"
Python副作用を断ち切り、純粋なロジックだけを素早く検証できます。
失敗パスを確実に試す
class ErrorSession:
def get(self, url: str, timeout: float):
class R:
def raise_for_status(self): raise RuntimeError("HTTP 500")
return R()
client = ApiClient(ErrorSession(), "https://api.example.com")
try:
client.get_user("x")
except RuntimeError:
pass # 期待どおりに失敗ハンドリングが働くか検証
Python本番では起きにくいエラーも、注入で自在に再現できます。
深掘り(ライフタイム・所有・設定と工場・小さく始める)
ライフタイムと所有を決める
長生きする依存(セッション、接続プール)は上位で一度作って共有し、短命の依存(一回きりの戦略)はメソッド注入で十分。所有者を明確にするとリソース管理が安定します。
設定値と「作り方」は工場に集約
生成手順が複雑ならFactoryを用意し、サービスは“でき上がった相手”だけを受け取る。変更は工場内に閉じ込められます。
まずはコンストラクタ注入から
フレームワークやDIコンテナは後で考えれば十分。最初は「必要な相手をinitで受け取る」だけで効果が体感できます。
よくある落とし穴(サービスロケータ・静的依存・過剰抽象)
サービスロケータに依存しすぎない
グローバルな“依存取得箱”から取り出す形は、見えない結合を増やしがち。可能なら明示的な注入で依存を見える化します。
静的メソッド・グローバルの濫用
テストや差し替えが難しくなります。状態や外部I/Oを伴う相手は、インスタンスとして注入できる形に寄せましょう。
抽象だらけにしない
最小の窓口だけをインターフェース化し、内部詳細は隠す。過剰な抽象は理解と改修のコストを上げます。
まとめ(「外から渡す」だけでコードは強く・優しくなる)
依存性注入は、必要な相手を外から渡すだけのシンプルな設計です。コンストラクタ注入を基本に、インターフェースで窓口を固定し、テストではモックを注いで副作用を排除する。ライフタイムと所有を意識し、工場に“作り方”を集約。過剰な抽象や見えない結合を避け、最小の公開面に集中する。この型を身につければ、初心者でも「差し替えに強く、テストしやすく、保守が軽い」OOPを自然に書けるようになります。
