Goãš50%ãããã®çè§£ã§ã¯ãããã¯ãªãŒã³ãšããããªããªã³ãªã¢ãŒããã¯ãã£
ç©èªããŠããClean Architectureæ¬ãèªäºããã®ã§ãããããŸãã¡å®è·µçãªã€ã¡ãŒãžæ¹§ããªãã£ãããããªããªã³ã¢ãŒããã¯ãã£ãå®éã«Goã§å®è£ ãããšãã話ã§ãã
æ³å®èªè
- ã¯ãªãŒã³ã¢ãŒããã¯ãã£ãåŠç¿ãããã©ããŸãã¡ãã³ãšããªãã£ã人
- Goã§ã¬ã€ã€ãŒãã¢ãŒããã¯ãã£ãå®è£ ããã人
- ãªããªã³ã¢ãŒããã¯ãã£ã«ã€ããŠç¥ããã人
æ¬èšäºã§èª¬æããªãããš
- DDDã«ã€ããŠãå°ãè§Šããäºå®ã§ãããDDDã®è©³çްã«ã€ããŠã¯èª¬æããŸãã
- ã¯ãªãŒã³ã¢ãŒããã¯ãã£ã®è©³çް
ã¯ãªãŒã³ã¢ãŒããã¯ãã£ããªããã³ãšããªãã®ã
å人çãªææ³ã§ãã
åå¿åã®è©±ã«ãªããã¡

