27
5

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.

uber-go/digを触りました。

Last updated at Posted at 2019-12-01

はじめに

こんにちは。今回Media Do Advent Calendar 2日目を担当する新卒2年目のおかざわです。
社内でubar-go/digを勉強する機会があったので、その時のまとめになります。
参考文献が少なく公式ドキュメントを読み漁りながら作りました。
分かりやすさ重視なので内容はそこまでですが、暖かい目で見てやってください。

概要

パッケージdigは、オブジェクトの依存関係を解決するための意見に基づいた方法を提供します。

uber-go/digのdocのgoogle翻訳より

ぱっと見よくわからないですが、下記のコードのfunc main() のように依存関係を構築するコードが、uber-go/digを使うことで少し楽に書くことができるようになります。

package main

import (
	"fmt"

	"go.uber.org/dig"
)

type Usecase interface {
	Use()
}

type Repository interface {
	RepoPrint()
}

type usecase struct {
	repo Repository
}

func NewUsecase(r Repository) Usecase {
	return &usecase{
		repo: r,
	}
}
func (u usecase) Use() {
	u.repo.RepoPrint()
}

type repository struct{}

func NewRepository() Repository {
	return &repository{}
}

func (r repository) RepoPrint() {
	fmt.Println("1")
}

func main() {
	// 通常の依存関係構築
	repo := NewRepository()
	usecase := NewUsecase(repo)
	usecase.Use()
}

インストール

go get go.uber.org/dig@v1

go modulesをしている前提のインストールです。
それ以外はubar-go/digを参照ください。

使う

概要で軽く触れましたが例で挙げたコードのmainの部分(下記の部分)だけ変更します。
(importも勝手に追加されます)

func main() {
	// 通常の依存関係構築
	repo := NewRepository()
	usecase := NewUsecase(repo)
	usecase.Use()
}

uber-go/digを使うと下記になります。

func main() {
	c := dig.New()
	c.Provide(NewUsecase)
	c.Provide(NewRepository)
	c.Invoke(func(u Usecase) {
		u.Use()
	})
}

一つずつ見ていきます。

コンテナ作成

c := dig.New()

dig.New()でコンテナを生成します。
uber-go/digでは「コンテナ」と言う依存関係になる構造体を入れる用のインスタンスを作成し処理します。

オブジェクト登録

c.Provide(NewUsecase)

Provide()は先ほど作成したコンテナにオブジェクト(構造体)を登録します。
引数に指定しているものはファンクションで、上記のように既存のファンクションを指定するか、下記のようにファンクションをその場で作成する方法の2通りのの方法があります。

c.Provide(func() Repository {
	return &repository{}
})

Provide(func(引数)戻り値{処理})で構築できます。

オブジェクト構築/実行

c.Invoke(func(usecase DogUsecase) {
	fmt.Println(usecase.All())
})

Invoke()は、コンテナに登録したオブジェクト達をよしなに解決して、指定したインターフェースの処理を実行することができます。
生成されるのはInvoke()内のみなのでInvoke()の外で実行することはできません。

以上が例題をuber-go/digで処理してみた結果です。

美味しい?

uber-go/digで依存関係を構築することができることがわかりました。
ですが、上記の例ではあまりuber-go/digを使う美味しさがわからなかったと思います。

もう少しuber-go/digについて掘り下げて行きます。

dig.In

下記の例を使って説明します。

最低限機能しか使っていないuber-go/digの例
package main

import (
	"fmt"

	"go.uber.org/dig"
)

type Usecase interface {
	use()
}

type FooRepository interface {
	foo()
}

type BarRepository interface {
	bar()
}

type usecase struct {
	Foo FooRepository
	Bar BarRepository
}

func (u *usecase) use() {
	u.Foo.foo()
	u.Bar.bar()
}

type fooRepo struct {
	f string
}

func (u *fooRepo) foo() { fmt.Println(u.f) }

type barRepo struct {
	b string
}

func (u *barRepo) bar() { fmt.Println(u.b) }

