1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

試して理解するDependency Injection in Go

Last updated at Posted at 2020-03-21

はじめに

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を活用していない実装

部屋情報の実装

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)
}

サンプルコード

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

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()メソッドを修正します。

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)
}

サンプルコード

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

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

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)
		}
	}
}

サンプルコード

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

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

DIを活用した実装

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

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

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

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でいうところの注入する)ことができます。

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

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)
		}
	}
}

サンプルコード

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

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

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

// モックの定義
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)
}

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

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

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)
		}
	}
}

サンプルコード

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

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

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

おわりに

今回はGoでDIを活用する方法を解説しました。

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

1
0
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?