Goのコードを書きながらDIの理解を深めてみた

この記事は[Schoo Advent Calendar2024](https://qiita.com/advent-calendar/2024/schoo)の14日目の記事です！

https://qiita.com/advent-calendar/2024/schoo

株式会社Schoo 新卒1年目の [@hiroto_0411](https://x.com/hiroto_0411)です！
DI(依存性の注入)がなぜ必要なのかを理解するのに苦戦していたのですが、業務や研修を通して段々と理解を深めることができたので、DIについてまとめてみました！

DIの理解に苦戦していた私ですが、DIは実際にコードを書いてみると理解を深めることができるなぁと実感したので、記事を参考にコードを書きながら読んでもらえたら嬉しいです！

 
## 対象読者
- なぜDI実装をすべきなのか知りたい方
- GoでのDI実装方法を知りたい方

 
## DIとは
Dependency Injection(依存性の注入)のことです。依存性を外部から注入することを指します。
その結果、上位モジュールを下位モジュールの具体的な実装に直接依存させないようにするデザインパターンです。上位モジュールが具体的な実装ではなく、抽象であるinterfaceに依存することで、実装の詳細を知らずに実装できます。これによるメリットは、モジュール間の結合度が低くなり、テストや保守が容易になります。

 
## なぜinterfaceに依存するとよいのか？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3810533/3deb16bf-cb60-0751-a3ea-2fcaebbd2af3.png)
こちらの図では、DIを実装しておらずcontroller層がmodel層の実装に依存してしまっています。

 <br />
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3810533/7111065c-3026-e2f7-c530-5269a93f3942.png)

一方こちらでは、DIを実装することでmodel層はinterfaceを実装すればよく、controller層はinterfaceに依存するためmodelの実装に依存しなくなります。

### interfaceに依存させるメリット
- 他のモジュールの実装ではなく、interfaceに依存することでモジュール同士が疎結合になる
- 疎結合になり上位のモジュールが下位のモジュールについて知らなくなることで、テストがしやすくなる
- 下位のモジュールに変更が加わっても(DBからのデータの取り方が変わった、外部APIの使用が変わった など)影響を受けにくい。下位モジュールの実装が変わっても、interfaceを実装していれば問題ない

 文章で読んでいるだけだと分かりにくいと思うので実際にコードで確認してみましょう！

## GoでDIを実装してみる


### DIできていない例
**ディレクトリ構成**
~~~
.
├── controller
│   ├── todo.go
│   └── todo_test.go
├── go.mod
├── go.sum
├── main.go
└── model
    └── todo.go
~~~

**model層**
~~~go: ./model/todo.go
package model

import "gorm.io/gorm"

type Todo struct {
	ID    int
	Title string
}

type TodoModel struct {
	DB *gorm.DB
}

func NewTodoModel(db *gorm.DB) *TodoModel {
	return &TodoModel{DB: db}
}

func (m *TodoModel) FetchTodos() ([]Todo, error) {
	var todos []Todo
	if err := m.DB.Find(&todos).Error; err != nil {
		return nil, err
	}
	return todos, nil
}

~~~

**controller層**
~~~go: ./controller/todo.go
package controller

import (
	"golang-playground/model"
	"log"

	"gorm.io/gorm"
)

type TodoController struct {
	Model *model.TodoModel
}

// model層でgorm.DBを使っていることを知っている。つまりmodel層に依存している。
func NewTodoController(m *model.TodoModel) *TodoController {
	return &TodoController{Model: m}
}

func (c *TodoController) FetchTodos() ([]model.Todo, error) {
	posts, err := c.Model.FetchTodos()
	if err != nil {
		log.Println("Failed to get todos: ", err)
		return nil, err
	}
	return posts, nil
}

~~~
:::note alert
controllerがmodelに`func NewTodoController(m *model.TodoModel)*TodoController`のような形で直接依存してしまっています。直接依存することでmodelに変更が加わった場合に影響を受けやすくなってしまっています。
:::

次のテストコードでどのような影響があるのか見てみましょう！
 
**テストコード**
~~~go: ./controller/todo_test.go
package controller

import (
	"testing"

	"github.com/DATA-DOG/go-sqlmock"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func TestFetchTodos(t *testing.T) {
	// モックDBの作成
	db, mock, err := sqlmock.New()
	if err != nil {
		t.Fatalf("Failed to create sqlmock: %v", err)
	}
	defer db.Close()

	// sqlite_version()を設定
    // SQLiteを使っていることを知ってしまっている
	mock.ExpectQuery(`select sqlite_version\(\)`).WillReturnRows(
		sqlmock.NewRows([]string{"sqlite_version"}).AddRow("3.36.0"),
	)

	// gorm.DB を初期化
	gormDB, err := gorm.Open(sqlite.Dialector{Conn: db}, &gorm.Config{})
	if err != nil {
		t.Fatalf("Failed to create gorm.DB: %v", err)
	}

	// モックのクエリと返り値の設定
	mock.ExpectQuery("SELECT \\* FROM `todos`").WillReturnRows(
		sqlmock.NewRows([]string{"id", "title"}).
			AddRow(1, "Task1").
			AddRow(2, "Task2"),
	)

	// TodoControllerのセットアップ
    m := model.NewTodoModel(gormDB)
	controller := NewTodoController(m)

	// メソッドをテスト
	todos, err := controller.FetchTodos()

	// エラーがないことを確認
	if err != nil {
		t.Fatalf("Expected no error, but got %v", err)
	}

	// 期待する結果
	expected := []model.Todo{
		{ID: 1, Title: "Task1"},
		{ID: 2, Title: "Task2"},
	}

	// reflect.DeepEqual で結果を検証
	if !reflect.DeepEqual(todos, expected) {
		t.Errorf("Expected %v, got %v", expected, todos)
	}

	// モックが期待通りに呼び出されたか確認
	if err := mock.ExpectationsWereMet(); err != nil {
		t.Errorf("There were unmet expectations: %v", err)
	}
}

~~~

:::note alert
テストを実装しようとするとmodel層での実装(gormを使っていることやSQLiteを使っていること)を完全に知った状態になってしまっています。gormやSQLiteを使わなくなったらこのテストコードも書き換える必要があるので良くない状態です。
:::

 

**mainの実装イメージ**
DBを実装していないので実際の動作は確認していません。
~~~go: main.go
package main

import (
	"fmt"
	"golang-playground/controller"
	"golang-playground/model"

	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func main() {
	// データベースの初期化
	db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

	// モデル層の初期化
	todoModel := model.NewTodoModel(db)

	// controller層の初期化
	todoController := controller.NewTodoController(todoModel.DB)

	// controllerのメソッドを呼び出し
	todos, err := todoController.FetchTodos()
	if err != nil {
		fmt.Printf("Error fetching todos: %v\n", err)
		return
	}

	fmt.Println("Fetched Todos:")
	for _, todo := range todos {
		fmt.Printf("- ID: %d, Title: %s\n", todo.ID, todo.Title)
	}
}
~~~


### interfaceを使ってDIを実装してみる
**ディレクトリ構成**
~~~
.
├── controller
│   ├── todo.go
│   └── todo_test.go
├── go.mod
├── go.sum
├── main.go
└── model
    ├── iface
    │   └── todo.go
    ├── implements
    │   └── todo.go
    └── todo.go
~~~

**model層**

~~~go: ./model/todo.go
package model

type Todo struct {
	ID    int
	Title string
}

~~~

~~~go: ./model/implements/todo.go
package implements

import (
	"go-playground/model"
	"go-playground/model/iface"
	"gorm.io/gorm"
)

type TodoModel struct {
	DB *gorm.DB
}

func NewTodoModel(db *gorm.DB) iface.TodoModeler {
	return &TodoModel{DB: db}
}

func (m *TodoModel) FetchTodos() ([]model.Todo, error) {
	var todos []model.Todo
	m.DB.Find(&todos)
	return todos, nil
}

~~~

~~~go: ./model/iface/todo.go
package iface

import (
	"go-playground/model"
)

type TodoModeler interface {
	FetchTodos() ([]model.Todo, error)
}
~~~
ここでinterfaceを実装しています。interfaceは予約語でpackage名として使用できないためifaceとしています。

controller層

~~~go: ./controller/todo.go
package controller

import (
	"fmt"
	"log"

	"go-playground/model"
	"go-playground/model/iface"
)

type TodoController struct {
	Model iface.TodoModeler
}

// model.TodoModeler(interface)に依存している。つまり、model層の実装は知らず、interfaceを実装していれば引数に取れる。
func NewTodoController(m iface.TodoModeler) *TodoController {
	return &TodoController{Model: m}
}

func (c *TodoController) FetchTodos() ([]model.Todo, error) {
	posts, err := c.Model.FetchTodos()
	if err != nil {
		log.Println("Failed to get posts: ", err)
		return nil, err
	}
	fmt.Println(posts)
	return posts, nil
}

~~~
:::note info
interfaceを使ってDIしたことで、controller層はinterfaceに依存する形になり、model層の実装を知らない状態にすることができました。(modelがgormを使っているかを気にせずに、controllerを実装できています)
:::

 

**テストコード**
~~~go: ./controller/todo_test.go
package controller

import (
	"reflect"
	"testing"

	"go-playground/model"
)

type MockTodoModel struct {
	models []model.Todo
	err    error
}

//interface(TodoModeler)を実装している
func (m *MockTodoModel) FetchTodos() ([]model.Todo, error) {
	return m.models, m.err
}

// テストコード
func TestTodoController(t *testing.T) {
	// モックのセットアップ
	mockModel := &MockTodoModel{
		models: []model.Todo{
			{ID: 1, Title: "Task1"},
			{ID: 2, Title: "Task2"},
		},
		err: nil,
	}

	// controllerのセットアップ
	controller := NewTodoController(mockModel)

	// メソッドの実行
	todos, err := controller.FetchTodos()
	if err != nil {
		t.Error("Expected no error, got ", err)
	}

	expected := []model.Todo{
		{ID: 1, Title: "Task1"},
		{ID: 2, Title: "Task2"},
	}

	if !reflect.DeepEqual(todos, expected) {
		t.Errorf("Expected %v, got %v", expected, todos)
	}
}

~~~

:::note info
controllerのセットアップの仕方に大きな違いがあるのが分かると思います。
DIできていない例では`controller := NewTodoController(gormDB)`となっており、gormを使っていることをcontroller層が知ってしまっていますが、今回の例では`controller := NewTodoController(mockModel)`となっており、mockを利用することができています。つまり、gormやSQLiteについて気にする必要がなくなっているのが分かると思います。よってmodel層の実装に変更が加わってもその影響を受けにくい状態になっています。コードもだいぶシンプルになりました！
:::

 

**mainの実装イメージ**

~~~go
package main

import (
	"go-playground/controller"
	"go-playground/model/implements"
	"gorm.io/driver/sqlite"
	"gorm.io/gorm"
)

func main() {
	// データベースの初期化
	db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
	if err != nil {
		panic("failed to connect database")
	}

	//NewTodoModelでinterfaceを作成している
	todoModel := implements.NewTodoModel(db)
	//interface(依存性)をcontroller層へ注入している
	todoController := controller.NewTodoController(todoModel)
	todoController.FetchTodos()
}

~~~

## まとめ
DIすることで「疎結合にできるよ！」「テストしやすくなるよ！」というのはよく聞きますが、「なぜそうなるのだろう？」というところを理解するのは難しく感じる方もいると思います。
自分で実装して試してみると理解を深めることができ、DIのメリットを実感することができるかなと思います。是非この記事を参考に色々試していただけたら嬉しいです！
 

## 参考
https://qiita.com/yoshinori_hisakawa/items/a944115eb77ed9247794

https://techblog.recruit.co.jp/article-124/

https://qiita.com/marnie_ms4/items/5925f136d23c8a0b4a4c

https://zenn.dev/naas/articles/c743a3d046fa78

https://qiita.com/k2491p/items/686ee5dd72b4baf9a81a

---
**Schooでは一緒に働く仲間を募集しています！**

https://corp.schoo.jp/careers/graduates

https://corp.schoo.jp/careers