最近在对之前的代码做重构,从之前的 MVC 结构切换到 Clean Arch 的结构,但是在切换的时候关于代码风格出现了一些困惑。
在下面的代码中 repository 是存储库,主要用于封装数据库查询或者是第三方微服务的调用,它实现了 domain.IAzRepository 接口,其他层的代码都只依赖这个接口而不依赖具体的实现
三种代码风格
风格一
在 Go 中我们常常“返回实现(struct),依赖接口”,其实就是在函数返回的时候我们返回一个具体的实现,函数的参数或者是 Struct 的成员部分我们依赖接口,这个风格看起来是违背了这个原则的
// repository 存储库
type repository struct {
db *gorm.DB
}
// NewAZRepository NewAZRepository
func NewAZRepository(db *gorm.DB) domain.IAzRepository {
return &repository{db: db}
}
风格二
这个风格返回了实现,并且由于并没有导出看起来也具有封装的特性,但是如果你运行 golint 你就会发现会抛出错误,因为这么写,会导致我们用导出的方法将没有导出 struct 给暴露了出去
// repository 存储库
type repository struct {
db *gorm.DB
}
// NewAZRepository NewAZRepository
func NewAZRepository(db *gorm.DB) *repository {
return &repository{db: db}
}
风格三
这个写法的主要问题是,由于 Repository 被导出,所以在外部其他的包中就可以直接通过 &Repository{} 进行初始化,这样初始化之后使用就会导致 panic,因为成员函数是一个 nil 指针
// Repository 存储库
type Repository struct {
db *gorm.DB
}
// NewAZRepository NewAZRepository
func NewAZRepository(db *gorm.DB) *Repository {
return &Repository{db: db}
}
选择
选择总是困难的,带着这个问题我咨询了同组的同事还有好几个 Go 语言交流群的同学,其中大部分都会选择风格三,小部分会选择风格一,风格二几乎没有人选择。最后我选什么呢?
最后我的选择是风格一,这是针对场景来的,因为我们的这个包其实不希望其他包直接依赖实现,因为后续有可能随着发展被单独拆分成一个微服务或者是需要更换存储库,如果外部有包直接依赖 repository 会导致后续的重构比较困难
除此之外,我们在其他地方一般还是会选择风格三,因为结构体名不导出,外部其实没有比较好的办法进行初始化,例如想要 var r Repository ,至于前面提到的直接字面量初始化的问题,我们可以通过统一代码风格解决。
在 外部包 中除了用于参数传递的 Option 结构之外,其余的不允许直接通过 &XXX{} 的方式进行初始化