func main() {

	c := dig.New()
	c.Provide(func() FooRepository { return &fooRepo{f: "Foo"} })
	c.Provide(func() BarRepository { return &barRepo{b: "Bar"} })
	c.Provide(func(f FooRepository, b BarRepository) Usecase {
		return &usecase{
			Foo: f,
			Bar: b,
		}
	})
	c.Invoke(func(u Usecase) {
		u.use()
	})
}

Invoke(func(u Usecase))を実行するとusecaseが生まれ、usecaseに定義されている
FooRepositoryBarRepositoryを満たすfooRepobarRepoも自動的に生成/関連付けされます。

下記部分はusecaseの生成方法の登録を行っています。

c.Provide(func(f FooRepository, b BarRepository) Usecase {
	return &usecase{
		Foo: f,
		Bar: b,
	}
})

例ではFooRepositoryBarRepositoryの2つしか引数に取っていないが、実装によっては何個も定義を書く必要があり、可読性を大きく損なう可能性があります。(引数4つ以上あると、もう見辛い)
uber-go/digではこの解決方法として「パラメータオブジェクト」を使用します。

このような状況で読みやすくするために使用されるパターンは、関数のすべてのパラメーターをフィールドとしてリストする構造体を作成し、代わりにその構造体を受け入れるように関数を変更することです。これは、パラメータオブジェクトと呼ばれます。

uber-go/digのdocのgoogle翻訳より

上記の通り、パラメータ(引数とか)をフィールドとした構造体を定義し、関数にはその構造体を受け入れるように変更を加えることで可読性の低下を阻止します。
上記の例の中に下記の構造体を追加し、関数を下記の構造体を受け入れるように変更します。

type inUsecase struct {
	dig.In
	Foo FooRepository
	Bar BarRepository
}
c.Provide(func(u inUsecase) Usecase {
	return &usecase{
		Foo: u.Foo,
		Bar: u.Bar,
	}
})

dig.Inを宣言することで「パラメータオブジェクト」になります。
パラメータが少ない場合は無理にdig.Inする必要は無いですが、複数パラメータを宣言しなければいけない時は便利だと思います。

`dig.In`を追加した場合の全体像
package main

import (
	"fmt"

	"go.uber.org/dig"
)

type Usecase interface {
	use()
}

type FooRepository interface {
	foo()
}

type BarRepository interface {
	bar()
}

// dig.Inが定義されているオブジェクト
type inUsecase struct {
	dig.In
	Foo FooRepository
	Bar BarRepository
}

type usecase struct {
	Foo FooRepository
	Bar BarRepository
}

func (u *usecase) use() {
	u.Foo.foo()
	u.Bar.bar()
}

type fooRepo struct {
	f string
}

func (u *fooRepo) foo() { fmt.Println(u.f) }

type barRepo struct {
	b string
}

func (u *barRepo) bar() { fmt.Println(u.b) }

func main() {

	// dig.Inを使った場合
	c := dig.New()
	c.Provide(func() FooRepository { return &fooRepo{f: "Foo"} })
	c.Provide(func() BarRepository { return &barRepo{b: "Bar"} })
	// dig.Inが定義されている構造体を指定することにより可読性の低下を抑える
	c.Provide(func(u inUsecase) Usecase {
		return &usecase{
			Foo: u.Foo,
			Bar: u.Bar,
		}
	})
	c.Invoke(func(u Usecase) {
		u.use()
	})
}

nameオプション

uber-go/digは依存関係を「よしな」にしてくれるすごいライブラリです。
ですが、下記の例の様にdig.Inに重複する型がある場合「よしな」が発動しません。

「よしな」にならない例

package main

import (
	"fmt"

	"go.uber.org/dig"
)

type Usecase interface {
	use()
}

type FooBarRepository interface {
	foobar()
}

type inUsecase struct {
	dig.In
	FB1 FooBarRepository
	FB2 FooBarRepository
}

type usecase struct {
	FB1 FooBarRepository
	FB2 FooBarRepository
}

func (u *usecase) use() {
	u.FB1.foobar()
	u.FB2.foobar()
}

type foobarRepo struct {
	fb string
}

func (u *foobarRepo) foobar() { fmt.Println(u.fb) }

