スポンサーリンク

Pythonの依存関係注入(DI)をやさしく解説|テストしやすい設計への第一歩

クラス設計・OOP入門

Pythonでコードを書いていると、「あとから直しづらいな…」「テストを書くのがしんどいな…」と感じたことはありませんか?

その原因の多くは、クラス同士が強く結びつきすぎている設計にあります。 この状態をそのままにしていると、ちょっとした変更でも思わぬ場所が壊れたり、テストのたびに外部APIやデータベースに振り回されたりしてしまいます。

そこで登場するのが、依存関係注入(DI:Dependency Injection)という考え方です。 DIは「難しい上級者向けテクニック」に見えがちですが、実はコードをやさしく、長生きさせるための設計の工夫なんです。

この記事では、Python初心者〜中級者の方を想定して、

  • なぜDIが必要になるのか
  • DI・IoC・DIPの関係はどうなっているのか
  • PythonではどうやってDIを実装するのか
  • テストがなぜ一気に書きやすくなるのか

といったポイントを、できるだけ噛み砕いて解説していきます。

FastAPIで Depends() を見かけたことがある方や、 「モックって何のためにあるの?」とモヤっとしている方にも、きっとヒントになるはずです。

コードをガチガチに固める設計から、 必要に応じて入れ替えられる柔らかい設計へ。 その第一歩として、DIの世界を一緒にのぞいてみましょう ✨


  1. 1. なぜDIが必要なのか?密結合が生む3つの問題
    1. 問題① クラス同士が強く結びついてしまう(密結合)
    2. 問題② テストがとにかく書きづらい
    3. 問題③ 将来の変更コストがどんどん膨らむ
  2. 2. DI・IoC・DIPの関係を整理しよう
    1. DIは「実装テクニック」
    2. IoCは「考え方・設計の方向性」
    3. DIPは「守るべき設計原則」
    4. 3つの関係を一言でまとめると
    5. 設計思想を深く理解したい人へ
  3. 3. PythonでDIを実装する基本パターン
    1. 手順① 依存先を「抽象」で表現する
    2. 手順② 具体的な実装クラスを用意する
    3. 手順③ コンストラクタ注入(いちばんおすすめ)
    4. メソッド注入という選択肢
    5. 「DIっぽく書く」だけでも効果はある
  4. 4. リファクタリング視点で見るDI導入の効果
    1. 「変更したいのに、触るのが怖い」コードの正体
    2. DIは「安全に切り出すための足場」
    3. DIは「あとから効いてくる」設計投資
    4. 既存コードを改善したい人へ
  5. 5. テストとDIの相性が最強な理由
    1. DIがないと、テストはなぜつらいのか
    2. DIがあると「差し替え」が自然にできる
    3. 「呼ばれたかどうか」だけを検証できる安心感
    4. DIとテストはセットで考えると強い
    5. テスト駆動の視点で理解したい人へ
  6. 6. DIフレームワークはいつ使うべきか
    1. 手動DIで十分なケース
    2. DIフレームワークが欲しくなる瞬間
    3. 代表的なDIフレームワーク
      1. dependency-injector
      2. injector
    4. FastAPIのDepends()はDIの入門に最適
    5. 最初の判断基準はこれでOK
  7. まとめ
    1. あわせて読みたい
    2. 参考文献
  8. よくある質問(Q&A)
    1. 関連投稿:

1. なぜDIが必要なのか?密結合が生む3つの問題

まずは、DI(依存関係注入)がなぜ必要とされるのかから見ていきましょう。 多くの場合、問題は「動いてはいるけど、あとがつらいコード」から始まります。

Pythonでクラス設計をするとき、次のような書き方をしてしまうことはよくあります。


class UserService:
    def __init__(self):
        self.mailer = GmailSender()

    def notify(self, user):
        self.mailer.send(user.email)

一見するとシンプルで、何も問題がなさそうに見えますよね。 でもこの設計、じわじわと効いてくる落とし穴がいくつもあります。

