GoでDDDっぽいコードを書く方法をGo初心者が考えてみる

こちらは [ラクス Advent Calendar 2021](https://qiita.com/advent-calendar/2021/rakus) の13日目の記事です。

初めまして。今年の10月にラクスに入社した@Imamottyです。
入社して2ヶ月余りが経過しました。日々覚えることが多く大変ですが、成長を感じる毎日です。
新参者ですが、よろしくお願いします。

# テーマ：「アドベントカレンダー」のドメインモデルを考えて、GoでDDDっぽいコードを書いてみる

さて、本題ですが、チームで開発を担当するアプリケーションのバックエンドにGoを採用することになり、
せっかくなのでGoのコーディングの練習にもなりそうな形でアドベントカレンダーのテーマを考えました。

今回は **「アドベントカレンダー」** をテーマにドメインモデルを考えて、
DDDっぽくコーディングすることを目指していきたいと思います。

ちなみに[A Tour of Go](https://go-tour-jp.appspot.com/)をなんとか完走した感じのGo初心者なので、実装の稚拙さはお許しください。。
（改善のアドバイスとかもらえると嬉しいです）

## 前提条件
### 使用ツール・ライブラリ
+ go 1.17
+ gin-gonic/gin v1.7.7 (Webフレームワーク）
+ gorm.io/gorm v1.22.4 （ORM）
+ google/wire v0.5.0 （DIツール）
+ postgres　v14.1　（DB)

### アプリケーション仕様
+ 以下の操作ができる
    + ユーザ名を指定してユーザを登録する
    + カレンダー名を指定してカレンダーを登録する
    + ユーザが任意のカレンダーにエントリーする
    + 特定のユーザがエントリー済みのカレンダーをすべて参照する
    + 特定のカレンダーにエントリー済みのユーザをすべて参照する

## 設計
### ドメインモデル

こちらです。（自分でテーマと仕様を決めておきながら答えが分からなかった・・・）

![ドメインモデル.drawio.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26577/dc304327-3507-2a4e-e870-b0d8cfdd6972.png)


### API一覧
以下の７本のAPIを作成します。

+ POST `/user/create` ユーザ登録
+ GET `/user/:user_id` ユーザ参照
+ POST `/calendar/create` カレンダー登録
+ GET `/calendar/:calendar_id` カレンダー参照
+ POST `/entry/create` エントリー登録
+ GET `/entries/calendar/:calendar_id` 特定のカレンダーにエントリー済みのユーザをすべて参照
+ GET `/entries/user/:user_id`　特定のユーザがエントリー済みのカレンダーをすべて参照

## 実装方針
以下のような実装ルールで実装しました。
### レイヤーと依存関係
+ レイヤーはDomain層、UI層、Infrastructure層、Application層に分ける
+ 依存関係は UI層・Infrastructure層 -> Application層 -> Domain層
+ UI層、Infrastructure層、Application層はwireを使ってDIでインスタンスを生成

### 依存関係逆転の原則（DIP）

+ Domain層にRepositoryのinterfaceを置き、Infrastructure層で実装する
+ ApplicationService等はDomain層のinterfaceに依存する

### トランザクションのスコープ
+ DBのトランザクションはAPIのリクエスト全体で共通管理する
+ すべての処理が正常終了でcommit、途中でエラーが発生した場合はrollback

### ディレクトリ構成
以下のようなディレクトリ構成にします。

```bash
.
├── docker-compose.yml
├── initdb
│   └── setting.sql #postgresのコンテナ起動時に読み込ませるDDL
└── src
    ├── application #Application層のAppServiceを置く
    │   ├── calendar
    │   ├── entry
    │   ├── user
    │   └── wire.go #wire用にAppServiceのProvider関数セットを定義
    ├── domain #Domain層のstruct、interface等を置く
    │   ├── calendar
    │   ├── entry
    │   └── user
    ├── infrastructure #Infrastructure層のRepository実装やORM用のモデルを置く
    │   ├── calendar
    │   ├── entry
    │   ├── user
    │   └── wire.go #wire用にRepositoryのProvider関数セットを定義
    ├── userinterface #UI層のControllerを置く
    │   ├── calendar
    │   ├── entry
    │   ├── user
    │   └── controller.go #Controllerのinterfaceを定義（すべてのControllerで実装する）
    ├── main.go #エントリーポイント
    ├── router.go #ルーティングを定義
    ├── api_transaction_manager.go #ルーティングで使うミドルウェア。HTTPリクエスト単位でDBトランザクションを管理できるようにする
    ├── Dockerfile
    ├── go.mod
    ├── go.sum
    ├── wire.go #wireで各Provider関数をControllerにDIする設定を定義
    └── wire_gen.go #wireの成果物(編集不可)
```

# 実装内容

実装したソースコードを[こちらのリポジトリ](https://github.com/imamoto-hikaru/go-ddd-sample)にpushしました。
個人的な工夫ポイントは以下のあたりです。

1. リクエストスコープのトランザクションを実現する共通処理として`ApiTransactionManager`を追加
1. [google/wire](https://github.com/google/wire) を使ってControllerの生成関数をコンパイル時に作成
1. すべてのControllerについて、Routingの設定を共通化してちょっとコードを短くする

それぞれ説明したいと思います。

## 1. トランザクションをリクエストスコープにする

パスごとの挙動を制御する関数である`gin.HandlerFunc func(*gin.Context)`を同一パスに複数バインドすることができるので、
共通処理としてトランザクション開始、commit/rollbackを行う`ApiTransactionManager`を使ってトランザクションの制御をしています。
`tx := db.Begin()`でトランザクションを開始して、`c.Set("db", tx)`で後続の`HandlerFunc`にトランザクションを開始したDBオブジェクトを渡すことができます。
その後、`c.Next()`で後続の`HandlerFunc`の処理を実行します。
最終的に`c.Errors`に値が入っている場合はrollback、それ以外はcommitしています。
また、途中でpanicが発生した場合も、`defer func()`で捕捉してrollbackしています。

```golang:/src/api_transaction_manager.go
package main

import (
	"github.com/gin-gonic/gin"
	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

func ApiTransactionManager(c *gin.Context) {
	defer func() {
		if r := recover(); r != nil {
			db, _ := c.Get("db")
			if db != nil {
				db.(*gorm.DB).Rollback()
			}
			c.JSON(200, gin.H{
				"status": "ng",
				"errors": []string{r.(string)},
			})

		}
	}()

	dsn := "host=db user=test password=password dbname=test01 port=5432 sslmode=disable TimeZone=Asia/Tokyo"
	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if err != nil {
		c.AbortWithError(500, err)
	}

	tx := db.Begin()
	c.Set("db", tx)
	c.Next()

	if len(c.Errors) > 0 {
		tx.Rollback()
		errorList := c.Errors
		c.JSON(200, gin.H{
			"status": "ng",
			"errors": errorList.Errors(),
		})
	}

	tx.Commit()
}
```

## 2. `google/wire`でDI

[google/wire](https://github.com/google/wire)　は、必要な関数を選択して、DI済みのオブジェクトを返却する関数を生成してくれるライブラリです。

たとえば、`*entry.EntryCreateController`を返却する関数である`InitializeEntryCreateController`は、
以下のように変数を生成する多くの関数を呼ぶ必要があります。

```golang
func InitializeEntryCreateController(db *gorm.DB) *entry.EntryCreateController {
	entryRepository := entry2.NewEntryRepository(db)
	userRepository := user2.NewUserRepository(db)
	userFindService := user3.NewUserFindService(userRepository)
	calendarRepository := calendar2.NewCalendarRepository(db)
	calendarFindService := calendar3.NewCalendarFindService(calendarRepository)
	entryCreateService := entry3.NewEntryCreateService(entryRepository, userFindService, calendarFindService)
	entryCreateController := entry.NewEntryCreateController(entryCreateService)
	return entryCreateController
}
```
この実装は`google/wire`によって自動生成された後の関数で、元々の実装はこんな感じです。

```golang
func InitializeEntryCreateController(db *gorm.DB) *entry.EntryCreateController {
	wire.Build(
		infrastructure.RepositoryProviderSet,
		application.ServiceProviderSet,
		entry.NewEntryCreateController,
	)
	return new(entry.EntryCreateController)
}
```
`infrastructure.RepositoryProviderSet`、`application.ServiceProviderSet`は、
それぞれRepositoryとApplicationServiceを生成する関数（Provider関数と呼ばれる）のセットになっています。
`wire.Build`にProvider関数をセットすることで、関数の戻り値の型となる関数を自動生成しています。

各ProviderSetはそれぞれ`/src/infrastructure/wire.go`、`/src/application/wire.go`　に記述しています。

また、自分がハマった注意点なのですが、RepositoryはDomain層のinterfaceに依存しているため、
以下のように`wire.Bind`を使って実体を返却するProvider関数とinterfaceの型を紐づける必要があります。

```golang:/src/infrastructure/wire.go
//go:build wireinject

package infrastructure

import (
	calendar_domain "advent-calendar/domain/calendar"
	entry_domain "advent-calendar/domain/entry"
	user_domain "advent-calendar/domain/user"
	"advent-calendar/infrastructure/calendar"
	"advent-calendar/infrastructure/entry"
	"advent-calendar/infrastructure/user"

	"github.com/google/wire"
)

var RepositoryProviderSet = wire.NewSet(
	user.NewUserRepository,
	wire.Bind(new(user_domain.UserRepository), new(*user.UserRepository)),
	calendar.NewCalendarRepository,
	wire.Bind(new(calendar_domain.CalendarRepository), new(*calendar.CalendarRepository)),
	entry.NewEntryRepository,
	wire.Bind(new(entry_domain.EntryRepository), new(*entry.EntryRepository)),
)
```

## 3. Routingの共通化

`gin.HandlerFunc`をルーターに渡してパスに処理をバインドしますが、
ルーティング周りが煩雑になりすぎるので、各Controllerに`gin.HandlerFunc`を返却するよう実装してみました。

まずはControllerの共通interfaceを定義します。

```golang:/src/userinterface/controller.go
package userinterface

import (
	"github.com/gin-gonic/gin"
)

type Controller interface {
	Handler() gin.HandlerFunc
}
```
このinterfaceを実装するように、各Controllerを定義していきます。
以下の`UserFindController`も`Handler()`メソッドを実装しています。

```golang:/src/userinterface/user/user_find_controller.go
package user

import (
	user_app "advent-calendar/application/user"
	"advent-calendar/domain/user"
	"errors"

	"github.com/gin-gonic/gin"
)

type UserFindController struct {
	userFindService user_app.UserFindService
}

func NewUserFindController(userService user_app.UserFindService) *UserFindController {
	c := new(UserFindController)
	c.userFindService = userService
	return c
}

func (controller *UserFindController) Handler() gin.HandlerFunc {
	return func(c *gin.Context) {
		v := c.Param("user_id")
		if len(v) == 0 {
			c.Error(errors.New("user_id is required"))
			return
		}

		id, err := user.CreateIdFrom(v)
		if err != nil {
			c.Error(err)
			return
		}

		user, err := controller.userFindService.Find(id)
		if err != nil {
			c.Error(err)
			return
		}
		c.JSON(200, gin.H{
			"status": "ok",
			"user": gin.H{
				"id":   user.Id,
				"name": user.Name,
			},
		})
	}
}
```
そして、ルーティングの設定を管理してルーターを生成する`router.go`を定義します。
privateな`endPoint`型で`HttpMethod`、`Path`、`CtlInitializer`（Controllerを生成する関数）を定義して、
`CreateRouter()`でループを回してルーティングの設定をしています。

```golang:/src/router.go
package main

import (
	"advent-calendar/userinterface"
	"net/http"

	"github.com/gin-gonic/gin"
	"gorm.io/gorm"
)

type endPoint struct {
	HttpMethod     string
	Path           string
	CtlInitializer func(db *gorm.DB) userinterface.Controller
}

var routings = []endPoint{
	{
		HttpMethod:     http.MethodPost,
		Path:           "/user/create",
		CtlInitializer: func(db *gorm.DB) userinterface.Controller { return InitializeUserCreateController(db) },
	},
	{
		HttpMethod:     http.MethodGet,
		Path:           "/user/:user_id",
		CtlInitializer: func(db *gorm.DB) userinterface.Controller { return InitializeUserFindController(db) },
	},
	{
		HttpMethod:     http.MethodGet,
		Path:           "/calendar/:calendar_id",
		CtlInitializer: func(db *gorm.DB) userinterface.Controller { return InitializeCalendarFindController(db) },
	},
	{
		HttpMethod:     http.MethodPost,
		Path:           "/calendar/create",
		CtlInitializer: func(db *gorm.DB) userinterface.Controller { return InitializeCalendarCreateController(db) },
	},
	{
		HttpMethod:     http.MethodGet,
		Path:           "/entries/calendar/:calendar_id",
		CtlInitializer: func(db *gorm.DB) userinterface.Controller { return InitializeCalendarEntryListFindController(db) },
	},
	{
		HttpMethod:     http.MethodGet,
		Path:           "/entries/user/:user_id",
		CtlInitializer: func(db *gorm.DB) userinterface.Controller { return InitializeUserEntryListFindController(db) },
	},
	{
		HttpMethod:     http.MethodPost,
		Path:           "/entry/create",
		CtlInitializer: func(db *gorm.DB) userinterface.Controller { return InitializeEntryCreateController(db) },
	},
}

func CreateRouter() *gin.Engine {
	r := gin.Default()
	for _, ep := range routings {
		ctlInitializer := ep.CtlInitializer
		handlerFunc := func(c *gin.Context) {
			db := c.MustGet("db").(*gorm.DB)
			(ctlInitializer(db).Handler())(c)
		}
		r.Handle(ep.HttpMethod, ep.Path, ApiTransactionManager, handlerFunc)
	}

	return r
}
```
`router.go`でルーターの設定まで終わっているので、`main.go`はとてもシンプルになります。

```golang:/src/main.go
package main

func main() {
	CreateRouter().Run() // listen and serve on 0.0.0.0:8080 (for windows "localhost:8080")
}
```

# 終わりに

**「自分頑張った！」** と思う箇所を中心に説明させていただきましたが、いかがだったでしょうか。
実際はもう少しシンプルにできたら良いのにと思う部分が多く、
リファクタリングの余地もありまくりなので、（てかそもそもバグがあるので、）
しばらくコツコツとコードの修正をしていきたいと思います。
そして、ゆくゆくは今回得た知識を業務で生かせる場面を見つけられると良いなーと思っております。

明日は　@rs_tukki　さんの記事になります！お楽しみにdesu！
それでは、メリーーーー