Help us understand the problem. What is going on with this article?

【必須科目 DI】DIの仕組みをGoで実装して理解する

はじめに

クリーンアーキテクチャ、オニオンアーキテクチャなど色々概念が提唱されているが、
私はその勉強をして挫けてしまった。
というのもそもそもDIとか依存についてよくわかってなかったからだ。
そう、勉強の順番を間違えていた。まーそれで気づけたからよかったよかった。

そこで、DIについて出来るだけ詳しくわかりやすくまとめてみた。
この記事を読めば、必ずDIのメリットを理解し、実装できるようになる

DIとは?

DIはDependency Injection(オブジェクトの注入)の略称のこと
※依存性の注入ともいうが、オブジェクトの注入として覚えてもらいたい
処理に必要なオブジェクトを外部から注入できるようにするデザインパターン

DIのデザインパターンを簡単に説明すると、
オブジェクトをインタフェースとして定義し、使う側は実装オブジェクトでなく、インタフェースを利用するようにする。
実装オブジェクトは外部からそのインタフェースに外部から注入する事で、実装を入れ替えたりできる。

DIコンテナとは?

DIコンテナはDI実現をお手伝いするためのフレームワーク
※ 機会があれば、これも実際に実装して紹介する予定だ。

言葉の定義

学んでいく中で、よくわからない言葉があり、すごく混乱した。
なのでまずは言葉の定義を合わせよう。

・外部からってどこだよ?
 他のオブジェクトからだよ!

・依存ってなんだよ?
 Aオブジェクトが必要なBオブジェクトのこと
 言い換えると、AオブジェクトがBオブジェクトの中身を知っている状態
 とりあえずは、Newしてたら依存していると思ってもいいかも

・依存しないって言ってるけど、依存してね?
 BオブジェクトがAインタフェースに注入されたAオブジェクト使ってんじゃん!!
 これ依存だろ?だろ?ってずっと思っていた。ここが一番頭を悩ませた。
 正確には、BオブジェクトはAインタフェースに依存している。
 なのでBオブジェクトはAオブジェクトには依存していない。(抽象に依存と言ったりする)
 これの何がいいかというと、BオブジェクトはAオブジェクトを知らずに実装できる。
 詳しくは後述する。
 

DIを使う事でハッピーになれる事

「なんのために?DIを使うのか」ここが一番大事です。
これを知らずDIを使っても使えてると思えない。
常に「なんのために」その手法を使うのかの目的を意識することが重要である。
それではDIを使う事でハッピーになれる事を書いていく。

・外部のDBに依存しないので、変更に強くなる。
 理由:インタフェースを通して実装オブジェクトを注入するので、
 インタフェースを使うロジック側は実装オブジェクトを意識せずに使用できる。

・ユニットテストがしやすくなる
 理由:上で言ってることと同じだが、DBをモック(偽DBみたいな)することで
 インタフェースを使う側のロジックは変更せずにテストができる。
 結果二つとも、外部を知らないから(依存しない)切り替えても問題という事だ

・単にコーディングがしやすくなる。
 理由:AとBのクラスがあるとしよう。AはBを必要(依存)としている。
 その場合、BがなければAを実装できない。
 だが、AがBのインタフェースを使った実装をすれば、Bがなくても実装できる。
 インタフェースを通してBオブジェクトを注入すれば、AがBを使ってることと同じことになる。

コードを見て違いを認識する

ここまででDIの概念や仕組みについて説明してきた。
ここからは実際のコードを見て理解していく。
最初は理解出来なくても何度も読めば理解できるはず!実際に私がそうだった。

作成するコードは、以下の3つからできてるものを作る。
・repository層・・・DBと接続するための層
・service層・・・repositoryを呼びだす層
・呼び出し層・・・全体を操作する層

依存関係のあるコード

最初に依存関係のあるぐちゃぐちゃコードを見てみる。
(※プログラミングを始めたてはこんなコード書いてたと思う。。)

repository層
作成したDBを直接UserRepositoryに与えている。
(悪いところ:repository層がdbに依存している。mysqlと知っている)

package repository