問題① クラス同士が強く結びついてしまう(密結合)

この UserService は、GmailSender という具体的な実装に直接依存しています。 これはつまり、

  • メール送信方法を変更したい
  • 別の送信サービスを使いたい

といった要望が出たときに、UserService自体を修正しなければならないということです。

クラス同士が「溶接」されたような状態になり、 一部分を変えたいだけなのに、周囲まで影響が広がってしまいます。

問題② テストがとにかく書きづらい

さらに厄介なのがテストです。

このままでは、テスト時にも本物の GmailSender が使われてしまいます。 すると、

  • テスト実行で実際にメールが飛ぶ
  • 外部サービスの状態にテスト結果が左右される
  • テストが遅く、不安定になる

といった問題が起きがちです。

「本当はメール送信なんてしたくない。呼ばれたかどうかだけ確認したい」 そんな当たり前のテストが、書けなくなってしまうんですね。

問題③ 将来の変更コストがどんどん膨らむ

最初は小さなクラスでも、機能追加や仕様変更を重ねるうちに、

  • あちこちに同じ修正が必要
  • どこを直せばいいのかわからない
  • 触るのが怖くて変更を避ける

という状態に陥りがちです。

このような「変更に弱いコード」の正体は、 依存関係がコードの中に埋め込まれてしまっていることにあります。

DIは、この依存関係を外に出すことで、

  • クラス同士の結びつきを弱め
  • テストしやすくし
  • 変更に強い設計にする

ための考え方です。

次のセクションでは、 DI・IoC・DIP という、よく一緒に語られる言葉の関係を整理しながら、 DIの正体をもう少しクリアにしていきます。




2. DI・IoC・DIPの関係を整理しよう

DIの話を調べていると、ほぼ確実に一緒に出てくる言葉があります。

  • IoC(Inversion of Control:制御の反転)
  • DIP(Dependency Inversion Principle:依存関係逆転の原則)

どれも似た文脈で使われるので混乱しがちですが、 それぞれの役割と立ち位置を整理すると、DIの理解が一気に楽になります。

DIは「実装テクニック」

まず、DI(依存関係注入)はとてもシンプルです。

「必要なものを、自分で作らず、外から渡してもらう」
これがDIの本質です。

先ほどの例で言えば、

  • GmailSenderをクラス内部でnewしない
  • 外から渡されたメール送信オブジェクトを使う

という形に変えることがDIです。

DIはコードとして実装できる具体的な手法であり、 Pythonではコンストラクタ引数や関数引数で自然に表現できます。

IoCは「考え方・設計の方向性」

次に IoC(制御の反転) です。

これは少し抽象的で、

「何を使うか」「どう作るか」という主導権を、 クラス自身から外部に移す考え方

を指します。

DIは、IoCを実現する代表的な手段のひとつです。

つまり、

  • IoC:設計思想
  • DI:それをコードで実現する方法

という関係になります。

DIPは「守るべき設計原則」

最後に DIP(依存関係逆転の原則) です。

DIPでは、次の2点が重要になります。

  • 上位モジュールは下位モジュールに依存してはいけない
  • どちらも「抽象」に依存すべきである

ここで言う「抽象」とは、Pythonであれば

  • 抽象クラス(abc)
  • プロトコル(typing.Protocol)

などを指します。

DIを使って依存関係を外から注入し、 その依存先を「抽象」にしておくことで、 DIPを自然に満たす設計になります。

3つの関係を一言でまとめると

  • IoC:設計の考え方
  • DIP:守るべき原則
  • DI:それを実装するための手段

この関係が見えてくると、 「DIを使うかどうか」で悩むよりも、

「変更しやすく、テストしやすい構造になっているか?」

を基準に設計を考えられるようになります。

設計思想を深く理解したい人へ

ここまで読んで、

  • なぜ「抽象」に依存するのか
  • 依存の向きをどう設計すべきか