å®éClean Architectureæ¬ã®äžã§ãåå¿åãçšããã¢ãŒããã¯ãã£ã®è©±ã¯æ°ããŒãžã®ã¿ã§ããå®éã®ãããžã§ã¯ããäœæããããšãããšãå ·äœçãªãã£ã¬ã¯ããªæ§æãããã±ãŒãžã®æ§æã®è©±ã«ãªããã¡ãªããããšæããŸããå°ãªããšãClean Architectureæ¬ã§èª¬æãããŠããSOLIDååãã³ã³ããŒãã³ãã®å®å®åºŠãšæœè±¡åºŠã®è©±ãšããšåå¿åã®ã¢ãŒããã¯ãã£ã®è©±ãªã©å«ããŠã¯ãªãŒã³ã¢ãŒããã¯ãã£ãšããæŠå¿µã§ãã(èè ã¯ããçè§£ããŸããã)
ãããŠãäžèšã®åå¿åã§éèŠãªããšã¯ä»¥äžã®2ç¹ã§ãããšããèšãããŠããŸãã
- äŸåé¢ä¿ã¯å€åŽããå åŽãžã®äžæ¹åã®ã¿ã«åãã
- å åŽããå€åŽãžã®äŸåã¯äŸåé¢ä¿é転ã®ååã«ããããããšã§å®çŸãã
ãã®ãããåå¿åã®ãããª4å±€ã®ã¬ã€ã€ãŒæ§é ã§ãªããšãè¯ããšæžãããŠããŸããããã®åç§°ãªã©ããããŸã§éèŠã§ã¯ãšæšå¯ã§ããŸãã
æ¢ã«ããããªãšããã§èšãããŠããŸããããŸãåå¿åã«ãã ãããããªãæ¹ãã¯ãªãŒã³ã¢ãŒããã¯ãã£ã掻çšããŠãããã®ã§ã¯ãªãããªãšæããŸããã
åç§°ã«æãããã
äžè¿°ããåå¿åã®äžå¿ã«ã¯ãšã³ãã£ãã£ãååšããŸãããDDD(ãã¡ã€ã³é§åéçº)ã®æèã§ããšã³ãã£ãã£ãšããåç§°ã䜿ãããŠããŸãããã®äºã€ã¯ãã¡ã€ã³ã衚çŸãããšããç¹ã§ã¯ãŸã£ããã®å¥ç©ã§ããªãæ°ãããã®ã§ããã¯ãªãŒã³ã¢ãŒããã¯ãã£ãšDDDãšã§ãã¯ã説æããŠããæŠå¿µãéããããæ··åããããšæ··ä¹±ããŠããŸãããç¥ããŸããã(ãããã¯ããŸããã)
å ããŠããããã¯JVMç³»ã§ã®éçºãããŸãŸã§ããããããããã¬ãŒã ã¯ãŒã¯ãšããŠSpringã䜿çšããŠããã®ã§ããããã®äžã§ãEntityãRepositoryãServiceãšããçšèªãå€çšããããšã«ãªããŸããããããããŸãã¯ãªãŒã³ã¢ãŒããã¯ãã£ãDDDã§èšãããŠããçšèªãšã¯ç°ãªãæå³åããæã£ãŠããŸãã
ãããããããRuby on Railsãªã©ã®ãã¬ãŒã ã¯ãŒã¯ã䜿çšããŠããæ¹ã§ããšMVCã¢ãŒããã¯ãã£ãšæ¯èŒããæ··åããŠããŸãæ¹ããããããããŸããã
ãããã¯ãªãŒã³ã¢ãŒããã¯ãã£ãåŠç¿ããäžã§ä»ã®ã¢ãŒããã¯ãã£ãšæ··åãæ··ä¹±ããŠãããåç§°ã¯åãã ã説æããŠãããã®ã¯éãããç¥ããªããšããããšãæèãããšçè§£ãé²ãããç¥ããŸããã
以äžã®èšäºã¯ã¢ãŒããã¯ãã£ãšDDDã«ã€ããŠãšãŠããããããã解説ãããŠããã®ã§ãèå³ãããæ¹ã¯ãã²èªãã§ã¿ãŠãã ããã
å ·äœçãªå®è£ äŸããªã
å°ãªããšãæ¬æžã«ã¯ãªãã§ãã調ã¹ãã°ããã€ãèŠã€ãããŸããã©ãããã£ã¬ã¯ããªã»ããã±ãŒãžæ§æã埮åŠã«éããŸãããåç§°ãããããã§èšèªããã¬ãŒã ã¯ãŒã¯ã«ããå·®ç°ãåºãŠããŸãã
ãŸããSpringã¿ãããªDIãã¬ãŒã ã¯ãŒã¯ã䜿çšãããªãããŸãèããªããŠãããã®ã§ããGoãªã©ã§å®è£ ããããšãããšèªåã§DIã³ã³ãããå®è£ ããå¿ èŠãããã®ã§ãããã®å®è£ ã€ã¡ãŒãžãæ¹§ããªããšå šäœçãªå®è£ ã€ã¡ãŒãžãæ¹§ããªããšæããŸãã
以äžã®èšäºã¯Goã®ã¯ãªãŒã³ã¢ãŒããã¯ãã£ã®å®è£ äŸããŸãšããŠãããŠãããã®ã§ãã
ãªã®ã§ãããã¯ãªãŒã³ã¢ãŒããã¯ãã£ã®åŠç¿ãçµããŠãå®è£ ããŠã¿ãããšãªã£ããšãã«å°æããŸãã(ãããã¯ããŸããã)
ç¹°ãè¿ãã«ãªããŸãããã¯ãªãŒã³ã¢ãŒããã¯ãã£ãšããããã®åå¿åãäžçªäŒãããããšã¯ããããæŠå¿µçãªè©±ã§åç§°ã圢ã¯ããã»ã©éèŠã§ã¯ãªãããäžçªãã³ãšããæ§æã§ããã®ããããšæã£ãŠããŸãã
ããã§ãªããªã³ã¢ãŒããã¯ãã£
ã¯ãªãŒã³ã¢ãŒããã¯ãã£ãåŠãã§ãã³ãšããæ¹ã¯ãã®ãŸãŸã¯ãªãŒã³ã¢ãŒããã¯ãã£ã䜿çšããã°ãããšæããŸãããã ãããããããšåãããã«ãªããšãªãçè§£ãããã©å ·äœçãªå®è£ ã€ã¡ãŒãžãæ¹§ããªããšããæ¹ã¯ãªããªã³ã¢ãŒããã¯ãã£ã®æ¹ããã³ãšãããããããŸããã
ãªããªã³ã¢ãŒããã¯ãã£ãšã¯
2008幎ã«Jeffery Palermoã以äžã®èšäºã§æå±ããã¢ãŒããã¯ãã£ã§ããè±èªã§ããæéã¯ãããŸã§å€ããªãã®ã§èå³ãããæ¹ã¯èªãã§ã¿ããšããããããšæããŸãã
以äžã®èšäºã倧å€ããããããã®ã§ãã¡ããåç §ã
ãªããªã³ã¢ãŒããã¯ãã£ã ãšäœãããããã®ã