type UserRepository struct {
    db sql.DB
}

func NewUserRepository() *UserRepository {
    // DB作成
    db, _ := sql.Open("mysql", "root:@/database")
    // 作成したDBを直接repositoryに
    return &UserRepository{*db}
}

service層
repositoryをnewして直接serviceに与えている。
(悪いところ:service層がrepositoryに依存している。)

package service

type UserService struct {
    ur repository.UserRepository
}

func NewUserService() *UserService {
    // リポジトリ作成
    ur := repository.NewUserRepository()
    // 作成したリポジトリを直接serviceに
    return &UserService{*ur}
}


呼び出し層
serviceをnewすれば、repositoryとdbも決まったものが作成される。

package main

func main() {

    us := service.NewUserService()
    // usを使ってゴニョゴニョ
}

一旦悪いコードを見てきた。
すごく依存していることがわかるかと思う。
念のため再確認だが、依存とはあるオブジェクトが、あるオブジェクトの中身を知っている状態

DIを導入したコード

ここからはDIを導入した大変美しいコードを見ていく。
(※初めてDIが使われているコードを見たときは、すごく汚く見えが。。。)

コードを見る前に一つ重要な事を忘れていた。
DIを実現するには4つの方法がある。
1.Constructor Injection
2.Setter Injection
3.Interface Injection
4.Field Injection

今回は1のConstructor Injectionを使っていく。
自分の中では、これが一番いいと思っている。

repository層
NewUserRepositoryではコンストラクタにdbをとっている。
そのため、dbに依存していない。
また、returnではインタフェースを返している。
これによってrepository層とservice層の依存関係も解消される。

package repository

type UserRepository interface {
    CreateUser(ctx context.Context, user *model.User)
}

type userRepository struct {
    db sql.DB
}

func NewUserRepository(db *sql.DB) UserRepository {
    return &userRepository{*db}
}

func (ur *userRepository) CreateUser(ctx context.Context, user *model.User) {
    ur.db.Query("INSERT INTO テーブル名(列名1,列名2,……)")
}

service層
NewUserServiceではコンストラクタにリポジトリのインタフェースをとっている。
そのため、リポジトリに依存していない。
また、returnではインタフェースを返している。
これによってservice層と使う側の層の依存関係も解消される。

package service

type UserService interface {
    CreateUser(ctx context.Context, user *model.User)
}

type userService struct {
    ur repository.UserRepository
}

func NewUserService(ur repository.UserRepository) UserService {
    return &userService{ur}
}

func (us *userService) CreateUser(ctx context.Context, user *model.User) {
    us.ur.CreateUser(ctx, user)
}

使う側の層
ur := repository.NewUserRepository(db)
dbはどんなdbであれば、なんでも受け付けてもらえる。

us := service.NewUserService(ur)
ur(リポジトリ)はリポジトリでさえあれば、どんなリポジトリでも受け付けてもらえる。

これによってテストの時には他のリポジトリやDBに入れ替えたりするのが用意で、
お互いを知らないから依存関係がなく変更が容易にできる。

(※説明の都合上、dbの作成をmainに書いています)

package main

func main() {
    db, err := sql.Open("mysql", "root:@/database")
    if err != nil {
        panic(err.Error())
    }
    defer db.Close()

    ur := repository.NewUserRepository(db)
    us := service.NewUserService(ur)

    // usを使ってゴニョゴニョ
}

まとめ

DIについて、説明してきた。
実際に実装したり、こうやって記事にまとめる事で、
自分の頭の中がすごくスッキリした。
また、理解したつもりでも案外実装するとなると出来なかったりするものだと感じだ。
ぜひDIを勉強中の人は、実際に自分で実装して見てほしい。

参考文献

https://recruit-tech.co.jp/blog/2017/12/11/go_dependency_injection/
https://github.com/akito0107/dicon
http://inukirom.hatenablog.com/entry/di-in-go

yoshinori_hisakawa
Go言語/クリーンアーキテクチャ/DDD/Docker/OOP/Angular/Java/設計/ Fringe81に興味ある方は、メールください! yoshinori_hisakawa@fringe81.com
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした