アプリケーション開発において、ある処理を実装するときに、複数のコンポーネントを組み合わせて実装することがよくあると思います。DIとは、ソフトウェアのデザインパターンの1つで、制御の反転を実現します。コンポーネントの作成時に依存するコンポーネントを注入する考えです。
google/wire を用いて DI を実現する例を考えてみます。
Wire が作られた背景などは Compile-time Dependency Injection With Go Cloud's Wire が詳しいです。
ナイーブな実装
例として、以下のような構造を持つ、ユーザのパスワードを登録するアプリケーションを考えてみます。
ユーザから、名前とパスワードをうけとって、パスワードをハッシュ化して、永続化する...というシーンを想定します。
ナイーブな実装では、構造体を生成( NewUserService
)する際に依存する構造体を生成しています。依存する構造体を密結合しており、結合度の高いコンポーネントと言えます。
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で疎結合にします。以下の例はセッターインジェクションを用いて手動で依存関係の構造体を注入する例です。
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
の引数に指定する関数は関数名のみでよく、関数のシグネチャも柔軟に変更することができます。
//+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
を実行することで再生成されます。
// 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 の実装は自動生成された関数を呼び出すだけになります。
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 らしいと感じました。