今回は Go
言語で、以前 Kotlin
で書いたサーバーと同程度の物を作ろうと思います。
それと、Clean Architecture の記事も書いたので、それらしいレイヤー構成にしてみます。
Data Layer
Entity の定義です。
JSONのKey名を指定したい場合は バッククォート
で定義します。
また、テーブル名は標準だと Entity の複数形になるので、別名にする場合は TableName
メソッドを用意します。
package entity
type User struct {
ID int64 `json:"-"`
Name data.Name `json:"名前"`
Age data.Age `json:"歳"`
Address data.Address `json:"住所"`
}
func (User) TableName() string {
return "user"
}
Repository としてはI/Fを定義するだけです。
package repository
type UserRepository interface {
Get([]string) (*[]entity.User, error)
GetAll() (*[]entity.User, error)
Delete(string) (*entity.User, error)
Create(*entity.User) (*entity.User, error)
}
var User UserRepository
GORM
に依存した形の Repository の実装です。
package db
type UserRepositoryImpl struct {
DB *gorm.DB
}
func (r *UserRepositoryImpl) Get(ids []string) (*[]entity.User, error) {
users := []entity.User{}
err := r.DB.Where(ids).Find(&users).Error
return &users, err
}
func (r *UserRepositoryImpl) GetAll() (*[]entity.User, error) {
users := []entity.User{}
err := r.DB.Find(&users).Error
return &users, err
}
func (r *UserRepositoryImpl) Delete(id string) (*entity.User, error) {
user := entity.User{}
err := r.DB.Where("id = ?", id).Delete(&user).Error
return &user, err
}
func (r *UserRepositoryImpl) Create(user *entity.User) (*entity.User, error) {
err := r.DB.Create(user).Error
return user, err
}
Domain Layer
UseCase の実装です。
本来なら Model を定義して変換した方がよいのですが、今回は端折りました。
package usecase
func GetUsers() (*[]entity.User, error) {
return repository.User.GetAll()
}
func GetUser(id string) (*entity.User, error) {
users, err := repository.User.Get([]string{id})
if len(*users) == 0 {
return nil, errors.New("record not found")
}
return &(*users)[0], err
}
func CreateUser(user *entity.User) (*entity.User, error) {
return repository.User.Create(user)
}
func DeleteUser(id string) (*entity.User, error) {
return repository.User.Delete(id)
}
Presentation Layer
package presentation
var Api WebApi
type WebApi interface {
setup()
}
func Setup() {
Api.setup()
}
package presentation
type GinApi struct {
Address string
}
func (p *GinApi) setup() {
router := gin.Default()
router.Use(limit.MaxAllowed(100))
user := router.Group("/user")
{
user.GET("/", func(ctx *gin.Context) {
framework(ctx, func() (interface{}, error) {
return usecase.GetUsers()
})
})
user.GET("/:id", func(ctx *gin.Context) {
framework(ctx, func() (interface{}, error) {
id := ctx.Param("id")
return usecase.GetUser(id)
})
})
user.POST("/", func(ctx *gin.Context) {
framework(ctx, func() (interface{}, error) {
user := entity.User{}
ctx.BindJSON(&user)
return usecase.CreateUser(&user)
})
})
user.DELETE("/:id", func(ctx *gin.Context) {
framework(ctx, func() (interface{}, error) {
id := ctx.Param("id")
return usecase.DeleteUser(id)
})
})
}
router.Run(p.Address)
}
// コールバック使った共通処理をどうやるのかなと思って試してます。
// 一般的なエラーハンドリングではないかもしれないです。
func framework(ctx *gin.Context, handler func() (interface{}, error)) {
ret, err := handler()
if err != nil {
// 共通のエラーハンドリング作ってみたり
ctx.JSON(http.StatusInternalServerError, gin.H{"message": err})
return
}
ctx.JSON(200, ret)
}
Web F/Wとして、 Gin
を利用していますが、Ginに依存したコードをこのファイル内に限定させ、Domain(usecase)レイヤーに渡してません。
こうすることで、別のWeb F/Wに変更したり、 AWS Lambda
などのサーバーレスに変更する際に Domain レイヤー以下の修正を無くすことができます。
エントリーポイント
DIコンテナとか調べなかったので、main関数で各I/Fに依存性を設定しています。
package main
func main() {
d := initDB()
// Repository には、DBの実装を設定
repository.User = &db.UserRepositoryImpl{DB: d}
// WebApiとしては Gin を利用
presentation.Api = &presentation.GinApi{Address: ":8083"}
presentation.Setup()
}
func initDB() *gorm.DB {
d, _ := gorm.Open("mysql", "root@tcp(127.0.0.1:3306)/hogehoge")
d.LogMode(true)
return d
}
まとめ
Domainレイヤーが薄っぺら過ぎて、この規模だと Clean Architecture の旨味が全然分からないですね。。。
まぁサービスを立ち上げる場合はもっとビジネスロジックが入ってくるはずなので、その時に効果を発揮してくれるかなってことで、やり方確認できたから良しとしよう。
そんな感じで今回 Go をまったく触ったことなかったですが、割とサクッと作ることができました。
今回時間使ったのは、 Clean Architecture
っぽいレイヤー構成にするのに、Interface
だクラスだの書き方が、Java
や Kotlin
、Swift
なんかと結構違ったのでそういった所に戸惑いました。
ただサーバー作るだけならもっとサクッと出来そうです。