をもっと体系的に理解したくなった方には、次の一冊がとても相性がいいです。

Clean Architecture 達人に学ぶソフトウェアの構造と設計
Amazonでチェックする | ✅ 楽天でチェックする

DIやDIPが「テクニック」ではなく、 ソフトウェアを長く保つための考え方だと実感できる一冊です。

次のセクションでは、 いよいよPythonでDIをどう書くのかを、 シンプルなコード例で見ていきます。




3. PythonでDIを実装する基本パターン

ここからは、Pythonで依存関係注入(DI)をどう書くのかを見ていきます。 難しい仕組みは不要で、まずは素のPythonで理解するのがいちばんです。

手順① 依存先を「抽象」で表現する

DIの第一歩は、具体的なクラスではなく、役割(インターフェース)に依存することです。

Pythonでは abc モジュールを使って、抽象クラスを定義できます。


from abc import ABC, abstractmethod

class MailSender(ABC):
    @abstractmethod
    def send(self, email: str) -> None:
        pass

ここでは、

  • 「メールを送る」という役割だけを定義
  • どのサービスを使うかは決めない

という状態を作っています。

手順② 具体的な実装クラスを用意する

次に、この抽象を実装するクラスを作ります。


class GmailSender(MailSender):
    def send(self, email: str) -> None:
        print(f"Gmailで {email} にメールを送信しました")

将来、別のサービスに切り替えたくなった場合でも、

  • YahooSender
  • MockMailSender(テスト用)

などを追加するだけで対応できます。

手順③ コンストラクタ注入(いちばんおすすめ)

ここで、依存関係を外から渡すようにします。


class UserService:
    def __init__(self, mail_sender: MailSender):
        self.mail_sender = mail_sender

    def notify(self, email: str) -> None:
        self.mail_sender.send(email)

ポイントは、

  • GmailSender を直接生成していない
  • MailSender という抽象に依存している

という点です。

実際に使うときは、外側で組み立てます。


mail_sender = GmailSender()
service = UserService(mail_sender)

service.notify("user@example.com")

これがコンストラクタ注入です。 オブジェクト生成時に必要な依存がすべて揃うので、 安全で読みやすいのが大きなメリットです。

メソッド注入という選択肢

場合によっては、依存関係をメソッド引数で渡すこともあります。


class UserService:
    def notify(self, email: str, mail_sender: MailSender) -> None:
        mail_sender.send(email)

この方法は、

  • その処理でしか使わない依存
  • 一時的な振る舞いの切り替え

に向いています。

ただし、クラス全体で使う依存は コンストラクタ注入の方が意図が明確なので、基本はこちらを選びましょう。

「DIっぽく書く」だけでも効果はある

最初から抽象クラスを用意しなくても、

  • 外からオブジェクトを渡す
  • 内部でnewしない

これだけでも、設計はかなり改善されます。

DIは「完璧にやること」よりも、 依存を外に出す意識を持つことが何より大切です。

次のセクションでは、 このDIがリファクタリングの現場でどう役立つのかを、 実務視点で見ていきます。




4. リファクタリング視点で見るDI導入の効果

DIは「最初からきれいに設計できる人のためのもの」と思われがちですが、 実際の現場ではリファクタリングの途中で導入されるケースの方が圧倒的に多いです。

ここでは、DIがあることで何がどう楽になるのかを、 リファクタリングの視点から見ていきましょう。

「変更したいのに、触るのが怖い」コードの正体

長く使われているコードほど、こんな状態になりがちです。

  • クラスの中で依存オブジェクトを直接生成している
  • 同じような処理があちこちに散らばっている
  • テストがなく、動作確認は手動のみ

この状態で仕様変更が入ると、

  • どこを直せばいいかわからない
  • 直したつもりが別の場所で壊れる

という負のループに陥ります。

DIは「安全に切り出すための足場」

DIを導入すると、依存関係が外に見える形になります。

