GoのDIライブラリWireを試す

はじめに

GoのWebアプリは公私でなんとが作成したことがあったのですが、DIのライブラリをあまり使ったことが無かったなとふと思い。探してみたらGoogle製のアプリ、Wireがなんとなく目立ってたような気がしたので、Hello, worldとしてチュートリアルをやりつつ理解を深めようかと思います。

やってみる

環境

$ go version
go version go1.16 linux/amd64

$ uname -srvmpio
Linux 5.4.0-65-generic #73-Ubuntu SMP Mon Jan 18 17:25:17 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux

$ lsb_release -a
LSB Version:    core-11.1.0ubuntu2-noarch:security-11.1.0ubuntu2-noarch
Distributor ID: Ubuntu
Description:    Ubuntu 20.04.2 LTS
Release:    20.04
Codename:   focal

Wireの概要

DIをする際の課題の1つが初期化処理が煩雑になりがちというとこがあります。
Wireは構造体、初期化関数、それらの依存関係を定義する関数を作成してやり、wireコマンドを実行すると、一連の初期化処理を自動生成してくれます。

基本的な流れ

Wireを使う際の基本的な流れは以下のようになります。

  • DI対象の構造体を作成
  • 構造体の初期化関数(プロバイダー)を作る
  • 初期化関数の依存関係を定義するインジェクターを作成
  • wireコマンドを使って初期化関数を作成

実際にDIしてみる

DI対象の構造体を作成

まずはDI対象の構造体を3つほどと1つのインターフェース作っておきます。 今回はケーキショップをモデルとした構造体をいくつか作っておきます。
(ちょっとモデリングがいびつかもしれませんが、適当に作っただけなのでご容赦ください)

栗を表す構造体

type Chestnut struct {
}

ケーキインターフェースとモンブランを表す構造体

type Cake interface {
    Eaten()
}

type MontBlanc struct {
    Chestnut Chestnut
}


func (m MontBlanc) Eaten() {
    fmt.Printf("I'm MontBlanc, How is it?")
}

ケーキショップを表す構造体

type CakeShop struct {
    Cake Cake
}

func (c CakeShop) Sell() {
    c.Cake.Eaten()
}

構造としては、モンブランは栗(Chesnut)に依存し、ケーキショップはCakeインターフェースを通してモンブランなどのEaten()関数を呼び出します。

構造体の初期化関数(プロバイダー)を作る

以下のように、作った構造体の初期化関数であるプロバイダーを作成しておきます。

func ChestnutProvider() Chestnut {
    return Chestnut{}
}

func MontBlancProvider(c Chestnut) MontBlanc {
    return MontBlanc{Chestnut: c}
}


func CakeShopWithMontBlancProvider(c MontBlanc) CakeShop {
    return CakeShop{Cake: c}
}

それぞれ、書いてあるとおりではChestnutProvider()Chestnutを作成してMontBlancProvider(c Chestnut)は受け取った栗を使ってMontBlancを作成します。そして、最後のCakeShopWithMontBlancProvider(c MontBlanc)はCakeインターフェースを実装しているMontBlancを受け取ってCakeShopを作成するプロバイダーになってます。

初期化関数の依存関係を定義するインジェクターを作成

実装としては最後であるインジェクターの作成を行います。
ここでは、プロバイダーの依存関係を定義することができます。

// +build wireinject

package config

import (
    "github.com/google/wire"
    "github.com/samuraiball/go-sandbox/message"
    "github.com/samuraiball/go-sandbox/sweets"
)


func InitializeCakeShop() sweets.CakeShop {
    wire.Build(
        sweets.CakeShopWithMontBlancProvider,
        sweets.MontBlancProvider,
        sweets.ChestnutProvider,
    )
    return sweets.CakeShop{}
}

Wireでは、wire.Build()の中でそれぞれの初期化に使うプロバイダーを記述していきます。そして、それぞれのプロバイダーで初期化される構造体が、別のプロバイダーの引数として利用されることになります(これは後ほどもう少し細かく説明します)。
ここで、コンパイルエラーを避けるためにインジェクターの戻り値では空のCakeShop{}を返しています。このCakeShop{}は実際は使われることはなく単純に無視されます。なので、ここで値を初期化して入れていたとしても意味がありません(// +build wireinjectを先頭に記述することで、ビルドの対象外にしています)。

wireコマンドを使って初期化関数を作成

wireコマンドをインジェクターが定義されているディレクトリーで実行します。するとwire_gen.goというコードが自動生成されます。

// Code generated by Wire. DO NOT EDIT.

//go:generate go run github.com/google/wire/cmd/wire
//+build !wireinject

package config

import (
    "github.com/samuraiball/go-sandbox/sweets"
)

// Injectors from dependency.go:

func InitializeCakeShop() sweets.CakeShop {
    chestnut := sweets.ChestnutProvider()
    montBlanc := sweets.MontBlancProvider(chestnut)
    cakeShop := sweets.CakeShopWithMontBlancProvider(montBlanc)
    return cakeShop
}

ここではインジェクターと同じ名前の関数で、初期化処理のコードが生成されているのが見て取れます。
Wireの基本的な使い方はここまでです。
最後に処理をmain関数から呼び出してみます。

package main

import "github.com/samuraiball/go-sandbox/config"

func main() {
    shop := config.InitializeCakeShop()
    shop.Sell()
}

実行結果

$ go build
$ ./go-sandbox 
I'm MontBlanc, How is it?

最後に

今回の例はコンポーネント間の依存関係も複雑では無かったので、そこまで恩恵が得られませんでしたが(と言うより、手数がむしろ増えた気がします)、より複雑なアプリケーションを作成したり、テストをやりだしたりすると、プロバイダーとインジェクターの組み合わせで結構自由にDIの設定を記述できそうな感じがしました。