Go语言的简洁架构.docx
《Go语言的简洁架构.docx》由会员分享,可在线阅读,更多相关《Go语言的简洁架构.docx(21页珍藏版)》请在冰点文库上搜索。
![Go语言的简洁架构.docx](https://file1.bingdoc.com/fileroot1/2023-5/29/0e2b2b23-5dd4-43af-9612-42226187387c/0e2b2b23-5dd4-43af-9612-42226187387c1.gif)
Go语言的简洁架构
Go语言的简洁架构
我想告诉你的是什么
目前简洁架构已是众所周知。
然而,我们可能无法很好的知道具体实现的细节。
所以我尝试使用gRPC编写一个具有简洁架构意识的例子。
hatajoe/8am
在Github上创建了一个账户用于开发hatajoe/8am。
详见:
1
这个小型项目表示用户注册的例子,请随意回复任何内容。
项目结构
8am是基于简介架构的,这个项目的结构如下:
%tree
.
├──Makefile
├──README.md
├──app
│├──domain
││├──model
││├──repository
││└──service
│├──interface
││├──persistence
││└──rpc
│├──registry
│└──usecase
├──cmd
│└──8am
│└──main.go
└──vendor
├──vendorpackages
|...
顶层目录包含3个分支:
app:
应用包的根目录
cmd:
main包目录
vender:
几个依赖包目录
简洁架构有几个概念层如下:
简洁架构有4层,蓝色层、绿色层、红色层和黄色层,顺序如上所示,除了蓝色代表了app目录外,这些层分别代表了:
接口:
绿色层
用例:
红色层
域:
黄色层
关于简洁架构最重要的事就是编写访问层之间的接口。
实体——黄色层
IMO,实体层在结构层次中更像是domain层。
所以我命名这个层为app/domain是为了防止与DDD实体混淆。
app/domain有三个包:
模型:
包含聚合、实体和值对象
仓库:
聚合的仓库接口
服务:
包含依赖于各种模型的应用服务
我解释下对于每个包的执行细节:
模型
模型的用户聚合如下所示:
这里不是实际的聚合,前提是各种实体和值对象将在未来添加。
packagemodel
typeUserstruct{
idstring
emailstring
}
funcNewUser(id,emailstring)*User{
return&User{
id:
id,
email:
email,
}
}
func(u*User)GetID()string{
returnu.id
}
func(u*User)GetEmail()string{
returnu.email
}
聚合是事务为了保持他们业务规则的一致性的边界。
因此每个聚合会存在一个对应的仓库。
仓库
在仓库层,仓库只是一个接口,是因为仓库无需知道持久化实现的细节。
但持久化也是仓库层的重要本质。
用户聚合仓库的实现是:
packagerepository
import"
typeUserRepositoryinterface{
FindAll()([]*model.User,error)
FindByEmail(emailstring)(*model.User,error)
Save(*model.User)error
}
FindAll获取系统保留的所有用户,持久化保存到系统中。
我再说一遍,这一层不应该知道对象在何处保存或序列化。
服务
服务层用于收集业务逻辑,这些业务逻辑不包含在模型中。
例如,应用不允许注册存在的邮件地址。
如果模型具有此验证,我们会感觉到如下的一些错误:
func(u*User)Duplicated(emailstring)bool{
//Finduserbyemailfrompersistence
layer...
}
Duplicated函数与User模型不相关。
为了解决这个,我们可以像下面这样添加服务层:
typeUserServicestruct{
reporepository.UserRepository
}
func(s*UserService)Duplicated(emailstring)error{
user,err:
=s.repo.FindByEmail(email)
ifuser!
=nil{
returnfmt.Errorf("%salreadyexists",
email)
}
iferr!
=nil{
returnerr
}
returnnil
}
实体通过其它层包含业务逻辑和接口。
业务逻辑应该被包含在模型和服务层中,而不应该依赖其他层。
如果我们需要访问任何其他层,我们应该使用仓库接口。
通过这样的反向依赖,可以使包独立,获得更好的测试和维护。
用例-红色层
用例是应用的一次操作单元。
在8am中,用户列表和用户注册均被定义为用例。
这些用例被如下的接口所代表:
typeUserUsecaseinterface{
ListUser()([]*User,error)
RegisterUser(emailstring)error
}
为什么是接口?
这是因为用例被使用于接口层——绿色层。
如果要在层之间进行访问,我们应该始终定义接口来实现。
UserUsecase的实现很简单,如下:
typeuserUsecasestruct{
reporepository.UserRepository
service*service.UserService
}
funcNewUserUsecase(reporepository.UserRepository,
service*service.UserService)*userUsecase{
return&userUsecase{
repo:
repo,
service:
service,
}
}
func(u*userUsecase)ListUser()([]*User,error){
users,err:
=u.repo.FindAll()
iferr!
=nil{
returnnil,err
}
returntoUser(users),nil
}
func(u*userUsecase)RegisterUser(emailstring)error
{
uid,err:
=uuid.NewRandom()
iferr!
=nil{
returnerr
}
iferr:
=u.service.Duplicated(email);err!
=nil
{
returnerr
}
user:
=model.NewUser(uid.String(),email)
iferr:
=u.repo.Save(user);err!
=nil{
returnerr
}
returnnil
}
userUsercase依赖于两个包,接口repository.UserRepository和结构体*service.UserService。
当使用用例的用户,初始化用例时必须引用这两个包。
这些独立性通常通过DI容器解决,这将写在后续的条目中。
ListUser用例获取所有注册的用户,如果用户没有被相同的email地址注册时,用RegisterUser将此用户注册到系统中。
一个要点,User不是model.User。
model.User可能有很多种业务知识,但是其他层无法知道这些。
所以我为用例的用户定义DAO来概括这些知识。
typeUserstruct{
IDstring
Emailstring
}
functoUser(users[]*model.User)[]*User{
res:
=make([]*User,len(users))
fori,user:
=rangeusers{
res[i]=&User{
ID:
user.GetID(),
Email:
user.GetEmail(),
}
}
returnres
}
所以,为什么你认为服务用作具体的实现而不是使用接口?
这是因为服务不依赖于其他层。
相反的,仓库在各层间访问,依赖于服务的细节不被其他层所知道而实现的,所以仓库被定义为接口。
我认为这在架构中是最重要的事情。
接口——绿色层
这一层体现的是具体的对象,如API端点处理程序、RDB的仓库或其他边界的接口。
在这种情况下,我添加了2个具体的对象,内存存储访问器和gRPC服务。
内存存储访问器
我添加了具体的用户仓库作为内存存储访问器。
typeuserRepositorystruct{
mu*sync.Mutex
usersmap[string]*User
}
funcNewUserRepository()*userRepository{
return&userRepository{
mu:
&sync.Mutex{},
users:
map[string]*User{},
}
}
func(r*userRepository)FindAll()([]*model.User,
error){
r.mu.Lock()
deferr.mu.Unlock()
users:
=make([]*model.User,len(r.users))
i:
=0
for_,user:
=ranger.users{
users[i]=model.NewUser(user.ID,user.Email)
i++
}
returnusers,nil
}
func(r*userRepository)FindByEmail(emailstring)
(*model.User,error){
r.mu.Lock()
deferr.mu.Unlock()
for_,user:
=ranger.users{
ifuser.Email==email{
returnmodel.NewUser(user.ID,user.Email),
nil
}
}
returnnil,nil
}
func(r*userRepository)Save(user*model.User)error
{
r.mu.Lock()
deferr.mu.Unlock()
r.users[user.GetID()]=&User{
ID:
user.GetID(),
Email:
user.GetEmail(),
}
returnnil
}
这是仓库的具体实现。
如果需要在RDB或其他中保存用户,则需要其他的实现方式。
但即使在这种情况下,我们不需要改变模型层。
模型层依赖于独立的仓库接口,而不关心实现细节。
这真惊人。
User被定义为仅在此包适用。
这也是为了解决拆分层之间的关系。
typeUserstruct{
IDstring
Emailstring
}
gRPC服务
我认为gRPC服务也包含在接口层内。
gRPC被定义在如下的app/interface/rpc目录中:
user_service.go是包装gRPC的端点处理程序:
typeuserServicestruct{
userUsecaseusecase.UserUsecase
}
funcNewUserService(userUsecaseusecase.UserUsecase)
*userService{
return&userService{
userUsecase:
userUsecase,
}
}
func(s*userService)ListUser(ctxcontext.Context,in
*protocol.ListUserRequestType)
(*protocol.ListUserResponseType,error){
users,err:
=s.userUsecase.ListUser()
iferr!
=nil{
returnnil,err
}
res:
=&protocol.ListUserResponseType{
Users:
toUser(users),
}
returnres,nil
}
func(s*userService)RegisterUser(ctx
context.Context,in*protocol.RegisterUserRequestType)
(*protocol.RegisterUserResponseType,error){
iferr:
=
s.userUsecase.RegisterUser(in.GetEmail());err!
=nil
{
return&protocol.RegisterUserResponseType{},
err
}
return&protocol.RegisterUserResponseType{},nil
}
functoUser(users[]*usecase.User)[]*protocol.User{
res:
=make([]*protocol.User,len(users))
fori,user:
=rangeusers{
res[i]=&protocol.User{
Id:
user.ID,
Email:
user.Email,
}
}
returnres
}
userService仅依赖于用例接口。
如果你想从其他层中(如CUI)使用用例,你可以实现你想要的接口。
v1.go用于解决使用DI容器的对象依赖关系:
funcApply(server*grpc.Server,ctn
*registry.Container){
protocol.RegisterUserServiceServer(server,
NewUserService(ctn.Resolve("user-usecase").
(usecase.UserUsecase)))
}
v1.go应用包被从*registry.Container到gRPC服务中检索。
最后,让我们看一下DI容器的实现。
注册表
注册表就是DI容器,用于解决对象间的依赖。
我曾使用
sarulabs/di
go语言(golang)中的依赖注入容器,在Github上创建一个账户用于开发sarulabs/di,详见:
1
typeContainerstruct{
ctndi.Container
}
funcNewContainer()(*Container,error){
builder,err:
=di.NewBuilder()
iferr!
=nil{
returnnil,err
}
iferr:
=builder.Add([]di.Def{
{
Name:
"user-usecase",
Build:
buildUserUsecase,
},
}...);err!
=nil{
returnnil,err
}
return&Container{
ctn:
builder.Build(),
},nil
}
func(c*Container)Resolve(namestring)interface{}{
returnc.ctn.Get(name)
}
func(c*Container)Clean()error{
returnc.ctn.Clean()
}
funcbuildUserUsecase(ctndi.Container)(interface{},
error){
repo:
=memory.NewUserRepository()
service:
=service.NewUserService(repo)
returnusecase.NewUserUsecase(repo,service),nil
}
上面的例子,我通过使用buildUserUsecase函数将字符串user-usecase与具体的用例实现相结合。
所以我们可以在一个注册表中替代任何用例的具体实现。
谢谢您阅读本文,如果您有任何建议和改善,欢迎反馈。
容器时代志愿者招募
如果你对技术懵懵懂懂,想要入门却不知从何下手;
如果你求知若渴,想要学习更多技术、思想;
如果你对于技术有着一种狂热的喜爱并且热爱开源,以其为信仰。
志愿者计划JOINUS
容器时代志愿编辑
志愿内容
公众号运营——比如晨读文章推荐、周推荐等;(特别欢迎在校大学生)
翻译——容器生态圈相关教程、文章、资讯等的翻译;
∙发表于:
2018-11-06
∙原文链接:
∙腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
∙如有侵权,请联系yunjia_community@删除。
使用Golang构建整洁架构
什么是整洁架构?
在《CleanArchitecture:
ACraftsman’sGuidetoSoftwareStructureandDesign》一书中,著名作家Robert“UncleBob”Martin提出了一种具有一些重要特性的体系结构,如框架、数据库和接口的可测试性和独立性。
整洁架构的约束条件是:
∙独立的框架。
该体系结构并不依赖于某些带有特性的软件库的存在。
这允许您使用这些框架作为工具,而不是将您的系统束缚在有限的约束中。
∙可测试的。
业务规则可以在没有UI、数据库、Web服务器或任何其他外部元素的情况下进行测试。
∙独立的UI。
UI可以很容易地更改,而不会改变系统的其他部分。
例如,可以用控制台UI替换WebUI,而不需要更改业务规则。
∙独立的数据库。
您可以将Oracle或SQLServer替换为Mongo、BigTable、CouchDB或其他数据库。
您的业务规则不绑定到数据库。
∙独立的任意外部代理。
事实上,你的业务规则根本就不用了解外部的构成。
了解更多请查看:
因此,基于这些约束,每一层都必须是独立的和可测试的。
从UncleBob的架构中,我们可以将代码分成4层:
∙实体:
封装企业范围的业务规则。
Go中的实体是一组数据结构和函数。
∙用例:
这个层中的软件包含应用程序特定的业务规则。
它封装并实现了系统的所有用例。
∙控制器:
该层中的软件是一组适配器,它将数据从最方便的用例和实体转换为最方便的外部代理,例如数据库或Web。
∙框架和驱动程序:
这个层通常由框架和工具(如数据库、Web框架等)组成。
使用Golang构建整洁架构
让我们以user包为例:
ls-lnpkg/user
-rw-r — r — 1501205078Feb1609:
58entity.go
-rw-r — r — 1501203747Feb1610:
03mongodb.go
-rw-r — r — 150120509Feb1609:
59repository.go
-rw-r — r — 1501202403Feb1610:
30service.go
在entity.go文件中,我们有自己的实体:
//UserdatatypeUserstruct{
IDentity.ID`json:
"id"bson:
"_id,omitempty"`
Picturestring`json:
"picture"bson:
"picture,omitempty"`
Emailstring`json:
"email"bson:
"email"`
Passwordstring`json:
"password"bson:
"password,omitempty"`
TypeType`json:
"type"bson:
"type"`
Company[]*Company`json:
"company"bson:
"company,omitempty"`
CreatedAttime.Time`json:
"created_at"bson:
"created_at"`
ValidatedAttime.Time`json:
"validated_at"bson:
"validated_at,omitempty"`}
在repository.go文件中我们定义存储库的接口,用于保存存储实体。
在这种情况下,存储库意味着UncleBob架构中的框架和驱动层。
它的内容是:
packageuser
import"
//Repositoryrepositoryinterface
typeRepositoryinterface{
Find(identity.ID)(*User,error)
FindByEmail(emailstring)(*User,error)
FindByChangePasswordHash(hashstring)(*User,error)
FindByValidationHash(hashstring)(*User,error)
FindAll()([]*User,error)
Update(user*User)error
Store(user*User)(entity.ID,error)
AddCompany(identity.ID,company*Company)error
AddInvite(userIDentity.ID,companyIDentity.ID)error}
该接口可以在任何类型的存储层中实现,如MongoDB、MySQL等。
在我们的例子中,我们使用MongoDB来实现,就像在mongodb.go中看到的那样:
packageuser
import(
"errors"
"os"
"
"g