📖

依存性注入をGoの具体例を交えて理解する

2024/07/07に公開

背景

理解が曖昧だったので整理しました。
車がエンジンを利用したいときに、依存性逆転の原則で考えると、上位モジュール(車)が下位の具象モジュール(具体的なエンジン)に依存してはいけないです。これをGoのコードで理解したいと思います。

良くない例

車のStartメソッドで、GasolineEngineを生成してしまっている

package main

import "fmt"

type GasolineEngine struct {}

func (e GasolineEngine) Start() string {
	return "GasolineEngine Started"
}

type Car struct {}

func (c *Car) Start() string {
	ge := GasolineEngine{}
	return ge.Start()
}

func main() {
	c := Car{}
	fmt.Println(c.Start())
}

車のStartメソッドで、GasolineEngineという具象モジュールに依存してしまっている

package main

import "fmt"

type GasolineEngine struct {}

func (e GasolineEngine) Start() string {
	return "GasolineEngine Started"
}

type Car struct {
	ge GasolineEngine
}

func (c *Car) Start() string {
	return c.ge.Start()
}

func main() {
	c := Car{}
	fmt.Println(c.Start())
}

以上のテストでは、モックを使った柔軟なテストができない(GasolineEngineのテストしかできない)という残念なテストしかできません。

package main

import "testing"

func TestCarStart(t *testing.T) {
	car := Car{}
	expected := "GasolineEngine Started"
	got := car.Start()

	if got != expected {
		t.Errorf("Car.Start() = %q; want %q", got, expected)
	}
}

良い例

package main

import "fmt"

type Engine interface {
	Start() string
}

type GasolineEngine struct{}

func (e GasolineEngine) Start() string {
	return "GasolineEngine Started"
}

type Car struct {
	engine Engine
}

func NewCar(engine Engine) *Car {
	return &Car{engine: engine}
}

func (c *Car) Start() string {
	return c.engine.Start()
}

func main() {
	ge := GasolineEngine{}
	c := NewCar(ge)
	fmt.Println(c.Start())
}

改善例を書いてきます。

  • 抽象的なエンジンのインターフェースを宣言する
  • 依存性を注入するNewCarのファクトリー関数を定義する
    この実装だと、仮に電気エンジンを追加したいときでも、車は抽象的なエンジンを参照しているので、再利用性が向上することと、テストでモックを使いやすくなります。
package main

import "testing"

type MockEngine struct{}

func (e MockEngine) Start() string {
	return "MockEngine Started"
}

func TestCarStart(t *testing.T) {
	mockEngine := MockEngine{}
	car := NewCar(mockEngine)
	expected := "MockEngine Started"
	got := car.Start()

	if got != expected {
		t.Errorf("Car.Start() = %q; want %q", got, expected)
	}
}

ここで自分がGoを学び始めたとき、「EngineとGasolineEngineは何の結びつきも記述してないのに、なぜGasolineEngineを渡せるのか?」と思いました。
結論から言うと、これはGoの仕様であり、同じStart()メソッドが実装されているので同一の型であると解釈してくれます。公式では以下のType assertionsの項目で解説されます。
https://go.dev/ref/spec#Interfaces
例えば他言語のJavaだと、以下のようなimplementsが明示的に必要です。
Goはそういうものだと思って、実装を合わせればいいと解釈しましょう。

public class GasolineEngine implements Engine {
    @Override
    public String start() {
        return "GasolineEngine Started";
    }
}

実務では、階層構造で一方向に向かっていくオニオンアーキテクチャで実装を見ることが多いのかなと思います。
また、素朴に実装すると、インターフェース宣言やモックで記述は多くなってしまうので、GoのDIライブラリであるwireやモック生成のgomockで自動生成しても良さそうです。

参考

https://zenn.dev/tokium_dev/articles/dependency-injection-watanabe
https://zenn.dev/koudai/articles/27f8f64d45c795#やり方その1%3A-条件を全て満たす
https://qiita.com/uhooi/items/03ec6b7f0adc68610426
https://user-first.ikyu.co.jp/entry/google-wire
https://github.com/uber-go/mock

Discussion