すると、

  • このクラスは何に依存しているのか
  • どこまでが責務なのか

が明確になり、 安心してクラスを分割・整理できるようになります。

いきなり完璧な設計を目指す必要はありません。

  • まずは new している部分を外に出す
  • 引数で受け取れる形にする

これだけでも、コードは一段階「柔らかく」なります。

DIは「あとから効いてくる」設計投資

DIの効果は、導入した瞬間よりも、

  • 仕様変更が入ったとき
  • テストを書き始めたとき
  • 別の人がコードを触るとき

に強く実感できます。

「最初は少し面倒だったけど、今は助かっている」 DIは、そんなタイプの設計です。

既存コードを改善したい人へ

すでに動いているコードにDIを入れたい場合、 どう直せば安全なのかで悩むことが多いと思います。

そんなときにとても参考になるのが、次の一冊です。

リファクタリング(第2版)
Amazonでチェックする | ✅ 楽天でチェックする

DIを直接解説する本ではありませんが、

  • 変更を小さく、安全に進める考え方
  • 「直していいコード」と「触る前に準備が必要なコード」の見極め

が非常にわかりやすく整理されています。

次のセクションでは、 DIとテストがなぜ相性抜群なのかを、 具体的なテストの流れと一緒に見ていきます。




5. テストとDIの相性が最強な理由

DIの価値がいちばんわかりやすく表れるのが、テストを書くときです。 ここでは、「なぜDIがあるとテストが一気に楽になるのか」を具体的に見ていきましょう。

DIがないと、テストはなぜつらいのか

DIを使っていないコードでは、テスト中でも

  • 実際のデータベースに接続してしまう
  • 外部APIを本当に呼んでしまう
  • ファイルやネットワークに依存してしまう

といった問題が起きがちです。

すると、

  • テストが遅い
  • 環境によって結果が変わる
  • 失敗した理由がわかりにくい

という、あまりうれしくない状況になります。

DIがあると「差し替え」が自然にできる

DIを使っていれば、テスト時に本物の依存先を使う必要がありません

たとえば、先ほどの MailSender を、 テスト用のクラスに差し替えることができます。


class FakeMailSender(MailSender):
    def __init__(self):
        self.called = False

    def send(self, email: str) -> None:
        self.called = True

これをテスト対象に注入すると、


def test_notify_calls_send():
    fake_sender = FakeMailSender()
    service = UserService(fake_sender)

    service.notify("test@example.com")

    assert fake_sender.called

外部環境に一切依存しない、 高速で再現性のあるテストが書けるようになります。

「呼ばれたかどうか」だけを検証できる安心感

テストで本当に確認したいのは、

  • メールが送信されたか
  • DBに保存されたか

ではなく、

「その責務を持つ処理が、正しく呼ばれたか」

であることがほとんどです。

DIを使えば、 テスト対象のクラスは自分の仕事だけに集中できます。

DIとテストはセットで考えると強い

DIだけを導入しても、テストを書かなければ恩恵は半分です。 逆に、テストを書こうとすると、DIのありがたみをすぐに実感します。

この2つは、お互いを支え合う関係なんですね。

テスト駆動の視点で理解したい人へ

「そもそも、なぜテストしやすい設計が大事なのか」 「設計とテストはどうつながっているのか」

を体系的に理解したい方には、次の一冊がとても参考になります。

テスト駆動開発
Amazonでチェックする | ✅ 楽天でチェックする

DIを「テクニック」ではなく、 設計と品質を守るための考え方として理解できるようになります。

次のセクションでは、 DIフレームワークやFastAPIの Depends()いつ・どこで使うべきかを整理していきます。




6. DIフレームワークはいつ使うべきか

ここまでで、素のPythonだけでもDIは十分に実装できることが分かりました。 では、DIフレームワークはいつ必要になるのでしょうか?

結論から言うと、 「依存関係の組み立てがつらくなったとき」が導入のタイミングです。

手動DIで十分なケース

