ソフトウェア設計

依存性注入(DI)とは? newをクラスの外に出すことから(例・たとえで総整理)

コード内で必要なオブジェクトを、その場で生成して使うことはよくあります。しかしテストコードを書こうとすると壁にぶつかります。クラス内に埋め込まれた生成処理のせいで、簡単に差し替えられないからです。

読了 6 分
依存性注入(DI)とは? newをクラスの外に出すことから(例・たとえで総整理)のカバー画像

コード内で必要なオブジェクトを、その場で生成して使うことはよくあります。しかしテストコードを書こうとすると壁にぶつかります。クラス内に埋め込まれた生成処理のせいで、簡単に差し替えられないからです。

依存性注入(DI)を検索しても、「制御の反転」や「IoCコンテナ」といった言葉ばかり出てきて、かえって混乱しがちです。

そこで今日は、できるだけ簡単に説明してみます。先に結論をお伝えします。

依存性注入(DI)とは、クラス内で直接生成していたオブジェクトを、外で生成して渡すことです。この一つの移動が本質です。オブジェクト生成の場所をクラスの外に出すこと。それがDIのほぼすべてです。

最後まで読めば、なぜDIが必要なのか、コードがどう変わるのか、そしてSwinjectのようなDIライブラリがなぜ代わりに処理してくれるのかが分かります。

オブジェクトをクラス内で生成すると、何が問題なのでしょうか?

注文を処理するクラスがあるとします。このクラスには、決済のための決済モジュールが必要です。

最もよくある初心者向けのコードは、次のような形です。

class OrderService {
    // クラス内で決済オブジェクトを直接生成
    private let pay = KakaoPay()

    func order() { pay.pay() }
}

問題は、このOrderServiceがKakaoPayに強く結合してしまうことです。

KakaoPayをNaver Payに変更したい場合は?クラス内に入って、生成部分を修正する必要があります。

テストで偽の決済オブジェクトを入れたくても方法がありません。すでに内部で本物のKakaoPayを生成しているからです。

つまり、オブジェクト生成をクラス内に置くと、そのクラスは特定の実装に縛られてしまいます。


そこでオブジェクト生成を外に出しました

解決策は思ったより簡単です。自分で生成せず、外で生成したものを受け取るだけでよいのです。

class OrderService {
    private let pay: Pay
    // 外で生成して渡されたものを受け取るだけ
    init(pay: Pay) { self.pay = pay }

    func order() { pay.pay() }
}

変わったのは一つだけです。直接生成がなくなり、イニシャライザで受け取る形になりました。

これでOrderServiceは、決済がKakaoPayかNaver Payかを知る必要がありません。「Payというものを受け取って使う」ことだけを知っています。

DIの本質は大げさなものではありません。オブジェクト生成をクラスの外に移し、何を渡すか決める権限を外側に委ねるだけです。

このように外からオブジェクトを渡すことを「注入する(injection)」と呼びます。依存するオブジェクトを注入するので、依存性注入です。

名前が難しいだけで、やっていることは今見た内容がすべてです。


DIにはどんなメリットがありますか?

言葉だけで良いと言われても実感しにくいので、実際に何が変わるのか整理しました。

  • 差し替えが簡単になります:KakaoPayをNaver Payに変えても、OrderServiceは一行も修正しません。渡すオブジェクトだけ変えれば済みます。
  • テストが楽になります:本物の決済の代わりに偽の(Mock)決済オブジェクトを渡せば、実際に課金せずロジックだけ検証できます。
  • 役割が分かれます:OrderServiceは「注文処理」に集中し、「どの決済を使うか」は外側が決めます。

私は三つ目が最も大きいと感じました。クラスが自分の仕事だけに集中するので、コードがずっと読みやすくなります。

まとめると、次のとおりです。

区分 内部で生成 外部で生成(DI)
決済の差し替え クラスの修正が必要 渡すオブジェクトだけ差し替え
テスト 偽のオブジェクトを入れられない Mockを注入できる
関心 生成とロジックが混在 ロジックだけに集中
newを外に出すと、コードはこのようにすっきりします
newを外に出すと、コードはこのようにすっきりします

では、SwinjectのようなDIライブラリはなぜ必要なのでしょうか?

ここまで来ると、自然な疑問が浮かびます。「外から渡すのは分かったけれど、その『外側』は誰が管理するのでしょう?」

オブジェクトが少なければ、手作業で生成して渡せます。しかし実務のプロジェクトでは、何百ものオブジェクトが互いに関係しています。

これを人が一つずつ順番どおりに生成して渡すのは、まさに地獄です。

そこで、この「外で生成して渡す作業」を代わりに行うツールが登場します。SwiftのSwinjectやFactoryのようなDIコンテナが、まさにその役割を担います。

コンテナが生成し、必要な場所に差し込む構造です
コンテナが生成し、必要な場所に差し込む構造です
オブジェクトが何百個もあると、代わりに管理してくれるコンテナのありがたさが分かります
オブジェクトが何百個もあると、代わりに管理してくれるコンテナのありがたさが分かります

必要なオブジェクトを登録しておけば、コンテナが自動で生成し、必要な場所に渡してくれます。自分で生成する作業はほとんどなくなります。

よく聞く「制御の反転(IoC)」もここから生まれます。オブジェクトを生成して接続する制御権が、自分のコードからコンテナに移ったという意味です。

つまり、DIは概念であり、DIコンテナはその概念を代わりに実行する作業者です。


よくある質問

Q. DIとIoCは同じ意味ですか? いいえ。IoC(制御の反転)は「制御権を渡す」という広い原則で、DIはそれを実現する具体的な方法の一つです。DIはIoCの一種だと考えてください。

Q. コンストラクタ以外の注入方法もありますか? はい。コンストラクタ注入、プロパティ注入、メソッド注入があります。ただしSwiftではコンストラクタ注入を推奨します。依存性をletで固定できて変更されず、テストもしやすいからです。

Q. DIには必ずライブラリが必要ですか? いいえ。先ほど手作業でコンストラクタに渡したものも、れっきとしたDIです。ライブラリはそれを自動化するだけです。


今日は、依存性注入を「オブジェクト生成の場所を移す」という小さな移動として説明しました。

DIが難しく感じたら、一つだけ覚えてください。「クラス内で作らず、外で作って渡す」。この一文だけで、残りは自然についてきます。

テストコードを前に途方に暮れたことがあるなら、今日の例を一度手で書き換えてみてください。目で見るより、ずっと確実に理解できます。

あわせて読みたい