🧅

【Golang】オニオンアーキテクチャでの実装

2022/07/22に公開

※弊社はgo-swaggerでスキーマ駆動実装をしているので、コードはそちらの影響を受けている部分があります。
※エラーハンドリングやトランザクションなど他にも色々実装はありますがその部分は簡略化しています。

ディレクトリ構成

/gen(go-swaggerで生成されるコード群)
/pkg
	/domain
		/model
		/repository(di用のインターフェース)
	/handler
	/usecase
	/infrastructure
		/persistence

モデル

モデルに関しては以前投稿したこちらに詳細記載しているのでこちらをご覧ください。

インターフェース

package repository

type User interface {
	Get(db)()[]*model.User, error)
	//その他CreateやUpdateなど
}

ハンドラ

  • 構造体のフィールドでusecaseを設ける
    • 実際にusecaseが渡されるのはmainファイル(go-swaggerではconfigure_factory)
package handler

type User struct {
	uc usecase.User
}

//handlerのnew関数はmainファイル(go-swaggerではconfigure_factoryで実行する)
func NewUser (uc usecase.User) *User {
	return &User{uc: uc}
}

//↓go-swaggerの話
//引数がapi個別に生成されたparams、戻り値がmiddleware.Responderである関数をconfigure_factoryで設置したいのでこのようなメソッドを定義している
func (u *User) GetUser(params user.UserParams) middleware.Responder {
	//usecaseの使用
	user, err := a.uc.Get()
	
	//リクエスト成功時の構造体(生成される)で返したいので整形
	OKBody := &user.UserOKBody{	
		User: user,
	}
	
	//user.NewUserGetOkは戻り値がmiddleware.Responder型の、generateされる関数
	return user.NewUserGetOk().WithPayload(&OKBody)
}

createやupdateもあれば同じファイルに記載

ユースケース

  • usescaseのuser構造体にてrepositoryを受け取る
  • 受けとったrepositoryの型をインターフェースの型にすることで「usecase に repository インターフェースをDIしている」を実現する
  • 戻り値や引数はモデルにする、モデルの一部にする。詳しくはこちら↓

https://zenn.dev/marimoofficial/articles/e8c1ee95a62b06

package usecase

//usecaseのフィールドでリポジトリを受け取るが、
//その型はinterface(repository.User)を設定することで、
//「usecase に repository インターフェースをDIしている」ことになる。
type User struct {
	repo repository.User
}

func NewUser(repo repository.User) User{
	return &User{repo: repo}		
}

//usecaseの戻り値や引数はモデル、モデルの一部にする。
//引数や戻り値に Open API の構造体を指定すると
//HTTP に依存してしまうのでstringやintなどプリミティブ型を使う
func(u *User) Get()([]model.User, error){
	user, err := a.repo.Get(db)
	if err != nil {
		return nil, error
	}
	
	//(書いてないが、)トランザクションの開始、終了、ロールバックは
	//usecaseにかく。複数のリポジトリをまたいで制御できるので。
	return user, nil
}

パーシスタンス(永続化処理)

  • DB処理を実装
  • DBのテーブルのデータを持つ構造体で、RDBのテーブル定義から生成するものにメソッドを生やさない。
  • あくまでモデルに対するCRUD処理を実装することで、リポジトリパターンを実現できる
package persistence

type User struct {
}

func NewUser() *User{
	return &User{}
}

//dbから値を取得するためだけの構造体(*1)
type userSelect struct {
	ID         string `db:"id,omitempty"`
	Name      string `db:"name,omitempty"`
	Status    string `db:"status,omitempty"`
	CreatedAt  int64  `db:"created_at,omitempty"`
	UpdatedAt  int64  `db:"updated_at,omitempty"`
}

func (u *User) Get(db)([]*model.User, error) {
	var user []userSelect
	err := dbx.Select(&user, "select id, name, status, created_at, updated_at from user")
	if err != nil {
		fmt.Printf("%+v", errors.WithStack(err))
		return nil, err
	}

	var  userItems []*model.User
	for _, v := range user {

	//モデルの構造体で返したいのでマッピング
	//new関数はバリデーションのための意味合いが強く、
	//DB内のリソースは正しいことが保証されているので
	//構造体をnewで生成したものでなく、取得した値をマッピングしたものを返してOK
		userItems = append(userItems, &model.user{
			ID:         v.ID,
			Name:       v.Name,
			Status:     v.Status,
			CreatedAt:  v.CreatedAt,
			UpdatedAt:  v.UpdatedAt,
		})
	}


	return userItems, nil
}

//postやupdateもあれば同じファイルに実装
//modelが異なる場合は別packageに実装するのが良い(*2)

*1: dbから取得するだけの構造体の詳細はこちら
https://zenn.dev/marimoofficial/articles/e8c1ee95a62b06
*2:「modelが異なる場合は別packageに実装するのが良い」の詳細はこちら
https://zenn.dev/marimoofficial/articles/2eaf3880f53155

メインファイル(configure_factory)

  • handler、usecase、perisostenceのnew関数実行
    • persistenceはusecase、usecaseはhandlerで利用(new関数を実行)するが、persistenceはusecaseに、usecaseはhandlerに依存してしまうため、mainファイル(configure_factory)で実行するようにする。
package main

//handler構造体爆誕
//usecase構造体爆誕
//persistence構造体爆誕
handler.NewUser(usecase.NewUser(persistence.NewUser()))

//※go-swaggerの場合

//上記の構造体をxxxHandlerに格納
 userHandler := handler.NewUser(usecase.NewUser(persistence.NewUser()))
 //上記のhandlerを生成された関数の引数に渡す *3
 api.userXxxHandler = news.UserHandlerFunc(userHander.Get)

*3 詳しくはこちら
【Golang】go-swaggerなどで自動生成された、引数が固定されたメソッドにその引数以外のものを渡したい
https://zenn.dev/marimoofficial/articles/6444111ff593a7

Discussion