译者:baiyutang
原文:https://eminetto.medium.com/clean-architecture-using-golang-b63587aa5e3f
什么是整洁架构
以《架构整洁之道》出名的作者罗伯特 “Bob 大叔” 马丁,提出了一种架构,包含几个非常重要的点,诸如:可测试性、架构独立性、数据库和接口。
整洁架构的约束条件:
架构独立性:框架不依赖于未来加载的软件库。
可测试性:业务规则可以被测试,并且不依赖 UI、数据库、Web 服务器或其他外部要素。
UI 独立:UI 可以轻易变化,但不应改变其他系统。一个 Web UI 可以被控制台 UI 代替,但不改变业务规则。
数据库独立:可以将 Oracle
或 SQL Server
,替换为 Mongo
、BigTable
、CouchDB
或其他。业务规则不应该被数据库限制。
外部代理商独立:事实上,业务规则根本不了解外部世界。
更多在 >> https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html
那么,根据这些限制条件,每层都应该独立和可测试。
按照 Bob 大叔的架构,我们可以把代码分为 4 层:
Entities
Use Cases
Controller
Framework & Driver
Golang 中的整洁架构
文件目录
让我们用 user
包,看一个简单的例子
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
文件 entity.go
中,是我们的实体:
//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
文件 repository.go
中,是我们定义的仓储层的接口,它是实体被保存的地方。在本例中,仓储层是 “Bob 大叔” 架构中的 Framework & Driver
,内容如下:
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
, MySQL
等等。在本案例中我们用 MongoDB 来实现,见 mongodb.go
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
文件 service.go
代表“Bob 大叔”定义的用例层。这个文件中有 Service
的接口和实现,Service
的接口如下:
//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
最后一层,在我们架构中的 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,我们能看到如何使用 Service
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)
}
}
复制代码
总结
运用整洁架构,我们可以把数据库从 MongoDB 改为 Neo4j,而不会破坏应用程序的其余部分。我们可以在不降低质量和速度的前提下,发展我们的软件。
评论