├── adapters └── web │ ├── appengine.go │ ├── greetings.go │ ├── standalone.go │ └── web.go ├── app ├── app.go ├── app.yaml ├── appengine.go ├── config.go ├── index.yaml └── templates │ └── guestbook.html ├── domain ├── greeting.go └── time.go ├── engine ├── factory.go ├── greeter.go ├── greeter_add.go ├── greeter_list.go ├── query.go └── storage.go ├── providers ├── appengine │ ├── factory.go │ ├── greetings.go │ └── query.go └── mongodb │ ├── factory.go │ ├── greetings.go │ ├── id.go │ └── query.go └── readme.md /adapters/web/appengine.go: -------------------------------------------------------------------------------- 1 | // +build appengine 2 | 3 | package web 4 | 5 | import ( 6 | "github.com/gin-gonic/gin" 7 | "golang.org/x/net/context" 8 | "google.golang.org/appengine" 9 | ) 10 | 11 | // getContext provides the appengine context that 12 | // all of the appengine services rely on. Notice 13 | // the build tag that only includes this version 14 | // if we're running on appengine. The standalone 15 | // version is in standalong.go 16 | func getContext(c *gin.Context) context.Context { 17 | return appengine.NewContext(c.Request) 18 | } 19 | -------------------------------------------------------------------------------- /adapters/web/greetings.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | 9 | "github.com/captaincodeman/clean-go/engine" 10 | ) 11 | 12 | type ( 13 | greeter struct { 14 | engine.Greeter 15 | } 16 | ) 17 | 18 | // wire up the greetings routes 19 | func initGreetings(e *gin.Engine, f engine.EngineFactory, endpoint string) { 20 | greeter := &greeter{f.NewGreeter()} 21 | g := e.Group(endpoint) 22 | { 23 | g.GET("", greeter.list) 24 | g.POST("", greeter.add) 25 | } 26 | } 27 | 28 | // list converts the parameters into an engine 29 | // request and then marshalls the results based 30 | // on the format requested, returning either an 31 | // html rendered page or JSON (to simulate basic 32 | // content negotiation). It's simpler if the UI 33 | // is a SPA and the web interface is just an API. 34 | func (g greeter) list(c *gin.Context) { 35 | ctx := getContext(c) 36 | count, err := strconv.Atoi(c.Query("count")) 37 | if err != nil || count == 0 { 38 | count = 5 39 | } 40 | req := &engine.ListGreetingsRequest{ 41 | Count: count, 42 | } 43 | res := g.List(ctx, req) 44 | if c.Query("format") == "json" { 45 | c.JSON(http.StatusOK, res.Greetings) 46 | } else { 47 | c.HTML(http.StatusOK, "guestbook.html", res) 48 | } 49 | } 50 | 51 | // add accepts a form post and creates a new 52 | // greoting in the system. It could be made a 53 | // lot smarter and automatically check for the 54 | // content type to handle forms, JSON etc... 55 | func (g greeter) add(c *gin.Context) { 56 | ctx := getContext(c) 57 | req := &engine.AddGreetingRequest{ 58 | Author: c.PostForm("Author"), 59 | Content: c.PostForm("Content"), 60 | } 61 | g.Add(ctx, req) 62 | // TODO: set a flash cookie for "added" 63 | // if this was a web request, otherwise 64 | // send a nice JSON response ... 65 | c.Redirect(http.StatusFound, "/") 66 | } 67 | -------------------------------------------------------------------------------- /adapters/web/standalone.go: -------------------------------------------------------------------------------- 1 | // +build !appengine 2 | 3 | package web 4 | 5 | import ( 6 | "github.com/gin-gonic/gin" 7 | "golang.org/x/net/context" 8 | ) 9 | 10 | // getContext doesn't seem to do much but this is 11 | // the standalone version and here so we can swap 12 | // implementations using the build tag at the top. 13 | // Look at the appengine.go file for the appengine 14 | // specific implementation. 15 | func getContext(c *gin.Context) context.Context { 16 | return c 17 | } 18 | -------------------------------------------------------------------------------- /adapters/web/web.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | "github.com/captaincodeman/clean-go/engine" 9 | ) 10 | 11 | func init() { 12 | gin.SetMode(gin.ReleaseMode) 13 | } 14 | 15 | // NewWebAdapter creates a new web adaptor which will 16 | // handle the web interface and pass calls on to the 17 | // engine to do the real work (that's why the engine 18 | // factory is passed in - so anything that *it* needs 19 | // is unknown to this). 20 | // Because the web adapter ends up quite lightweight 21 | // it easier to replace. We could use any one of the 22 | // Go web routers / frameworks (Gin, Echo, Goji etc...) 23 | // or just stick with the standard framework. Changing 24 | // should be far less costly. 25 | func NewWebAdapter(f engine.EngineFactory, log bool) http.Handler { 26 | var e *gin.Engine 27 | if log { 28 | e = gin.Default() 29 | } else { 30 | e = gin.New() 31 | } 32 | 33 | e.LoadHTMLGlob("templates/*") 34 | 35 | initGreetings(e, f, "/") 36 | 37 | return e 38 | } 39 | -------------------------------------------------------------------------------- /app/app.go: -------------------------------------------------------------------------------- 1 | // +build !appengine 2 | 3 | package main 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/captaincodeman/clean-go/adapters/web" 9 | "github.com/captaincodeman/clean-go/engine" 10 | "github.com/captaincodeman/clean-go/providers/mongodb" 11 | ) 12 | 13 | // when running in traditional or 'standalone' mode 14 | // we're going to use MongoDB as the storage provider 15 | // and start the webserver running ourselves. 16 | func main() { 17 | s := mongodb.NewStorage(config.MongoURL) 18 | e := engine.NewEngine(s) 19 | http.ListenAndServe(":8080", web.NewWebAdapter(e, true)) 20 | } 21 | -------------------------------------------------------------------------------- /app/app.yaml: -------------------------------------------------------------------------------- 1 | module: default 2 | version: alpha 3 | runtime: go 4 | api_version: go1 5 | 6 | handlers: 7 | - url: /.* 8 | script: _go_app -------------------------------------------------------------------------------- /app/appengine.go: -------------------------------------------------------------------------------- 1 | // +build appengine 2 | 3 | package main 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/captaincodeman/clean-go/adapters/web" 9 | "github.com/captaincodeman/clean-go/engine" 10 | "github.com/captaincodeman/clean-go/providers/appengine" 11 | ) 12 | 13 | // for appengine we don't use main to start the server 14 | // because that is done for us by the platform. Instead 15 | // we attach to the standard mux router. Note that we're 16 | // using the appengine provider for storage and wiring 17 | // it up to the engine and then the engine to the web. 18 | func init() { 19 | s := appengine.NewStorage() 20 | e := engine.NewEngine(s) 21 | http.Handle("/", web.NewWebAdapter(e, false)) 22 | } 23 | -------------------------------------------------------------------------------- /app/config.go: -------------------------------------------------------------------------------- 1 | // +build !appengine 2 | 3 | package main 4 | 5 | type ( 6 | // Config is an example of provider-specific configuration 7 | // in this case it's for the standalone version only to set 8 | // the mongodb database connection string 9 | Config struct { 10 | MongoURL string 11 | } 12 | ) 13 | 14 | var ( 15 | config *Config 16 | ) 17 | 18 | func init() { 19 | // this would likely be loaded from flags or a conf file 20 | config = &Config{ 21 | MongoURL: "mongodb://localhost/clean", 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/index.yaml: -------------------------------------------------------------------------------- 1 | indexes: 2 | 3 | - kind: greeting 4 | ancestor: yes 5 | properties: 6 | - name: date 7 | direction: desc 8 | 9 | # AUTOGENERATED 10 | 11 | # This index.yaml is automatically updated whenever the dev_appserver 12 | # detects that a new type of query is run. If you want to manage the 13 | # index.yaml file manually, remove the above marker line (the line 14 | # saying "# AUTOGENERATED"). If you want to manage some indexes 15 | # manually, move them above the marker line. The index.yaml file is 16 | # automatically uploaded to the admin console when you next deploy 17 | # your application using appcfg.py. 18 | -------------------------------------------------------------------------------- /app/templates/guestbook.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Guestbook Demo 5 | 6 | 7 |
8 |
9 |
10 |
11 |
12 | 13 | {{range .Greetings }} 14 |

15 | {{with .Author}}{{.}}{{else}}An anonymous person{{end}} 16 | on {{.Date.Format "3:04pm, Mon 2 Jan"}} 17 | wrote

{{.Content}}
18 |

19 | {{end}} 20 | 21 | -------------------------------------------------------------------------------- /domain/greeting.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type ( 8 | // Greeting is the struct that would contain any 9 | // domain logic if we had any. Because it's simple 10 | // we're going to send it over the wire directly 11 | // so we add the JSON serialization tags but we 12 | // could use DTO specific structs for that 13 | Greeting struct { 14 | ID int64 `json:"id"` 15 | Author string `json:"author"` 16 | Content string `json:"content"` 17 | Date time.Time `json:"timestamp"` 18 | } 19 | ) 20 | 21 | // NewGreeting creates a new Greeting ... who'da thunk it! 22 | func NewGreeting(author, content string) *Greeting { 23 | return &Greeting{ 24 | Author: author, 25 | Content: content, 26 | Date: now(), 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /domain/time.go: -------------------------------------------------------------------------------- 1 | package domain 2 | 3 | import "time" 4 | 5 | var ( 6 | // now returns the current UTC time 7 | // It is a replaceable function to allow for easy unit testing 8 | now = defaultNow 9 | ) 10 | 11 | // set it back to this function to restore normal functionality 12 | func defaultNow() time.Time { 13 | return time.Now().UTC() 14 | } 15 | -------------------------------------------------------------------------------- /engine/factory.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | type ( 4 | // EngineFactory interface allows us to provide 5 | // other parts of the system with a way to make 6 | // instances of our use-case / interactors when 7 | // they need to 8 | EngineFactory interface { 9 | // NewGreeter creates a new Greeter interactor 10 | NewGreeter() Greeter 11 | } 12 | 13 | // engine factory stores the state of our engine 14 | // which only involves a storage factory instance 15 | engineFactory struct { 16 | StorageFactory 17 | } 18 | ) 19 | 20 | // NewEngine creates a new engine factory that will 21 | // make use of the passed in StorageFactory for any 22 | // data persistence needs. 23 | func NewEngine(s StorageFactory) EngineFactory { 24 | return &engineFactory{s} 25 | } 26 | -------------------------------------------------------------------------------- /engine/greeter.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | ) 6 | 7 | type ( 8 | // Greeter is the interface for our interactor 9 | Greeter interface { 10 | // Add is the add-a-greeting use-case 11 | Add(c context.Context, r *AddGreetingRequest) *AddGreetingResponse 12 | 13 | // List is the list-the-greetings use-case 14 | List(c context.Context, r *ListGreetingsRequest) *ListGreetingsResponse 15 | } 16 | 17 | // greeter implementation 18 | greeter struct { 19 | repository GreetingRepository 20 | } 21 | ) 22 | 23 | // NewGreeter creates a new Greeter interactor wired up 24 | // to use the greeter repository from the storage provider 25 | // that the engine has been setup to use. 26 | func (f *engineFactory) NewGreeter() Greeter { 27 | return &greeter{ 28 | repository: f.NewGreetingRepository(), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /engine/greeter_add.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/captaincodeman/clean-go/domain" 7 | ) 8 | 9 | type ( 10 | AddGreetingRequest struct { 11 | Author string 12 | Content string 13 | } 14 | 15 | AddGreetingResponse struct { 16 | ID int64 17 | } 18 | ) 19 | 20 | func (g *greeter) Add(c context.Context, r *AddGreetingRequest) *AddGreetingResponse { 21 | // this is where all our app logic would go - the 22 | // rules that apply to adding a greeting whether it 23 | // is being done via the web UI, a console app, or 24 | // whatever the internet has just been added to ... 25 | greeting := domain.NewGreeting(r.Author, r.Content) 26 | g.repository.Put(c, greeting) 27 | return &AddGreetingResponse{ 28 | ID: greeting.ID, 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /engine/greeter_list.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/captaincodeman/clean-go/domain" 7 | ) 8 | 9 | type ( 10 | ListGreetingsRequest struct { 11 | Count int 12 | } 13 | 14 | ListGreetingsResponse struct { 15 | Greetings []*domain.Greeting 16 | } 17 | ) 18 | 19 | func (g *greeter) List(c context.Context, r *ListGreetingsRequest) *ListGreetingsResponse { 20 | q := NewQuery("greeting").Order("date", Descending).Slice(0, r.Count) 21 | return &ListGreetingsResponse{ 22 | Greetings: g.repository.List(c, q), 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /engine/query.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | // Direction represents a query sort direction 4 | type Direction byte 5 | 6 | const ( 7 | // Ascending means going up, A-Z 8 | Ascending Direction = 1 << iota 9 | 10 | // Descending means reverse order, Z-A 11 | Descending 12 | ) 13 | 14 | // Condition represents a filter comparison operation 15 | // between a field and a value 16 | type Condition byte 17 | 18 | const ( 19 | // Equal if it should be the same 20 | Equal Condition = 1 << iota 21 | 22 | // LessThan if it should be smaller 23 | LessThan 24 | 25 | // LessThanOrEqual if it should be smaller or equal 26 | LessThanOrEqual 27 | 28 | // GreaterThan if it should be larger 29 | GreaterThan 30 | 31 | // GreaterThanOrEqual if it should be equal or greater than 32 | GreaterThanOrEqual 33 | ) 34 | 35 | type ( 36 | // Query represents a query specification for filtering 37 | // sorting, paging and limiting the data requested 38 | Query struct { 39 | Name string 40 | Offset int 41 | Limit int 42 | Filters []*Filter 43 | Orders []*Order 44 | } 45 | 46 | // QueryBuilder helps with query creation 47 | QueryBuilder interface { 48 | Filter(property string, value interface{}) QueryBuilder 49 | Order(property string, direction Direction) 50 | } 51 | 52 | // Filter represents a filter operation on a single field 53 | Filter struct { 54 | Property string 55 | Condition Condition 56 | Value interface{} 57 | } 58 | 59 | // Order represents a sort operation on a single field 60 | Order struct { 61 | Property string 62 | Direction Direction 63 | } 64 | ) 65 | 66 | // NewQuery creates a new database query spec. The name is what 67 | // the storage system should use to identify the types, usually 68 | // a table or collection name. 69 | func NewQuery(name string) *Query { 70 | return &Query{ 71 | Name: name, 72 | } 73 | } 74 | 75 | // Filter adds a filter to the query 76 | func (q *Query) Filter(property string, condition Condition, value interface{}) *Query { 77 | filter := NewFilter(property, condition, value) 78 | q.Filters = append(q.Filters, filter) 79 | return q 80 | } 81 | 82 | // Order adds a sort order to the query 83 | func (q *Query) Order(property string, direction Direction) *Query { 84 | order := NewOrder(property, direction) 85 | q.Orders = append(q.Orders, order) 86 | return q 87 | } 88 | 89 | // Slice adds a slice operation to the query 90 | func (q *Query) Slice(offset, limit int) *Query { 91 | q.Offset = offset 92 | q.Limit = limit 93 | return q 94 | } 95 | 96 | // NewFilter creates a new property filter 97 | func NewFilter(property string, condition Condition, value interface{}) *Filter { 98 | return &Filter{ 99 | Property: property, 100 | Condition: condition, 101 | Value: value, 102 | } 103 | } 104 | 105 | // NewOrder creates a new query order 106 | func NewOrder(property string, direction Direction) *Order { 107 | return &Order{ 108 | Property: property, 109 | Direction: direction, 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /engine/storage.go: -------------------------------------------------------------------------------- 1 | package engine 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | 6 | "github.com/captaincodeman/clean-go/domain" 7 | ) 8 | 9 | type ( 10 | // GreetingRepository defines the methods that any 11 | // data storage provider needs to implement to get 12 | // and store greetings 13 | GreetingRepository interface { 14 | // Put adds a new Greeting to the datastore 15 | Put(c context.Context, greeting *domain.Greeting) 16 | 17 | // List returns existing greetings matching the 18 | // query provided 19 | List(c context.Context, query *Query) []*domain.Greeting 20 | } 21 | 22 | // StorageFactory is the interface that a storage 23 | // provider needs to implement so that the engine can 24 | // request repository instances as it needs them 25 | StorageFactory interface { 26 | // NewGreetingRepository returns a storage specific 27 | // GreetingRepository implementation 28 | NewGreetingRepository() GreetingRepository 29 | } 30 | ) 31 | -------------------------------------------------------------------------------- /providers/appengine/factory.go: -------------------------------------------------------------------------------- 1 | // +build appengine 2 | 3 | package appengine 4 | 5 | import ( 6 | "github.com/captaincodeman/clean-go/engine" 7 | ) 8 | 9 | type ( 10 | storageFactory struct{} 11 | ) 12 | 13 | // NewStorage creates a new instance of this datastore storage factory 14 | func NewStorage() engine.StorageFactory { 15 | return &storageFactory{} 16 | } 17 | 18 | // NewGreetingRepository creates a new datastore greeting repository 19 | func (f *storageFactory) NewGreetingRepository() engine.GreetingRepository { 20 | return newGreetingRepository() 21 | } 22 | -------------------------------------------------------------------------------- /providers/appengine/greetings.go: -------------------------------------------------------------------------------- 1 | // +build appengine 2 | 3 | package appengine 4 | 5 | import ( 6 | "time" 7 | 8 | "golang.org/x/net/context" 9 | "google.golang.org/appengine/datastore" 10 | 11 | "github.com/captaincodeman/clean-go/domain" 12 | "github.com/captaincodeman/clean-go/engine" 13 | ) 14 | 15 | type ( 16 | greetingRepository struct{} 17 | 18 | // greeting is the internal struct we use for persistence 19 | // it allows us to attach the datastore PropertyLoadSaver 20 | // methods to the struct that we don't own 21 | greeting struct { 22 | domain.Greeting 23 | } 24 | ) 25 | 26 | var ( 27 | greetingKind = "greeting" 28 | ) 29 | 30 | func newGreetingRepository() engine.GreetingRepository { 31 | return &greetingRepository{} 32 | } 33 | 34 | func (r greetingRepository) Put(c context.Context, g *domain.Greeting) { 35 | d := &greeting{*g} 36 | k := datastore.NewIncompleteKey(c, greetingKind, guestbookEntityKey(c)) 37 | datastore.Put(c, k, d) 38 | } 39 | 40 | func (r greetingRepository) List(c context.Context, query *engine.Query) []*domain.Greeting { 41 | g := []*greeting{} 42 | q := translateQuery(greetingKind, query) 43 | q = q.Ancestor(guestbookEntityKey(c)) 44 | 45 | k, _ := q.GetAll(c, &g) 46 | o := make([]*domain.Greeting, len(g)) 47 | for i := range g { 48 | o[i] = &g[i].Greeting 49 | o[i].ID = k[i].IntID() 50 | } 51 | return o 52 | } 53 | 54 | func guestbookEntityKey(c context.Context) *datastore.Key { 55 | return datastore.NewKey(c, "guestbook", "", 1, nil) 56 | } 57 | 58 | func (x *greeting) Load(props []datastore.Property) error { 59 | for _, prop := range props { 60 | switch prop.Name { 61 | case "author": 62 | x.Author = prop.Value.(string) 63 | case "content": 64 | x.Content = prop.Value.(string) 65 | case "date": 66 | x.Date = prop.Value.(time.Time) 67 | } 68 | } 69 | return nil 70 | } 71 | 72 | func (x *greeting) Save() ([]datastore.Property, error) { 73 | ps := []datastore.Property{ 74 | datastore.Property{Name: "author", Value: x.Author, NoIndex: true, Multiple: false}, 75 | datastore.Property{Name: "content", Value: x.Content, NoIndex: true, Multiple: false}, 76 | datastore.Property{Name: "date", Value: x.Date, NoIndex: false, Multiple: false}, 77 | } 78 | return ps, nil 79 | } 80 | -------------------------------------------------------------------------------- /providers/appengine/query.go: -------------------------------------------------------------------------------- 1 | // +build appengine 2 | 3 | package appengine 4 | 5 | import ( 6 | "google.golang.org/appengine/datastore" 7 | 8 | "github.com/captaincodeman/clean-go/engine" 9 | ) 10 | 11 | // translateQuery converts an application query spec into 12 | // an appengine datastore specific query 13 | func translateQuery(kind string, query *engine.Query) *datastore.Query { 14 | q := datastore.NewQuery(kind) 15 | 16 | for _, filter := range query.Filters { 17 | switch filter.Condition { 18 | case engine.Equal: 19 | q = q.Filter(filter.Property + " =", filter.Value) 20 | case engine.LessThan: 21 | q = q.Filter(filter.Property + " <", filter.Value) 22 | case engine.LessThanOrEqual: 23 | q = q.Filter(filter.Property + " <=", filter.Value) 24 | case engine.GreaterThan: 25 | q = q.Filter(filter.Property + " >", filter.Value) 26 | case engine.GreaterThanOrEqual: 27 | q = q.Filter(filter.Property + " >=", filter.Value) 28 | } 29 | } 30 | 31 | for _, order := range query.Orders { 32 | switch order.Direction { 33 | case engine.Ascending: 34 | q = q.Order(order.Property) 35 | case engine.Descending: 36 | q = q.Order("-" + order.Property) 37 | } 38 | } 39 | 40 | if query.Offset > 0 { 41 | q = q.Offset(query.Offset) 42 | } 43 | 44 | if query.Limit > 0 { 45 | q = q.Limit(query.Limit) 46 | } 47 | 48 | return q 49 | } 50 | -------------------------------------------------------------------------------- /providers/mongodb/factory.go: -------------------------------------------------------------------------------- 1 | package mongodb 2 | 3 | import ( 4 | "time" 5 | 6 | "gopkg.in/mgo.v2" 7 | 8 | "github.com/captaincodeman/clean-go/engine" 9 | ) 10 | 11 | type ( 12 | storageFactory struct { 13 | session *mgo.Session 14 | } 15 | ) 16 | 17 | // NewStorage creates a new instance of this mongodb storage factory 18 | func NewStorage(url string) engine.StorageFactory { 19 | session, _ := mgo.DialWithTimeout(url, 10*time.Second) 20 | ensureIndexes(session) 21 | return &storageFactory{session} 22 | } 23 | 24 | // NewGreetingRepository creates a new datastore greeting repository 25 | func (f *storageFactory) NewGreetingRepository() engine.GreetingRepository { 26 | return newGreetingRepository(f.session) 27 | } 28 | 29 | func ensureIndexes(s *mgo.Session) { 30 | index := mgo.Index{ 31 | Key: []string{"date"}, 32 | Background: true, 33 | } 34 | c := s.DB("").C(greetingCollection) 35 | c.EnsureIndex(index) 36 | } 37 | -------------------------------------------------------------------------------- /providers/mongodb/greetings.go: -------------------------------------------------------------------------------- 1 | package mongodb 2 | 3 | import ( 4 | "golang.org/x/net/context" 5 | "gopkg.in/mgo.v2" 6 | "gopkg.in/mgo.v2/bson" 7 | 8 | "github.com/captaincodeman/clean-go/domain" 9 | "github.com/captaincodeman/clean-go/engine" 10 | ) 11 | 12 | type ( 13 | greetingRepository struct { 14 | session *mgo.Session 15 | } 16 | ) 17 | 18 | var ( 19 | greetingCollection = "greeting" 20 | ) 21 | 22 | func newGreetingRepository(session *mgo.Session) engine.GreetingRepository { 23 | return &greetingRepository{session} 24 | } 25 | 26 | func (r greetingRepository) Put(c context.Context, g *domain.Greeting) { 27 | s := r.session.Clone() 28 | defer s.Close() 29 | 30 | col := s.DB("").C(greetingCollection) 31 | if g.ID == 0 { 32 | g.ID = getNextSequence(s, greetingCollection) 33 | } 34 | col.Upsert(bson.M{"_id": g.ID}, g) 35 | } 36 | 37 | func (r greetingRepository) List(c context.Context, query *engine.Query) []*domain.Greeting { 38 | s := r.session.Clone() 39 | defer s.Close() 40 | 41 | col := s.DB("").C(greetingCollection) 42 | g := []*domain.Greeting{} 43 | q := translateQuery(col, query) 44 | q.All(&g) 45 | 46 | return g 47 | } 48 | -------------------------------------------------------------------------------- /providers/mongodb/id.go: -------------------------------------------------------------------------------- 1 | package mongodb 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | "gopkg.in/mgo.v2/bson" 6 | ) 7 | 8 | type ( 9 | // ID represents the last used integer 10 | // id for any collection 11 | ID struct { 12 | Next int64 `bson:"n"` 13 | } 14 | ) 15 | 16 | var ( 17 | idCollection = "id" 18 | ) 19 | 20 | // simple way of using integer IDs with MongoDB 21 | func getNextSequence(s *mgo.Session, name string) int64 { 22 | c := s.DB("").C(idCollection) 23 | change := mgo.Change{ 24 | Update: bson.M{"$inc": bson.M{"n": 1}}, 25 | Upsert: true, 26 | ReturnNew: true, 27 | } 28 | id := new(ID) 29 | c.Find(bson.M{"_id": name}).Apply(change, id) 30 | return id.Next 31 | } 32 | -------------------------------------------------------------------------------- /providers/mongodb/query.go: -------------------------------------------------------------------------------- 1 | package mongodb 2 | 3 | import ( 4 | "gopkg.in/mgo.v2" 5 | "gopkg.in/mgo.v2/bson" 6 | 7 | "github.com/captaincodeman/clean-go/engine" 8 | ) 9 | 10 | // translateQuery converts an application query spec into 11 | // a mongodb specific query 12 | func translateQuery(c *mgo.Collection, query *engine.Query) *mgo.Query { 13 | m := bson.M{} 14 | for _, filter := range query.Filters { 15 | switch filter.Condition { 16 | case engine.Equal: 17 | m[filter.Property] = filter.Value 18 | case engine.LessThan: 19 | m[filter.Property] = bson.M{"$lt": filter.Value} 20 | case engine.LessThanOrEqual: 21 | m[filter.Property] = bson.M{"$lte": filter.Value} 22 | case engine.GreaterThan: 23 | m[filter.Property] = bson.M{"$gt": filter.Value} 24 | case engine.GreaterThanOrEqual: 25 | m[filter.Property] = bson.M{"$gte": filter.Value} 26 | } 27 | } 28 | q := c.Find(m) 29 | 30 | for _, order := range query.Orders { 31 | switch order.Direction { 32 | case engine.Ascending: 33 | q = q.Sort(order.Property) 34 | case engine.Descending: 35 | q = q.Sort("-" + order.Property) 36 | } 37 | } 38 | 39 | if query.Offset > 0 { 40 | q = q.Skip(query.Offset) 41 | } 42 | 43 | if query.Limit > 0 { 44 | q = q.Limit(query.Limit) 45 | } 46 | 47 | return q 48 | } 49 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Clean Architecture in Go 2 | An example of "Clean Architecture" in Go to demonstrate developing a testable 3 | application that can be run on AppEngine with Google Cloud Storage or with 4 | traditional hosting and MongoDB for storage (but not limited to either). 5 | 6 | There are a number of different application architectures that are all simlar 7 | variations on the same theme which is to have clean separation of concerns and 8 | dependencies that follow the best practices of "the dependency invesion principle": 9 | 10 | A. High-level modules should not depend on low-level modules. Both should depend on abstractions. 11 | 12 | B. Abstractions should not depend on details. Details should depend on abstractions. 13 | 14 | Variations of the approach include: 15 | 16 | * [The Clean Architecture](https://blog.8thlight.com/uncle-bob/2012/08/13/the-clean-architecture.html) advocated by Robert Martin ('Uncle Bob') 17 | * Ports & Adapters or [Hexagonal Architecture](http://alistair.cockburn.us/Hexagonal+architecture) by Alistair Cockburn 18 | * [Onion Architecture](http://jeffreypalermo.com/blog/the-onion-architecture-part-1/) by Jeffrey Palermo 19 | 20 | From more in-depth practical application of many of the ideas I can strongly 21 | recommend the excellent book [Implementing Domain-Driven Design](http://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577) 22 | by Vaughn Vernon that goes into far greater detail. 23 | 24 | Besides the clean codebase, the approaches also bring other advantages - significant 25 | parts of the system can be unit tested quickly and easily without having to fire 26 | up the full web stack, something that is often difficult when the dependencies 27 | go the wrong way (if you need a database and a web-server running to make your 28 | tests work, you're doing it wrong). 29 | 30 | I'd used it before in the world of .NET but forgot about it after moving to coding 31 | more in Python. After switching languages again (yeah, right) to the wonderful 32 | world of go I came across a blog post which re-ignited my interest in it: 33 | [Applying The Clean Architecture to Go applications](http://manuel.kiessling.net/2012/09/28/applying-the-clean-architecture-to-go-applications/) 34 | 35 | It's a great read but I found the example a little overly-complex with too much of 36 | the focus on relational database model parts and at the same time it was light 37 | on some issues I wanted to resolve such as switching between different storage types 38 | and web UI or framework (and Go has so many of those to chose from!). 39 | 40 | I've also been looking for a way to make my application usable both standalone 41 | and on AppEngine as well as being easier to test, so this seemed like a good opportunity 42 | to do some experimenting and this is what I came up with as a prototype which I've 43 | hopefully simplified to show the techniques. 44 | 45 | ## Dependency Rings 46 | We've all heard of n-tier or layered architecture, especially if you've come 47 | from the world of .NET or Java and it's unfair that it gets a bad rep. Probably 48 | because it was often implemented so poorly with the typical mistake of everything 49 | relying on the database layer at the bottom which made software rigid, difficult 50 | to test and closely tied to the vendor's database implementation (hardly surprising 51 | that they promoted it so hard). 52 | 53 | Reversing the dependencies though has a wonderful transformative effect on your 54 | code. Here is my interpretation of the layers or rings implemented using the Go 55 | language (or 'Golang' for Google). 56 | 57 | ### Domain 58 | At the center of the dependencies is the domain. These are the business objects 59 | or entities and should represent and encapsulate the fundamental business rules 60 | such as "can a closed customer account create a new order?". There is usually a 61 | single root object that represents the system and which has the factory methods 62 | to create other objects (which in turn may have their own methods to create others). 63 | This is where the domain-driven design lives. 64 | 65 | Looking at this should give you an idea of the business model for the application 66 | and what the system is working with. This package allows code such as unit tests 67 | to excercise the core parts of the app for testing to ensure that basic rules are 68 | enforced. 69 | 70 | ### Engine / Use-Cases 71 | The application level rules and use-cases orchestrate the domain model and add richer 72 | rules and logic including persistence. I prefer the term engine for this package 73 | because it is the engine of what the app actually does. The rules implemented at this 74 | level should not affect the domain model rules but obviously may depend on them. 75 | The rules of the application also shouldn't rely on the UI or the persistence 76 | frameworks being used. 77 | 78 | Why would the business rules change depending on what UI framework is the new flavour 79 | of the month or if we want to change from an RDBMS to MongoDB or some cloud datastore? 80 | Those are implementation details that pull the levers of the use cases or are used by 81 | the engine via abstract interfaces. 82 | 83 | ### Interface Adapters 84 | These are concerned with converting data from a form that the use-cases handle to 85 | whatever the external framework and drivers use. A use-case may expect a request 86 | struct with a set of parameters and return a response struct with the results. The 87 | public facing part of the app is more likely to expect to send requests as JSON or 88 | http form posts and return JSON or rendered HTML. The database may return results 89 | in a structure that needs to be adapted into something the rest of the app can use. 90 | 91 | ### Frameworks and Drivers 92 | These are the ports that allow the system to talk to 'outside things' which could be 93 | a database for persistence or a web server for the UI. None of the inner use cases 94 | or domain entities should know about the implementation of these layers and they may 95 | change over time because ... well, we used to store data in SQL, then MongoDB and 96 | now cloud datastores. Changing the storage should not change the application or any 97 | of the business rules. I tend to call these "providers" because ... well, .NET. 98 | 99 | # Run 100 | Within app folder ... 101 | 102 | ## App Engine 103 | Install the AppEngine SDK for Go: 104 | 105 | goapp serve 106 | 107 | ## Standalone 108 | Start mongodb and build / run the go app as normal: 109 | 110 | mongod --config /usr/local/etc/mongod.conf 111 | go run app.go 112 | 113 | ## Run Tests 114 | Not yet added 115 | 116 | ginkgo watch -cover domain 117 | go tool cover -html=domain/domain.coverprofile 118 | 119 | # Implementation Notes 120 | 121 | ## Build tags 122 | Go has build tags to control which code is included and when running on AppEngine 123 | the `appengine` tag is automatically applied. This provides an easy way to include 124 | or exclude code that will only work on one platform or the other. i.e. there is no 125 | point building the appengine provider into a standalone build and some code can't 126 | be executed on appengine classic - this provides a way to keep things separated. 127 | 128 | ## Dependency Injection 129 | Surely it's needed for such a thing? No it isn't. While DI can be a useful tool, 130 | very often it takes over a project and becomes an entangled part of the application 131 | architecture masquerading as the framework. Seriously, you don't need it and it 132 | often comes with a huge cost in terms of complexity and runtime performance. 133 | Whatever a DI framework does, you can do yourself with some factories - what we 134 | used before the world went DI crazy and thought Spring was a good idea (oh, how 135 | we laugh about it now). 136 | 137 | ## Query spec 138 | The Query spec provides a way to pass a query definition to the providers in a 139 | storage agnostic way without depending on any database specific querying language. 140 | This was attempted in .NET with Linq to mixed results - you often ended up coding 141 | for the specifics of certain databases (usually SQL server) but in this case the 142 | query language is much simpler and designed to be more lightweight as it only has 143 | to provide some filtering capability for what is going to be a NoSQL database or 144 | a SQL database being used in a non-relational way. 145 | 146 | ## Storage providers 147 | I picked AppEngine Datastore and MongoDB because they are kind of similar in that 148 | they are both NoSQL stores but are pretty different in how connections and state 149 | are maintained. The MongoDB storage has the connection passed in through the 150 | factory setup. The Datastore has no permanent connection and uses the context 151 | from each request. 152 | 153 | ## Enhancements 154 | There's a lot missing. Some obvious things would be to pass request information 155 | such as authenticated user, request IP etc... in a standard struct embedded within 156 | each interactor request. The responses should also return errors that the web ui 157 | adapter could use in the response. 158 | 159 | A console adapter could be created to demonstrate the ability to use the engine 160 | app logic and the storage without the web ui. Speaking of which ... some unit tests 161 | would also show how the majority of the system can be tested without having to 162 | fire up a web server or a database. Test storage instances can be used to test 163 | the engine and test engine instances can help test the web handlers. 164 | 165 | ## What's with the imports? 166 | Why do I separate the imports in Go? I just like it ... I divide the imports into: 167 | 168 | Standard packages (e.g. `strings` or `fmt`) 169 | 170 | Extensions to packages (e.g. `net/http` or `encoding/json`) 171 | 172 | 3rd party packages (e.g. `google.golang.org/appengine/datastore`) 173 | 174 | Application packages (e.g. `github.com/captaincodeman/clean-go/domain`) --------------------------------------------------------------------------------