はじめに
依存性の注入(Dependency Injection, DI)は、習得するのは難しいですが、理解すると簡単にできるようになります。また、依存性の注入と間違えやすい概念として、SOLID原則の一つである依存性逆転の原則(Dependency Inversion Principle, DIP)があります。
今回は、依存性の注入と依存性逆転の原則という概念をドラ🔵もんで分かりやすく解説したいと思います。
ちなみにスライドもあります。(スライドにはコード例は載ってません)
依存性の注入(DI)
DIをドラ🔵もんで解説する
の○太はドラ🔵もんに依存しています。ドラ🔵もんを他のロボットに変えるとの🟡太は死にます。生死がドラ🔵もんにかかっていますが、の🟡太は誰にも相談しないので、誰もドラ🔵もんがの🟡太と関わっていることを知りません。この状態がDIしていない状態です。
上記のままだと、誰もの🟡太を助けようがありません。
そこで、の🟡太が周りに相談します。の🟡太がドラ🔵もんに依存しているということが周知となりました。
この状態が依存性を注入した状態です。
依存性の注入の誤解
- DIをしたら、依存関係がなくなる → 依存関係は無くなりません
- DIをしたので、テストや切り替えが可能となる → できる場合とできない場合がある
テストできる場合
抽象なものに依存しているときはテストできます。
例えば、
func main() {
f()
}
func f() {
hoge()
foo()
}
func hoge() {
return 1
}
func foo() {
return 9
}
上記のコードをDIすると、
func main() {
result := sum(hoge() + foo())
fmt.Println(result)
}
func sum(a int, b int) int {
return a + b
}
func hoge() int {
return 1
}
func foo() int {
return 9
}
となりますが、以下のようにテストができます。注入しているものがintなので、他のものに置換可能です。
func Test_sum(t *testing.T) {
expected := 10
actual := sum(2, 8)
if actual != expected {
t.Errorf("sum() = %v; want %v", actual, expected)
}
}
テストできない場合
具体に依存していたら、モックができないのでテストできません。
例えば、先ほどのドラ🔵もんの例は置換ができません。(ドラ🔵もんの代わりになるものなんていない!)
結局DIとは何なのか?
DIとは、色々な意味で使用されることがありますが、以下の2つだと考えております。
狭義のDI
明示的に周りに何を必要とするかを宣言すること
広義のDI
単一責任の原則、リスコフの置換原則、Adapterパターン、Compositeパターンなど、様々な原則とパターンを集めたもの。
DIの目的
コード疎結合にして、保守容易性を向上させる。
依存性逆転の原則(DIP)
依存性逆転の原則をドラ🔵もんで解説する
の○太はドラ○もんに依存しています。ドラ🔵もんを他のロボットに変えるとの🟡太は死にます。
ドラ🔵もんは所詮四次⚪️ポケットであることに気づきます。
四次⚪️ポケットさえあればの○太は生きていけます。
ドラ○もんじゃなくても、四次⚪️ポケットを持っていれば、ぶっちゃけド🟠ミちゃんでも誰でもいいです。
つまり、ドラ🔵もんを代わりのものに置き換えることが可能となります。この状態が、依存関係の逆転した状態です。
依存性逆転の原則とは
具象から抽象へ依存させることで、依存関係を逆転すること。
つまり、interfaceや抽象クラスに依存させることによって、依存関係を逆転させ、実装の切り替えや置換を可能とします。
四次⚪️ポケットは具象ではないのか?
ここでは、ドラ🔵もん、ド🟠ミちゃんなどの「秘密道具を出す」という振る舞いを抽象化しているので、四次⚪️ポケットにしています。
上記の例で、より抽象的なポケットにしてしまうと、ズボンのポケットを持っている者だったら誰でもドラ🔵もんの代わりができてしまいます。
次に、ドラ🔵もんやド🟠ミちゃんを抽象化しものが、ロボットであった場合、ドラ🔵もんの代わりを鉄腕ア⚪︎ムやガ⚪︎ダムができるようになってしまいます。
つまり、「秘密道具を出す」という振る舞いに限定したいので、その 振る舞いに適した抽象度 である四次⚪️ポケットとしております。(四次⚪️ポケット以外でも SecretToolsSupplier とかでもいい)
実装例
実際に上記の例をGoで実装してみます。
DIしていない例
の🟡太がドラ🔵もんに依存しているのを周り(main)から見えていない状態です。
type Doraemon struct{}
func (d Doraemon) GetItem() (string, error) {
return "暗記パン🍞", nil
}
func NewDoraemon() Doraemon {
return Doraemon{}
}
// の🟡太がドラ🔵もんに依存している
type Nobita struct {
doraemon Doraemon
}
func NewNobita() Nobita {
doraemon := NewDoraemon()
return Nobita{
doraemon: doraemon,
}
}
func (n Nobita) study() string {
item, err := n.doraemon.GetItem()
if err != nil {
return "勉強できないよ〜😭"
}
return fmt.Sprintf("%sを使って勉強できた!", item)
}
func main() {
// ドラ🔵もんがmainから見えていない
nobita := NewNobita()
result := nobita.study()
fmt.Println(result)
}
時間があれば上記の一度テストをしてみてください。失敗テストを実装するのが困難だと思います。
DIした例
の🟡太がドラ🔵もんに依存していることを、周り(main)に明示的に宣言します。(NewNobita)
type Doraemon struct{}
func (d Doraemon) GetItem() (string, error) {
return "暗記パン🍞", nil
}
func NewDoraemon() Doraemon {
return Doraemon{}
}
// の🟡太はドラ🔵もんに依存している
type Nobita struct {
doraemon Doraemon
}
// 外部からドラ🔵もん注入している
func NewNobita(doraemon Doraemon) Nobita {
return Nobita{
doraemon: doraemon,
}
}
func (n Nobita) study() string {
item, err := n.doraemon.GetItem()
if err != nil {
return "勉強できないよ〜😭"
}
return fmt.Sprintf("%sを使って勉強できた!", item)
}
func main() {
// ドラ🔵もんはmainで作成できる
doraemon := NewDoraemon()
nobita := NewNobita(doraemon)
result := nobita.study()
fmt.Println(result)
}
さて、こちらも時間があれば上記の一度テストをしてみてください。DIしていない例と同様、失敗テストを実装するのが困難です。
依存関係の逆転
type FourDimensionalPocket interface {
GetItem() (string, error)
}
type Doraemon struct {
}
func (d Doraemon) GetItem() (string, error) {
return "暗記パン🍞", nil
}
func NewDoraemon() *Doraemon {
return &Doraemon{}
}
// の🟡太は四次⚪️ポケットに依存する
type Nobita struct {
pocket FourDimensionalPocket
}
func NewNobita(pocket FourDimensionalPocket) Nobita {
return Nobita{
pocket: pocket,
}
}
func (n Nobita) study() string {
item, err := n.pocket.GetItem()
if err != nil {
return "勉強できないよ〜😭"
}
return fmt.Sprintf("%sを使って勉強できた!", item)
}
func main() {
doraemon := NewDoraemon()
nobita := NewNobita(doraemon)
result := nobita.study()
fmt.Println(result)
}
さて、上記のテストは簡単にできます。
テストの例
// ドラ🔵もんをモックする
type mockDoraemon struct {
want bool
}
func mockNewDoraemon(want bool) mockDoraemon {
return mockDoraemon{want: want}
}
func mockNewNobita() FourDimensionalPocket {
return mockDoraemon{}
}
func (d mockDoraemon) GetItem() (string, error) {
if d.want {
return "", assert.AnError
}
return "暗記パン🍞", nil
}
func TestNobita_study(t *testing.T) {
tests := []struct {
name string
want string
wantErr bool
}{
{
name: "Success Test",
want: "暗記パン🍞を使って勉強できた!",
wantErr: false,
},
{
name: "Fail Test",
want: "勉強できないよ〜😭",
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
doraemon := mockNewDoraemon(tt.wantErr)
nobita := NewNobita(doraemon)
if got := nobita.study(); got != tt.want {
t.Errorf("Nobita.study() = %v, want %v", got, tt.want)
}
})
}
}
終わりに
DIや依存関係の逆転は間違えて使ってしまうと、かえって悪化させてしまう可能性があるので、適切な使い方で使えるようにしましょう。
ちなみに、今回はGoでコード例実装してみましたが、GoのinterfaceとJavaのinterfaceでは扱いが少し異なります。気になる方は、以下の記事を参考にしてください。