はじめに
Go でいつものオブジェクト指向言語のノリでサービス層をリポジトリの抽象に依存させようとした場面があったのですが、言語の機能としてimplements
がなく永遠に頭を抱えてしまったので実装法を記します
DI とは
あるオブジェクトにおける依存関係をオブジェクトのソースコード内に記述せず全く違うオブジェクトから注入させるシステム開発におけるデザインパターンの一つです
メリット
DI を行う最大のメリットはテストが容易になることです
例えば、DB登録を含む処理
をテストしたい時、外部に依存しているDB登録処理
は一旦テストから除外したいとします。そんなとき、DB登録処理
はある引数を渡したら定数が返ってくるようにする(モック化)と外部サービスに一切依存することがないため、DB登録を含む処理
のテストに集中できそうです
そこで、DI を用いて DB 登録処理の抽象に対し、モック化したDB登録処理
の関数を外部から引数として渡すことで目的通りのテストができるというわけです
今回の使ったソースコード
ソースコードのディレクトリ
今回はレイヤードアーキテクチャを参考した構成になってます
.
├── application
│ └── services
│ └── create_user_service.go
├── config
│ └── env.go
├── domain
│ ├── models
│ │ └── user
│ │ ├── user.go
│ │ └── user_id.go
│ └── repository_impl
│ └── user_repository_impl.go
├── go.mod
├── go.sum
├── infrastructure
│ ├── connection.go
│ └── repository
│ └── user_repository.go
├── main.go
├── presentation
│ └── handlers
│ └── create_user_handler.go
└── utils
├── ulid.go
└── validate.go
やること
今回は application 層に対して DI(抽象に依存)を行います
リポジトリの抽象を作成する
src/domain/repository_impl
package repository_impl
import (
"database/sql"
user_models_domain "github.com/karasuneo/go-di/src/domain/models/user"
)
type UserRepositoryImpl interface {
Save(
db *sql.DB,
u *user_models_domain.User,
) (*user_models_domain.User, error)
}
リポジトリ具象を抽象に依存させるようにする
go には interface を参照することを示すimplements
が存在しないため自身でinterface
が返り値の型となり、返り値が具象となるコンストラクタを作成します
型合ってないからコンパイルエラーが出るのでは?と思う方もいるかもしれませんが、interface
として定義したメソッドが構造体のメソッドとして実装してあり、引数と返り値を正しければちゃんと通るようになってます
src/infrastructure/repository
package repository
import (
"database/sql"
"time"
user_models_domain "github.com/karasuneo/go-di/src/domain/models/user"
"github.com/karasuneo/go-di/src/domain/repository_impl"
)
type UserRepository struct{}
func NewUserRepository() repository_impl.UserRepositoryImpl {
return &UserRepository{}
}
func (ur *UserRepository) Save(
db *sql.DB,
u *user_models_domain.User,
) (*user_models_domain.User, error) {
var id string
var name string
var mail string
var createdAt, updatedAt time.Time
err := db.QueryRow(`
INSERT INTO users (id, name, mail)
VALUES ($1, $2, $3)
RETURNING id, name, mail, created_at, updated_at
`, u.GetId(), u.GetName(), u.GetMail()).Scan(&id, &name, &mail, &createdAt, &updatedAt)
if err != nil {
panic(err)
}
resUser, err := user_models_domain.NewUser(
&id,
name,
mail,
)
if err != nil {
return nil, err
}
return resUser, nil
}
リポジトリを使う処理を含む記述する
type CreateUserService struct {
userRepo repository_impl.UserRepositoryImpl
}
func NewCreateUserService(
userRepo repository_impl.UserRepositoryImpl,
) *CreateUserService {
return &CreateUserService{
userRepo: userRepo,
}
}
以上の部分でリポジトリの抽象に依存させます
コンストラクタでrepository_impl.UserRepositoryImpl
を引数としている部分です
src/application/services
package services
import (
user_models_domain "github.com/karasuneo/go-di/src/domain/models/user"
"github.com/karasuneo/go-di/src/domain/repository_impl"
"github.com/karasuneo/go-di/src/infrastructure"
)
type CreateUserService struct {
userRepo repository_impl.UserRepositoryImpl
}
func NewCreateUserService(
userRepo repository_impl.UserRepositoryImpl,
) *CreateUserService {
return &CreateUserService{
userRepo: userRepo,
}
}
func (cus *CreateUserService) Run(
u *user_models_domain.User,
) (
*user_models_domain.User,
error,
) {
dbConnection := infrastructure.DBConnection{}
conn, err := dbConnection.Connect()
if err != nil {
return nil, err
}
resUser, err := cus.userRepo.Save(conn, u)
if err != nil {
return nil, err
}
return resUser, nil
}
外部からアプリケーション層に依存性(リポジトリ)を注入
今回はリポジトリの具象を注入しています
func CreateUserHandler(r *gin.Engine) {
r.POST("api/user/create", func(c *gin.Context) {
var req CreateUserRequest
// リクエストのバリデーション
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
cus := services.NewCreateUserService(
repository.NewUserRepository(),
)
user, err := user_models_domain.NewUser(
nil,
req.Name,
req.Mail,
)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
}
resUser, err := cus.Run(user)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
res := CreateUserResponse{
Id: resUser.GetId(),
Name: resUser.GetName(),
Mail: resUser.GetMail(),
}
// レスポンスを返却
c.JSON(http.StatusCreated, res)
})
}
参考資料