コード内で必要なオブジェクトを、その場で生成して使うことはよくあります。しかしテストコードを書こうとすると壁にぶつかります。クラス内に埋め込まれた生成処理のせいで、簡単に差し替えられないからです。
依存性注入(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を注入できる |
| 関心 | 生成とロジックが混在 | ロジックだけに集中 |
では、SwinjectのようなDIライブラリはなぜ必要なのでしょうか?
ここまで来ると、自然な疑問が浮かびます。「外から渡すのは分かったけれど、その『外側』は誰が管理するのでしょう?」
オブジェクトが少なければ、手作業で生成して渡せます。しかし実務のプロジェクトでは、何百ものオブジェクトが互いに関係しています。
これを人が一つずつ順番どおりに生成して渡すのは、まさに地獄です。
そこで、この「外で生成して渡す作業」を代わりに行うツールが登場します。SwiftのSwinjectやFactoryのようなDIコンテナが、まさにその役割を担います。
必要なオブジェクトを登録しておけば、コンテナが自動で生成し、必要な場所に渡してくれます。自分で生成する作業はほとんどなくなります。
よく聞く「制御の反転(IoC)」もここから生まれます。オブジェクトを生成して接続する制御権が、自分のコードからコンテナに移ったという意味です。
つまり、DIは概念であり、DIコンテナはその概念を代わりに実行する作業者です。
よくある質問
Q. DIとIoCは同じ意味ですか? いいえ。IoC(制御の反転)は「制御権を渡す」という広い原則で、DIはそれを実現する具体的な方法の一つです。DIはIoCの一種だと考えてください。
Q. コンストラクタ以外の注入方法もありますか? はい。コンストラクタ注入、プロパティ注入、メソッド注入があります。ただしSwiftではコンストラクタ注入を推奨します。依存性をletで固定できて変更されず、テストもしやすいからです。
Q. DIには必ずライブラリが必要ですか? いいえ。先ほど手作業でコンストラクタに渡したものも、れっきとしたDIです。ライブラリはそれを自動化するだけです。
今日は、依存性注入を「オブジェクト生成の場所を移す」という小さな移動として説明しました。
DIが難しく感じたら、一つだけ覚えてください。「クラス内で作らず、外で作って渡す」。この一文だけで、残りは自然についてきます。
テストコードを前に途方に暮れたことがあるなら、今日の例を一度手で書き換えてみてください。目で見るより、ずっと確実に理解できます。