func main() {

	c := dig.New()
	c.Provide(func() FooBarRepository { return &foobarRepo{fb: "Foo"} })
	c.Provide(func() FooBarRepository { return &foobarRepo{fb: "Bar"} })
	// inUsecaseは上記2つのFooBarRepositoryの内、先に宣言された"Foo"のみを取得する
	c.Provide(func(u inUsecase) Usecase {
		return &usecase{
			FB1: u.FB1, // Foo
			FB2: u.FB2, // Foo
		}
	})
	c.Invoke(func(u Usecase) {
		u.use()
	})
}

コード内にも書いていますが、先に宣言された「Foo」しか表示されません。
このように同じ型を宣言してしまった場合の回避方法としてnameオプションを使用することができます。
パラメータに対してnameを宣言することにより、登録時に同じnameのオブジェクトが関連付けされるようになります。

type inUsecase struct {
	dig.In
	FB1 FooBarRepository `name:"Foo"`
	FB2 FooBarRepository `name:"Bar"`
}

dig.Name()を登録時に第二引数で指定することにより、オブジェクトにnameが付きます。

c.Provide(func() FooBarRepository { return &foobarRepo{fb: "Foo"} }, dig.Name("Foo"))
c.Provide(func() FooBarRepository { return &foobarRepo{fb: "Bar"} }, dig.Name("Bar"))

実行すると
Foo
Bar
と表示され、期待通りの関連付けができます。

`name`オプションを追加した場合の全体像
package main

import (
	"fmt"

	"go.uber.org/dig"
)

type Usecase interface {
	use()
}

type FooBarRepository interface {
	foobar()
}

type inUsecase struct {
	dig.In
	FB1 FooBarRepository `name:"Foo"`
	FB2 FooBarRepository `name:"Bar"`
}

type usecase struct {
	FB1 FooBarRepository
	FB2 FooBarRepository
}

func (u *usecase) use() {
	u.FB1.foobar()
	u.FB2.foobar()
}

type foobarRepo struct {
	fb string
}

func (u *foobarRepo) foobar() { fmt.Println(u.fb) }

func main() {

	c := dig.New()
	c.Provide(func() FooBarRepository { return &foobarRepo{fb: "Foo"} }, dig.Name("Foo"))
	c.Provide(func() FooBarRepository { return &foobarRepo{fb: "Bar"} }, dig.Name("Bar"))
	c.Provide(func(u inUsecase) Usecase {
		return &usecase{
			FB1: u.FB1,
			FB2: u.FB2,
		}
	})
	c.Invoke(func(u Usecase) {
		u.use()
	})
}

dig.Out

goは複数戻り値を得ることができる言語です。
Provideでオブジェクトを登録するときも一つのProvide内で複数のオブジェクトを登録することができます。
あまり使用する機会は無いかもしれませんが、沢山オブジェクトを生成するコード = 戻り値が沢山あるコードとなり、可読性を損ないます。(引数と同じで戻り値も沢山あると見辛い)
戻り値もパラメータオブジェクト化することができます。

上記にある「nameオプションを追加した場合の全体像」を例として使います。

例に下記の構造体を追加し、FooBarRepositoryを登録している部分をパラメータオブジェクトを受け入れる様に変更します。

type outUsecase struct {
	dig.Out
	FB1 FooBarRepository `name:"Foo"`
	FB2 FooBarRepository `name:"Bar"`
}
c.Provide(func() outUsecase {
	return outUsecase{
		FB1: &foobarRepo{fb: "Foo"},
		FB2: &foobarRepo{fb: "Bar"},
	}
})

dig.Outを使うことにより複数オブジェクトを登録することができたり、nameオプションの宣言をdig.Out内で行うことができます。
nameオプションなどをmain内に書かなくてよくなるのは良いと思いました。

dig.Outを追加した場合の全体像
package main

import (
	"fmt"

	"go.uber.org/dig"
)

type Usecase interface {
	use()
}

type FooBarRepository interface {
	foobar()
}

type inUsecase struct {
	dig.In
	FB1 FooBarRepository `name:"Foo"`
	FB2 FooBarRepository `name:"Bar"`
}

// dig.Inの反対でdig.Outはオブジェクトの出力の形式を宣言する
// dig.Outに`name:`を宣言することでProvide()の中で宣言しなくてよくなる
type outUsecase struct {
	dig.Out
	FB1 FooBarRepository `name:"Foo"`
	FB2 FooBarRepository `name:"Bar"`
}

