Bootstrap

Golang 中的整洁架构

译者:baiyutang

原文:

什么是整洁架构

以《架构整洁之道》出名的作者罗伯特 “Bob 大叔” 马丁,提出了一种架构,包含几个非常重要的点,诸如:可测试性、架构独立性、数据库和接口。

整洁架构的约束条件:

  • 架构独立性:框架不依赖于未来加载的软件库。

  • 可测试性:业务规则可以被测试,并且不依赖 UI、数据库、Web 服务器或其他外部要素。

  • UI 独立:UI 可以轻易变化,但不应改变其他系统。一个 Web UI 可以被控制台 UI 代替,但不改变业务规则。

  • 数据库独立:可以将 或 ,替换为 、、 或其他。业务规则不应该被数据库限制。

  • 外部代理商独立:事实上,业务规则根本不了解外部世界。

更多在 >>

那么,根据这些限制条件,每层都应该独立和可测试。

按照 Bob 大叔的架构,我们可以把代码分为 4 层:

  • Entities

  • Use Cases

  • Controller

  • Framework & Driver

Golang 中的整洁架构

文件目录

让我们用 包,看一个简单的例子

ls -ln pkg/user
-rw-r — r — 1 501 20 5078 Feb 16 09:58 entity.go
-rw-r — r — 1 501 20 3747 Feb 16 10:03 mongodb.go
-rw-r — r — 1 501 20 509  Feb 16 09:59 repository.go
-rw-r — r — 1 501 20 2403 Feb 16 10:30 service.go

Entities

文件 中,是我们的实体:

//User data
type User struct {
	ID                 entity.ID    `json:"id" bson:"_id,omitempty"`
	Picture            string       `json:"picture" bson:"picture,omitempty"`
	Email              string       `json:"email" bson:"email"`
	Password           string       `json:"password" bson:"password,omitempty"`
	Type               Type         `json:"type" bson:"type"`
	Company            []*Company   `json:"company" bson:"company,omitempty"`
	CreatedAt          time.Time    `json:"created_at" bson:"created_at"`
	ValidatedAt        time.Time    `json:"validated_at" bson:"validated_at,omitempty"`
}

Repositories

文件 中,是我们定义的仓储层的接口,它是实体被保存的地方。在本例中,仓储层是 “Bob 大叔” 架构中的 ,内容如下:

package user

import "github.com/thecodenation/stamp/pkg/entity"

type Reader interface {
	Find(id entity.ID) (*User, error)
	FindByEmail(email string) (*User, error)
	FindByChangePasswordHash(hash string) (*User, error)
	FindByValidationHash(hash string) (*User, error)
	FindAll() ([]*User, error)
}

type Writer interface {
	Update(user *User) error
	Store(user *User) (entity.ID, error)
	AddCompany(id entity.ID, company *Company) error
	AddInvite(userID entity.ID, companyID entity.ID) error
}

//Repository repository interface
type Repository interface {
	Reader
	Writer	
}

接口可以被任何一种存储层实现,像 , 等等。在本案例中我们用 MongoDB 来实现,见

package user

import (
	"errors"
	"os"
	"github.com/juju/mgosession"
	"github.com/thecodenation/stamp/pkg/entity"
	mgo "gopkg.in/mgo.v2"
	"gopkg.in/mgo.v2/bson"
)

type repo struct {
	pool *mgosession.Pool
}

//NewMongoRepository create new repository
func NewMongoRepository(p *mgosession.Pool) Repository {
	return &repo{
		pool: p,
	}
}

func (r *repo) Find(id entity.ID) (*User, error) {
	result := User{}
	session := r.pool.Session(nil)
	coll := session.DB(os.Getenv("MONGODB_DATABASE")).C("user")
	err := coll.Find(bson.M{"_id": id}).One(&result)
	if err != nil {
		return nil, err
	}
	return &result, nil
}

func (r *repo) FindByEmail(email string) (*User, error) {
}

func (r *repo) FindByChangePasswordHash(hash string) (*User, error) {
}

func (r *repo) FindAll() ([]*User, error) {
}

func (r *repo) Update(user *User) error {
}

func (r *repo) Store(user *User) (entity.ID, error) {
}

func (r *repo) AddCompany(id entity.ID, company *Company) error {
}

func (r *repo) AddInvite(userID entity.ID, companyID entity.ID) error {
}

func (r *repo) FindByValidationHash(hash string) (*User, error) {
}

Services

文件 代表“Bob 大叔”定义的用例层。这个文件中有 的接口和实现, 的接口如下:

//Service service interface
type Service interface {
	Reader
	Writer
	Register(user *User) (entity.ID, error)
	ForgotPassword(user *User) error
	ChangePassword(user *User, password string) error
	Validate(user *User) error
	Auth(user *User, password string) error
	IsValid(user *User) bool
	GetRepo() Repository
}

Controller

最后一层,在我们架构中的 是我们 Api 实现的内容:

cd api ; tree
.
|____handler
| |____company.go
| |____user.go
| |____address.go
| |____skill.go
| |____invite.go
| |____position.go
|____rice-box.go
|____main.go

下面的代码来自 api/main.go,我们能看到如何使用

session, err := mgo.Dial(os.Getenv("MONGODB_HOST"))
if err != nil {
	elog.Error(err)
}
mPool := mgosession.NewPool(nil, session, 1)
queueService, err := queue.NewAWSService()
if err != nil {
		elog.Error(err)
}
userRepo := user.NewMongoRepository(mPool)
userService := user.NewService(userRepo, queueService)

Tests

现在我们很容易在我们的包内创建测试用户,如:

package user

import (
	"testing"
	"time"

	"github.com/thecodenation/stamp/pkg/entity"
	"github.com/thecodenation/stamp/pkg/queue"
)

func TestIsValidUser(t *testing.T) {
	u := User{
		ID:        entity.NewID(),
		FirstName: "Bill",
		LastName:  "Gates",
	}
	userRepo := NewInmemRepository()
	queueService, _ := queue.NewInmemService()
	userService := NewService(userRepo, queueService)

	if userService.IsValid(&u) == true {
		t.Errorf("got %v want %v",
			true, false)
	}

	u.ValidatedAt = time.Now()
	if userService.IsValid(&u) == false {
		t.Errorf("got %v want %v",
			false, true)
	}
}

总结

运用整洁架构,我们可以把数据库从 改为 ,而不会破坏应用程序的其余部分。我们可以在不降低质量和速度的前提下,发展我们的软件。