Skip to content

Instantly share code, notes, and snippets.

@ReSTARTR
Last active April 6, 2021 10:22
Show Gist options
  • Save ReSTARTR/ac0e4f2f547fe3e0959c8a79ec3cc7b5 to your computer and use it in GitHub Desktop.
Save ReSTARTR/ac0e4f2f547fe3e0959c8a79ec3cc7b5 to your computer and use it in GitHub Desktop.
layered architecture in go

Example of the application of layered architecture style in go.

This application is based on the post.

File structure is...

$ tree $GOPATH/src/example.com/layered-arch-in-go
.
├── cmd
│   └── app
│       └── main.go
└── user
    └── user.go

The point is...

  • factory methods like NewXXX should return concrete type.
  • if you wanna use transaction, check if the repository satisfy the sqlDB interface or not.

Pros

  • Repositories in infrastructure layer don't need to know the interface of Repository
  • less objects (without connection object)

Cons

  • need type assersion repo.(sqlDB) in all application method
    • But, I think, it might be a long time before you wanna switch another infrastructure.

自分はどうしているのかというと、 インフラレイヤの差し替えは考慮して実装していない。 差し替えが発生した場合、アプリケーションレイヤの修正も発生するのは仕方ないと思っている。

差し替え対象によって考慮する要素が替わってくるし、 差し替えが発生するかどうかも分からないので、 必要になった際にそれなりの工数をかけるのがシンプルな気がする。 もちろん、直近で想定されるのであれば、それなりの仕組みを実装するのがいいかもしれないが・・・。

👍

package main
// cmd/server/main.go
import (
"database/sql"
"net/http"
"example.com/layered-arch-in-go/user"
)
var useWebAPI bool
func postUser(repo user.Repository) func(w http.ResponseWriter, r *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
app := user.NewApplication(repo)
u := user.User{Name: "Bob"}
if err := app.Add(&u); err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
}
}
func main() {
var repo user.Repository
if useWebAPI {
repo = user.NewWebAPIRepository()
} else {
var db *sql.DB // TODO: init
repo = user.NewDBRepository(db)
}
http.HandleFunc("/users", postUser(repo))
http.ListenAndServe(":8000", nil)
}
package user
// user/user.go
import (
"bytes"
"database/sql"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
)
// domain layer
type User struct {
Name string
}
// domain layer
type Repository interface {
Add(u *User) error
}
// application layer
type Application struct {
repo Repository
}
func NewApplication(repo Repository) *Application {
return &Application{repo}
}
type sqlDB interface {
Begin() (*sql.Tx, error)
}
func (a *Application) Add(u *User) error {
r, ok := a.repo.(sqlDB)
if !ok {
return a.repo.Add(u)
}
tx, err := r.Begin()
if err != nil {
return err
}
if err := a.repo.Add(u); err != nil {
tx.Rollback()
}
return tx.Commit()
}
func NewDBRepository(db *sql.DB) *DBRepository {
return &DBRepository{db}
}
// infrastructure
type DBRepository struct {
*sql.DB
}
func (r *DBRepository) Add(u *User) error {
_, err := r.Exec(fmt.Sprintf("INSERT INTO users VALUES '%s'", u.Name))
return err
}
// infrastructure
type WebAPIRepository struct {
endpoint string
api *http.Client
}
func NewWebAPIRepository() *WebAPIRepository {
c := &http.Client{}
return &WebAPIRepository{"http://example.com/users", c}
}
func (r *WebAPIRepository) Add(u *User) error {
b, err := json.Marshal(u)
if err != nil {
return err
}
resp, err := r.api.Post(r.endpoint, "application/json", bytes.NewReader(b))
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("could not post data: %v", resp.Status)
}
_, err = ioutil.ReadAll(resp.Body)
if err != nil {
return fmt.Errorf("could not read: %v", err)
}
return nil
}
// check types
var _ sqlDB = &DBRepository{}
var _ Repository = &DBRepository{}
var _ Repository = &WebAPIRepository{}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment