Python | OOP:依存性注入(簡単版)

Python Python
スポンサーリンク
  1. 概要(依存性注入は「必要な相手を外から渡す」だけでコードが劇的に扱いやすくなる)
  2. 何が問題で、DIでどう解くか(密結合→疎結合へ)
    1. 問題のある設計(自分で相手を作る)
    2. DIで解決(外から渡す)
  3. 基本パターン(コンストラクタ注入・メソッド注入・デフォルト差し替え)
    1. コンストラクタ注入(最も標準的で安全)
    2. メソッド注入(単発の相手を都度渡す)
    3. デフォルト実装+差し替え可能(初心者にも運用しやすい)
  4. インターフェースで窓口を固定する(ABC・Protocolで多態性を安全に)
    1. ABCで“必須メソッド”を強制
    2. Protocolで“継承なしでも受け入れる”
  5. 実務例(通知サービス・APIクライアント・リポジトリ)
    1. 通知サービスの差し替え(メール→Slack→SMS)
    2. APIクライアントのセッション注入
    3. リポジトリ(DB/メモリ)の切替
  6. テストでの効果(モック・スタブを注入して速く・確実に)
    1. ネットワークやI/Oを止める
    2. 失敗パスを確実に試す
  7. 深掘り(ライフタイム・所有・設定と工場・小さく始める)
    1. ライフタイムと所有を決める
    2. 設定値と「作り方」は工場に集約
    3. まずはコンストラクタ注入から
  8. よくある落とし穴(サービスロケータ・静的依存・過剰抽象)
    1. サービスロケータに依存しすぎない
    2. 静的メソッド・グローバルの濫用
    3. 抽象だらけにしない
  9. まとめ(「外から渡す」だけでコードは強く・優しくなる)

概要(依存性注入は「必要な相手を外から渡す」だけでコードが劇的に扱いやすくなる)

依存性注入(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を自然に書けるようになります。

タイトルとURLをコピーしました