type usecase struct {
	FB1 FooBarRepository
	FB2 FooBarRepository
}

func (u *usecase) use() {
	u.FB1.foobar()
	u.FB2.foobar()
}

type foobarRepo struct {
	fb string
}

func (u *foobarRepo) foobar() { fmt.Println(u.fb) }

func main() {

	// dig.Outを使った場合
	c := dig.New()
	// dig.Outを使うことにより複数オブジェクトを登録することができたり
	// nameオプションの宣言をdig.Out内で行うことができる
	c.Provide(func() outUsecase {
		return outUsecase{
			FB1: &foobarRepo{fb: "Foo"},
			FB2: &foobarRepo{fb: "Bar"},
		}
	})
	c.Provide(func(u inUsecase) Usecase {
		return &usecase{
			FB1: u.FB1,
			FB2: u.FB2,
		}
	})
	c.Invoke(func(u Usecase) {
		u.use()
	})
}

optionalオプション

uber-goにはその構造体が「あっても無くてもOK」を実装することができます。
それがoptionalオプションです。
optionalオプションがついているパラメータは依存関係構築(Invoke)時に
対象となるオブジェクトがあれば使い、
対象となるオブジェクトがなければ「ゼロ値」を生成します。

`optional`オプションの実装
package main

import (
	"fmt"

	"go.uber.org/dig"
)

type Usecase interface {
	use()
}

type FooBarRepository interface {
	foobar()
}

type inUsecase1 struct {
	dig.In
	FB1 FooBarRepository `name:"Foo"`
	FB2 FooBarRepository `name:"Bar"`
}

// dig.Inに`optional:"true"`を宣言することにより
// 取得できなかった時にその型のゼロ値を取得する
type inUsecase2 struct {
	dig.In
	FB1 FooBarRepository `name:"Foo" optional:"true"`
	FB2 FooBarRepository `name:"Bar" optional:"true"`
}

// 今回は使用しない
type outUsecase struct {
	dig.Out
	FB1 FooBarRepository `name:"Foo"`
	FB2 FooBarRepository `name:"Bar"`
}

type usecase struct {
	FB1 FooBarRepository
	FB2 FooBarRepository
}

func (u *usecase) use() {
	// optional:"true"が宣言されており、登録オブジェクトが無い場合はゼロ値で処理される
	if u.FB1 != nil {
		u.FB1.foobar()
	} else {
		fmt.Println("usecase FB1 is nil")
	}

	if u.FB2 != nil {
		u.FB2.foobar()
	} else {
		fmt.Println("usecase FB2 is nil")
	}
}

type foobarRepo struct {
	fb string
}

func (u *foobarRepo) foobar() { fmt.Println(u.fb) }

func main() {

	// optionalを用いることで「あっても無くても良いオブジェクト」を宣言することができる
	// optional:"true"が宣言されていなかった場合
	c1 := dig.New()
	// オブジェクトを登録しない
	c1.Provide(func(u inUsecase1) Usecase {
		return &usecase{
			FB1: u.FB1,
			FB2: u.FB2,
		}
	})
	// 実行時にエラーがでる
	err1 := c1.Invoke(func(u Usecase) {
		u.use()
	})
	if err1 != nil {
		fmt.Printf("Invoke error :\n%s\n\n", err1)
	}

	// optional:"true"が宣言されていた場合
	c2 := dig.New()
	// オブジェクトを登録しない
	c2.Provide(func(u inUsecase2) Usecase {
		return &usecase{
			FB1: u.FB1,
			FB2: u.FB2,
		}
	})
	// ゼロ値が登録されて実行される(構造体ならnil)
	err2 := c2.Invoke(func(u Usecase) {
		u.use()
	})
	if err2 != nil {
		fmt.Printf("Invoke error :\n%s", err2)
	}
}

optional:"true"を宣言していない方(inUsecase1)は依存関係構築時にエラーを吐きます。
optional:"true"を宣言している方(inUsecase2)は依存関係構築時にゼロ値のオブジェクトを生成し、u.use()を実行しています。(構造体のゼロ値はnil)
あらかじめoptionalのパラメータを受け取る関数内では、ゼロ値かどうかの判定を作る必要があります。

