├── slides ├── assets │ ├── cleanarch.jpg │ ├── cmd-api.imports.png │ ├── app-controllers-api.imports.png │ ├── app-repos-sprints-gorm.imports.png │ └── bizrules-usecases-get_sprint.imports.png ├── manfred - clean architecture.pdf ├── Makefile └── README.md ├── example ├── .gitignore ├── bizrules │ ├── gateways │ │ ├── issue.go │ │ ├── sprint.go │ │ └── .import-graph.svg │ ├── usecases │ │ ├── ping │ │ │ ├── io │ │ │ │ ├── io.go │ │ │ │ └── .import-graph.svg │ │ │ ├── ping.go │ │ │ ├── ping_test.go │ │ │ ├── dto │ │ │ │ ├── dto.go │ │ │ │ └── .import-graph.svg │ │ │ └── .import-graph.svg │ │ ├── add_sprint │ │ │ ├── io │ │ │ │ ├── io.go │ │ │ │ └── .import-graph.svg │ │ │ ├── addsprint.go │ │ │ ├── addsprint_test.go │ │ │ ├── dto │ │ │ │ ├── dto.go │ │ │ │ └── .import-graph.svg │ │ │ └── .import-graph.svg │ │ ├── get_sprint │ │ │ ├── getsprint.go │ │ │ ├── io │ │ │ │ ├── io.go │ │ │ │ └── .import-graph.svg │ │ │ ├── dto │ │ │ │ ├── dto.go │ │ │ │ └── .import-graph.svg │ │ │ ├── getsprint_test.go │ │ │ └── .import-graph.svg │ │ ├── close_sprint │ │ │ ├── io │ │ │ │ ├── io.go │ │ │ │ └── .import-graph.svg │ │ │ ├── closesprint.go │ │ │ ├── closesprint_test.go │ │ │ ├── dto │ │ │ │ ├── .import-graph.svg │ │ │ │ └── dto.go │ │ │ └── .import-graph.svg │ │ ├── test.go │ │ └── .import-graph.svg │ └── entities │ │ ├── .import-graph.svg │ │ ├── issue_test.go │ │ ├── sprint_test.go │ │ ├── sprint.go │ │ └── issue.go ├── README.md ├── Makefile ├── app │ ├── controllers │ │ └── api │ │ │ ├── ping.go │ │ │ ├── closesprint.go │ │ │ ├── getsprint.go │ │ │ └── .import-graph.svg │ └── repos │ │ └── sprints │ │ ├── mem │ │ ├── repo.go │ │ └── .import-graph.svg │ │ └── gorm │ │ ├── repo.go │ │ └── .import-graph.svg └── cmd │ └── api │ ├── main.go │ └── .import-graph.svg ├── requestors.go ├── responders.go ├── usecase.go ├── tools └── gen-import-path └── README.md /slides/assets/cleanarch.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moul/cleanarch/master/slides/assets/cleanarch.jpg -------------------------------------------------------------------------------- /slides/assets/cmd-api.imports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moul/cleanarch/master/slides/assets/cmd-api.imports.png -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # dev files 2 | test.db 3 | 4 | # binaries 5 | /api 6 | 7 | # junk 8 | *~ 9 | .#* 10 | *# 11 | -------------------------------------------------------------------------------- /requestors.go: -------------------------------------------------------------------------------- 1 | package cleanarch 2 | 3 | type UseCaseRequest interface{} 4 | 5 | type UseCaseRequestBuilder interface{} 6 | -------------------------------------------------------------------------------- /responders.go: -------------------------------------------------------------------------------- 1 | package cleanarch 2 | 3 | type UseCaseResponse interface{} 4 | 5 | type UseCaseResponseAssembler interface{} 6 | -------------------------------------------------------------------------------- /usecase.go: -------------------------------------------------------------------------------- 1 | package cleanarch 2 | 3 | type UseCase interface { 4 | Execute(UseCaseRequest) (UseCaseResponse, error) 5 | } 6 | -------------------------------------------------------------------------------- /slides/manfred - clean architecture.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moul/cleanarch/master/slides/manfred - clean architecture.pdf -------------------------------------------------------------------------------- /slides/assets/app-controllers-api.imports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moul/cleanarch/master/slides/assets/app-controllers-api.imports.png -------------------------------------------------------------------------------- /example/bizrules/gateways/issue.go: -------------------------------------------------------------------------------- 1 | package gateways 2 | 3 | // Issues is the gateway to the Issue entity. 4 | type Issues interface { 5 | } 6 | -------------------------------------------------------------------------------- /slides/assets/app-repos-sprints-gorm.imports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moul/cleanarch/master/slides/assets/app-repos-sprints-gorm.imports.png -------------------------------------------------------------------------------- /slides/assets/bizrules-usecases-get_sprint.imports.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moul/cleanarch/master/slides/assets/bizrules-usecases-get_sprint.imports.png -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | Golang implementationPOC of the clean architecture 3 | 4 | [![GoDoc](https://godoc.org/github.com/moul/cleanarch/example?status.svg)](https://godoc.org/github.com/moul/cleanarch/example) 5 | -------------------------------------------------------------------------------- /tools/gen-import-path: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | PACKAGE=$(go list .) 4 | PACKAGE_ROOT=$(echo "${PACKAGE}" | cut -d/ -f-3) 5 | 6 | FORMAT="${FORMAT:-png}" 7 | FILTER="${FILTER:-dot}" 8 | 9 | godepgraph -s "${PACKAGE}" | sed "s@${PACKAGE_ROOT}/@./@g" | "${FILTER}" -T"${FORMAT}" -o .import-graph."${FORMAT}" 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cleanarch 2 | :shower: clean architecture in Golang 3 | 4 | [![GoDoc](https://godoc.org/github.com/moul/cleanarch?status.svg)](https://godoc.org/github.com/moul/cleanarch) 5 | 6 | Tests, POC, tools, benchs, feedbacks around [the "clean" architecture](https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html) 7 | -------------------------------------------------------------------------------- /example/Makefile: -------------------------------------------------------------------------------- 1 | api: $(shell find app bizrules cmd -type f -name "*.go") 2 | go build -o api ./cmd/api/main.go 3 | 4 | .PHONY: import-paths 5 | import-paths: 6 | for path in $(shell go list ./...); do \ 7 | cd "$(GOPATH)/src/$$path" && \ 8 | FORMAT=svg FILTER=dot gen-import-path; \ 9 | done 10 | 11 | .PHONY: test 12 | test: 13 | go test -v $(shell go list ./... | grep -v /vendor/) 14 | -------------------------------------------------------------------------------- /slides/Makefile: -------------------------------------------------------------------------------- 1 | all: 2 | mkdir -p assets 3 | convert ../example/app/controllers/api/.import-graph.svg assets/app-controllers-api.imports.png 4 | convert ../example/app/repos/sprints/gorm/.import-graph.svg assets/app-repos-sprints-gorm.imports.png 5 | convert ../example/bizrules/usecases/get_sprint/.import-graph.svg assets/bizrules-usecases-get_sprint.imports.png 6 | convert ../example/cmd/api/.import-graph.svg assets/cmd-api.imports.png 7 | -------------------------------------------------------------------------------- /example/bizrules/gateways/sprint.go: -------------------------------------------------------------------------------- 1 | package gateways 2 | 3 | import "github.com/moul/cleanarch/example/bizrules/entities" 4 | 5 | // Sprints is the gateway to the Sprint entity. 6 | type Sprints interface { 7 | Add(*entities.Sprint) error 8 | New() (*entities.Sprint, error) 9 | Find(int) (*entities.Sprint, error) 10 | FindSprintToClose() (*entities.Sprint, error) 11 | FindAverageClosedIssues() float64 12 | Update(*entities.Sprint) error 13 | } 14 | -------------------------------------------------------------------------------- /example/bizrules/usecases/ping/io/io.go: -------------------------------------------------------------------------------- 1 | package pingio 2 | 3 | type Response interface { 4 | // cleanarch.UseCaseResponse 5 | GetPong() string 6 | } 7 | 8 | type ResponseAssembler interface { 9 | Write(string) (Response, error) 10 | } 11 | 12 | type Request interface { 13 | // cleanarch.UseCaseRequest 14 | } 15 | 16 | type RequestBuilder interface { 17 | // cleanarch.UseCaseRequestBuilder 18 | Create() RequestBuilder 19 | Build() Request 20 | } 21 | -------------------------------------------------------------------------------- /example/bizrules/usecases/ping/ping.go: -------------------------------------------------------------------------------- 1 | package ping 2 | 3 | import "github.com/moul/cleanarch/example/bizrules/usecases/ping/io" 4 | 5 | type UseCase struct { 6 | // cleanarch.UseCase 7 | 8 | resp pingio.ResponseAssembler 9 | } 10 | 11 | func New(resp pingio.ResponseAssembler) UseCase { 12 | return UseCase{resp: resp} 13 | } 14 | 15 | func (uc *UseCase) Execute(req pingio.Request) (pingio.Response, error) { 16 | return uc.resp.Write("pong") 17 | } 18 | -------------------------------------------------------------------------------- /example/bizrules/usecases/add_sprint/io/io.go: -------------------------------------------------------------------------------- 1 | package addsprintio 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/moul/cleanarch/example/bizrules/entities" 7 | ) 8 | 9 | type Response interface { 10 | // cleanarch.UseCaseResponse 11 | 12 | GetCreatedAt() time.Time 13 | GetID() int 14 | } 15 | type ResponseAssembler interface { 16 | Write(*entities.Sprint) (Response, error) 17 | } 18 | 19 | type Request interface { 20 | // cleanarch.UseCaseRequest 21 | } 22 | type RequestBuilder interface { 23 | // cleanarch.UseCaseRequestBuilder 24 | 25 | Create() RequestBuilder 26 | Build() Request 27 | } 28 | -------------------------------------------------------------------------------- /example/bizrules/usecases/ping/ping_test.go: -------------------------------------------------------------------------------- 1 | package ping 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/moul/cleanarch/example/bizrules/usecases/ping/dto" 7 | . "github.com/smartystreets/goconvey/convey" 8 | ) 9 | 10 | func TestUseCase(t *testing.T) { 11 | Convey("Testing UseCase", t, FailureContinues, func() { 12 | // prepare 13 | uc := New(pingdto.ResponseAssembler{}) 14 | req := pingdto.RequestBuilder{}.Create().Build() 15 | 16 | // execute usecase 17 | resp, err := uc.Execute(req) 18 | So(err, ShouldBeNil) 19 | So(resp, ShouldNotBeNil) 20 | So(resp.GetPong(), ShouldEqual, "pong") 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /example/bizrules/usecases/add_sprint/addsprint.go: -------------------------------------------------------------------------------- 1 | package addsprint 2 | 3 | import ( 4 | "github.com/moul/cleanarch/example/bizrules/gateways" 5 | "github.com/moul/cleanarch/example/bizrules/usecases/add_sprint/io" 6 | ) 7 | 8 | type UseCase struct { 9 | // cleanarch.UseCase 10 | 11 | gw gateways.Sprints 12 | resp addsprintio.ResponseAssembler 13 | } 14 | 15 | func New(gw gateways.Sprints, resp addsprintio.ResponseAssembler) UseCase { 16 | return UseCase{ 17 | gw: gw, 18 | resp: resp, 19 | } 20 | } 21 | 22 | func (uc *UseCase) Execute(req addsprintio.Request) (addsprintio.Response, error) { 23 | newSprint, err := uc.gw.New() 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return uc.resp.Write(newSprint) 29 | } 30 | -------------------------------------------------------------------------------- /example/bizrules/usecases/get_sprint/getsprint.go: -------------------------------------------------------------------------------- 1 | package getsprint 2 | 3 | import ( 4 | "github.com/moul/cleanarch/example/bizrules/gateways" 5 | "github.com/moul/cleanarch/example/bizrules/usecases/get_sprint/io" 6 | ) 7 | 8 | type UseCase struct { 9 | // cleanarch.UseCase 10 | 11 | gw gateways.Sprints 12 | resp getsprintio.ResponseAssembler 13 | } 14 | 15 | func New(gw gateways.Sprints, resp getsprintio.ResponseAssembler) UseCase { 16 | return UseCase{ 17 | gw: gw, 18 | resp: resp, 19 | } 20 | } 21 | 22 | func (uc *UseCase) Execute(req getsprintio.Request) (getsprintio.Response, error) { 23 | sprint, err := uc.gw.Find(req.GetID()) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | return uc.resp.Write(sprint) 29 | } 30 | -------------------------------------------------------------------------------- /example/bizrules/usecases/ping/dto/dto.go: -------------------------------------------------------------------------------- 1 | package pingdto 2 | 3 | import "github.com/moul/cleanarch/example/bizrules/usecases/ping/io" 4 | 5 | type RequestBuilder struct { 6 | pingio.RequestBuilder 7 | request *Request 8 | } 9 | 10 | type Request struct{ pingio.Request } 11 | 12 | type ResponseAssembler struct{ pingio.ResponseAssembler } 13 | type Response struct{ pong string } 14 | 15 | func (b RequestBuilder) Create() pingio.RequestBuilder { 16 | b.request = &Request{} 17 | return b 18 | } 19 | 20 | func (b RequestBuilder) Build() pingio.Request { return b.request } 21 | 22 | func (r Response) GetPong() string { return r.pong } 23 | 24 | func (a ResponseAssembler) Write(pong string) (pingio.Response, error) { 25 | return Response{pong: pong}, nil 26 | } 27 | -------------------------------------------------------------------------------- /example/bizrules/usecases/close_sprint/io/io.go: -------------------------------------------------------------------------------- 1 | package closesprintio 2 | 3 | type Response interface { 4 | // cleanarch.UseCaseResponse 5 | 6 | GetAverageClosedIssues() float64 7 | GetClosedIssuesCount() int 8 | GetSprintID() int 9 | } 10 | 11 | type ResponseBuilder interface { 12 | Create() ResponseBuilder 13 | WithAverageClosedIssues(float64) ResponseBuilder 14 | WithClosedIssuesCount(int) ResponseBuilder 15 | WithSprintID(int) ResponseBuilder 16 | Build() (Response, error) 17 | } 18 | 19 | type Request interface { 20 | // cleanarch.UseCaseRequest 21 | GetSprintID() int 22 | } 23 | 24 | type RequestBuilder interface { 25 | // cleanarch.UseCaseRequestBuilder 26 | 27 | Create() RequestBuilder 28 | WithSprintID(int) RequestBuilder 29 | Build() Request 30 | } 31 | -------------------------------------------------------------------------------- /example/bizrules/usecases/get_sprint/io/io.go: -------------------------------------------------------------------------------- 1 | package getsprintio 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/moul/cleanarch/example/bizrules/entities" 7 | ) 8 | 9 | type Response interface { 10 | // cleanarch.UseCaseResponse 11 | 12 | GetCreatedAt() time.Time 13 | GetEffectiveClosedAt() time.Time 14 | GetExpectedClosedAt() time.Time 15 | GetID() int 16 | GetStatus() string 17 | } 18 | 19 | type ResponseAssembler interface { 20 | Write(*entities.Sprint) (Response, error) 21 | } 22 | 23 | type Request interface { 24 | // cleanarch.UseCaseRequest 25 | 26 | GetID() int 27 | } 28 | 29 | type RequestBuilder interface { 30 | // cleanarch.UseCaseRequestBuilder 31 | 32 | Create() RequestBuilder 33 | WithSprintID(int) RequestBuilder 34 | Build() Request 35 | } 36 | -------------------------------------------------------------------------------- /example/app/controllers/api/ping.go: -------------------------------------------------------------------------------- 1 | package apicontrollers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/moul/cleanarch/example/bizrules/usecases/ping" 9 | "github.com/moul/cleanarch/example/bizrules/usecases/ping/dto" 10 | ) 11 | 12 | type Ping struct { 13 | uc *ping.UseCase 14 | } 15 | 16 | func NewPing(uc *ping.UseCase) *Ping { 17 | return &Ping{uc: uc} 18 | } 19 | 20 | func (ctrl *Ping) Execute(ctx *gin.Context) { 21 | req := pingdto.RequestBuilder{}.Create().Build() 22 | 23 | resp, err := ctrl.uc.Execute(req) 24 | if err != nil { 25 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("%v", err)}) 26 | return 27 | } 28 | 29 | ctx.JSON(http.StatusOK, gin.H{"result": PingResponse{ 30 | Pong: resp.GetPong(), 31 | }}) 32 | } 33 | 34 | type PingResponse struct { 35 | Pong string `json:"pong"` 36 | } 37 | -------------------------------------------------------------------------------- /example/bizrules/entities/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | ./example/bizrules/entities 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/bizrules/usecases/ping/io/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | ./example/bizrules/usecases/ping/io 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/bizrules/usecases/close_sprint/io/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | ./example/bizrules/usecases/close_sprint/io 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /example/bizrules/usecases/test.go: -------------------------------------------------------------------------------- 1 | package usecases 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/moul/cleanarch/example/bizrules/entities" 7 | ) 8 | 9 | var SprintStub1 = entities.NewSprint() 10 | var SprintStub2 = entities.NewSprint() 11 | var IssueStub1 = entities.NewIssue() 12 | var IssueStub2 = entities.NewIssue() 13 | 14 | func init() { 15 | IssueStub1.SetID(10) 16 | IssueStub1.SetTitle("Issue 1") 17 | IssueStub1.SetDescription("Description of the first issue") 18 | 19 | IssueStub2.SetID(20) 20 | if err := IssueStub2.SetDone(); err != nil { 21 | panic(err) 22 | } 23 | IssueStub2.SetDoneAt(time.Unix(1234567890, 0)) 24 | IssueStub1.SetTitle("Issue 2") 25 | IssueStub1.SetDescription("Description of the second issue") 26 | 27 | SprintStub1.SetID(42) 28 | if err := SprintStub1.Close(); err != nil { 29 | panic(err) 30 | } 31 | SprintStub1.AddIssue(IssueStub1) 32 | SprintStub1.AddIssue(IssueStub2) 33 | 34 | SprintStub2.SetID(43) 35 | SprintStub2.AddIssue(IssueStub1) 36 | SprintStub2.AddIssue(IssueStub2) 37 | } 38 | -------------------------------------------------------------------------------- /example/app/controllers/api/closesprint.go: -------------------------------------------------------------------------------- 1 | package apicontrollers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | "github.com/moul/cleanarch/example/bizrules/usecases/add_sprint" 10 | "github.com/moul/cleanarch/example/bizrules/usecases/add_sprint/dto" 11 | ) 12 | 13 | type AddSprint struct { 14 | uc *addsprint.UseCase 15 | } 16 | 17 | func NewAddSprint(uc *addsprint.UseCase) *AddSprint { return &AddSprint{uc: uc} } 18 | 19 | func (ctrl *AddSprint) Execute(ctx *gin.Context) { 20 | req := addsprintdto.RequestBuilder{}. 21 | Create(). 22 | Build() 23 | 24 | resp, err := ctrl.uc.Execute(req) 25 | if err != nil { 26 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("%v", err)}) 27 | return 28 | } 29 | 30 | ctx.JSON(http.StatusOK, gin.H{"result": AddSprintResponse{ 31 | CreatedAt: resp.GetCreatedAt(), 32 | ID: resp.GetID(), 33 | }}) 34 | } 35 | 36 | type AddSprintResponse struct { 37 | CreatedAt time.Time `json:"created-at"` 38 | ID int `json:"id"` 39 | } 40 | -------------------------------------------------------------------------------- /example/bizrules/usecases/close_sprint/closesprint.go: -------------------------------------------------------------------------------- 1 | package closesprint 2 | 3 | import ( 4 | "github.com/moul/cleanarch/example/bizrules/gateways" 5 | "github.com/moul/cleanarch/example/bizrules/usecases/close_sprint/io" 6 | ) 7 | 8 | type UseCase struct { 9 | // cleanarch.UseCase 10 | 11 | gw gateways.Sprints 12 | resp closesprintio.ResponseBuilder 13 | } 14 | 15 | func New(gw gateways.Sprints, resp closesprintio.ResponseBuilder) UseCase { 16 | return UseCase{ 17 | gw: gw, 18 | resp: resp, 19 | } 20 | } 21 | 22 | func (uc *UseCase) Execute(req closesprintio.Request) (closesprintio.Response, error) { 23 | sprint, err := uc.gw.Find(req.GetSprintID()) 24 | if err != nil { 25 | return nil, err 26 | } 27 | 28 | if err := sprint.Close(); err != nil { 29 | return nil, err 30 | } 31 | 32 | if err := uc.gw.Update(sprint); err != nil { 33 | return nil, err 34 | } 35 | 36 | return uc.resp. 37 | Create(). 38 | WithAverageClosedIssues(uc.gw.FindAverageClosedIssues()). 39 | WithClosedIssuesCount(sprint.GetIssuesCount()). 40 | WithSprintID(sprint.GetID()). 41 | Build() 42 | } 43 | -------------------------------------------------------------------------------- /example/bizrules/usecases/add_sprint/addsprint_test.go: -------------------------------------------------------------------------------- 1 | package addsprint 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/moul/cleanarch/example/app/repos/sprints/mem" 7 | "github.com/moul/cleanarch/example/bizrules/usecases" 8 | "github.com/moul/cleanarch/example/bizrules/usecases/add_sprint/dto" 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | func dummyUseCase() UseCase { 13 | // prepare sprint repo 14 | repo := sprintsmem.New() 15 | repo.Add(usecases.SprintStub1) 16 | repo.Add(usecases.SprintStub2) 17 | 18 | resp := addsprintdto.ResponseAssembler{} 19 | 20 | // prepare usecase 21 | return New(repo, resp) 22 | } 23 | 24 | func TestUseCase(t *testing.T) { 25 | Convey("Testing UseCase", t, FailureContinues, func() { 26 | // prepare 27 | uc := dummyUseCase() 28 | req := addsprintdto.RequestBuilder{}.Create().Build() 29 | 30 | // execute usecase 31 | resp, err := uc.Execute(req) 32 | So(err, ShouldBeNil) 33 | So(resp, ShouldNotBeNil) 34 | So(resp.GetID(), ShouldEqual, 1) 35 | 36 | actualSprint, err := uc.gw.Find(1) 37 | So(err, ShouldBeNil) 38 | So(actualSprint.IsClosed(), ShouldBeFalse) 39 | So(len(actualSprint.GetIssues()), ShouldEqual, 0) 40 | So(actualSprint.GetCreatedAt(), ShouldResemble, resp.GetCreatedAt()) 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /example/bizrules/usecases/add_sprint/dto/dto.go: -------------------------------------------------------------------------------- 1 | package addsprintdto 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/moul/cleanarch/example/bizrules/entities" 7 | "github.com/moul/cleanarch/example/bizrules/usecases/add_sprint/io" 8 | ) 9 | 10 | /* Requestbuilder */ 11 | 12 | type RequestBuilder struct { 13 | addsprintio.RequestBuilder 14 | 15 | request *Request 16 | } 17 | 18 | func (b RequestBuilder) Create() addsprintio.RequestBuilder { 19 | b.request = &Request{} 20 | return b 21 | } 22 | 23 | func (b RequestBuilder) Build() addsprintio.Request { return b.request } 24 | 25 | /* Request */ 26 | 27 | type Request struct { 28 | addsprintio.Request 29 | 30 | id int 31 | } 32 | 33 | func (r Request) GetSprintID() int { return r.id } 34 | 35 | /* Response */ 36 | 37 | type Response struct { 38 | addsprintio.Response 39 | 40 | id int 41 | createdAt time.Time 42 | } 43 | 44 | func (r Response) GetCreatedAt() time.Time { return r.createdAt } 45 | func (r Response) GetID() int { return r.id } 46 | 47 | /* ResponseAssembler */ 48 | 49 | type ResponseAssembler struct { 50 | addsprintio.ResponseAssembler 51 | } 52 | 53 | func (a ResponseAssembler) Write(sprint *entities.Sprint) (addsprintio.Response, error) { 54 | resp := Response{ 55 | createdAt: sprint.GetCreatedAt(), 56 | id: sprint.GetID(), 57 | } 58 | return resp, nil 59 | } 60 | -------------------------------------------------------------------------------- /example/bizrules/usecases/close_sprint/closesprint_test.go: -------------------------------------------------------------------------------- 1 | package closesprint 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/moul/cleanarch/example/app/repos/sprints/mem" 7 | "github.com/moul/cleanarch/example/bizrules/usecases" 8 | "github.com/moul/cleanarch/example/bizrules/usecases/close_sprint/dto" 9 | . "github.com/smartystreets/goconvey/convey" 10 | ) 11 | 12 | func dummyUseCase() UseCase { 13 | // prepare sprint repo 14 | repo := sprintsmem.New() 15 | repo.Add(usecases.SprintStub1) 16 | repo.Add(usecases.SprintStub2) 17 | 18 | resp := closesprintdto.ResponseBuilder{} 19 | 20 | // prepare usecase 21 | return New(repo, resp) 22 | } 23 | 24 | func TestUseCase(t *testing.T) { 25 | Convey("Testing UseCase", t, FailureContinues, func() { 26 | // prepare 27 | uc := dummyUseCase() 28 | req := closesprintdto.RequestBuilder{}.Create().WithSprintID(usecases.SprintStub2.GetID()).Build() 29 | 30 | // execute usecase 31 | resp, err := uc.Execute(req) 32 | So(err, ShouldBeNil) 33 | So(resp, ShouldNotBeNil) 34 | So(resp.GetClosedIssuesCount(), ShouldEqual, 1) 35 | So(resp.GetAverageClosedIssues(), ShouldEqual, 1.5) 36 | So(resp.GetSprintID(), ShouldEqual, usecases.SprintStub2.GetID()) 37 | 38 | actualSprint, err := uc.gw.Find(usecases.SprintStub2.GetID()) 39 | So(err, ShouldBeNil) 40 | So(actualSprint.IsClosed(), ShouldBeTrue) 41 | So(len(actualSprint.GetIssues()), ShouldEqual, 1) 42 | So(actualSprint.GetIssues()[0].IsClosed(), ShouldBeTrue) 43 | }) 44 | } 45 | -------------------------------------------------------------------------------- /example/app/controllers/api/getsprint.go: -------------------------------------------------------------------------------- 1 | package apicontrollers 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "strconv" 7 | "time" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/moul/cleanarch/example/bizrules/usecases/get_sprint" 11 | "github.com/moul/cleanarch/example/bizrules/usecases/get_sprint/dto" 12 | ) 13 | 14 | type GetSprint struct { 15 | uc *getsprint.UseCase 16 | } 17 | 18 | func NewGetSprint(uc *getsprint.UseCase) *GetSprint { 19 | return &GetSprint{uc: uc} 20 | } 21 | 22 | func (ctrl *GetSprint) Execute(ctx *gin.Context) { 23 | sprintID, err := strconv.Atoi(ctx.Param("sprint-id")) 24 | if err != nil { 25 | ctx.JSON(http.StatusNotFound, gin.H{"error": "Invalid 'sprint-id'"}) 26 | return 27 | } 28 | 29 | req := getsprintdto.RequestBuilder{}. 30 | Create(). 31 | WithSprintID(sprintID). 32 | Build() 33 | 34 | resp, err := ctrl.uc.Execute(req) 35 | if err != nil { 36 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("%v", err)}) 37 | return 38 | } 39 | 40 | ctx.JSON(http.StatusOK, gin.H{"result": GetSprintResponse{ 41 | CreatedAt: resp.GetCreatedAt(), 42 | EffectiveClosedAt: resp.GetEffectiveClosedAt(), 43 | ExpectedClosedAt: resp.GetExpectedClosedAt(), 44 | Status: resp.GetStatus(), 45 | }}) 46 | } 47 | 48 | type GetSprintResponse struct { 49 | CreatedAt time.Time `json:"created-at"` 50 | EffectiveClosedAt time.Time `json:"effective-closed-at"` 51 | ExpectedClosedAt time.Time `json:"expected-closed-at"` 52 | Status string `json:"status"` 53 | } 54 | -------------------------------------------------------------------------------- /example/bizrules/gateways/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | ./example/bizrules/entities 16 | 17 | 18 | 1 19 | 20 | ./example/bizrules/gateways 21 | 22 | 23 | 1->0 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/bizrules/usecases/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | ./example/bizrules/entities 16 | 17 | 18 | 1 19 | 20 | ./example/bizrules/usecases 21 | 22 | 23 | 1->0 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/bizrules/usecases/ping/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | ./example/bizrules/usecases/ping 16 | 17 | 18 | 1 19 | 20 | ./example/bizrules/usecases/ping/io 21 | 22 | 23 | 0->1 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/bizrules/usecases/add_sprint/io/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | ./example/bizrules/entities 16 | 17 | 18 | 1 19 | 20 | ./example/bizrules/usecases/add_sprint/io 21 | 22 | 23 | 1->0 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/bizrules/usecases/get_sprint/io/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | ./example/bizrules/entities 16 | 17 | 18 | 1 19 | 20 | ./example/bizrules/usecases/get_sprint/io 21 | 22 | 23 | 1->0 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/bizrules/usecases/ping/dto/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | ./example/bizrules/usecases/ping/dto 16 | 17 | 18 | 1 19 | 20 | ./example/bizrules/usecases/ping/io 21 | 22 | 23 | 0->1 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/bizrules/usecases/close_sprint/dto/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | ./example/bizrules/usecases/close_sprint/dto 16 | 17 | 18 | 1 19 | 20 | ./example/bizrules/usecases/close_sprint/io 21 | 22 | 23 | 0->1 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/bizrules/usecases/close_sprint/dto/dto.go: -------------------------------------------------------------------------------- 1 | package closesprintdto 2 | 3 | import "github.com/moul/cleanarch/example/bizrules/usecases/close_sprint/io" 4 | 5 | type RequestBuilder struct { 6 | closesprintio.RequestBuilder 7 | 8 | request *Request 9 | } 10 | 11 | type Request struct { 12 | closesprintio.Request 13 | 14 | id int 15 | } 16 | 17 | func (r Request) GetSprintID() int { return r.id } 18 | 19 | func (b RequestBuilder) Create() RequestBuilder { 20 | b.request = &Request{} 21 | return b 22 | } 23 | 24 | func (b RequestBuilder) WithSprintID(id int) RequestBuilder { 25 | b.request.id = id 26 | return b 27 | } 28 | 29 | func (b RequestBuilder) Build() *Request { return b.request } 30 | 31 | type Response struct { 32 | closesprintio.Response 33 | 34 | averageClosedIssues float64 35 | closedIssuesCount int 36 | sprintID int 37 | } 38 | 39 | func (r Response) GetAverageClosedIssues() float64 { return r.averageClosedIssues } 40 | func (r Response) GetClosedIssuesCount() int { return r.closedIssuesCount } 41 | func (r Response) GetSprintID() int { return r.sprintID } 42 | 43 | type ResponseBuilder struct { 44 | closesprintio.ResponseBuilder 45 | 46 | response *Response 47 | } 48 | 49 | func (a ResponseBuilder) Create() closesprintio.ResponseBuilder { 50 | a.response = &Response{} 51 | return a 52 | } 53 | 54 | func (a ResponseBuilder) WithAverageClosedIssues(val float64) closesprintio.ResponseBuilder { 55 | a.response.averageClosedIssues = val 56 | return a 57 | } 58 | 59 | func (a ResponseBuilder) WithClosedIssuesCount(val int) closesprintio.ResponseBuilder { 60 | a.response.closedIssuesCount = val 61 | return a 62 | } 63 | 64 | func (a ResponseBuilder) WithSprintID(val int) closesprintio.ResponseBuilder { 65 | a.response.sprintID = val 66 | return a 67 | } 68 | 69 | func (a ResponseBuilder) Build() (closesprintio.Response, error) { 70 | return a.response, nil 71 | } 72 | -------------------------------------------------------------------------------- /example/cmd/api/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/jinzhu/gorm" 8 | "github.com/moul/cleanarch/example/app/controllers/api" 9 | "github.com/moul/cleanarch/example/app/repos/sprints/gorm" 10 | "github.com/moul/cleanarch/example/app/repos/sprints/mem" 11 | "github.com/moul/cleanarch/example/bizrules/gateways" 12 | "github.com/moul/cleanarch/example/bizrules/usecases/add_sprint" 13 | "github.com/moul/cleanarch/example/bizrules/usecases/add_sprint/dto" 14 | "github.com/moul/cleanarch/example/bizrules/usecases/get_sprint" 15 | "github.com/moul/cleanarch/example/bizrules/usecases/get_sprint/dto" 16 | "github.com/moul/cleanarch/example/bizrules/usecases/ping" 17 | "github.com/moul/cleanarch/example/bizrules/usecases/ping/dto" 18 | 19 | _ "github.com/mattn/go-sqlite3" 20 | ) 21 | 22 | func main() { 23 | // Setup gateways 24 | var sprintsGw gateways.Sprints 25 | if len(os.Args) > 1 && os.Args[1] == "--mem" { 26 | // configure a memory-based sprints gateway 27 | sprintsGw = sprintsmem.New() 28 | } else { 29 | // configure a sqlite-based sprints gateway 30 | db, err := gorm.Open("sqlite3", "test.db") 31 | if err != nil { 32 | panic(err) 33 | } 34 | defer db.Close() 35 | sprintsGw = sprintsgorm.New(db) 36 | } 37 | 38 | // Setup usecases 39 | getSprint := getsprint.New(sprintsGw, getsprintdto.ResponseAssembler{}) 40 | addSprint := addsprint.New(sprintsGw, addsprintdto.ResponseAssembler{}) 41 | ping := ping.New(pingdto.ResponseAssembler{}) 42 | //closeSprint := closesprint.New(sprintsGw, closesprintdto.ResponseBuilder{}) 43 | 44 | // Setup API 45 | gin := gin.Default() 46 | gin.GET("/sprints/:sprint-id", apicontrollers.NewGetSprint(&getSprint).Execute) 47 | gin.POST("/sprints", apicontrollers.NewAddSprint(&addSprint).Execute) 48 | gin.GET("/ping", apicontrollers.NewPing(&ping).Execute) 49 | //gin.DELETE("/sprints/:sprint-id", apicontrollers.NewCloseSprint(&closeSprint).Execute) 50 | 51 | // Start 52 | gin.Run() 53 | } 54 | -------------------------------------------------------------------------------- /example/bizrules/usecases/get_sprint/dto/dto.go: -------------------------------------------------------------------------------- 1 | package getsprintdto 2 | 3 | import ( 4 | "time" 5 | 6 | "github.com/moul/cleanarch/example/bizrules/entities" 7 | "github.com/moul/cleanarch/example/bizrules/usecases/get_sprint/io" 8 | ) 9 | 10 | /* Request */ 11 | 12 | type Request struct { 13 | getsprintio.Request 14 | 15 | id int 16 | } 17 | 18 | func (r Request) GetID() int { return r.id } 19 | 20 | /* RequestBuilder */ 21 | 22 | type RequestBuilder struct { 23 | getsprintio.RequestBuilder 24 | 25 | request *Request 26 | } 27 | 28 | func (b RequestBuilder) Create() getsprintio.RequestBuilder { 29 | b.request = &Request{} 30 | return b 31 | } 32 | 33 | func (b RequestBuilder) WithSprintID(id int) getsprintio.RequestBuilder { 34 | b.request.id = id 35 | return b 36 | } 37 | 38 | func (b RequestBuilder) Build() getsprintio.Request { return b.request } 39 | 40 | /* Response */ 41 | 42 | type Response struct { 43 | getsprintio.Response 44 | 45 | createdAt time.Time 46 | effectiveClosedAt time.Time 47 | expectedClosedAt time.Time 48 | id int 49 | status string 50 | } 51 | 52 | func (r Response) GetCreatedAt() time.Time { return r.createdAt } 53 | func (r Response) GetEffectiveClosedAt() time.Time { return r.effectiveClosedAt } 54 | func (r Response) GetExpectedClosedAt() time.Time { return r.expectedClosedAt } 55 | func (r Response) GetID() int { return r.id } 56 | func (r Response) GetStatus() string { return r.status } 57 | 58 | /* ResponseAssembler */ 59 | 60 | type ResponseAssembler struct { 61 | getsprintio.ResponseAssembler 62 | } 63 | 64 | func (a ResponseAssembler) Write(sprint *entities.Sprint) (getsprintio.Response, error) { 65 | resp := Response{ 66 | createdAt: sprint.GetCreatedAt(), 67 | effectiveClosedAt: sprint.GetEffectiveClosedAt(), 68 | expectedClosedAt: sprint.GetExpectedClosedAt(), 69 | id: sprint.GetID(), 70 | status: sprint.GetStatus(), 71 | } 72 | return resp, nil 73 | } 74 | -------------------------------------------------------------------------------- /example/app/repos/sprints/mem/repo.go: -------------------------------------------------------------------------------- 1 | package sprintsmem 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/moul/cleanarch/example/bizrules/entities" 7 | "github.com/moul/cleanarch/example/bizrules/gateways" 8 | ) 9 | 10 | const maxSprintID = 42 11 | 12 | type Repo struct { 13 | gateways.Sprints 14 | 15 | sprints []entities.Sprint 16 | } 17 | 18 | func New() *Repo { 19 | return &Repo{ 20 | sprints: make([]entities.Sprint, 0), 21 | } 22 | } 23 | 24 | func (r *Repo) New() (*entities.Sprint, error) { 25 | for i := 1; i < maxSprintID; i++ { 26 | exists := false 27 | for _, sprint := range r.sprints { 28 | if sprint.GetID() == i { 29 | exists = true 30 | break 31 | } 32 | } 33 | if !exists { 34 | newSprint := entities.NewSprint() 35 | newSprint.SetID(i) 36 | if err := r.Add(newSprint); err != nil { 37 | return nil, err 38 | } 39 | return newSprint, nil 40 | } 41 | } 42 | return nil, fmt.Errorf("too much sprint in the repo") 43 | } 44 | 45 | func (r *Repo) Add(sprint *entities.Sprint) error { 46 | r.sprints = append(r.sprints, *sprint) 47 | return nil 48 | } 49 | 50 | func (r Repo) Find(id int) (*entities.Sprint, error) { 51 | for _, sprint := range r.sprints { 52 | if sprint.GetID() == id { 53 | return &sprint, nil 54 | } 55 | } 56 | return nil, entities.SprintNotFoundError{} 57 | } 58 | 59 | func (r Repo) FindSprintToClose() (*entities.Sprint, error) { 60 | return nil, fmt.Errorf("Not implemented") 61 | } 62 | 63 | func (r Repo) FindAverageClosedIssues() float64 { 64 | sprintsCount := 0 65 | issuesCount := 0 66 | for _, sprint := range r.sprints { 67 | sprintsCount++ 68 | issuesCount += len(sprint.GetIssues()) 69 | } 70 | if sprintsCount > 0 { 71 | return float64(issuesCount) / float64(sprintsCount) 72 | } 73 | return float64(0) 74 | } 75 | 76 | func (r *Repo) Update(updated *entities.Sprint) error { 77 | for idx, sprint := range r.sprints { 78 | if sprint.GetID() == updated.GetID() { 79 | r.sprints[idx] = *updated 80 | return nil 81 | } 82 | } 83 | return entities.SprintNotFoundError{} 84 | } 85 | -------------------------------------------------------------------------------- /example/bizrules/usecases/get_sprint/getsprint_test.go: -------------------------------------------------------------------------------- 1 | package getsprint 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/moul/cleanarch/example/app/repos/sprints/mem" 7 | "github.com/moul/cleanarch/example/bizrules/entities" 8 | "github.com/moul/cleanarch/example/bizrules/usecases" 9 | "github.com/moul/cleanarch/example/bizrules/usecases/get_sprint/dto" 10 | . "github.com/smartystreets/goconvey/convey" 11 | ) 12 | 13 | func dummyUseCase() UseCase { 14 | // prepare sprint repo 15 | repo := sprintsmem.New() 16 | repo.Add(usecases.SprintStub1) 17 | repo.Add(usecases.SprintStub2) 18 | 19 | resp := getsprintdto.ResponseAssembler{} 20 | 21 | // prepare usecase 22 | uc := New(repo, resp) 23 | return uc 24 | } 25 | 26 | func TestUseCase(t *testing.T) { 27 | Convey("Testing UseCase", t, func() { 28 | // prepare 29 | uc := dummyUseCase() 30 | req := getsprintdto.RequestBuilder{}.Create().WithSprintID(42).Build() 31 | 32 | // execute usecase 33 | resp, err := uc.Execute(req) 34 | So(err, ShouldBeNil) 35 | So(resp, ShouldNotBeNil) 36 | So(resp.GetID(), ShouldEqual, usecases.SprintStub1.GetID()) 37 | So(resp.GetStatus(), ShouldEqual, usecases.SprintStub1.GetStatus()) 38 | So(resp.GetCreatedAt(), ShouldNotEqual, usecases.SprintStub1.GetCreatedAt()) 39 | So(resp.GetCreatedAt(), ShouldResemble, usecases.SprintStub1.GetCreatedAt()) 40 | So(resp.GetEffectiveClosedAt(), ShouldNotEqual, usecases.SprintStub1.GetEffectiveClosedAt()) 41 | So(resp.GetEffectiveClosedAt(), ShouldResemble, usecases.SprintStub1.GetEffectiveClosedAt()) 42 | So(resp.GetExpectedClosedAt(), ShouldNotEqual, usecases.SprintStub1.GetExpectedClosedAt()) 43 | So(resp.GetExpectedClosedAt(), ShouldResemble, usecases.SprintStub1.GetExpectedClosedAt()) 44 | }) 45 | 46 | Convey("Testing NotFound", t, func() { 47 | // prepare 48 | uc := dummyUseCase() 49 | req := getsprintdto.RequestBuilder{}.Create().WithSprintID(123456789).Build() 50 | 51 | // execute usecase 52 | resp, err := uc.Execute(req) 53 | So(err, ShouldNotBeNil) 54 | So(resp, ShouldBeNil) 55 | So(err, ShouldResemble, entities.SprintNotFoundError{}) 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /example/app/repos/sprints/mem/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | ./example/app/repos/sprints/mem 16 | 17 | 18 | 1 19 | 20 | ./example/bizrules/entities 21 | 22 | 23 | 0->1 24 | 25 | 26 | 27 | 28 | 2 29 | 30 | ./example/bizrules/gateways 31 | 32 | 33 | 0->2 34 | 35 | 36 | 37 | 38 | 2->1 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /example/bizrules/usecases/add_sprint/dto/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | ./example/bizrules/entities 16 | 17 | 18 | 1 19 | 20 | ./example/bizrules/usecases/add_sprint/dto 21 | 22 | 23 | 1->0 24 | 25 | 26 | 27 | 28 | 2 29 | 30 | ./example/bizrules/usecases/add_sprint/io 31 | 32 | 33 | 1->2 34 | 35 | 36 | 37 | 38 | 2->0 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /example/bizrules/usecases/get_sprint/dto/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | ./example/bizrules/entities 16 | 17 | 18 | 1 19 | 20 | ./example/bizrules/usecases/get_sprint/dto 21 | 22 | 23 | 1->0 24 | 25 | 26 | 27 | 28 | 2 29 | 30 | ./example/bizrules/usecases/get_sprint/io 31 | 32 | 33 | 1->2 34 | 35 | 36 | 37 | 38 | 2->0 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /example/app/repos/sprints/gorm/repo.go: -------------------------------------------------------------------------------- 1 | package sprintsgorm 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/jinzhu/gorm" 8 | "github.com/moul/cleanarch/example/bizrules/entities" 9 | "github.com/moul/cleanarch/example/bizrules/gateways" 10 | ) 11 | 12 | type Repo struct { 13 | gateways.Sprints 14 | 15 | db *gorm.DB 16 | } 17 | 18 | type issueModel struct { 19 | gorm.Model 20 | status string 21 | } 22 | 23 | type sprintModel struct { 24 | gorm.Model 25 | status string 26 | expectedClosedAt time.Time 27 | effectiveClosedAt time.Time 28 | issues []issueModel 29 | } 30 | 31 | func New(db *gorm.DB) *Repo { 32 | // create table if needed 33 | db.AutoMigrate(&issueModel{}) 34 | db.AutoMigrate(&sprintModel{}) 35 | 36 | return &Repo{ 37 | db: db, 38 | } 39 | } 40 | 41 | func (r *Repo) New() (*entities.Sprint, error) { 42 | ret := entities.NewSprint() 43 | entity := sprintModel{ 44 | status: ret.GetStatus(), 45 | } 46 | if err := r.db.Create(&entity).Error; err != nil { 47 | return nil, err 48 | } 49 | 50 | ret.SetID(int(entity.ID)) 51 | ret.SetCreatedAt(entity.CreatedAt) 52 | return ret, nil 53 | } 54 | 55 | func (r *Repo) Add(sprint *entities.Sprint) error { 56 | entity := sprintModel{} 57 | entity.status = sprint.GetStatus() 58 | entity.effectiveClosedAt = sprint.GetEffectiveClosedAt() 59 | entity.expectedClosedAt = sprint.GetExpectedClosedAt() 60 | entity.ID = uint(sprint.GetID()) 61 | entity.CreatedAt = sprint.GetCreatedAt() 62 | // FIXME: populate issues 63 | 64 | return r.db.Create(&entity).Error 65 | } 66 | 67 | func (r Repo) Find(id int) (*entities.Sprint, error) { 68 | obj := sprintModel{} 69 | if err := r.db.First(&obj, "id = ?", id).Error; err != nil { 70 | return nil, err 71 | } 72 | 73 | ret := entities.NewSprint() 74 | ret.SetCreatedAt(obj.CreatedAt) 75 | ret.SetID(int(obj.ID)) 76 | ret.SetStatus(obj.status) 77 | ret.SetEffectiveClosedAt(obj.effectiveClosedAt) 78 | ret.SetExpectedClosedAt(obj.expectedClosedAt) 79 | 80 | return ret, nil 81 | } 82 | 83 | func (r Repo) FindSprintToClose() (*entities.Sprint, error) { 84 | return nil, fmt.Errorf("Not implemented") 85 | } 86 | 87 | func (r Repo) FindAverageClosedIssues() float64 { 88 | // Not Implemented 89 | return float64(-1) 90 | } 91 | 92 | func (r *Repo) Update(updated *entities.Sprint) error { 93 | obj := sprintModel{} 94 | obj.ID = uint(updated.GetID()) 95 | obj.status = updated.GetStatus() 96 | obj.CreatedAt = updated.GetCreatedAt() 97 | obj.expectedClosedAt = updated.GetExpectedClosedAt() 98 | obj.effectiveClosedAt = updated.GetEffectiveClosedAt() 99 | 100 | // FIXME: populate issues 101 | return r.db.Save(&obj).Error 102 | } 103 | -------------------------------------------------------------------------------- /example/bizrules/usecases/close_sprint/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | ./example/bizrules/entities 16 | 17 | 18 | 1 19 | 20 | ./example/bizrules/gateways 21 | 22 | 23 | 1->0 24 | 25 | 26 | 27 | 28 | 2 29 | 30 | ./example/bizrules/usecases/close_sprint 31 | 32 | 33 | 2->1 34 | 35 | 36 | 37 | 38 | 3 39 | 40 | ./example/bizrules/usecases/close_sprint/io 41 | 42 | 43 | 2->3 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /example/bizrules/entities/issue_test.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func Test_Issue(t *testing.T) { 12 | Convey("Testing Issue", t, func() { 13 | issue := NewIssue() 14 | 15 | now := time.Now() 16 | issue.id = 42 17 | issue.title = "Issue 42" 18 | issue.description = "A dummy issue" 19 | issue.createdAt = now 20 | issue.closedAt = now 21 | issue.doneAt = now 22 | 23 | So(issue.GetID(), ShouldEqual, 42) 24 | So(issue.GetTitle(), ShouldEqual, "Issue 42") 25 | So(issue.GetDescription(), ShouldEqual, "A dummy issue") 26 | So(issue.GetCreatedAt(), ShouldResemble, now) 27 | So(issue.GetClosedAt(), ShouldResemble, now) 28 | So(issue.GetDoneAt(), ShouldResemble, now) 29 | }) 30 | } 31 | 32 | func Test_IssueSetDone(t *testing.T) { 33 | Convey("Testing Issue.SetDone()", t, func() { 34 | issue := NewIssue() 35 | 36 | So(issue.GetStatus(), ShouldEqual, "") 37 | 38 | So(issue.SetDone(), ShouldBeNil) 39 | So(issue.GetStatus(), ShouldEqual, IssueDone) 40 | 41 | err := issue.SetDone() 42 | So(err, ShouldHaveSameTypeAs, &IssueAlreadyDoneError{}) 43 | So(issue.GetStatus(), ShouldEqual, IssueDone) 44 | So(err.Error(), ShouldEqual, "Issue already done") 45 | 46 | err = issue.SetDone() 47 | So(err, ShouldHaveSameTypeAs, &IssueAlreadyDoneError{}) 48 | So(issue.GetStatus(), ShouldEqual, IssueDone) 49 | So(err.Error(), ShouldEqual, "Issue already done") 50 | }) 51 | } 52 | 53 | func Test_IssueClose(t *testing.T) { 54 | Convey("Testing Issue.Close()", t, func() { 55 | issue := NewIssue() 56 | 57 | So(issue.GetStatus(), ShouldEqual, "") 58 | 59 | So(issue.Close(), ShouldBeNil) 60 | So(issue.GetStatus(), ShouldEqual, IssueClosed) 61 | 62 | err := issue.Close() 63 | So(err, ShouldHaveSameTypeAs, &IssueAlreadyClosedError{}) 64 | So(err.Error(), ShouldEqual, "Issue already closed") 65 | So(issue.GetStatus(), ShouldEqual, IssueClosed) 66 | 67 | err = issue.Close() 68 | So(err, ShouldHaveSameTypeAs, &IssueAlreadyClosedError{}) 69 | So(err.Error(), ShouldEqual, "Issue already closed") 70 | So(issue.GetStatus(), ShouldEqual, IssueClosed) 71 | }) 72 | } 73 | 74 | func Example_Issue() { 75 | issue := NewIssue() 76 | issue.SetID(42) 77 | issue.SetTitle("Issue 42") 78 | issue.SetStatus(IssueOpen) 79 | issue.SetCreatedAt(time.Now()) 80 | issue.SetDescription("A dummy issue") 81 | issue.SetClosedAt(time.Now()) 82 | issue.SetDoneAt(time.Now()) 83 | 84 | fmt.Println(issue.GetID()) 85 | fmt.Println(issue.GetTitle()) 86 | fmt.Println(issue.GetDescription()) 87 | 88 | fmt.Println(issue.GetStatus() == IssueOpen) 89 | fmt.Println(issue.IsDone()) 90 | fmt.Println(issue.IsClosed()) 91 | 92 | fmt.Println(issue.Close()) 93 | 94 | fmt.Println(issue.GetStatus()) 95 | fmt.Println(issue.IsDone()) 96 | fmt.Println(issue.IsClosed()) 97 | 98 | // Output: 99 | // 42 100 | // Issue 42 101 | // A dummy issue 102 | // true 103 | // false 104 | // false 105 | // 106 | // CLOSED 107 | // false 108 | // true 109 | } 110 | -------------------------------------------------------------------------------- /example/bizrules/usecases/add_sprint/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | ./example/bizrules/entities 16 | 17 | 18 | 1 19 | 20 | ./example/bizrules/gateways 21 | 22 | 23 | 1->0 24 | 25 | 26 | 27 | 28 | 2 29 | 30 | ./example/bizrules/usecases/add_sprint 31 | 32 | 33 | 2->1 34 | 35 | 36 | 37 | 38 | 3 39 | 40 | ./example/bizrules/usecases/add_sprint/io 41 | 42 | 43 | 2->3 44 | 45 | 46 | 47 | 48 | 3->0 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /example/bizrules/usecases/get_sprint/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | ./example/bizrules/entities 16 | 17 | 18 | 1 19 | 20 | ./example/bizrules/gateways 21 | 22 | 23 | 1->0 24 | 25 | 26 | 27 | 28 | 2 29 | 30 | ./example/bizrules/usecases/get_sprint 31 | 32 | 33 | 2->1 34 | 35 | 36 | 37 | 38 | 3 39 | 40 | ./example/bizrules/usecases/get_sprint/io 41 | 42 | 43 | 2->3 44 | 45 | 46 | 47 | 48 | 3->0 49 | 50 | 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /example/bizrules/entities/sprint_test.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | "time" 7 | 8 | . "github.com/smartystreets/goconvey/convey" 9 | ) 10 | 11 | func Test_Sprint(t *testing.T) { 12 | Convey("Testing Sprint", t, func() { 13 | sprint := NewSprint() 14 | sprint.id = 42 15 | now := time.Now() 16 | sprint.expectedClosedAt = now 17 | sprint.effectiveClosedAt = now 18 | sprint.createdAt = now 19 | 20 | So(sprint.GetID(), ShouldEqual, 42) 21 | So(sprint.GetCreatedAt(), ShouldResemble, now) 22 | So(sprint.GetExpectedClosedAt(), ShouldResemble, now) 23 | So(sprint.GetEffectiveClosedAt(), ShouldResemble, now) 24 | }) 25 | } 26 | 27 | func Test_Sprint_AddIssue(t *testing.T) { 28 | Convey("Testing Sprint.AddIssue()", t, func() { 29 | sprint := NewSprint() 30 | 31 | So(sprint.GetIssuesCount(), ShouldEqual, 0) 32 | So(sprint.GetIssuesCount(), ShouldEqual, 0) 33 | sprint.AddIssue(NewIssue()) 34 | So(sprint.GetIssuesCount(), ShouldEqual, 1) 35 | So(sprint.GetIssuesCount(), ShouldEqual, 1) 36 | sprint.AddIssue(NewIssue()) 37 | So(sprint.GetIssuesCount(), ShouldEqual, 2) 38 | So(sprint.GetIssuesCount(), ShouldEqual, 2) 39 | }) 40 | } 41 | 42 | func Test_Sprint_Close(t *testing.T) { 43 | Convey("Testing Sprint.Close()", t, func() { 44 | Convey("without issues", func() { 45 | sprint := NewSprint() 46 | 47 | So(sprint.IsClosed(), ShouldBeFalse) 48 | So(sprint.IsClosed(), ShouldBeFalse) 49 | 50 | err := sprint.Close() 51 | So(err, ShouldBeNil) 52 | 53 | So(sprint.IsClosed(), ShouldBeTrue) 54 | So(sprint.IsClosed(), ShouldBeTrue) 55 | 56 | err = sprint.Close() 57 | So(err, ShouldHaveSameTypeAs, &SprintAlreadyClosedError{}) 58 | So(err.Error(), ShouldResemble, "Sprint already closed") 59 | }) 60 | 61 | Convey("with issues", func() { 62 | sprint := NewSprint() 63 | 64 | inst := NewIssue() 65 | sprint.AddIssue(inst) 66 | 67 | inst = NewIssue() 68 | err := inst.SetDone() 69 | So(err, ShouldBeNil) 70 | sprint.AddIssue(inst) 71 | 72 | So(sprint.IsClosed(), ShouldBeFalse) 73 | So(sprint.IsClosed(), ShouldBeFalse) 74 | 75 | err = sprint.Close() 76 | So(err, ShouldBeNil) 77 | 78 | So(sprint.IsClosed(), ShouldBeTrue) 79 | So(sprint.IsClosed(), ShouldBeTrue) 80 | 81 | err = sprint.Close() 82 | So(err, ShouldHaveSameTypeAs, &SprintAlreadyClosedError{}) 83 | }) 84 | }) 85 | } 86 | 87 | func Example_Sprint() { 88 | sprint := NewSprint() 89 | 90 | sprint.SetID(42) 91 | sprint.SetExpectedClosedAt(time.Now()) 92 | sprint.SetEffectiveClosedAt(time.Now()) 93 | 94 | fmt.Println(sprint.GetID()) 95 | 96 | fmt.Println(len(sprint.GetIssues())) 97 | fmt.Println(sprint.GetIssuesCount()) 98 | 99 | fmt.Println(sprint.GetStatus() == SprintOpen) 100 | fmt.Println(sprint.IsClosed()) 101 | 102 | fmt.Println(sprint.Close()) 103 | 104 | fmt.Println(sprint.GetStatus()) 105 | fmt.Println(sprint.IsClosed()) 106 | 107 | fmt.Println(len(sprint.GetIssues())) 108 | fmt.Println(sprint.GetIssuesCount()) 109 | 110 | // Output: 111 | // 42 112 | // 0 113 | // 0 114 | // true 115 | // false 116 | // 117 | // CLOSED 118 | // true 119 | // 0 120 | // 0 121 | } 122 | -------------------------------------------------------------------------------- /example/app/repos/sprints/gorm/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | github.com/jinzhu/gorm 16 | 17 | 18 | 1 19 | 20 | github.com/jinzhu/inflection 21 | 22 | 23 | 0->1 24 | 25 | 26 | 27 | 28 | 2 29 | 30 | ./example/app/repos/sprints/gorm 31 | 32 | 33 | 2->0 34 | 35 | 36 | 37 | 38 | 3 39 | 40 | ./example/bizrules/entities 41 | 42 | 43 | 2->3 44 | 45 | 46 | 47 | 48 | 4 49 | 50 | ./example/bizrules/gateways 51 | 52 | 53 | 2->4 54 | 55 | 56 | 57 | 58 | 4->3 59 | 60 | 61 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /example/bizrules/entities/sprint.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | var ( 9 | // Open is the status of a sprint that is still open 10 | SprintOpen = "OPEN" 11 | 12 | // Close is the status of an sprint that were closed 13 | SprintClosed = "CLOSED" 14 | ) 15 | 16 | // Sprint represents an sprint. 17 | type Sprint struct { 18 | id int 19 | status string 20 | createdAt time.Time 21 | expectedClosedAt time.Time 22 | effectiveClosedAt time.Time 23 | issues []*Issue 24 | } 25 | 26 | // New returns an instanciated instance of Sprint. 27 | func NewSprint() *Sprint { 28 | return &Sprint{ 29 | issues: make([]*Issue, 0), 30 | status: SprintOpen, 31 | createdAt: time.Now(), 32 | } 33 | } 34 | 35 | /* Setters */ 36 | 37 | // SetID sets the ID of the sprint. 38 | func (i *Sprint) SetID(val int) { i.id = val } 39 | 40 | // SetStatus sets the Status of the sprint. 41 | func (i *Sprint) SetStatus(val string) { i.status = val } 42 | 43 | // SetCreatedAt sets the CreatedAt of the sprint. 44 | func (i *Sprint) SetCreatedAt(val time.Time) { i.createdAt = val } 45 | 46 | // SetExpectedClosedAt sets the ExpectedClosedAt of the sprint. 47 | func (i *Sprint) SetExpectedClosedAt(val time.Time) { i.expectedClosedAt = val } 48 | 49 | // SetEffectiveClosedAt sets the EffectiveClosedAt of the sprint. 50 | func (i *Sprint) SetEffectiveClosedAt(val time.Time) { i.effectiveClosedAt = val } 51 | 52 | /* Getters */ 53 | 54 | // GetID returns the ID of the sprint. 55 | func (i *Sprint) GetID() int { return i.id } 56 | 57 | // GetStatus returns the status of the sprint. 58 | func (i *Sprint) GetStatus() string { return i.status } 59 | 60 | // GetCreatedAt returns the creation date of the sprint. 61 | func (i *Sprint) GetCreatedAt() time.Time { return i.createdAt } 62 | 63 | // GetExpectedClosedAt returns the finish date of the sprint. 64 | func (i *Sprint) GetExpectedClosedAt() time.Time { return i.expectedClosedAt } 65 | 66 | // GetEffectiveClosedAt returns the finish date of the sprint. 67 | func (i *Sprint) GetEffectiveClosedAt() time.Time { return i.effectiveClosedAt } 68 | 69 | /* ---- */ 70 | 71 | // AddIssue adds an issue to the sprint. 72 | func (i *Sprint) AddIssue(issue *Issue) { 73 | i.issues = append(i.issues, issue) 74 | } 75 | 76 | // GetIssues returns the issues of the sprint. 77 | func (i *Sprint) GetIssues() []*Issue { 78 | return i.issues 79 | } 80 | 81 | // GetIssuesCount returns the count of issues in the sprint. 82 | func (i *Sprint) GetIssuesCount() int { 83 | return len(i.issues) 84 | } 85 | 86 | // IsClosed returns true if the sprint status is "CLOSED". 87 | func (i *Sprint) IsClosed() bool { return i.GetStatus() == SprintClosed } 88 | 89 | // Close closes an open sprint 90 | func (i *Sprint) Close() error { 91 | if i.IsClosed() { 92 | return &SprintAlreadyClosedError{} 93 | } 94 | 95 | for idx := len(i.issues) - 1; idx >= 0; idx-- { 96 | issue := i.issues[idx] 97 | 98 | if issue.IsDone() { 99 | if err := issue.Close(); err != nil { 100 | return err 101 | } 102 | } else { 103 | i.issues = append(i.issues[:idx], i.issues[idx+1:]...) 104 | } 105 | } 106 | 107 | i.effectiveClosedAt = time.Now() 108 | i.status = SprintClosed 109 | return nil 110 | } 111 | 112 | /* Errors */ 113 | 114 | // SprintAlreadyClosedError is raised when trying to close an already closed sprint. 115 | type SprintAlreadyClosedError struct{} 116 | 117 | func (f SprintAlreadyClosedError) Error() string { 118 | return fmt.Sprintf("Sprint already closed") 119 | } 120 | 121 | // SprintNotFoundError is raised when trying to close an not found sprint. 122 | type SprintNotFoundError struct{} 123 | 124 | func (f SprintNotFoundError) Error() string { 125 | return fmt.Sprintf("Sprint not found") 126 | } 127 | -------------------------------------------------------------------------------- /example/bizrules/entities/issue.go: -------------------------------------------------------------------------------- 1 | package entities 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | var ( 9 | // Open is the status of an active issue 10 | IssueOpen = "OPEN" 11 | 12 | // Done is the status of an issue that were finished but not yet closed 13 | IssueDone = "DONE" 14 | 15 | // Close is the status of an issue that were closed 16 | IssueClosed = "CLOSED" 17 | ) 18 | 19 | // Issue represents an issue. 20 | type Issue struct { 21 | id int 22 | status string 23 | title string 24 | description string 25 | createdAt time.Time 26 | doneAt time.Time 27 | closedAt time.Time 28 | } 29 | 30 | // New returns an instanciated instance of Issue. 31 | func NewIssue() *Issue { 32 | return &Issue{ 33 | createdAt: time.Now(), 34 | } 35 | } 36 | 37 | /* generic setters */ 38 | // SetID sets the ID of the issue. 39 | func (i *Issue) SetID(val int) { i.id = val } 40 | 41 | // SetStatus sets the status of the issue. 42 | func (i *Issue) SetStatus(val string) { i.status = val } 43 | 44 | // SetTitle sets the title of the issue. 45 | func (i *Issue) SetTitle(val string) { i.title = val } 46 | 47 | // SetDescription sets the description of the issue. 48 | func (i *Issue) SetDescription(val string) { i.description = val } 49 | 50 | // SetCreatedAt sets the creation date of the issue. 51 | func (i *Issue) SetCreatedAt(val time.Time) { i.createdAt = val } 52 | 53 | // SetDoneAt sets the finish date of the issue. 54 | func (i *Issue) SetDoneAt(val time.Time) { i.doneAt = val } 55 | 56 | // SetClosedAt sets the closing date of the issue. 57 | func (i *Issue) SetClosedAt(val time.Time) { i.closedAt = val } 58 | 59 | /* generic getters */ 60 | 61 | // GetID returns the ID of the issue. 62 | func (i *Issue) GetID() int { return i.id } 63 | 64 | // GetStatus returns the status of the issue. 65 | func (i *Issue) GetStatus() string { return i.status } 66 | 67 | // GetTitle returns the title of the issue. 68 | func (i *Issue) GetTitle() string { return i.title } 69 | 70 | // GetDescription returns the description of the issue. 71 | func (i *Issue) GetDescription() string { return i.description } 72 | 73 | // GetCreatedAt returns the creation date of the issue. 74 | func (i *Issue) GetCreatedAt() time.Time { return i.createdAt } 75 | 76 | // GetDoneAt returns the finish date of the issue. 77 | func (i *Issue) GetDoneAt() time.Time { return i.doneAt } 78 | 79 | // GetClosedAt returns the closing date of the issue. 80 | func (i *Issue) GetClosedAt() time.Time { return i.closedAt } 81 | 82 | /* other methods */ 83 | 84 | // IsDone returns true if the issue status is "DONE". 85 | func (i *Issue) IsDone() bool { return i.GetStatus() == IssueDone } 86 | 87 | // IsClosed returns true if the issue status is "CLOSED". 88 | func (i *Issue) IsClosed() bool { return i.GetStatus() == IssueClosed } 89 | 90 | // SetDone sets the issue status to "DONE" 91 | func (i *Issue) SetDone() error { 92 | if i.IsDone() { 93 | return &IssueAlreadyDoneError{} 94 | } 95 | 96 | i.doneAt = time.Now() 97 | i.status = IssueDone 98 | return nil 99 | } 100 | 101 | // Close closes an open issue 102 | func (i *Issue) Close() error { 103 | if i.IsClosed() { 104 | return &IssueAlreadyClosedError{} 105 | } 106 | 107 | i.closedAt = time.Now() 108 | i.status = IssueClosed 109 | return nil 110 | } 111 | 112 | /* Errors */ 113 | 114 | // IssueAlreadyClosedError is raised when trying to close an already closed issue. 115 | type IssueAlreadyClosedError struct{} 116 | 117 | func (f IssueAlreadyClosedError) Error() string { 118 | return fmt.Sprintf("Issue already closed") 119 | } 120 | 121 | // IssueAlreadyDoneError is raised when trying to close an already done issue. 122 | type IssueAlreadyDoneError struct{} 123 | 124 | func (f IssueAlreadyDoneError) Error() string { 125 | return fmt.Sprintf("Issue already done") 126 | } 127 | -------------------------------------------------------------------------------- /slides/README.md: -------------------------------------------------------------------------------- 1 | # [fit] "Clean" Architecture 2 | 3 | ### 2016, by Manfred Touron (@moul) 4 | 5 | --- 6 | 7 | # overview 8 | 9 | * the "clean" architecture, "Yet Another New Architecture" 10 | * by uncle Bob 11 | * discovered 3 months ago at OpenClassrooms with Romain Kuzniak 12 | * recent, no real spec, no official implementation 13 | * I don't use "clean" architecture in production 14 | * I'm not a "clean" architecture expert 15 | 16 | --- 17 | 18 | # design slogans 1/2 [^1] 19 | 20 | * YAGNI (You Ain't Gonna Need It) 21 | * KISS (Keep It Simple, Stupid) 22 | * DRY (Don't Repeat Yourself) 23 | * S.O.L.I.D (SRP, OCP, LS, IS, DI) 24 | * TDD (Test Driven Development) 25 | 26 | [^1]: more info: http://fr.slideshare.net/RomainKuzniak/design-applicatif-avec-symfony2 27 | 28 | --- 29 | 30 | # design slogans 2/2 [^1] 31 | 32 | * BDD (Behavior Driven Development) 33 | * DDD (Domain Driven Design) 34 | * ... 35 | 36 | --- 37 | 38 | # design types [^1] 39 | 40 | * MVC 41 | * N3 Architectures 42 | * Domain Driven Design 43 | * Clean Architecture 44 | 45 | --- 46 | 47 | # the "clean" architecture [^2] 48 | 49 | * Not a revolution, a mix of multiple existing principles 50 | * The other designs are not "dirty" architectures 51 | * Recent examples: Hexagonal Architecture, Onion Architecture, Screaming Architecture, DCI, BCE 52 | * Dependency injection at the buildtime or at leat at the runtime init 53 | 54 | [^2]: https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html 55 | 56 | --- 57 | 58 | ![fit](assets/cleanarch.jpg) 59 | 60 | --- 61 | 62 | # `./cmd/api` 63 | 64 | ![fit](assets/cmd-api.imports.png) 65 | 66 | ```go 67 | func main() { 68 | // Setup gateways 69 | var sprintsGw gateways.Sprints 70 | if len(os.Args) > 1 && os.Args[1] == "--mem" { 71 | // configure a memory-based sprints gateway 72 | sprintsGw = sprintsmem.New() 73 | } else { 74 | // configure a sqlite-based sprints gateway 75 | db, err := gorm.Open("sqlite3", "test.db") 76 | if err != nil { 77 | panic(err) 78 | } 79 | defer db.Close() 80 | sprintsGw = sprintsgorm.New(db) 81 | } 82 | 83 | // Setup usecases 84 | getSprint := getsprint.New(sprintsGw, getsprintdto.ResponseAssembler{}) 85 | addSprint := addsprint.New(sprintsGw, addsprintdto.ResponseAssembler{}) 86 | ping := ping.New(pingdto.ResponseAssembler{}) 87 | //closeSprint := closesprint.New(sprintsGw, closesprintdto.ResponseBuilder{}) 88 | 89 | // Setup API 90 | gin := gin.Default() 91 | gin.GET("/sprints/:sprint-id", apicontrollers.NewGetSprint(&getSprint).Execute) 92 | gin.POST("/sprints", apicontrollers.NewAddSprint(&addSprint).Execute) 93 | gin.GET("/ping", apicontrollers.NewPing(&ping).Execute) 94 | //gin.DELETE("/sprints/:sprint-id", apicontrollers.NewCloseSprint(&closeSprint).Execute) 95 | 96 | // Start 97 | gin.Run() 98 | } 99 | ``` 100 | 101 | --- 102 | 103 | # `./app/controllers/api` 104 | 105 | ![fit](assets/app-controllers-api.imports.png) 106 | 107 | ```go 108 | type GetSprint struct { 109 | uc *getsprint.UseCase 110 | } 111 | 112 | func (ctrl *GetSprint) Execute(ctx *gin.Context) { 113 | sprintID, err := strconv.Atoi(ctx.Param("sprint-id")) 114 | if err != nil { 115 | ctx.JSON(http.StatusNotFound, gin.H{"error": "Invalid 'sprint-id'"}) 116 | return 117 | } 118 | 119 | req := getsprintdto.RequestBuilder{}. 120 | Create(). 121 | WithSprintID(sprintID). 122 | Build() 123 | 124 | resp, err := ctrl.uc.Execute(req) 125 | if err != nil { 126 | ctx.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("%v", err)}) 127 | return 128 | } 129 | 130 | ctx.JSON(http.StatusOK, gin.H{"result": GetSprintResponse{ 131 | CreatedAt: resp.GetCreatedAt(), 132 | EffectiveClosedAt: resp.GetEffectiveClosedAt(), 133 | ExpectedClosedAt: resp.GetExpectedClosedAt(), 134 | Status: resp.GetStatus(), 135 | }}) 136 | } 137 | 138 | type GetSprintResponse struct { 139 | CreatedAt time.Time `json:"created-at"` 140 | EffectiveClosedAt time.Time `json:"effective-closed-at"` 141 | ExpectedClosedAt time.Time `json:"expected-closed-at"` 142 | Status string `json:"status"` 143 | } 144 | ``` 145 | 146 | --- 147 | 148 | ### `./app/repos/sprints/gorm` 149 | 150 | ![right fit](assets/app-repos-sprints-gorm.imports.png) 151 | 152 | ```go 153 | type Repo struct { // implements gateways.Sprints 154 | db *gorm.DB 155 | } 156 | 157 | func (r Repo) Find(id int) (*entities.Sprint, error) { 158 | obj := sprintModel{} 159 | if err := r.db.First(&obj, "id = ?", id).Error; err != nil { 160 | return nil, err 161 | } 162 | 163 | ret := entities.NewSprint() 164 | ret.SetCreatedAt(obj.CreatedAt) 165 | ret.SetID(int(obj.ID)) 166 | ret.SetStatus(obj.status) 167 | ret.SetEffectiveClosedAt(obj.effectiveClosedAt) 168 | ret.SetExpectedClosedAt(obj.expectedClosedAt) 169 | 170 | return ret, nil 171 | } 172 | ``` 173 | 174 | --- 175 | 176 | ### `./bizrules/usecases/get_sprint` 177 | 178 | ![right fit](assets/bizrules-usecases-get_sprint.imports.png) 179 | 180 | ```go 181 | type UseCase struct { 182 | gw gateways.Sprints 183 | resp getsprintio.ResponseAssembler 184 | } 185 | 186 | func (uc *UseCase) Execute(req Request) (Response, error) { 187 | sprint, err := uc.gw.Find(req.GetID()) 188 | if err != nil { 189 | return nil, err 190 | } 191 | 192 | return uc.resp.Write(sprint) 193 | } 194 | ``` 195 | 196 | --- 197 | 198 | # pros 1/2 199 | 200 | * highly reusable 201 | * separate business rules <-> drivers 202 | * ease of switching to new backends 203 | * "LTS" business rules - heritage 204 | * unit-tests friendly 205 | * keep "good" performances (perhaps specific with Go (no needs for reflect)) 206 | 207 | --- 208 | 209 | # pros 2/2 210 | 211 | * TDD friendly (Test Driver Development) 212 | * BDD friendly (Behavior Driven Development) 213 | * TDD + BDD drives to good designs 214 | * ease of switching to new interfaces (or have multiple ones) 215 | * standardize exchanges; unit-tests requests and responses 216 | * the boundaries are clearly defined, it forces you to keep things at the right place 217 | 218 | --- 219 | 220 | # cons 221 | 222 | * a loooooot of files, classes, ... (annoying for creating new entities, usecases...) 223 | * code discovery, classes not directly linked to real objects, but to interfaces 224 | * make some optimizations harder, i.e: transactions 225 | 226 | --- 227 | 228 | ![](http://image.slidesharecdn.com/designapplicatifavecsymfony2-141216115702-conversion-gate02/95/design-applicatif-avec-symfony2-99-1024.jpg?cb=1418786874) 229 | 230 | --- 231 | 232 | # improvements ideas 233 | 234 | * gogenerate: less files, more readable code 235 | * add stats on the GitHub repo (impact on performances, LOC, complexity) 236 | * ... 237 | 238 | --- 239 | 240 | # conclusion 241 | 242 | * it's a Gasoil, the learning curve (start) is long 243 | * interesting for big projects, overkill for smaller, the center domain needs to be rich enough 244 | * should be done completely, or not at all 245 | * needs to be rigorous with the main and unit tests 246 | 247 | --- 248 | 249 | # questions ? 250 | 251 | ### github.com/moul/cleanarch 252 | ### @moul 253 | -------------------------------------------------------------------------------- /example/app/controllers/api/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | github.com/gin-gonic/gin 16 | 17 | 18 | 1 19 | 20 | github.com/gin-gonic/gin/binding 21 | 22 | 23 | 0->1 24 | 25 | 26 | 27 | 28 | 2 29 | 30 | github.com/gin-gonic/gin/render 31 | 32 | 33 | 0->2 34 | 35 | 36 | 37 | 38 | 3 39 | 40 | github.com/manucorporat/sse 41 | 42 | 43 | 0->3 44 | 45 | 46 | 47 | 48 | 4 49 | 50 | github.com/mattn/go-colorable 51 | 52 | 53 | 0->4 54 | 55 | 56 | 57 | 58 | 5 59 | 60 | golang.org/x/net/context 61 | 62 | 63 | 0->5 64 | 65 | 66 | 67 | 68 | 6 69 | 70 | gopkg.in/bluesuncorp/validator.v5 71 | 72 | 73 | 1->6 74 | 75 | 76 | 77 | 78 | 7 79 | 80 | ./example/app/controllers/api 81 | 82 | 83 | 7->0 84 | 85 | 86 | 87 | 88 | 8 89 | 90 | ./example/bizrules/usecases/add_sprint 91 | 92 | 93 | 7->8 94 | 95 | 96 | 97 | 98 | 9 99 | 100 | ./example/bizrules/usecases/add_sprint/dto 101 | 102 | 103 | 7->9 104 | 105 | 106 | 107 | 108 | 10 109 | 110 | ./example/bizrules/usecases/get_sprint 111 | 112 | 113 | 7->10 114 | 115 | 116 | 117 | 118 | 11 119 | 120 | ./example/bizrules/usecases/get_sprint/dto 121 | 122 | 123 | 7->11 124 | 125 | 126 | 127 | 128 | 12 129 | 130 | ./example/bizrules/usecases/ping 131 | 132 | 133 | 7->12 134 | 135 | 136 | 137 | 138 | 13 139 | 140 | ./example/bizrules/usecases/ping/dto 141 | 142 | 143 | 7->13 144 | 145 | 146 | 147 | 148 | 15 149 | 150 | ./example/bizrules/gateways 151 | 152 | 153 | 8->15 154 | 155 | 156 | 157 | 158 | 16 159 | 160 | ./example/bizrules/usecases/add_sprint/io 161 | 162 | 163 | 8->16 164 | 165 | 166 | 167 | 168 | 14 169 | 170 | ./example/bizrules/entities 171 | 172 | 173 | 9->14 174 | 175 | 176 | 177 | 178 | 9->16 179 | 180 | 181 | 182 | 183 | 10->15 184 | 185 | 186 | 187 | 188 | 17 189 | 190 | ./example/bizrules/usecases/get_sprint/io 191 | 192 | 193 | 10->17 194 | 195 | 196 | 197 | 198 | 11->14 199 | 200 | 201 | 202 | 203 | 11->17 204 | 205 | 206 | 207 | 208 | 18 209 | 210 | ./example/bizrules/usecases/ping/io 211 | 212 | 213 | 12->18 214 | 215 | 216 | 217 | 218 | 13->18 219 | 220 | 221 | 222 | 223 | 15->14 224 | 225 | 226 | 227 | 228 | 16->14 229 | 230 | 231 | 232 | 233 | 17->14 234 | 235 | 236 | 237 | 238 | 239 | -------------------------------------------------------------------------------- /example/cmd/api/.import-graph.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | godep 11 | 12 | 13 | 0 14 | 15 | github.com/gin-gonic/gin 16 | 17 | 18 | 1 19 | 20 | github.com/gin-gonic/gin/binding 21 | 22 | 23 | 0->1 24 | 25 | 26 | 27 | 28 | 2 29 | 30 | github.com/gin-gonic/gin/render 31 | 32 | 33 | 0->2 34 | 35 | 36 | 37 | 38 | 3 39 | 40 | github.com/manucorporat/sse 41 | 42 | 43 | 0->3 44 | 45 | 46 | 47 | 48 | 4 49 | 50 | github.com/mattn/go-colorable 51 | 52 | 53 | 0->4 54 | 55 | 56 | 57 | 58 | 5 59 | 60 | golang.org/x/net/context 61 | 62 | 63 | 0->5 64 | 65 | 66 | 67 | 68 | 6 69 | 70 | gopkg.in/bluesuncorp/validator.v5 71 | 72 | 73 | 1->6 74 | 75 | 76 | 77 | 78 | 7 79 | 80 | github.com/jinzhu/gorm 81 | 82 | 83 | 8 84 | 85 | github.com/jinzhu/inflection 86 | 87 | 88 | 7->8 89 | 90 | 91 | 92 | 93 | 9 94 | 95 | github.com/mattn/go-sqlite3 96 | 97 | 98 | 10 99 | 100 | ./example/app/controllers/api 101 | 102 | 103 | 10->0 104 | 105 | 106 | 107 | 108 | 11 109 | 110 | ./example/bizrules/usecases/add_sprint 111 | 112 | 113 | 10->11 114 | 115 | 116 | 117 | 118 | 12 119 | 120 | ./example/bizrules/usecases/add_sprint/dto 121 | 122 | 123 | 10->12 124 | 125 | 126 | 127 | 128 | 13 129 | 130 | ./example/bizrules/usecases/get_sprint 131 | 132 | 133 | 10->13 134 | 135 | 136 | 137 | 138 | 14 139 | 140 | ./example/bizrules/usecases/get_sprint/dto 141 | 142 | 143 | 10->14 144 | 145 | 146 | 147 | 148 | 15 149 | 150 | ./example/bizrules/usecases/ping 151 | 152 | 153 | 10->15 154 | 155 | 156 | 157 | 158 | 16 159 | 160 | ./example/bizrules/usecases/ping/dto 161 | 162 | 163 | 10->16 164 | 165 | 166 | 167 | 168 | 19 169 | 170 | ./example/bizrules/gateways 171 | 172 | 173 | 11->19 174 | 175 | 176 | 177 | 178 | 21 179 | 180 | ./example/bizrules/usecases/add_sprint/io 181 | 182 | 183 | 11->21 184 | 185 | 186 | 187 | 188 | 18 189 | 190 | ./example/bizrules/entities 191 | 192 | 193 | 12->18 194 | 195 | 196 | 197 | 198 | 12->21 199 | 200 | 201 | 202 | 203 | 13->19 204 | 205 | 206 | 207 | 208 | 22 209 | 210 | ./example/bizrules/usecases/get_sprint/io 211 | 212 | 213 | 13->22 214 | 215 | 216 | 217 | 218 | 14->18 219 | 220 | 221 | 222 | 223 | 14->22 224 | 225 | 226 | 227 | 228 | 23 229 | 230 | ./example/bizrules/usecases/ping/io 231 | 232 | 233 | 15->23 234 | 235 | 236 | 237 | 238 | 16->23 239 | 240 | 241 | 242 | 243 | 17 244 | 245 | ./example/app/repos/sprints/gorm 246 | 247 | 248 | 17->7 249 | 250 | 251 | 252 | 253 | 17->18 254 | 255 | 256 | 257 | 258 | 17->19 259 | 260 | 261 | 262 | 263 | 19->18 264 | 265 | 266 | 267 | 268 | 20 269 | 270 | ./example/app/repos/sprints/mem 271 | 272 | 273 | 20->18 274 | 275 | 276 | 277 | 278 | 20->19 279 | 280 | 281 | 282 | 283 | 21->18 284 | 285 | 286 | 287 | 288 | 22->18 289 | 290 | 291 | 292 | 293 | 24 294 | 295 | ./example/cmd/api 296 | 297 | 298 | 24->0 299 | 300 | 301 | 302 | 303 | 24->7 304 | 305 | 306 | 307 | 308 | 24->9 309 | 310 | 311 | 312 | 313 | 24->10 314 | 315 | 316 | 317 | 318 | 24->11 319 | 320 | 321 | 322 | 323 | 24->12 324 | 325 | 326 | 327 | 328 | 24->13 329 | 330 | 331 | 332 | 333 | 24->14 334 | 335 | 336 | 337 | 338 | 24->15 339 | 340 | 341 | 342 | 343 | 24->16 344 | 345 | 346 | 347 | 348 | 24->17 349 | 350 | 351 | 352 | 353 | 24->19 354 | 355 | 356 | 357 | 358 | 24->20 359 | 360 | 361 | 362 | 363 | 364 | --------------------------------------------------------------------------------