クリーンアーキテクチャの学習をしていて、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への保存処理を呼び出せます。
実際、TaskUsecase
はTaskRepository
ことを知らないけれど、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.repo
はTaskRepositoryInterface
です。
依存しているのはTaskRepositoryInterfaceに対してです。
図にすると次のようになっています。
つまり、TaskUsecase
としては、Save
メソッドをもつオブジェクトがあれば良いので、TaskRepositoryに変更が入ってもSaveメソッドさえあればTaskUsecaseに手を加えなくて済むことになります!!