むやみやたらにoptionalを使うとバグの原因になりそうですが、「あっても無くてもOK」を簡単に実装できるのは(あまり使わないと思いますが)便利です。

groupオプション

groupオプションはuber-go/digのバージョン1.2から追加された機能です。
dig.Indig.Out両方に宣言する必要があるオプションで、dig.Outされたオブジェクトをdig.Inにスライスで渡します。(複数オブジェクトを渡すことができる)

`group`オプションの実装
package main

import (
	"fmt"

	"go.uber.org/dig"
)

type Usecase interface {
	use()
}

type FooBarRepository interface {
	foobar()
}

// dig.Inのgroupオプションの宣言
type inUsecase struct {
	dig.In
	FB []FooBarRepository `group:"foobar"`
}

// dig.Outのgroupオプションの宣言
type outUsecase struct {
	dig.Out
	FB FooBarRepository `group:"foobar"`
}

// dig.Inから受け取る構造体もスライスにする必要がある
type usecase struct {
	FB []FooBarRepository
}

// 値の取り出しはfor文で取り出すことができる
// ただし取り出す順番をdigは保証しないらしい
// 実際実行するたびに順番が変わる
func (u *usecase) use() {
	for _, fb := range u.FB {
		fb.foobar()
	}
}

type foobarRepo struct {
	fb string
}

func (u *foobarRepo) foobar() { fmt.Println(u.fb) }

func main() {

	// groupオプションがついているdig.Outを使用すると
	// dig.Inについているgroupのスライスで受け取ることができる
	// ただし、digライブラリはスライスに登録される順番を保証しない
	c := dig.New()
	c.Provide(func() outUsecase { return outUsecase{FB: &foobarRepo{fb: "Foo"}} })
	c.Provide(func() outUsecase { return outUsecase{FB: &foobarRepo{fb: "Foo"}} })
	c.Provide(func() outUsecase { return outUsecase{FB: &foobarRepo{fb: "Bar"}} })
	c.Provide(func() outUsecase { return outUsecase{FB: &foobarRepo{fb: "Bar"}} })
	c.Provide(func(u inUsecase) Usecase {
		return &usecase{
			FB: u.FB,
		}
	})
	c.Invoke(func(u Usecase) {
		u.use()
	})
}

dig.Outgroupオプションを作成し、dig.Inにはdig.Outと同じ名前のgroupオプションをスライス型で定義します。
dig.Inを受け取るオブジェクト(usecase)もスライスで定義し、実行する時にfor文などでオブジェクトを取得します。
コード内にも書いていますが、digはgroupオプションで登録される順番を保証しません。
このプログラムを何度も動かしたらわかりますが、実行するたびに「Foo」「Bar」の順番が変わります。

同じ型のオブジェクトをゴルーチンを用いて並列処理する時とかに使うと便利そうだ思いました。

その他

dig.IsIn() で対象の構造体がdig.Inかどうか判定できる。

dig.IsOut() で対象の構造体がdig.Outかどうか判定できる。

**コンテナ.String()**で コンテナに登録されている情報を確認する ことができる 。
↑結構重要

コンテナ.Provide(),コンテナ.Invoke() は戻り値で エラー をとることができる。
↑結構重要

感想

uber-go/digを使うことにより依存関係の構築を簡単にすることができる。
ただ、ドキュメントが少なかったし、ポインタ周りでかなりハマったりしたので疲れた。
良い勉強になったと思う。
あとマークダウンを書くときに

このパカパカ

パカパカパカパカパカパカパカパカパカパカパカパカパカパカパカパカパカパカパカパカ

<details>
    <summary>開閉オブジェクト</summary>
    <div>
        中身
    </div>
</details>

パカパカパカパカパカパカパカパカパカパカパカパカパカパカパカパカパカパカパカパカ

が便利だと思った。

以上。

参考文献

doc :https://godoc.org/go.uber.org/dig
github :https://github.com/uber-go/dig
https://int128.hatenablog.com/entry/2019/01/28/143158
https://tech.raksul.com/2019/06/21/golang-favorite-di-tool/

27
5
0

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
27
5

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?