Go言語

DI(Dependency Injection)とは?Go言語で実装してみる

クリーンアーキテクチャの学習をしていて、DIを理解できていないと感じたので記事にしてみました。

アーキテクチャやデザインパターンを学習しても、イマイチ頭に入ってこない人は、まずはDIを理解することをおすすめします。

DIとは?

WikiのDependency injectionには次のように書かれています。

dependency injection is a technique in which an object receives other objects that it depends on. These other objects are called dependencies.

訳してみます。
「Dependency Injectionは、オブジェクトが必要としている他のオブジェクトを受け取るためのテクニック。これらの他のオブジェクトは依存関係という。」

つまり、オブジェクトAにオブジェクトBを受け渡すための方法で、このオブジェクトBのことをDependencyと言っているということになります。

DIのメリット

  • 結合度が低くなる
  • テストしやすくなる
  • オブジェクト間の依存関係の向きを好きに変えることができる
    • オブジェクトAがオブジェクトBに依存している状態をオブジェクトBがオブジェクトAに依存している状態に変更できる

特に最後の「依存関係の向きを変えることができる」は、クリーンアーキテクチャを理解するために重要になってきます。

文章で書いても伝わらないと思うので、実際のコードで説明したいと思います。

コードで説明

ToDoアプリケーションのタスク作成機能を実装するとします。
登場人物は、

  • Task(タスクを表す型)
  • TaskUsecase(タスクを作成するというユースケースを処理する)
  • TaskRepository (タスクをDBに保存する処理をする)

コード全体

タスクを表すTask(task.go)は次のとおりです。

package main

type Task struct {
	ID uint
	Title string
}

次に、タスクを作成するユースケースを担当するTaskUsecase(task_usecase.go)は

package main

// Saveメソッドの実装はTaskRepositoryで行っている
type TaskRepositoryInterface interface {
	Save(Task) (Task, error)
}

// structのフィールドにTaskRepositoryInterfaceを持たせる
// こうすることで、CreateTaskメソッドでDBへの保存処理を呼ぶことができる
type TaskUsecase struct {
	repo TaskRepositoryInterface
}

func NewTaskUsecase(repo TaskRepositoryInterface) TaskUsecase {
	return TaskUsecase{repo: repo}
}

func (usecase TaskUsecase) CreateTask(title string) (Task, error) {
	t := Task{Title: title}
	task, err := usecase.repo.Save(t)
	return task, err
}

DBへの保存処理を担当するTaskRepository(task_repository.go)は

package main

type TaskRepository struct{}

// TaskRepositoryInterfaceを満たすようにSaveメソッドを実装
func (repo TaskRepository) Save(t Task) (Task, error) {
	// 実際にはDBへの保存処理を行う
	task := Task{
		ID: 1,
		Title: t.Title,
	}
	return task, nil
}

最後にmain関数(main.go

package main

import "fmt"

func main() {
	repo := TaskRepository{}
	usecase := NewTaskUsecase(repo)
	task, _ := usecase.CreateTask("DIの勉強")
	fmt.Printf("ID: %d, Title: %s\n", task.ID, task.Title) // => ID: 1, Title: DIの勉強
}

解説

ポイントは、TaskRepositoryInterfaceです。
TaskUsecaseのフィールドにTaskRepositoryInterfaceをもたせて、interface経由でSaveメソッドを呼び出しています。

// Saveメソッドの実装はTaskRepositoryで行っている
type TaskRepositoryInterface interface {
	Save(Task) (Task, error)
}

// structのフィールドにTaskRepositoryInterfaceを持たせる
// こうすることで、CreateTaskメソッドがinterface経由でSaveメソッド(DBへの保存処理)を呼ぶことができる
type TaskUsecase struct {
	repo TaskRepositoryInterface
}

TaskRepositoryInterfaceの実装(ここではSaveメソッドの実装)はTaskRepositoryで行っています。

type TaskRepository struct{}

// TaskRepositoryInterfaceを満たすようにSaveメソッドを実装
func (repo TaskRepository) Save(t Task) (Task, error) {
// 実際にはDBへの保存処理を行う
	task := Task{
		ID: 1,
		Title: t.Title,
	}
	return task, nil
}

このようにInterfaceを定義することで、TaskUsecaseはTaskRepositoryに依存せずにDBへの保存処理を呼び出せます。

実際、TaskUsecaseTaskRepositoryことを知らないけれど、CreateTaskメソッド内でDBへの保存処理を呼び出せています。

func (usecase TaskUsecase) CreateTask(title string) (Task, error) {
	t := Task{Title: title}
	task, err := usecase.repo.Save(t)
	return task, err
}

ここでusecase.repo.Save(t) ってやってんじゃん。「TaskRepositoryに依存していない?」と思う人もいるでしょう。(私がそうでした)

しかし、それは違います。usecase.repoTaskRepositoryInterfaceです。

依存しているのはTaskRepositoryInterfaceに対してです。

図にすると次のようになっています。

dependency injection

 

つまり、TaskUsecaseとしては、Saveメソッドをもつオブジェクトがあれば良いので、TaskRepositoryに変更が入ってもSaveメソッドさえあればTaskUsecaseに手を加えなくて済むことになります!!

参考

イチオシ記事

1

自己紹介 フリーランスエンジニアをしているヨノと申します。 独学でプログラミングを学び、ソシャゲ・SaaS開発などを経て、2018年からフリーランスエンジニアとして活動しています。 主にバックエンド中 ...

2

はじめまして、フリーランスエンジニアのヨノと申します。 自己紹介 独学でプログラミングを学び、ソシャゲ・SaaS開発などを経て、2018年からフリーランスエンジニアとして活動しています。 主にバックエ ...

3

ネット上で色々言われているフリーランスエンジニア....。「本当はどうなの?」と思っている人は多いでしょう。 そこで本記事ではフリーランスエンジニア5年生の私が、ネット上の意見も引用しながら実態を解説 ...

-Go言語