試して理解するDependency Injection in Go

## はじめに
Goでクリーンアーキテクチャやレイヤードアーキテクチャを実装しようとすると、`DI(Dependency Injection）`や`DIP（Dependency Inversion Principle）`などの概念が出てきて思うようにアーキテクチャに対する理解が進みませんでした。

また、GoでDIについて解説された記事は既にいくつかあるのですが、いまいち腑に落ちていなかったので自身の知識を整理するためにこの記事を書きました。

この記事ではシンプルな実装でDIに触れ、DIのメリットを体験できることを目的とします。

DIについてはこちらの記事にわかりやすくまとまっていますので、そもそもDIって何？という方はぜひ参考にしてみてください。
https://qiita.com/hshimo/items/1136087e1c6e5c5b0d9f

## DIのメリット
DIについて細かく説明するのはこの記事の本題ではないのですが、DIを導入するメリットについては軽く触れておきたいと思います。

DIのメリットは以下の2つです。
- オブジェクト間の結合度を下げる
- 依存関係のあるオブジェクトをモック化でき、ユニットテストがしやすくなる

2つと書きましたが、オブジェクト間の結合度を下げることにより結果としてユニットテストがしやすくなると言えます。

特に外部ライブラリや外部サービスを使用する際に、依存関係のある箇所をモック化することによりテストが容易になります。

DBへの接続などをモック化したり、コントローラからDB接続を行っている層（リポジトリー層など）をモック化するケースが多いかと思います。

## 試してみる
本題の実装に入っていきたいと思います。

GoでDIを実現する方法は`interfaceを満たす`ことで実現できます。

まずはDIを活用してないコードを実装し、リファクタリングしていく形でDIを活用したコードを実装していきます。

サンプルコードは部屋の面積を算出する簡単な実装になります。

### DIを活用していない実装

**部屋情報の実装**

```go
type Room struct {
    RoomNumber int
    Height float64
    Width float64
}

func (r *Room) CalcArea() string {
	area := r.Height * r.Width
	return fmt.Sprintf("room %d's area is %.f", r.RoomNumber, area)
}
```

