尽量从一个模版开始,比如这个,关于每个文件夹应该如何组织可以参考 Readme 或者 这篇文章, 几个大原则:
现代软件往往是复杂而多变的,对于一个大型系统,一个最为显而易见且直接的办法是对这个大型系统进行拆分,一种最常见的拆分方式是:
分层有很多好处,以 TCP/IP 为例:
SOLID 原则尽管大多数时候用来描述对象设计的原则,但是其实也可以看成是解决分层、分模块之后彼此之间如何依赖和协作的一些参考原则,比如:
分层的方式多种多样,《企业应用架构模式》中主要采用这样的分层结构:
类似但是稍有不同的还有 其他划分办法 (Marinescu/Brown 等5层结构,Nilson 7层结构等),但是如同 tcp/ip 的7层/5层划分,尽管多层次让系统变得更为清晰,但是同时也带来了更多复杂度:
这里参考 阿里Java 中提出的分层模式设计了一套 Golang 适用的分层结构
这里的分层和上面讲的 3 层结构基本一致,只是由 Manager 层提供更多分层的灵活度。
另外和分层相关的一个重要问题是数据对象应该在各个层次如何使用。
在阿里巴巴编码规约中列举了下面几个领域模型规约:
一般来说,结构的分层对应了数据的分层使用。而实际上,太多的数据分层带来各个层次间数据转换的复杂度。一般来说,我们的项目中使用两层数据模型就可以了,即:
这里所指的依赖指的是各个层次、模块之间的依赖关系
依赖倒置原则是大原则,直接描述了各个层次或者模型之间的依赖指导原则,在这种原则之下,一个高层次不应该依赖低层次的实现,而是依赖一套接口
,依赖接口有很多好处
依赖对象新建一般来说常见有几种方法
依赖注入的研发模式在 JAVA 工程中应用非常广泛,而在 Go 项目中尚为普及,比如facebook inject、uber dig、google wire,这些框架使用起来并没有特别方便,这是和 golang 的动态
能力不够有关系。大部分的依赖注入框架使用 tag 标记或者代码生成的方式进行处理,往往并不像 JAVA 中那么 自动化
。
大部分时候我们退而求其次,使用(抽象)工厂模式解决对象依赖新建的问题。
举个例子,在 k8s 的代码中 (抽象)工厂模式使用非常常见,比如我们熟悉的 SharedInformerFactory
就是一个抽象工厂
//k8s.io/kubernetes/pkg/client/informers/informers_generated/internalversion/factory.go
func NewSharedInformerFactory(client internalclientset.Interface, defaultResync time.Duration) SharedInformerFactory {
return &sharedInformerFactory{
client: client,
defaultResync: defaultResync,
informers: make(map[reflect.Type]cache.SharedIndexInformer),
startedInformers: make(map[reflect.Type]bool),
}
}
// SharedInformerFactory provides shared informers for resources in all known
// API group versions.
type SharedInformerFactory interface {
internalinterfaces.SharedInformerFactory
ForResource(resource schema.GroupVersionResource) (GenericInformer, error)
WaitForCacheSync(stopCh <-chan struct{}) map[reflect.Type]bool
Admissionregistration() admissionregistration.Interface
Apps() apps.Interface
Autoscaling() autoscaling.Interface
Batch() batch.Interface
Certificates() certificates.Interface
Core() core.Interface
Extensions() extensions.Interface
Networking() networking.Interface
Policy() policy.Interface
Rbac() rbac.Interface
Scheduling() scheduling.Interface
Settings() settings.Interface
Storage() storage.Interface
}
// sharedInformerFactory 是具体的stuct
func (f *sharedInformerFactory) Apps() apps.Interface {
return apps.New(f)
}
我们也可以参考依赖注入的实现方式,在 Golang 中实现一套手动注入的抽象工厂,把依赖对象的创建逻辑集中到一处,比如下面的这个例子,本质也是一套抽象工厂
var st store.Interface
var storeOnce sync.Once
func ProvideStore() storeInterface {
storeOnce.Do(func() {
st = NewStore(config.DbUri)
})
return st
}
func ProvideRemoteAClient() remoteAInterface {
return NewRemoteAClient(config.remoteAUri)
}
func ProvideServiceA() serviceAInterface {
return NewServiceA(ProvideStore())
}
func ProvideServiceB() serviceBInterface {
return NewServiceB(ProvideStore(), ProvideRemoteAClient())
}
除了输入数据,context 在各层之间的数据传输和控制也很有用:
举个例子,当我们在表现层收到 Http 请求,在请求头有提取出了 RequestId 用于追踪请求,我们可以讲 RequestId 存于 Context 中,附加到所有相关链路的日志内容中,这样,关于这个请求在表现层、服务层、Manger层等的日志数据都可以得到很好的追踪.
除了正常的数据输出之外,Go程序习惯使用 error 对象来返回错误信息。这样 error 本身也就成为了一种 DO 或者 DTO,error 对象应该在每层如何表现?如果 A 层调用B层发生错误,A层怎么知道错误发生在 B 层,还是B层下面的 C层,D层呢。
一种推荐的处理方式和 DO,DTO 的处理方式类似,我们可以定义为 DEO,DTEO
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。