0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GoでDI (依存性注入) を実装する

Posted at

はじめに

Go でいつものオブジェクト指向言語のノリでサービス層をリポジトリの抽象に依存させようとした場面があったのですが、言語の機能としてimplementsがなく永遠に頭を抱えてしまったので実装法を記します

DI とは

あるオブジェクトにおける依存関係をオブジェクトのソースコード内に記述せず全く違うオブジェクトから注入させるシステム開発におけるデザインパターンの一つです

スクリーンショット 2024-04-02 19.34.16.png

メリット

DI を行う最大のメリットはテストが容易になることです

例えば、DB登録を含む処理をテストしたい時、外部に依存しているDB登録処理は一旦テストから除外したいとします。そんなとき、DB登録処理はある引数を渡したら定数が返ってくるようにする(モック化)と外部サービスに一切依存することがないため、DB登録を含む処理のテストに集中できそうです

そこで、DI を用いて DB 登録処理の抽象に対し、モック化したDB登録処理の関数を外部から引数として渡すことで目的通りのテストができるというわけです

スクリーンショット 2024-04-02 19.32.19.png

今回の使ったソースコード

ソースコードのディレクトリ

今回はレイヤードアーキテクチャを参考した構成になってます

.
├── 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

スクリーンショット 2024-04-02 18.15.28.png

やること

今回は application 層に対して DI(抽象に依存)を行います

リポジトリの抽象を作成する

src/domain/repository_impl

user_repository_impl.go
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

user_repository.go
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

create_user_service.go
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
}

外部からアプリケーション層に依存性(リポジトリ)を注入

今回はリポジトリの具象を注入しています

create_user_service.go
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)
	})
}

参考資料

0
0
0

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
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?