golangでDIを導入したいと思っていたところ、機会があったのでdigを導入してみましたので、書いてみます。
DIについて
先に設定しておいたオブジェクトを注入することで、クラス間の依存関係を排除する仕組みです。
wikiの説明はわかりやすいですね。
インターフェースとインスタンスの組み合わせを登録し、呼び出し時にはDIコンテナが登録したインターフェースからインスタンスを自動で解決してくれる仕組みだと思っています。
自分は、コンストラクタ呼び出しと関数呼び出しを使う場合が多いですね。
今回は実装の部分を中心にしたいので、説明はまた別の機会に書きたいです。(個人的には理解するのに一年ぐらいかかっています)
dig
GolangのDIで調べるとdigとwireを見かけますが、どちらも触ってみてdigが求めていた内容だったのでdigを採用しました。
dep ensure -add "go.uber.org/dig@v1"
ざっくりとしたソースコード。
namespace repository
type UsersRepository interface {
FindByID(id int64) (*models.Users, error)
}
namespace database
type usersRepository struct {
SqlHandler
}
func NewUsersRepository(conn SqlHandler) repository.UsersRepository {
return &usersRepository {SqlHandler: conn}
}
// FindByID ...
func (repo *usersRepository) FindByID(id int64) (*models.Users, error) {
var users []models.Users
var m = models.Users{}
err := repo.SqlHandler.Table(m.TableName()).
Where("ID = ?", id).Find(&users)
if err != nil {
return nil, err
}
return &users[0], err
}
namespace usecase
type UsersInteractor interface {
FindById(id int64) (*models.Users, error)
}
type usersInteractor struct {
UsersRepository repository.UsersRepository
}
func NewUsersInteractor(u repository.UsersRepository) UsersInteractor {
return &usersInteractor{
UsersRepository: u,
}
}
func (interactor *usersInteractor) FindByID(id int64) (*models.Users, error) {
u, err := interactor.UsersRepository.FindByID(id)
if err != nil {
return nil, err
}
return u, err
}
namespace controllers
type UsersController struct {
UsersInteractor usecase.UsersInteractor
}
// NewUsersController ...
func NewUsersController(us usecase.UsersInteractor) *UsersController {
return &UsersController{
UsersInteractor: us,
}
}
func (controller *UsersController) FindByID(c echo.Context) error {
res, err := controller.UsersInteractor.FindByID(1)
}
import (
"repository"
"database"
"usecase"
"controllers"
"go.uber.org/dig"
)
func provide() *dig.Container
container := dig.New()
db := database.SqlHandler{}
_ = container.Provide(func() repository.UserRepository{return database.NewUserRepository(db)})
_ = container.Provide(usecase.NewUserInteractor)
return container
)
func userRouter(){
container := provide()
var usersController *controllers.UsersController
err := container.Invoke(func(controller *controllers.userController) {
usersController = controller
})
usersController.find()
}
動いているソースから部分的に抜き出しているため、これだけでは動かないです。(すみません
まずサンプルソースからどのような内容か把握してもらいたいので、ざっくりと書きました。
まず重要なのはrouter.goの「usecase.NewUserInteractor」の部分です。
呼び出すためには「repository.UsersRepository」が引数として必要ですが指定していません。
これはdigに登録されている内容から対象のインスタンスを作成するようになっています。書かなくても動作するように「repository.UsersRepository」を先にDIコンテナにprovide(登録)しています。
インターフェース「repository.UsersRepository」には「database.usersRepository」が作成され、引数として解決してくれるようになっています。
今回の場合、databaseの依存性を外部から注入している状態になります。
じゃあ、これはなにがいいのか。コードが多くなっているだけじゃないのか。ちゃんとメリットがあるので、書いてみます。
メリット
DI(依存性の注入)を採用するメリットはいろいろとありますが、わかりやすいところから。
今回はdatabaseを利用していますが、このrepositoryをfileにしたり、スタブデータにすることが可能になっています。
namespace files
type usersRepository struct {
}
func NewUsersRepository() repository.UsersRepository {
return &usersRepository {}
}
// FindByID ...
func (repo *usersRepository) FindByID(id int64) (*models.Users, error) {
// 中身はダミー
return nil, nil
}
_ = container.Provide(func() repository.UserRepository{return files.NewUserRepository()})
DIに登録する内容を変更するだけで、動作するコードを触ることなく動作を変更することができるわけです。
テストをする場合にも便利になります。
まず一つ目のメリットでした。
次に感じるのは登録した内容であれば引数を解決してくれるため、呼び出し時に必要なクラスが増えた場合にも実装部分を変えるだけで使えるようになります。
func provide() *dig.Container
container := dig.New()
db := database.SqlHandler{}
_ = container.Provide(func() repository.UserRepository{return database.NewUserRepository(db)})
_ = container.Provide(func() repository.BookRepository{return database.NewBookRepository(db)})
_ = container.Provide(func() repository.RecipeRepository{return database.NewRecipeRepository(db)})
_ = container.Provide(usecase.NewUserInteractor)
return container
)
開発の規模が大きくなるにつれて、必要な内容が増えてきます。できればシンプルにしたいですが、なかなか難しいものです。
いままでUserInteractorはUserRepositoryだけ必要でしたが、BookRepositoryも必要になったときにDIを使っていない場合は「NewUserInteractor」の引数を追加するのとインスタンスを作成しているコードでも引数に追加する必要があるためBookRepositoryを作成する必要がでてきます。
2ヵ所変更するだけでいいのであれば、あまり気にならないかもしれません。
コードの規模が大きくなると同じユースケースを利用しているケースもあり、変更部分が多くなっていきます。実際に引数を増やすと直す部分が多くなった影響で、nilを指定してだましだまし作成した過去もあり、意図しないエラーになることもありました。
ユースケースにrepositoryを追加して呼び出すだけでいい場合でも、追加が面倒でいま指定しているrepositoryにコードを追加するケースもいくつか見てきました。
DIを利用することで面倒だから追加しない。という選択肢を取り除くことができるようになるのです。素敵です。
digは便利
GolangでDIを導入しようと思ったときに、wireとdigの選択肢がありました。
どちらも試しましたが、digのほうが求めていた感じだったためdigを採用しました。
wireは必要な設定を書いたコードから、依存関係を解決したコードを作成してくれる感じでした。
DIの必要性を理解している人であれば、wireでもいいかと思いましたが、導入するチームはDIについて知らないチームだったため、簡単になるというメリットを全面に押し出す必要があったため、digのほうが理解しやすく便利だと感じました。
Golangのシンプル性を重要視する場合は必要のない機能なんでしょうかね。
レイヤーごとにコードを書く際にはdigを使ってみてください。便利です。
DIについて記事を書いてみたくなったので、書いてみましたが、全然言葉にできずwikiやほかの人の記事を参考に言葉を選びましたが、人に説明できるレベルではないんだと感じました。
記事が多いから書かない。という選択より自分が説明できるかどうかをチェックするために記事を書くのもいいですね。
指摘してもらえる場所でもっと書こうかな・・・