[サンプルコード](https://play.golang.org/p/uTMrKr2FS2t)

`area := r.Height * r.Width`の部分は面積を計算する処理なので`CalcArea()`から切り出します。

```go
type IGeometry interface {
	Area(height, width float64) float64
}

type geometry struct{}

func NewGeometry() *geometry {
	return &geometry{}
}

func (g *geometry) Area(height, width float64) float64 {
	return height * width
}
```

`NewGeometry`でコンストラクタを定義し、`geometry`構造体を返しています。

上記に伴い、`CalcArea()`メソッドを修正します。

```go
func (r *Room) CalcArea() string {
	ig := NewGeometry()
	area := ig.Area(r.Height, r.Width)
	return fmt.Sprintf("room %d's area is %.f", r.RoomNumber, area)
}
```

[サンプルコード](https://play.golang.org/p/LBs7tBudS7v)

面積を計算する部分は`CalcArea()`メソッドから切り出すことができましたが、メソッド内で`NewGeometry`コンストラクタの初期化を行っているため`geometry`のと結合度が高くなっています。

次に`CalcArea()`メソッドに対するテストを追加していきます。

```go
func TestCalcArea(t *testing.T) {
	testCases := []struct {
		room     Room
		expected string
	}{
		{Room{RoomNumber: 101, Height: 5.0, Width: 7.0}, "room 101's area is 35"},
		{Room{RoomNumber: 201, Height: 4.0, Width: 7.5}, "room 201's area is 30"},
	}

	for _, tc := range testCases {
		actual := tc.room.CalcArea()
		if actual != tc.expected {
			t.Errorf("expected %v, but got %v", tc.expected, actual)
		}
	}
}
```

[サンプルコード](https://play.golang.org/p/tS-Cs6QYBXS)

上記のテストコードはテスト対象がシンプルなため問題なさそうですが、`CalcArea()`メソッド内の`ig.Area(r.Height, r.Width)`の内部処理を把握しておかないといけません。

また、関数やメソッド内でDBへの接続などを行っている場合は、DBを立ち上げておく必要があるなど、今回テストしたい`CalcArea()`のテストに必要ない箇所も考慮しなければいけません。

### DIを活用した実装
さきほど実装したコードをリファクタリングしていきたいと思います。

まずは`CalcArea()`内の`ig := NewGeometry()`をどうするか考えます。

今回はメソッドの引数に`IGeometry`を受け取れるようにすることで、DIを実現していきます。

```go
func (r *Room) CalcArea(ig IGeometry) string {
	area := ig.Area(r.Height, r.Width)
	return fmt.Sprintf("room %d's area is %.f", r.RoomNumber, area)
}
```

これだけで`IGeometry`をモック化して引数に渡す（DIでいうところの注入する）ことができます。

テストを修正していきます。

```go
func TestCalcArea(t *testing.T) {
	testCases := []struct {
		roomNumber int
		area       float64
		expected   string
	}{
		{101, 35.0, "room 101's area is 35"},
		{201, 30.0, "room 201's area is 30"},
	}

	for _, tc := range testCases {
		room := Room{
			RoomNumber: tc.roomNumber,
		}
		actual := room.CalcArea()
		if actual != tc.expected {
			t.Errorf("expected %v, but got %v", tc.expected, actual)
		}
	}
}
```
[サンプルコード](https://play.golang.org/p/jAxqPnl9KAV)

変更点としては`Room`を定義した際に`Height`と`Width`が期待する面積の値になるように指定していましたが、面積を算出する処理のことを気にせず、テスト実行時には`area`で指定した面積の値が`ig.Area(r.Height, r.Width)`の結果として返ってくるもの、としてテストを変更しています。

このままではテストが落ちてしまうので、`IGeometry`の部分をモック化し、`CalcArea()`の引数に渡していきます。

まずはモックを作成します。

```go
// モックの定義
type FakeIGeometry struct {
	IGeometry    // IGeometryを埋め込むことでAreaを呼び出せるように
	FakeCalcArea func(height, width float64) float64
}

// IGeometryインターフェースを満たすためにはAreaメソッドを定義する必要がある
func (g FakeIGeometry) Area(height, width float64) float64 {
	// FakeIGeometryのAreaメソッドは内部的にはFakeCalcAreaを呼び出しているだけ
	return g.FakeCalcArea(height, width)
}
```

モック化に関しては以下の記事を参考にさせていただきました。

- [Golangにおけるinterfaceをつかったテストで Mock を書く技法](http://haya14busa.com/golang-how-to-write-mock-of-interface-for-testing/)
- [Golangにおけるinterfaceをつかったテスト技法](https://deeeet.com/writing/2016/10/25/go-interface-testing/)

次にモックを利用してテストを修正します。

```go
func TestCalcArea(t *testing.T) {
	testCases := []struct {
		roomNumber int
		area       float64
		expected   string
	}{
		{101, 35.0, "room 101's area is 35"},
		{201, 30.0, "room 201's area is 30"},
	}

	for _, tc := range testCases {
		// モックの初期化
		mockIGeometry := FakeIGeometry{
			FakeCalcArea: func(height, width float64) float64 {
				// testCasesで指定したareaの値を返すだけ
				return tc.area
			},
		}
		room := Room{
			RoomNumber: tc.roomNumber,
		}
		actual := room.CalcArea(mockIGeometry)
		if actual != tc.expected {
			t.Errorf("expected %v, but got %v", tc.expected, actual)
		}
	}
}
```

[サンプルコード](https://play.golang.org/p/s_PDn3KUx3i)

これでモックを利用しテストが通るようになりました。

コード中のコメントにも記載しましたが、面積を算出する処理を`mockIGeometry`により`testCases`で指定した`area`の値を返すようにしているため、仮に面積を算出する処理にバグがある、もしくは未実装の場合でも`CalcArea()`メソッドのテストを行うことができるようになりました。

このようにすることでユニットテストが書きやすくなり、それぞれの責務でテストを行うことができます。

## おわりに
今回はGoでDIを活用する方法を解説しました。

わかりにくい部分や間違っている部分があれば指摘していただけると嬉しいです。