wire で Dependency Injection しよう
Magic Moment の @aqlwah です。
私たちが開発している Magic Moment Playbook では、モジュール間の依存の管理を楽にするため wire を活用しています。
とても便利なツールですが、wire が行う Dependency Injection (DI:依存性の注入)がいまいち直感的に理解しづらく、弊チームでもバックエンドの新規参画者を迷わせる要因となっています。
本稿では、これから弊チームで wire に手間取る人が出ないことを願って、ざっくり wire の役割をまとめます。
DI って何よ
wire は Dependency Injection (DI:依存性の注入)のためのツールです。
DI とは端的に言うと、「依存するモジュールを自分で生成せず、外部から受け取る(注入する)」デザインパターンです。
// without DI
type Greeter struct {}
func (g Greeter) HelloWorld() {
logger := Logger{} // ロジック自身で Logger を生成・初期化しているため密結合
logger.Log("Hello, World!")
}
func main() {
g := Greeter{}
g.HelloWorld()
}
// with DI
type Greeter struct {
Logger Logger
}
func (g Greeter) HelloWorld() {
g.Logger.Log("Hello, World!") // ロジックは Logger を利用するだけのため疎結合
}
func main() {
l := Logger{}
g := Greeter{Logger: l}
g.HelloWorld()
}
DI のないコードでは、ロジックの中で Logger
インスタンスを生成しています。
このようにした場合、例えば Logger
の初期化方法(インスタンス生成時に与える引数など)が変わった場合などに、ロジックも併せて修正する必要があります。
あるいは Logger
とは別の実装として Printer
モジュールを使いたくなった場合も、同様にロジックを修正する必要があるでしょう。
問題の原因はロジックと依存物が密結合してしまっていることです。
それならば、依存物を生成する役割をロジックの外部に切り出してしまって、ロジック自身は依存物を利用するだけにしよう、というのが DI の基本的な考え方です。
依存物をどのように受け取るかについてはさまざまな方法が考えられますが、コンストラクタで受け取ることが一般的かつ簡単だと思います。
依存物の生成とロジックを切り離すことで、変更影響の波及を抑えたり、依存物の生成方法を簡単に統一できたり、依存物の差し替え(実装を替える・モックに替えるなど)を簡単にできたりなどのメリットがあります。
また、ツールやフレームワークによっては「依存物を生成して、必要としているロジックに注入する」ことを集中的に担う Dependency Injection Container(DI コンテナ)という機構が存在します。
世の中にはこの DI コンテナを指して DI と表現しているものもありますので、用語が何を指しているかをしっかり意識しましょう。
wire って何よ
先述の通り、wire は DI のためのツールです。ここでいう DI はデザインパターンとしての DI です。
DI を愚直に実装しようとすると依存関係の管理が煩雑になります。つまり、どのロジックがどのモジュールを必要としていて、どのような順番でインスタンスを生成・初期化していけばよいか、というかなり面倒なことを考える必要が出てくるのです。
ここで活躍するのが wire で、各ロジックやモジュールの依存関係を適切に整理・解決したコードを生成します。
import (
"github.com/google/wire"
"gorm.io/gorm"
"github.com/mm-technologies/xxx/internal/service/adapter/handler"
"github.com/mm-technologies/xxx/internal/service/adapter/handler/rest"
"github.com/mm-technologies/xxx/internal/service/adapter/domain/repository"
"github.com/mm-technologies/xxx/internal/service/usecase"
"github.com/mm-technologies/xxx/internal/pkg/lib/database"
)
func Wire(
db *gorm.DB,
) *handler.Handlers {
wire.Build(
database.NewQueryer,
repository.NewUserRepository,
repository.NewCompanyRepository,
usecase.NewUserUsecase,
usecase.NewCompanyUsecase,
rest.NewUserHandler,
rest.NewCompanyHandler,
handler.NewHandlers,
)
return &handler.Handlers{}
}
上記が wire.go で定義する内容です。 User
ユースケースと Company
ユースケースを持つ素朴な API サーバを想定しています。
ここで定義されている Wire
関数は「インジェクター」といい、 wire.Build()
に「注入するインスタンスを生成する関数」(プロバイダ関数)を羅列して与えています。
Wire
関数の引数と戻り値は一見意味のないような記述になっていますが、wire によって生成されるコードはこのシグネチャに従って定義されます。
ここでモジュールが持つべき依存関係は以下を想定しています。
-
gorm.DB
をdatabase.Queryer
が利用する -
database.Queryer
をrepository.UserRepository
,repository.CompanyRepository
が利用する -
repository.UserRepository
をusecase.UserUsecase
が利用する -
repository.CompanyRepository
をusecase.CompanyUsecase
が利用する -
usecase.UserUsecase
をrest.UserHandler
が利用する -
usecase.CompanyUsecase
をrest.CompanyHandler
が利用する -
rest.UserHandler
,rest.CompanyHandler
はhandler.Handlers
に集約され、サーバのハンドラになる
wire.go の準備ができたら wire を実行します。
$ go install github.com/google/wire/cmd/wire@latest
$ wire gen
生成された wire_gen.go は以下のようになります。
import (
"github.com/mm-technologies/xxx/internal/service/adapter/domain/repository"
"github.com/mm-technologies/xxx/internal/service/adapter/handler"
"github.com/mm-technologies/xxx/internal/service/adapter/handler/rest"
"github.com/mm-technologies/xxx/internal/service/usecase"
"github.com/mm-technologies/xxx/internal/pkg/lib/database"
"gorm.io/gorm"
)
// Injectors from wire.go:
func Wire(db *gorm.DB) *handler.Handlers {
queryer := database.NewQueryer(db)
userRepository := repository.NewUserRepository(queryer)
companyRepository := repository.NewCompanyRepository(queryer)
userUsecase := usecase.NewUserUsecase(userRepository)
companyUsecase := usecase.NewCompanyUsecase(companyRepository)
userHandler := rest.NewUserHandler(userUsecase)
companyHandler := rest.NewCompanyHandler(companyUsecase)
handlers := handler.NewHandlers(userHandler, companyHandler)
return handlers
}
どのモジュールが何に依存していて、何を受け取らねばならないかが見事に解決されているのがわかるかと思います。
実際のプロダクトコードとして利用するのはこちらの wire_gen.go です。
wire_gen.go がエクスポートするインジェクター(Wire
関数)を main.go から呼び出すことで、依存関係が適切に解決されたインスタンスを利用できます。
上記のサンプルコードでは手作業でコーディングするのとあまり労力は変わりませんが、サービスがより大規模化し、モジュール同士が複雑な依存関係を持つようになってくると、より wire の価値が高まることになるでしょう。
まとめ
私たちが Magic Moment Playbook の開発で利用する wire について解説しました。
- wire は Dependency Injection (DI:依存性の注入)を行うためのツールです
- wire.go を用意し、wire コマンドを実行することで、依存関係が解決されたコードを生成します
- DI を行うことによって、ロジックとモジュールが疎結合になるとともに、モジュールの生成方法の管理が容易になります
最後に
弊社 Magic Moment では、フロントエンド・バックエンドにかかわらず全方位的にエンジニアを募集中です!Magic Momentに少しでも興味を持っていただけたら是非エントリーください!
8/30にはFindy様主催のイベントにMagic Momentから石田さんが登壇されます!
よろしければぜひご視聴ください!
さらに、こちらのイベントも8/29開催予定です!こちらはオンラインイベントです。Magic Momentの開発がどんなものか興味を持っていただいた方は是非ご参加ください!
Discussion