6
2

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.

アプリケーション開発において、ある処理を実装するときに、複数のコンポーネントを組み合わせて実装することがよくあると思います。DIとは、ソフトウェアのデザインパターンの1つで、制御の反転を実現します。コンポーネントの作成時に依存するコンポーネントを注入する考えです。

google/wire を用いて DI を実現する例を考えてみます。

Wire が作られた背景などは Compile-time Dependency Injection With Go Cloud's Wire が詳しいです。

ナイーブな実装

例として、以下のような構造を持つ、ユーザのパスワードを登録するアプリケーションを考えてみます。
ユーザから、名前とパスワードをうけとって、パスワードをハッシュ化して、永続化する...というシーンを想定します。

Wire.png

ナイーブな実装では、構造体を生成( NewUserService )する際に依存する構造体を生成しています。依存する構造体を密結合しており、結合度の高いコンポーネントと言えます。

main.go
package main

import (
	"fmt"
	"unicode/utf8"
)

type User struct {
	name     string
	password string
}

type UserService struct {
	passwordEncoder *PasswordEncoder
	userRepository  *UserRepository
}

func NewUserService() *UserService {
	p := &PasswordEncoder{}
	r := &UserRepository{}
	return &UserService{
		passwordEncoder: p,
		userRepository:  r,
	}
}

func (service *UserService) Register(user *User) {
	user.password = service.passwordEncoder.Encode(&user.password)
	service.userRepository.Save(user)
}

type PasswordEncoder struct{}

func (p *PasswordEncoder) Encode(s *string) string {
	n := utf8.RuneCountInString(*s)
	c := make([]rune, n)
	for i := 0; i < n; i++ {
		c[i] = '*'
	}
	return string(c)
}

type UserRepository struct{}

func (r *UserRepository) Save(user *User) {
	fmt.Printf("Completed save [User : %s, Password : %s]\n", user.name, user.password)
}

func main() {
	u := &User{
		name:     "John",
		password: "pass",
	}
	service := NewUserService()
	service.Register(u)
}

手作業による依存関係の注入

ナイーブな実装では、構造体を生成する際に依存する構造体を生成していました。ナイーブな実装をDIで疎結合にします。以下の例はセッターインジェクションを用いて手動で依存関係の構造体を注入する例です。

main.go
package main

import (
	"fmt"
	"unicode/utf8"
)

type User struct {
	name     string
	password string
}

type UserService struct {
	passwordEncoder *PasswordEncoder
	userRepository  *UserRepository
}

-func NewUserService() *UserService {
+func NewUserService(p *PasswordEncoder, r *UserRepository) *UserService {
-	p := &PasswordEncoder{}
-	r := &UserRepository{}
	return &UserService{
		passwordEncoder: p,
		userRepository:  r,
	}
}

func (service *UserService) Register(user *User) {
	user.password = service.passwordEncoder.Encode(&user.password)
	service.userRepository.Save(user)
}

type PasswordEncoder struct{}

func (p *PasswordEncoder) Encode(s *string) string {
	n := utf8.RuneCountInString(*s)
	c := make([]rune, n)
	for i := 0; i < n; i++ {
		c[i] = '*'
	}
	return string(c)
}

type UserRepository struct{}

func (r *UserRepository) Save(user *User) {
	fmt.Printf("Completed save [User : %s, Password : %s]\n", user.name, user.password)
}

func main() {
	u := &User{
		name:     "John",
		password: "pass",
	}
-	service := NewUserService()
+	p := &PasswordEncoder{}
+	r := &UserRepository{}
+	service := NewUserService(p, r)
	service.Register(u)
}

疎結合になったおかげで、プログラムを実行している環境によって、異なる実装を提供することができます。例えば、本例だとパスワードのエンコードのアルゴリズムを別のアルゴリズムに差し替える構造体に変更する、あるいは、ユーザーデータの永続化にSQLiteのデータベースを用いたり、といったようにです。

ただし、構造体を利用する main.go は複雑になりました。また、構造体間の依存関係グラフ内のすべての関係を認識する必要があります。小規模のプロジェクトの場合は問題にならないですが、大規模プロジェクトでは認識の負担が大きくなります。メンテナンスのコストも増加するでしょう。

WireによるDI

DIコンテナは、DIの疎結合な利点を保持しながら、手動で依存関係注入をしていた問題を解決します。Wireを用いてDIコンテナを実現します。

依存関係を手動で解決していた実装を container.go に切り出します(名称は任意)。wire.Build で必要なのは providers を用いてオブジェクトの構成を宣言することです。Wire では Injectors と呼ばれています。具体的なオブジェクトの依存関係を宣言する必要はありません。wire.Build の引数に指定する関数は関数名のみでよく、関数のシグネチャも柔軟に変更することができます。

container.go
//+build wireinject

package main

import "github.com/google/wire"

func CreateUserService() *UserService {
	wire.Build(
		wire.Struct(new(PasswordEncoder)),
		wire.Struct(new(UserRepository)),
		NewUserService,
	)
	return nil
}

wire コマンドを用いてコードを生成します。

> go get github.com/google/wire/cmd/wire
> wire

そうすると、以下のようなファイルが生成されることが分かります。手動でDIしていたような実装が明示的に生成されていることが分かります。また生成されたコードは Wire に依存しません。一度 wire_gen.go を生成すれば、次回以降は go generate を実行することで再生成されます。

wire_gen.go
// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

// Injectors from container.go:

func CreateUserService() *UserService {
	passwordEncoder := &PasswordEncoder{}
	userRepository := &UserRepository{}
	userService := NewUserService(passwordEncoder, userRepository)
	return userService
}

main.go の実装は自動生成された関数を呼び出すだけになります。

main.go
package main

import (
	"fmt"
	"unicode/utf8"
)

type User struct {
	name     string
	password string
}

type UserService struct {
	passwordEncoder *PasswordEncoder
	userRepository  *UserRepository
}

func NewUserService(p *PasswordEncoder, r *UserRepository) *UserService {
	return &UserService{
		passwordEncoder: p,
		userRepository:  r,
	}
}

func (service *UserService) Register(user *User) {
	user.password = service.passwordEncoder.Encode(&user.password)
	service.userRepository.Save(user)
}

type PasswordEncoder struct{}

func (p *PasswordEncoder) Encode(s *string) string {
	n := utf8.RuneCountInString(*s)
	c := make([]rune, n)
	for i := 0; i < n; i++ {
		c[i] = '*'
	}
	return string(c)
}

type UserRepository struct{}

func (r *UserRepository) Save(user *User) {
	fmt.Printf("Completed save [User : %s, Password : %s]\n", user.name, user.password)
}

func main() {
	u := &User{
		name:     "John",
		password: "pass",
	}
-	p := &PasswordEncoder{}
-	r := &UserRepository{}
-	service := NewUserService(p, r)
+	service := CreateUserService()
	service.Register(u)
}

以下のように動作することを確認できました。

go build -o sample.exe
sample.exe

Completed save [User : John, Password : ****]

Tips

自動生成以外にもいくつか wire コマンドがあって、一覧は wire commands で確認できます。

> wire commands
commands
flags
help
check
diff
gen
show

所感

DIといえば、動的に依存関係を解決するようなアプローチを主に把握していました。Wire のように依存関係を解決するコードを明示的に生成するアプローチは素朴ですがシンプルで Go らしいと感じました。

6
2
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
6
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?