はじめに
クリーンアーキテクチャやUTの実装を簡単にするための技術としてDIを導入することがほぼ必須となっているが今回はGo言語のDIライブラリであるWireを使ってサンプルプログラムを実装してみた。
筆者の経験として今までJavaでSpringによるアノテーションを使ったDIとScalaでAireframeを使ったDIがあり、JVM言語以外ではDIを使ったプログラミングをしたことはない。
Wire
Google製のDIライブラリ
wire.goに依存関係を記述してコマンドを実行すると別のGoファイルが生成されて依存関係を解決したインスタンスを作ることができる。
クラス図
下記の通りUserService構造体のメンバ変数としてIUserRepositoryインターフェースが定義されておりそれを継承した構造体としてuserRepository1とuserRepository2が存在している。
UserService構造体を生成するときrepoにuserRepository1とuserRepository2をそれぞれ渡すことで動きを変える事ができる。
実装サンプル
まずサービス構造体の宣言から。
下記のようにNewUserService関数を定義して外部から依存性を注入できるようにする。
package service
import (
"di_sample/domain"
"di_sample/repo"
)
type UserService struct {
repo repo.IUserRepository
}
func NewUserService(repo repo.IUserRepository) *UserService {
return &UserService{
repo: repo,
}
}
func (s *UserService) All() ([]domain.User, error) {
return s.repo.All()
}
レポジトリインターフェースは以下の通り
package repo
import "di_sample/domain"
type IUserRepository interface {
All() (users []domain.User, err error)
}
更に上記を実装したした構造体は下記のようになっている。
userRepository1ではAll関数から1個の要素のみ入っている配列を受け取る動きをしている。
package repo
import "di_sample/domain"
type userRepository1 struct{}
func (ur *userRepository1) All() (users []domain.User, err error) {
users = append(users, domain.User{
Name: "test1",
Email: "test@example.com",
})
return users, err
}
func NewUserRepo1() IUserRepository {
return &userRepository1{}
}
userRepository2ではAll関数から2個の要素が入っている配列を受け取る動きをしている。
package repo
import "di_sample/domain"
type userRepository2 struct{}
func (ur *userRepository2) All() (users []domain.User, err error) {
users = append(users, domain.User{
Name: "test",
Email: "test@example.com",
})
users = append(users, domain.User{
Name: "test2",
Email: "test2@example.com",
})
return users, err
}
func NewUserRepo2() IUserRepository {
return &userRepository2{}
}
上記のuserRepository1とuserRepository2をUserSiviceへ依存中注入する関係を記述したwire.goを実装する。
下記のように記述することでInitializeUserService1ではUserServiceにuserRepository1を依存注入したインスタンスが生成される。
また、InitializeUserService2ではUserServiceにuserRepository2を依存注入したインスタンスが生成される。
//+ wireinject
package main
import (
"di_sample/repo"
"di_sample/service"
"github.com/google/wire"
)
func InitializeUserService1() *service.UserService {
wire.Build(service.NewUserService, repo.NewUserRepo1)
return nil
}
func InitializeUserService2() *service.UserService {
wire.Build(service.NewUserService, repo.NewUserRepo2)
return nil
}
更にプロジェクトのルートディレクトリにてwireコマンドを実行することでwire_gen.goが生成される。
呼び出し元ではこのファイルに生成されている関数を呼び出す。
今回はmain関数から呼び出しを行った。
InitializeUserService1、InitializeUserService2関数を呼び出すだけで依存関係を解決したインスタンを取得することができる。
package main
import "fmt"
func main() {
userService1 := InitializeUserService1()
users, _ := userService1.All()
fmt.Println(users)
userService2 := InitializeUserService2()
users, _ = userService2.All()
fmt.Println(users)
}
上記のコードを実行した結果は以下の通りでuserService1とuserService2で動きが変わっていることが確認できる。
$ go run main.go wire_gen.go
[{test1 test@example.com}]
[{test test@example.com} {test2 test2@example.com}]
Singletonパターン
上記の問題として毎回インスタンスの初期化処理を行ってしまうことである。
特に今回のように初期化処理が毎回同じで生成されるインタンスも同じ場合処理コストが掛かってしまう。
そのような場合はSingletonパターンを導入して初期化コストを抑えることができるがWireの機能としてSingletonパターンをサポートしていないようなので下記を参考にSingletonパターンを導入してみた。
以下に差分のみ記述していく
UserServiceに関しては下記のように実装することでメンバ変数のinstanceが初回のみ初期化され構造体内に保持される。
わかりやすいように初期化時にログを出力させるようにしている。
次回以降は保持されたinstanceをそのまま返却するため初期化処理を行うことがなくinstanceを使い回すことができる。これにより初期化コストを抑えることが可能となる。
var instance *UserService
func NewUserServiceSingleton(repo repo.IUserRepository) *UserService {
if instance == nil {
log.Println("create new service instance")
instance = &UserService{
repo: repo,
}
}
return instance
}
wire.goとmain.goは特に前回との変わったことはないため説明は割愛するが実装は記載する。
main.goは結果がわかりやすいように2回関数を呼び出している。
1回目には仕込んだログが出力され2回目にはログが出ないことが期待される。
func InitializeUserServiceSingleton() *service.UserService {
wire.Build(service.NewUserServiceSingleton, repo.NewUserRepo1)
return nil
}
userService1Singleton1 := InitializeUserServiceSingleton()
users, _ = userService1Singleton1.All()
fmt.Println(users)
userService1Singleton2 := InitializeUserServiceSingleton()
users, _ = userService1Singleton2.All()
fmt.Println(users)
最後に実行結果は以下の通りである。
期待通りNewUserServiceSingleton関数で仕込んだ初期化ログは一回しか出力されていないことが確認できた。
$ go run main.go wire_gen.go
[{test1 test@example.com}]
[{test test@example.com} {test2 test2@example.com}]
2022/12/22 10:00:09 create new service instance
[{test1 test@example.com}]
[{test1 test@example.com}]
所感
経験のあるSpringやAirframeと比べると機能が少ない用に感じた。
その分シンプルで自由に使えると言ったメリットもあると思う。
また、wire.goに記述してwireコマンドにてコードを自動生成しないといけないのは癖があった。
サンプルコード