search
LoginSignup
0
Help us understand the problem. What are the problem?

More than 1 year has passed since last update.

ぷりぷりあぷりけーしょんず Advent Calendar 2020 Day 9

posted at

updated at

Go + wire と Docker で DI してみた

この記事はぷりぷりあぷりけーしょんず Advent Calendar 2020の9日目の記事です。

はじめに

今年学んだことのアウトプット第二弾です。
本記事に書く内容に関しては、結構前に実装した際のお話になります。
また、個人開発ではなくペアプロ(@MSHR-Dec さんとの共同開発)した際のものとなります。とても楽しかった開発でした😊
結構月日が立ってしまった為、忘れないうちに記事に落とし込もうと思います。

本記事で話さないこと

  • Go のセットアップ
  • Go の書き方
  • Docker のセットアップ
  • 今回作成したプロジェクト詳細
  • 個人的ニュース

DI とは

DI (dependency injection) はよく耳にした方が多いかと思われます。
調べると「 オブジェクトの注入 」とのような直訳した意味が多く見つかります。
こちらをもう少し噛み砕いて説明すると、オブジェクト指向プログラミングにおける開発手法の1つであり、インスタンスの生成や管理を行ってくれます。
オブジェクト指向な言語のフレームワークでは結構 DI が採用されています。

Go の DI ライブラリ wire

こちらの本題に入る前に、そもそもなぜ DI をしたかったのかを簡単に説明します。
本プロジェクトでは Clean architecture を採用しており(完璧な実装ではありませんが)、依存関係逆転の法則を守るために interface を用いて型を定義し、起動のタイミングでオブジェクトの注入を行いたかった。とか、各レイヤーで単体テストを書きたかったためモックと実際のロジックをいい感じに差し込みたかった。など、interface の定義が多く、起動のタイミングやテスト実行のタイミングでインスタンスの生成をいい感じにしたかったため、DI ライブラリを導入しようとなったのが経緯となります。

wire とは

Google 製の DI ライブラリとなっており、2018 年 12 月に公開されました。
特別なビルドタグをつけた Go のコードをデータソースとして、コンパイルタイムでインジェクタのコードを生成してくれるみたいです。

wire 取得の説明の前に、本実装での Go のバージョンは以下となります。

$ go version
go version go1.13 darwin/amd64

wire は以下のコマンドで取得できます。

$ go get github.com/google/wire/cmd/wire

get が完了したら wire コマンドが使用できるかと思われます。

$ wire help
Usage: wire <flags> <subcommand> <subcommand args>

Subcommands:
        check            print any Wire errors found
        commands         list all command names
        diff             output a diff between existing wire_gen.go files and what gen would generate
        flags            describe all known top-level flags
        gen              generate the wire_gen.go file for each package
        help             describe subcommands and their syntax
        show             describe all top-level provider sets


Use "wire flags" for a list of top-level flags

実際に使ってみる

今回は repository 周りを例に見ていきたいと思います。
まずは interface の定義から

package repository

import (
    "api/src/domain/model"
)

type IArticleRepository interface {
    Create(article *model.Article)
    Update(article *model.Article)
    FindAll() []model.Article
    FindOne(ID uint64) model.Article
}

次に、その interface を実装した struct を定義します。
ちなみに、本プロジェクトは ORM を使用しており、Gorm ライブラリを使用しています。RDBMS は MySQL を使用しています。

package datastore

import (
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"
    "github.com/thoas/go-funk"

    "api/src/domain/model"
    "api/src/domain/repository"
)

type ArticleDatastore struct {
    db *gorm.DB
}

func NewArticleDatastore(d *gorm.DB) repository.IArticleRepository {
    return &ArticleDatastore{
        db: d,
    }
}

func (a *ArticleDatastore) Create(article *model.Article) {
    a.db.Create(&article)
}

func (a *ArticleDatastore) Update(article *model.Article)  {
    a.db.Save(&article)
}

func (a *ArticleDatastore) FindAll() []model.Article {
    var articles []model.Article
    a.db.Select("id").Find(&articles)

    results := funk.Map(articles, func(article model.Article) model.Article {
        a.db.First(&article).Related(&article.User, "User").Related(&article.Shop, "Shop").Related(&article.Categories, "Categories")
        return article
    }).([]model.Article)

    return results
}

func (a *ArticleDatastore) FindOne(ID uint64) model.Article {
    article := model.Article{ID: ID}
    a.db.First(&article).Related(&article.User, "User").Related(&article.Shop, "Shop").Related(&article.Categories, "Categories")
    return article
}

ここまで定義されたオブジェクトを使用する処理が以下となります。

package interactor

import (
    "time"
    "api/src/domain/model"
    "api/src/domain/repository"
    "api/src/handler/request"
    "api/src/usecase"
)

type ArticleInteractor struct {
    Repository repository.IArticleRepository
}

func NewArticleInteractor(
    repository repository.IArticleRepository,
) usecase.IArticleUsecase {
    return &ArticleInteractor{
        Repository: repository,
    }
}

