DI(Dependency Injection:依存性の注入)とは?
プログラミングにおけるDIをひと言でいうと、「必要な道具(パーツ)を、自分で作らずに、外から持ってきてもらう仕組み」のことです。
これを現実世界の「お掃除ロボット」に例えてみましょう。
1. DIを使っていない状態(ガチガチに固定されたロボ)
「部屋を掃除するロボット」を開発したとします。
このロボットの内部に、「A社の普通の掃除機」を直接ハンダ付けして一体化させてしまいました。
問題点:
- もし「水拭きもしたいな」と思っても、中の掃除機を外せないので、ロボット自体を丸ごと作り直す必要があります。
- 掃除機の調子が悪くなったとき、ロボットを分解しないと修理やテストができません。
これをプログラムの世界では「依存度が固く結合している(密結合)」と言い、修正やテストがしにくい悪い状態とされます。
2. DIを使っている状態(アタッチメント式のロボ)
今度は、ロボット本体には「アタッチメントを差し込む穴(インターフェース)」だけを開けておきます。
そして、動かすときに「外から必要なパーツをカチッと差し込む」ようにしました。
メリット:
- 絨毯を掃除したいときは「普通の掃除機パーツ」を差し込む。
- フローリングをきれいにしたいときは「水拭きモップパーツ」を外から注入する。
- テストをしたいときは「ダミーの軽いパーツ」を差し込んで、ロボットの動きだけを確認する。
このように、ロボット本体(クラス)のコードを一切書き換えることなく、外から道具(オブジェクト)を差し替えるだけで機能を変更できる仕組み、これが「依存性の注入(DI)」です。
開発における最大のメリット
DIを採用する理由は、主に次の2点です。
テストが圧倒的に楽になる
例えば「本番のデータベースにデータを保存するパーツ」の代わりに、「テスト用の仮のパーツ」を外から注入できるため、本番データを汚さずに安全にテストができます。
部品の交換(仕様変更)に強くなる
「決済システムをA社からB社に変えたい」という時も、パーツを差し替えるだけで済み、システム全体を修正する必要がなくなります。
Go言語で実装してみる
Go言語は「インターフェース」が非常にシンプルかつ強力に機能するため、DI(依存性の注入)の概念を表現するのに最適な言語の一つです。
先ほどのお掃除ロボットの例を、Goのコードで実装してみましょう。
1. 道具の規格(インターフェース)を決める
まずは、ロボットに差し込むパーツの共通ルール(規格)を作ります。Goではinterfaceを使って定義します。
package main
import "fmt"
// Cleaner は、お掃除パーツが満たすべき共通の規格(インターフェース)
type Cleaner interface {
Clean() string
}
2. 具体的なパーツ(部品)を作る
次に、規格(Cleaner)に沿った具体的なパーツを2つ作ります。
Goでは、インターフェースに定義されているメソッド(今回は Clean())を実装していれば、自動的にそのインターフェースを満たしているとみなされます。
// VacuumCleaner は「普通の掃除機パーツ」
type VacuumCleaner struct{}
func (v VacuumCleaner) Clean() string {
return "ゴミを強力に吸い取っています!"
}
// WaterMop は「水拭きモップパーツ」
type WaterMop struct{}
func (w WaterMop) Clean() string {
return "床をピカピカに水拭きしています!"
}
3. お掃除ロボット本体を作る(DIの受け皿)
ロボット本体(Robot)は、具体的な「掃除機」や「モップ」のことは知りません。ただ、規格であるCleanerを持てるようにしておきます。
ここでのポイントは、構造体を初期化するときに外部からパーツを渡す(注入する)点です。
// Robot はお掃除ロボット本体
type Robot struct {
// 具体的なパーツではなく、インターフェース(規格)を持たせる
tool Cleaner
}
// NewRobot はロボットを組み立てる関数(ここでパーツを注入する)
func NewRobot(c Cleaner) *Robot {
return &Robot{tool: c}
}
// Start は掃除を開始するメソッド
func (r *Robot) Start() {
fmt.Println("ロボットが起動しました。")
// 注入されたパーツの能力を呼び出す
result := r.tool.Clean()
fmt.Println("動作状況:", result)
fmt.Println("掃除が完了しました。\n")
}
4. 動かしてみる(パーツの注入)
最後に main 関数で、ロボットに異なるパーツを「注入」して動かしてみます。
func main() {
// パターンA: 掃除機パーツを注入
vacuum := VacuumCleaner{}
robotA := NewRobot(vacuum) // ここでDI(注入)
robotA.Start()
// パターンB: 水拭きモップパーツを注入
mop := WaterMop{}
robotB := NewRobot(mop) // ここでDI(注入)
robotB.Start()
}
出力結果
ロボットが起動しました。
動作状況: ゴミを強力に吸い取っています!
掃除が完了しました。
ロボットが起動しました。
動作状況: 床をピカピカに水拭きしています!
掃除が完了しました。
GoにおけるDIのポイント
ロボット本体(Robot 構造体)のコードは一切書き換えていないのに、NewRobot() の引数に変えるだけで、ロボットの挙動(掃除機かモップか)を自由に変更できています。
もし「テスト用のダミーパーツ」を作りたい場合も、Clean() string を持つ構造体をサクッと作って NewRobot() に渡すだけで、本番環境を汚さずにテストが可能になります。