ãããããããªãšããã§èšãããŠããŸãããªããªã³ã¢ãŒããã¯ãã£ãã¯ãªãŒã³ã¢ãŒããã¯ãã£ãåºæ¬çãªæŠå¿µã¯äžç·ã§ããã©ã¡ããç®çã¯é¢å¿ããšã®åé¢ã§ãã
äžå¿ãäžèšãªããªã³ã¢ãŒããã¯ãã£ã«ã€ããŠæžãããããã°èšäºå ã§èª¬æãããŠãããªããªã³ã¢ãŒããã¯ãã£ã®å®çŸ©çãªãã®ã
Key tenets of Onion Architecture:
- The application is built around an independent object model
- Inner layers define interfaces. Outer layers implement interfaces
- Direction of coupling is toward the center
- All application core code can be compiled and run separate from infrastructure
ãªããªã³ã»ã¢ãŒããã¯ãã£ã®äž»èŠãªèãæ¹ïŒ
- ã¢ããªã±ãŒã·ã§ã³ã¯ãç¬ç«ãããªããžã§ã¯ãã»ã¢ãã«ãäžå¿ã«æ§ç¯ããã
- å åŽã®ã¬ã€ã€ãŒã¯ã€ã³ã¿ãŒãã§ãŒã¹ãå®çŸ©ããŸããå€åŽã®ã¬ã€ã€ãŒã¯ã€ã³ã¿ãŒãã§ãŒã¹ãå®è£ ãã
- çµåã®æ¹åã¯äžå¿ã«åãã£ãŠãã
- ãã¹ãŠã®ã¢ããªã±ãŒã·ã§ã³ã®ã³ã¢ã³ãŒãã¯ãã€ã³ãã©ã¹ãã©ã¯ãã£ãšã¯å¥ã«ã³ã³ãã€ã«ããŠå®è¡ããããšãã§ãã
ãªããªã³ã¢ãŒããã¯ãã£ã®å³ã«ããã¬ã€ã€ãŒãèŠãŠã¿ããšäžçªå€åŽã«testsãšinfrastructureãuser interfaceããããŸããtestãšDBãªã©ã®å
·äœçãªå®è£
ãå«ãŸããinfrastructureå±€ãäžçªå€åŽã«ããã®ã¯çè§£ã§ããã§ãããã
次ã«Application ServicesãšDomain(Object) Servicesã¬ã€ã€ãŒããããã±ã£ãšèŠéããããããŸãããå³ã®äŸã«Repositoryã€ã³ã¿ãŒãã§ã€ã¹ãDomainServicesã¬ã€ã€ãŒã«ããã®ãã¿ããšDBå®è£
ã®ã€ã³ã¿ãŒãã§ã€ã¹ãDomain Serviceã¬ã€ã€ãŒã«çœ®ãã°ãšããããè¯ãããã§ãã
Application Serviceã«ã¯Domain Serviceã䜿çšãé©åã«ãã¡ã€ã³æäœã®åããŸãšããšãã©ã³ã¶ã¯ã·ã§ã³ãªã©ã®åŠçã宿œããã°ããããã§ããSpringçµéšè ã®æ¹ã§ããã°ããããServiceã¢ãããŒã·ã§ã³ãä»ããã¯ã©ã¹ã§äŒããã§ãããã
äžå¿ã®Domain(Object) Modelã¯ã¢ããªã±ãŒã·ã§ã³ã®ã³ã¢ãšãªãããžãã¹ã¢ãã«çæŠå¿µãããã«åœãŠã¯ãŸãã§ãããããã®éšåã¯æãã¢ããªã±ãŒã·ã§ã³ã«åœ±é¿ã®ããã¬ã€ã€ãŒã®ããäœã«ãäŸåããŠããŸããã
ãšãªããšControllerãšåŒã°ããéšåã®å®è£ ã¯ã©ãããããšãªãã®ã§ããããªããªã³ã¢ãŒããã¯ãã£ã®å³ãèŠãŠã¿ããšäžçªå€åŽã®user interfaceã¬ã€ã€ã«çœ®ãããããšã«ãªããŸããControllerã®å®è£ ãinfrastructureãšåãäžçªå€åŽã«ããã®ã«éåæãæããŸãããããªããªã³ã¢ãŒããã¯ãã£ãæå±ãããŠããèšäºå ã§ã¯
CodeCampServerã¯ASP.NET MVC Frameworkã䜿çšããŠããã®ã§ãSpeakerControllerã¯ãŠãŒã¶ãŒã€ã³ã¿ãŒãã§ã€ã¹ã®äžéšãšãªããŸãã ãã®ã³ã³ãããŒã©ã¯ASP.NET MVC FrameworkãšçµåããŠããããããåé¿ããããšã¯ã§ããªãã(æ¥æ¬èªèš³)
ãšãããŸããControllerã®å®è£ ã¯ãã¬ãŒã ã¯ãŒã¯ã«åŒ·ãäŸåããŠããã®ã§äžçªå€åŽã«ãããšããããšã§ãããããã¯SpringãRuby on Railsãšãã£ããã¬ãŒã ã¯ãŒã¯ã§ãåãããšãèšããã§ãããã
ã©ãã§ããããã¯ãªãŒã³ã¢ãŒããã¯ãã£ã®åå¿åããå®è£
ã®ã€ã¡ãŒãžãã€ããŸãããïŒåè¿°ããQiitaã®èšäºã«ããããŸãããå人çã«ã¯ã¯ãªãŒã³ã¢ãŒããã¯ãã£ã®Use Casesã¬ã€ã€ãšInterface Adaptersã¬ã€ã€ã«äœãã©ãã«çœ®ãããããã®ããçµæ§ã²ãšãããããªæãããã®ãšåç§°ãPresentersãControllerãšãã£ããã®ããããã£ã¬ã¯ããªåç§°ããããžã§ã¯ãã«ãã£ãŠå€ããã®ã§ãããæ£è§£ã ãã¿ãããªã®ããªãã®ããããã¥ãããšæã£ãŠãŸãã
ãªããªã³ã¢ãŒããã¯ãã£ããããæ£è§£ã§ãã¿ãããªã®ã¯åœç¶ãªãã®ã§ããããŸã ã¯ãªãŒã³ã¢ãŒããã¯ãã£ããã¯éžæè¢ãå°ãªãããããããããªãšããããšã§ãã
å®è£ ããŠã¿ã
Goã§ç°¡åãªTodo APIããªããªã³ã¢ãŒããã¯ãã£ã§äœæããŸãã
äœæããææç©ã¯ãã¡ã
ãã£ã¬ã¯ããªæ§æã¯ä»¥äžã®ãããªæãã«ãªããŸããã詳ããã¯åŸè¿°ããŸãã
.
âââ README.md
âââ application
â  âââ service
âââ common
â  âââ todo.go
â  âââ user.go
âââ domain
â  âââ model
â  âââ repository
âââ go.mod
âââ go.sum
âââ go_onion_architecture.db
âââ infrastructure
â  âââ db.go
â  âââ repository
âââ main.go
âââ mocks
â  âââ mock_repository
â  âââ registory
â  âââ service
âââ registory
â  âââ registory.go
âââ test
â  âââ container.go
âââ testdata
â  âââ golden
âââ userinterface
âââ echo.go
âââ handler
âââ request
âââ response
domain.model
ãªããªã³ã¢ãŒããã¯ãã£ã®å³ã®æãäžå¿ã®Domain Modelã®éšåã§ããä»åã¯ã·ã³ãã«ã«ä»¥äžã®ãããªã¢ãã«ãäœæããŸããã
package model
import "time"
type TodoId int64
type Title string
type Description string
type Todo struct {
Id TodoId `json:"id"`
Title Title `json:"title"`
Description Description `json:"description"`
CreatedAt time.Time `json:"created_at"`
DeleteAt time.Time `json:"delete_at"`
}
ãããã¢ããªã±ãŒã·ã§ã³ã®ã³ã¢ãšãªãããžãã¹ã¢ãã«ã«ãªããŸãããªããªã³ã¢ãŒããã¯ãã£ã®å³ã®æãäžå¿ã®æŠå¿µã®ããã©ãã«ãäŸåããŠããããä»ã®ã¬ã€ã€ããäŸåãããããšã«ãªãéšåã§ãã
ãã®ãããä»ã®ã¬ã€ã€ã®å€æŽã®åœ±é¿ãåãããéã«ãã®ã¢ãã«ã®å€æŽã¯ä»ã®å šãŠã®äŸåã¬ã€ã€ã«åœ±é¿ãããããããšã«ãªããŸãã
domain.repository
ããã¯Domain Servicesã¬ã€ã€ã§ããã¬ã€ã€ã®åç§°ã¯Serviceã§ããRepositoryãšããåç§°ã«éŠŽæã¿ãããã®ã§domainãã£ã¬ã¯ããªã®é
äžã«repositoryãšãããã£ã¬ã¯ããªãäœæã以äžã®ãããªã€ã³ã¿ãŒãã§ã€ã¹ãé
眮ããŸããã
package repository
import "github.com/JY8752/go-onion-architecture-sample/domain/model"
type TodoRepository interface {
Create(model.UserId, model.Title, model.Description) (model.TodoId, error)
List(model.UserId) ([]model.Todo, error)
Delete(model.TodoId) error
}
ãã®ã¬ã€ã€ã«ã¯SQLãªã©ã®å ·äœçãªå®è£ ãç¥ã£ãŠã¯ãããªãã®ã§ã€ã³ã¿ãŒãã§ã€ã¹ã®ã¿ãé 眮ããŸãã
application.service
ããã¯Application Servicesã¬ã€ã€ã«ãªããŸããRepositoryã€ã³ã¿ãŒãã§ã€ã¹ã䜿çšããŠããžãã¹ã¢ãã«ã®æ°žç¶åãååŸãªã©ã宿œããŸããä»åã®äŸã§ã¯ã»ãšãã©ããžãã¯çãªãã®ã¯ãããŸãããããã«ãµãŒãã¹ããžãã¯çãªãã®ãããæ³å®ã§ãã
package service
import (
"github.com/JY8752/go-onion-architecture-sample/domain/model"
"github.com/JY8752/go-onion-architecture-sample/domain/repository"
)
type TodoService interface {
Create(model.UserId, model.Title, model.Description) (model.TodoId, error)
List(model.UserId) ([]model.Todo, error)
Delete(model.TodoId) error
}
type todoService struct {
todoRep repository.TodoRepository
}
func NewTodoService(todoRep repository.TodoRepository) TodoService {
return &todoService{
todoRep: todoRep,
}
}
func (t *todoService) Create(userId model.UserId, title model.Title, description model.Description) (model.TodoId, error) {
return t.todoRep.Create(userId, title, description)
}
func (t *todoService) List(userId model.UserId) ([]model.Todo, error) {
return t.todoRep.List(userId)
}
func (t *todoService) Delete(todoId model.TodoId) error {
return t.todoRep.Delete(todoId)
}
ãã®Serviceãã€ã³ã¿ãŒãã§ã€ã¹ãšå®è£ ããããé çœ®å Žæã«æ©ãã ã®ã§ããä»åã¯åãã¬ã€ã€ã«é 眮ããŸããããªããªã³ã¢ãŒããã¯ãã£ã§ã¯å€åŽã®ã¬ã€ã€ã«å®è£ ãå åŽã«ã€ã³ã¿ãŒãã§ã€ã¹ãšãããã€ã³ããããã®ã§å åŽã®Domain Serviceã¬ã€ã€ã«ã€ã³ã¿ãŒãã§ã€ã¹ãé 眮ããŠãããã®ãããããŸããã
infrastructure
äžçªå€åŽã®ã¬ã€ã€ã§DBã®å ·äœçãªè©³çްãå®è£ ããå Žæã§ããä»åã¯sqlite3ã䜿çšããŠå®è£ ããŸããã
package infrastructure
import (
"log"
"time"
"github.com/JY8752/go-onion-architecture-sample/domain/model"
"github.com/JY8752/go-onion-architecture-sample/domain/repository"
db "github.com/JY8752/go-onion-architecture-sample/infrastructure"
)
type todoRepository struct {
dbClient *db.DBClient
}
func NewTodoRepository(db *db.DBClient) repository.TodoRepository {
return &todoRepository{
dbClient: db,
}
}
func (t *todoRepository) Create(userId model.UserId, title model.Title, description model.Description) (model.TodoId, error) {
stmt, err := t.dbClient.Client.Prepare("INSERT INTO todos (user_id, title, description, created_at) VALUES (?, ?, ?, ?)")
if err != nil {
return 0, err
}
result, err := stmt.Exec(userId, title, description, time.Now())
if err != nil {
return 0, err
}
id, err := result.LastInsertId()
if err != nil {
return 0, err
}
return model.TodoId(id), nil
}
func (t *todoRepository) List(id model.UserId) ([]model.Todo, error) {
stmt, err := t.dbClient.Client.Prepare("SELECT id, title, description, created_at FROM todos WHERE user_id = ? AND delete_at IS NULL")
if err != nil {
return nil, err
}
rows, err := stmt.Query(id)
if err != nil {
return nil, err
}
var todos []model.Todo
for rows.Next() {
var todo model.Todo
err = rows.Scan(&todo.Id, &todo.Title, &todo.Description, &todo.CreatedAt)
if err != nil {
log.Printf("err: %s\n", err.Error())
continue
}
todos = append(todos, todo)
}
return todos, nil
}
func (t *todoRepository) Delete(id model.TodoId) error {
stmt, err := t.dbClient.Client.Prepare("UPDATE todos SET delete_at = ? WHERE id = ?")
if err != nil {
return err
}
_, err = stmt.Exec(time.Now(), id)
if err != nil {
return err
}
return nil
}
ããDBãsqlite3ããMongoã«å€æŽã ã£ããã䜿çšããORMã倿Žããããšã«ãªã£ãå Žåã«ãã®infrastructureã®å®è£ ããŸãã£ãšäœãæ¿ããã ãã§ããããã«æèããŠå®è£ ãããšãããšæããŸãã
user interface
ããããControllerãšããŠå®è£
ãããåŠçã§ããä»åã¯echoã䜿çšããŠå®è£
ããŸããã
package handler
import (
"log"
"github.com/JY8752/go-onion-architecture-sample/common"
"github.com/JY8752/go-onion-architecture-sample/domain/model"
"github.com/JY8752/go-onion-architecture-sample/registory"
"github.com/JY8752/go-onion-architecture-sample/userinterface/request"
"github.com/JY8752/go-onion-architecture-sample/userinterface/response"
"github.com/labstack/echo/v4"
)
func TodoHandler(client *echo.Echo, registory registory.Registory) {
client.POST("/:userId/todos", func(c echo.Context) error {
// ããªããŒã·ã§ã³
_, err := common.GetUserId(c.Param("userId"))
if err != nil {
return err
}
var r request.CreateTodoRequest
if err := c.Bind(&r); err != nil {
return err
}
id, err := registory.TodoService().Create(
model.UserId(r.UserId),
model.Title(r.Title),
model.Description(r.Description),
)
if err != nil {
return err
}
return c.JSON(200, response.CreateTodoResponse{Id: id})
})
client.GET("/:userId/todos", func(c echo.Context) error {
userId, err := common.GetUserId(c.Param("userId"))
if err != nil {
return err
}
todos, err := registory.TodoService().List(userId)
if err != nil {
log.Printf("err: %s\n", err.Error())
return c.JSON(404, []model.Todo{})
}
return c.JSON(200, response.GetTodosResponse{Todos: todos})
})
client.DELETE("/todos/:id", func(c echo.Context) error {
todoId, err := common.GetTodoId(c.Param("id"))
if err != nil {
return err
}
err = registory.TodoService().Delete(todoId)
if err != nil {
return err
}
return c.NoContent(204)
})
}
ãããäžçªæ©ãã ãã§ãããªãã¹ãechoãåãæšãŠããããããã£ãã®ã§ãããã©ãããŠãechoã®å®è£ ã«äŸåããŠããŸãã®ã§çµæçã«ãã®ãããªå®è£ ã«ãªããŸããããã£ãšããæãã®å®è£ ããããšæããŸãããã¶ãã
registory
ããã¯ãªããªã³ã¢ãŒããã¯ãã£ã¯é¢ä¿ãªãã®ã§ãããechoã®handler颿°ããService -> RepositoryãšåŒã³åºããŠããã®ã«äŸåé¢ä¿ã®æ³šå ¥ãè¡ãå¿ èŠãããããã®ããã®DIã³ã³ããã®å®è£ ã§ãã
Springãªã©ã®ãã¬ãŒã ã¯ãŒã¯ãªãã°ãã¬ãŒã ã¯ãŒã¯åŽãããæãã«ãã£ãŠãããŸããGoã®å Žåèªäœãããwireãªã©ã®ã¢ãžã¥ãŒã«ã䜿çšããå¿ èŠããããŸãã
package registory
import (
service "github.com/JY8752/go-onion-architecture-sample/application/service"
repository "github.com/JY8752/go-onion-architecture-sample/domain/repository"
db "github.com/JY8752/go-onion-architecture-sample/infrastructure"
infrastructure "github.com/JY8752/go-onion-architecture-sample/infrastructure/repository"
)
type Registory interface {
UserRep() repository.UserRepository
UserService() service.UserService
TodoRep() repository.TodoRepository
TodoService() service.TodoService
}
type registory struct {
dbClient *db.DBClient
}
func NewRegistory(db *db.DBClient) Registory {
return ®istory{
dbClient: db,
}
}
func (r *registory) UserRep() repository.UserRepository {
return infrastructure.NewUserRepository(r.dbClient)
}
func (r *registory) UserService() service.UserService {
return service.NewUserService(r.UserRep())
}
func (r *registory) TodoRep() repository.TodoRepository {
return infrastructure.NewTodoRepository(r.dbClient)
}
func (r *registory) TodoService() service.TodoService {
return service.NewTodoService(r.TodoRep())
}
registoryã®å®è£ ã¯ã¢ãŒããã¯ãã£ã®äžçªå€åŽãããã¯åã®å€åŽããå šãŠã®ã¬ã€ã€ã«äŸåããŠããã€ã¡ãŒãžã§å€§äžå€«ã ãšæããŸããå®è£ ã¯ä»¥äžã®èšäºãåèã«ãããŠããã ããŸããã
main
ããã§ã ãããå®è£ ã¯å®äºã§ããæåŸã«main颿°ã¯ä»¥äžã®ããã«ãªããŸããã
package main
import (
db "github.com/JY8752/go-onion-architecture-sample/infrastructure"
"github.com/JY8752/go-onion-architecture-sample/registory"
ui "github.com/JY8752/go-onion-architecture-sample/userinterface"
)
func main() {
// db
db := db.NewDBClient("./go_onion_architecture.db")
defer db.Client.Close()
// registory
registory := registory.NewRegistory(db)
// echo
apiClient := ui.NewApiClient(registory)
apiClient.RegisterRoute()
apiClient.Start()
}
ãã®ä»
ä»åãå
±éåŠççãªãã®ãcommonãã£ã¬ã¯ããªãäœæãé
眮ããŸãããããã®ãããªãŠãŒãã£ãªãã£ã¯äŸå€çã«äžçªå€åŽã®ã¬ã€ã€ã«ããŸããã
ãã ãåŒã³åºãåŽãå šãŠãã®ãŠãŒãã£ãªãã£ã«äŸåããããšã«ãªãã®ãšãããããŠãŒãã£ãªãã£ãäœããäœããªããã¿ãããªè©±ã«ãªããããªã®ã§ãããé©åã§ã¯ãªããããããŸããã
Clean Architectureæ¬ã«ã¯ãã®ãããªãŠãŒãã£ãªãã£ã¯ããããç®æããåŒã°ããå¯èœæ§ãããããå®å®åºŠãé«ããæœè±¡åºŠã¯äœãã倿Žãããã«ããã³ã³ããŒãã³ãã§ãããšæžãããŠããŸãã
ããããããã眮ãã«ããŠãäžå¿ã®domainã¬ã€ã€ã«çœ®ãæ¹ãé©åãããããŸããã
ããšã¯ãconfigç³»ã宿°ããã£ãã·ã¥ãªã©ãã©ãã«çœ®ããã¿ãããªè©±ãå®ãããžã§ã¯ãã§ã¯åºãŠãããã§ããåºæ¬çã«ã¯ã¢ããªã±ãŒã·ã§ã³ã®ããžãã¯ãšã¯ç¡é¢ä¿ã§å€æŽã®å¯èœæ§ããããã®ã¯å€åŽã«ãããã§ãªããã°å åŽã«çœ®ããããªæèã§å®è£ ããã°ãããšæããŸãã
ãã¹ããæžã
ã¯ãªãŒã³ã¢ãŒããã¯ãã£ããªããªã³ã¢ãŒããã¯ãã£ã®ã¡ãªãããšããŠããããã®ã¬ã€ã€ã§ãã¹ããæžãããããªããšãã£ãç¹ãããã®ã§ãã¹ããæžããŠãããŸãã
Clean Architectureæ¬ã«ããã¹ããã·ã¹ãã ã®äžéšã§ããããã¹ã察象ã®å®è£ ã«åŒ·ãäŸåããŠããããšãããŸãããã¹ããå®è£ ã«äŸåããŠããã°åœç¶ãå®è£ ã«å€æŽããã£ãå Žåã«ãã¹ãã圱é¿ãåããããä¿®æ£ããå¿ èŠãã§ãŠããŸãã
ãã¹ããããå£ãããšèšãããã®ã¯ãã®ãããªçç±ããã§ãããããã®ããããã¹ããå£ãã«ããããããã«ãªãã¹ãå®è£ ã«äŸåããªãããã«ããæ¹ããããšèè ã¯æã£ãŠããŸããã€ãŸããmockã䜿ããããšããããšã§ãã
mockã䜿ã䜿ããªãã¯æèŠããããããšããã§ããããšæããŸãããèè ã¯äžèšã®ãããªçç±ããä»ã®ã¬ã€ã€ãžã®äŸåé¢ä¿ã¯mockã«ãããã®ã¬ã€ã€ã®è²¬åã«ã®ã¿ã«çŠç¹ãåœãŠãŠåäœãã¹ããæžãããšã«ããŠããŸãã
infrastructureã®ãã¹ã
ä»åã¯sqlite3ã䜿çšããŠããã®ã§ãã€ã³ã¡ã¢ãªã®DBããã¹ãçšã«èµ·åããã¹ããå®è¡ããŸãã
package infrastructure_test
import (
"os"
"testing"
"github.com/JY8752/go-onion-architecture-sample/domain/model"
"github.com/JY8752/go-onion-architecture-sample/domain/repository"
db "github.com/JY8752/go-onion-architecture-sample/infrastructure"
infrastructure "github.com/JY8752/go-onion-architecture-sample/infrastructure/repository"
"github.com/stretchr/testify/assert"
)
var todoRep repository.TodoRepository
func TestMain(m *testing.M) {
d := db.NewDBClient("file:infrastructure_test_db?mode=memory")
todoRep = infrastructure.NewTodoRepository(d)
code := m.Run()
d.Client.Close() // Exitãããšdeferãå®è¡ãããªãã®ã§
os.Exit(code)
}
func TestCreate(t *testing.T) {
// when
userId := model.UserId(1)
id, err := todoRep.Create(userId, "title", "description")
if err != nil {
t.Fatal(err)
}
// then
todos, err := todoRep.List(userId)
if err != nil {
t.Fatal(err)
}
assert.Equal(t, 1, len(todos))
assert.Equal(t, id, todos[0].Id)
assert.Equal(t, model.Title("title"), todos[0].Title)
assert.Equal(t, model.Description("description"), todos[0].Description)
}
ãããMySQLãªã©ã®DBã®ãã¹ããããå Žåããã¹ãå®è¡æã«ã³ã³ããã®èµ·åã»åæ¢ãã³ãŒãäžã§æ±ããdockertestãªã©ãããããã§ãã
ããèå³ãããã°ä»¥äžã®èšäºãåèã«ãªããããããŸãã
serviceã®ãã¹ã
ããã§ã¯Repositoryã®å®è£ ã«äŸåããããªãã®ã§Repositoryã¯mockã䜿çšããŸããä»åã¯gomockã䜿çšããŸããã
package service_test
import (
"testing"
"github.com/JY8752/go-onion-architecture-sample/application/service"
"github.com/JY8752/go-onion-architecture-sample/domain/model"
"github.com/JY8752/go-onion-architecture-sample/mocks/mock_repository"
"github.com/golang/mock/gomock"
"github.com/stretchr/testify/assert"
)
func TestCreate(t *testing.T) {
// given
ctrl := gomock.NewController(t)
defer ctrl.Finish()
m := mock_repository.NewMockTodoRepository(ctrl)
m.EXPECT().Create(gomock.Any(), gomock.Any(), gomock.Any()).Return(model.TodoId(1), nil)
ts := service.NewTodoService(m)
// when
result, err := ts.Create(1, model.Title("title"), model.Description("description"))
if err != nil {
t.Fatal(err)
}
// when
assert.Equal(t, model.TodoId(1), result)
}
handlerã®ãã¹ã
ãµãŒãã¹ã®ãã¹ãåæ§ããµãŒãã¹ã®å®è£ ã«äŸåããããªãã®ã§mockã䜿çšããŸãã
package handler_test
import (
"net/http/httptest"
"strings"
"testing"
"github.com/JY8752/go-onion-architecture-sample/domain/model"
mock_registory "github.com/JY8752/go-onion-architecture-sample/mocks/registory"
mock_service "github.com/JY8752/go-onion-architecture-sample/mocks/service"
"github.com/JY8752/go-onion-architecture-sample/userinterface/handler"
"github.com/golang/mock/gomock"
"github.com/labstack/echo/v4"
"github.com/sebdah/goldie/v2"
"github.com/stretchr/testify/assert"
)
const (
goldenDir = "../../testdata/golden/"
)
func TestCreateUserHandler(t *testing.T) {
// given
e := echo.New()
ctrl := gomock.NewController(t)
defer ctrl.Finish()
// ãšã³ããã€ã³ãã®ç»é²
service := mock_service.NewMockUserService(ctrl)
service.EXPECT().Create("user1").Return(model.UserId(1), nil)
registory := mock_registory.NewMockRegistory(ctrl)
registory.EXPECT().UserService().Return(service)
handler.CreateUserHandler(e, registory)
// ãªã¯ãšã¹ãã®äœæ
body := `{"name": "user1"}`
w := httptest.NewRecorder()
r := httptest.NewRequest(echo.POST, "/users", strings.NewReader(body))
r.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
defer r.Body.Close()
// when
e.ServeHTTP(w, r)
// then
assert.Equal(t, 200, w.Code)
g := goldie.New(t, goldie.WithFixtureDir(goldenDir))
g.Assert(t, t.Name(), w.Body.Bytes())
}
handlerã®ãã¹ãã¯golden testã§å®è£ ããŸãããã¬ã¹ãã³ã¹ã®JSONã®é ç®ãå¢ããŠãããšã¢ãµãŒã·ã§ã³ã倧å€ãªããæåŸ ããæ å ±ããã¡ã€ã«ã§ç®¡çã§ããgolden testãšããŠãã¹ããæžãããšã§æ¥œã«ãã¹ããæžãããšãã§ããŸããã
Goã§golden testãæžãããã®ã¢ãžã¥ãŒã«ã¯ããã€ããããŸãããä»åã¯goldieã䜿çšããŸããã
ãŸãšã
äœåºŠãèšãããã§ããã¯ãªãŒã³ã¢ãŒããã¯ãã£ããªããªã³ã¢ãŒããã¯ãã£ãéèŠãªããšã¯ã¬ã€ã€ãåããããšãšããããã®ã¬ã€ã€ã®äŸåé¢ä¿ã®æ¹åã§ããç®çã¯é¢å¿ããšã®åé¢ã§ãã倿Žã«åŒ·ãã·ã¹ãã ãäœãããšã§ãã
ãã®ãããå ·äœçãªãããæ£è§£ã§ããšãã£ããã®ã¯ãªããããžã§ã¯ããçµç¹ã䜿çšããèšèªã«ãã£ãŠãäœãæ¹ã¯å€ãã£ãŠãããšæããŸãã
æœè±¡çãªæŠå¿µãªã®ã§å®å šã«çè§£ããããšã¯é£ããããããŸãã¯èªåã®ã¹ã¿ã€ã«ã§çŽåŸæã®ãããã®ããŸãã¯äœã£ãŠã¿ããšçè§£ã«ã€ãªãããããããŸããã
ãšã«ãããéèŠã ãªãšæã£ãããšã¯ãã¬ãŒã ã¯ãŒã¯ãDBãªã©å€éšäŸåã®éšåã¯ãã€ã§ãæšãŠãããããã«å®è£ ããããšã§ãããŸããæŠå¿µãçè§£ããªããã°å®è£ ããŠããŠãè ã«èœã¡ãªããšæããŸãã®ã§Clean Architecutureæ¬ããŸã èªãã ãªãæ¹ã¯ãã²èªãã§ã¿ãããšãããããããŸãã
ä»åã¯ä»¥äžã§ãðŒ
Discussion