func (a *ArticleInteractor) Create(ra *request.UpsertArticleRequest) uint64 {
    article := model.Article{
        Title:    ra.Title,
        Body:     ra.Body,
        Status:   ra.Status,
        UserID:   ra.UserID,
        ShopID:   ra.ShopID,
        CreateAt: time.Now(),
    }

    a.Repository.Create(&article)

    return article.ID
}

func (a *ArticleInteractor) Update(id uint64, ra *request.UpsertArticleRequest) {
    article := model.Article{
        ID:       id,
        Title:    ra.Title,
        Body:     ra.Body,
        Status:   ra.Status,
        UserID:   ra.UserID,
        ShopID:   ra.ShopID,
        CreateAt: time.Now(),
    }

    a.Repository.Update(&article)
}

func (a *ArticleInteractor) GetAll() []model.Article {
    return a.Repository.FindAll()
}

func (a *ArticleInteractor) GetOne(ID uint64) model.Article {
    return a.Repository.FindOne(ID)
}

本当は↑のオブジェクトも interface が定義されているのですが、今回は1つだけをサンプルとします。
最後に DI を定義していくのですが、ソースの先頭にビルドタグをつけるのを忘れないように気をつけましょう!
また、main パッケージの層にソースを配置しないとうまくインジェクタのコードを生成(wire generate)できませんでした。(ほんとは registory パッケージとかの中に入れたかったのですが)

wire.go
//+build wireinject

package main

import (
    "github.com/google/wire"
    "github.com/jinzhu/gorm"
    _ "github.com/jinzhu/gorm/dialects/mysql"

    "api/src/infrastructure/datastore"
    "api/src/interactor"
)

func InitArticleInteractor(d *gorm.DB) *interactor.ArticleInteractor {
    wire.Build(
        datastore.NewArticleDatastore,
        interactor.NewArticleInteractor,
    )
    return nil
}

こちらの定義だけだと、正直不安だらけでした笑
このソースでコンストラクタ引数にちゃんと必要なもの入れてくれるの?とか色々疑いまくりましたが、こちらのソースで実際の DI を generate したら以下のソースが吐き出されました。

$ wire src/wire.go
wire_gen.go
// Code generated by Wire. DO NOT EDIT.

//go:generate wire
//+build !wireinject

package main

import (
    "github.com/jinzhu/gorm"
    "api/src/infrastructure/datastore"
    "api/src/interactor"
)

import (
    _ "github.com/jinzhu/gorm/dialects/mysql"
)

// Injectors from wire.go:

func InitArticleInteractor(d *gorm.DB) *interactor.ArticleInteractor {
    iArticleRepository := datastore.NewArticleDatastore(d)
    articleInteractor := interactor.NewArticleInteractor(iArticleRepository)
    return articleInteractor
}

こちらのソースが吐き出された時は感動しました。
勝手にコンストラクタ引数を詰めてくれて、return nil と記述していたのに、しっかり該当のインスタンスを返してくれているではありませんか!
generate してみて感動はしましたが、wire.go の書き方には慣れが必要そうだなという印象です。(当初はなかなか慣れることができませんでした笑)

あとはエントリーポイントである main.go で InitArticleInteractor を呼べば、wire_gen.go で定義した方のメソッドが呼ばれるため、依存関係が解決した状態のインスタンスを使用することができます。

Docker と wire で DI を自動 Generate

本プロジェクトは Docker を使用しており、コンテナのビルド・立ち上げの際に先程の wire generate ができたらいいなと思い、やってみました。
とは言っても、以下の Dockerfile を定義する感じです。こちらは @MSHR-Dec さんにたくさん助けていただきました。

Dockerfile
FROM golang:1.13.7-alpine3.11 as build

WORKDIR /api

ENV GO111MODULE=on

COPY . .

RUN go get github.com/google/wire/cmd/wire \
  && wire src/wire.go \
  && cd src \
  && CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ../main \
  && cd ..

FROM alpine

COPY --from=build /api/main .

RUN addgroup go \
  && adduser -D -G go go \
  && chown -R go:go ./main

CMD ["./main"]

あとはビルドして、立ち上げれば自動 generate してくれて API が起動します。

まとめ

アベンドカレンダー2記事目ということもあり、勢いで書き上げたため、ちょっとわかりにくい箇所が多いかと思われます。🙇‍♂️
不備などありましたらコメントどんどんください!
今回 DI ライブラリを使用してみて、慣れない箇所には時間がかかりそうでしたが、実際 generate でソースを吐き出してみると、結構な感動だったため、今後も Golang で開発をする際は、wire を用いて開発をしていこうと思います。(DI するような実装なら)

おまけ

本記事には載せられませんでしたが、本プロジェクトではホットリロードとして realize ってのを使用しています。こちらも感動ものでした。
また Dockerfile を見ていただければわかるかもですが、マルチステージビルドってのも初めて使いました。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
0
Help us understand the problem. What are the problem?