GeekFactory

int128.hatenablog.com

uber-go/dig を使う

GoのDIコンテナ実装である uber-go/dig を使ってみました。

github.com

基本的な使い方はGoDocに書いてあります。あえて3行にまとめると以下になります。

c := dig.New()
c.Provide(func (/* 生成に必要な型... */) /* 生成される型 */ { /* 生成処理 */ })
c.Invoke(func (/* 実行に必要な型 */) { /* 実行処理 */ })

例えば、以下のように Provide メソッドを記述すると、 interface GetUserUsecase を実装するオブジェクトを生成するには interface UserRepository が必要であると宣言できます。

c := dig.New()
c.Provide(func (r UserRepository) GetUserUsecase {
    return &GetUserUsecaseImpl{UserRepository: r}
})

Springでいうconfiguration classの @Bean と同じです。

// これはJava/Springの例
@Bean
public GetUserUsecase(UserRepository r) GetUserUsecase {
  return new UserRepositoryImpl(r)
}

依存関係に基づいて実際に値を取得するには Invoke メソッドを実行します。例えば、以下のように記述すると interface GetUserUsecase を実装するオブジェクトを取得して実行できます。

c.Invoke(func (u GetUserUsecase) {
    u.Do()
})

Provide で宣言した依存関係は再帰的に解決されます。例えば、以下のように記述すると interface GetUserUsecaseinterface UserRepositoryinterface UserAPIClient の依存関係を宣言できます。

c := dig.New()
c.Provide(func (r UserRepository) GetUserUsecase {
    return &GetUserUsecaseImpl{UserRepository: r}
})
c.Provide(func (c UserAPIClient) UserRepository {
    return &UserRepositoryImpl{UserAPIClient: c}
})

実践的な使い方としては、依存関係を別のパッケージに切り出すとよいでしょう。

package di

func New() (*dig.Container, error) {
    c := dig.New()
    if err := c.Provide(func (r UserRepository) GetUserUsecase { return &GetUserUsecaseImpl{UserRepository: r} }); err != nil {
        return nil, errors.WithStack(err)
    }
    if err := c.Provide(func (c UserAPIClient) UserRepository { return &UserRepositoryImpl{UserAPIClient: c} }); err != nil {
        return nil, errors.WithStack(err)
    }
    return c, nil
}

宣言した依存関係が正しく解決できるかテストを書いておくとよいでしょう。

package di_test

func TestInvoke(t *testing.T) {
    c, err := di.New()
    if err != nil {
        t.Fatalf("error while di.New: %+v", err)
    }
    if err := c.Invoke(func(u GetUserUsecase) {
        t.Logf("%+v", u)
    }); err != nil {
        t.Fatalf("error while c.Invoke: %+v", err)
    }
}

構造体に dig.In を埋め込むと構造体のメンバに定義されている型を解決して代入してくれます。この方法を使うと Provide に渡す関数を簡潔に書けます。

type GetUserUsecaseImpl struct {
    dig.In
    UserRepository UserRepository
    FooRepository FooRepository
    BarRepository BarRepository
}
c.Provide(func (i GetUserUsecaseImpl) GetUserUsecase { return &i })

Springでいう @Autowired に近いですね。

// これはJava/Springの例
public class GetUserUsecaseImpl {
  @Autowired private UserRepository userRepository
  @Autowired private FooRepository fooRepository
  @Autowired private BarRepository barRepository
}

なお、小規模なアプリケーションの場合は、digを使わずに手作業で依存関係を解決する方法もあります。例えば、以下のようにmainにすべての依存関係を書いてしまいます。

func main() {
    u := &GetUserUsecaseImpl{
        UserRepository: &UserRepositoryImpl{
            UserAPIClient: &UserAPIClientImpl{
                Client: http.DefaultClient,
            },
        },
    }
    if err := u.Do(context.Background()); err != nil {
        log.Fatalf("Error: %s", err)
    }
}

依存関係が単純な場合は手作業による解決でも十分ですが、複雑になってきた場合はdigのようなDIコンテナの導入を検討するとよいでしょう。