├── 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 | [](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 | [](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 |
19 |
--------------------------------------------------------------------------------
/example/bizrules/usecases/ping/io/.import-graph.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
19 |
--------------------------------------------------------------------------------
/example/bizrules/usecases/close_sprint/io/.import-graph.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
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 |
29 |
--------------------------------------------------------------------------------
/example/bizrules/usecases/.import-graph.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
29 |
--------------------------------------------------------------------------------
/example/bizrules/usecases/ping/.import-graph.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
29 |
--------------------------------------------------------------------------------
/example/bizrules/usecases/add_sprint/io/.import-graph.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
29 |
--------------------------------------------------------------------------------
/example/bizrules/usecases/get_sprint/io/.import-graph.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
29 |
--------------------------------------------------------------------------------
/example/bizrules/usecases/ping/dto/.import-graph.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
29 |
--------------------------------------------------------------------------------
/example/bizrules/usecases/close_sprint/dto/.import-graph.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
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 |
44 |
--------------------------------------------------------------------------------
/example/bizrules/usecases/add_sprint/dto/.import-graph.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
44 |
--------------------------------------------------------------------------------
/example/bizrules/usecases/get_sprint/dto/.import-graph.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
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 |
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 |
54 |
--------------------------------------------------------------------------------
/example/bizrules/usecases/get_sprint/.import-graph.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
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 |
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 | 
59 |
60 | ---
61 |
62 | # `./cmd/api`
63 |
64 | 
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 | 
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 | 
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 | 
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 | 
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 |
239 |
--------------------------------------------------------------------------------
/example/cmd/api/.import-graph.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
364 |
--------------------------------------------------------------------------------