次のような場合は、フレームワークを使わなくても問題ありません。

  • 依存関係の数が少ない
  • オブジェクト生成の流れが単純
  • 小規模なスクリプトやツール

むしろ、最初からDIフレームワークを入れてしまうと、

  • 設定が多くて理解しづらい
  • コードの追跡が難しくなる

といった逆効果になることもあります。

DIフレームワークが欲しくなる瞬間

一方で、次のようなサインが出てきたら要注意です。

  • オブジェクト生成コードがあちこちに散らばっている
  • 依存関係の入れ替えが頻繁に起こる
  • 初期化順序を間違えてバグが出る

この段階になると、

「何をどこで生成しているのか分からない」

という状態になりがちです。

DIフレームワークは、 この組み立ての複雑さを一箇所に集約するための道具です。

代表的なDIフレームワーク

dependency-injector

  • 高速で柔軟
  • コンテナで依存関係を明示的に定義
  • 大規模アプリ向き

依存関係が増えても、 「設計図」として一覧できるのが大きな強みです。

injector

  • アノテーション中心のシンプルな記述
  • Guice風の書き味

Java経験者には馴染みやすいですが、 Pythonでは好みが分かれることもあります。

FastAPIのDepends()はDIの入門に最適

FastAPIを使っている方なら、 Depends() をすでに見たことがあるかもしれません。

これは、フレームワーク組み込みのDI機構です。

  • 依存関係を関数として定義できる
  • テスト時に簡単に差し替え可能
  • 非同期処理とも相性が良い

「DIをちゃんと使うのは初めて」という方でも、 FastAPIなら自然にDIの考え方に触れられます。

最初の判断基準はこれでOK

  • まずは手動DIで書いてみる
  • つらくなったらフレームワークを検討する

DIフレームワークは魔法の道具ではありません。 あくまで、複雑さが増えたときに助けてくれる整理係です。




まとめ

今回は、Pythonにおける依存関係注入(DI)について、 「なぜ必要なのか」という背景から、実装方法、テストやリファクタリングとの関係までを順番に見てきました。

  • DIはコードを複雑にするためのものではない
  • 変更しやすく、テストしやすい状態を保つための工夫
  • IoCやDIPとセットで考えると理解しやすい

という点が、この記事の大きなポイントです。

私自身も、最初は「DIって回りくどいな」と感じていました。 でも、仕様変更やテスト対応が増えるにつれて、

「DIが入っているコードは、触るのが怖くない」

と実感するようになりました。

すべてのコードにDIが必要なわけではありません。 ただ、少しでも

  • テストが書きづらい
  • 変更のたびに修正箇所が増える

と感じているなら、 DIはとても頼れる選択肢になります。

まずは小さなクラスひとつから、 「newしない設計」を意識してみてください。 それだけでも、コードの見通しはかなり変わりますよ 😊


あわせて読みたい


参考文献


よくある質問(Q&A)

Q
小規模なPythonスクリプトでもDIは使うべきですか?
A

無理に使う必要はありません。 ファイル1〜2本で完結するスクリプトなら、 直接依存させた方が読みやすいことも多いです。

ただし、「あとから機能追加するかも」「テストを書く予定がある」なら、 DIを意識しておくと将来が楽になります。

Q
abcを使わずにDIっぽく書いても問題ありませんか?
A

問題ありません。 Pythonでは、

  • ダックタイピング
  • typing.Protocol

なども立派な選択肢です。

大事なのは抽象に依存する意識であって、 必ずabcを使うことではありません。

Q
FastAPIのDepends()はDIフレームワークと同じものですか?
A

役割は似ていますが、スコープは少し違います。

FastAPIのDepends()は、

  • リクエスト単位の依存解決
  • Webアプリ向けに最適化

されています。

一方、DIフレームワークはアプリ全体の依存関係を管理します。 FastAPIを使っている場合は、まずDepends()から慣れていくのがおすすめです。

※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。

※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。

スポンサーリンク