一次 Golang 优化编程思考
背景
刚开始使用 Golang 进项目开发时,因为在开发时对一些理解没有足够的透彻,导致整个项目代码出现过多的冗余以及扩展性比较差。本文主要陈述在使用的Golang 时的一些结构设计思考的记录。
初始设计
刚开始构建项目框架时,主要讲整个结构划分了四层,详细可查看之前记录的文章:Golang 项目目录结构设计思考 - 掘金
-- handler (其实就是 controller)
-- service (核心:主要处理业务路基)
-- repository (提供和数据库操作基本方法 CRUD 方法)
-- entities(与数据库表对应)
在 controller 层中主要处理参数相关内容不进行任何核心业务的处理,在 service 主要处理核心业务逻辑功能,在 repository 层中没有业务流程主要和数据库交互完成基本的 CRUD 功能。但是在最开始使用的时候,没有充分利用 golang 接口的特性,基本就是方法集合堆积。
基于这样的四层设计其实和 Java 的有点像,各层级功能明确,以便在后续方便扩展。一个用户请求时从路由到 controller 接收参数,然后到 service 执行真正的业务逻辑,再与 DB 进行数据库完成 CRUD 的工作。
func List() (list []entities.User){
// to do something
return list
}
func FindById() (user entities.User) {
// to do something
return user
}
使用接口
Golang 的接口提供了一种约定,定义了对象应该具备的行为,而不关心对象的具体类型,通过使用接口就可以实现更易于重构和维护的代码结构。
type IUserRepo interface {
List() (list []entities.User)
FindById() (user entities.User)
}
type UserRepo struct {
Db *gorm.DB
}
func (u *UserRepo) List() (list []entities.User) {
// to do something
db := u.Db.Model(&entities.User{})
if err := db.Find("").Error; err !=nil {
println("err")
}
return list
}
func (u *UserRepo) FindById() (user entities.User) {
// to do something
return user
}
但是接口是针对一系列实体有相同行为时,通过接口约定相同的行为,然后让不同的实体类型来实现这个接口。其灵活性和可扩展性就是在这点上体现,所以接口的使用场景包括但不限于以下情况:
- 定义插件系统:通过定义接口,可以让不同的插件实现相同的接口,以便在程序运行时动态加载和使用插件功能。比如:一个系统可能要接mysql 、orl数据库
- 实现依赖注入:通过接口,可以定义组件的行为,并在其他组件中注入具体的实现。这样可以实现松耦合的组件之间的交互。
- 编写可测试的代码:通过依赖注入和接口的使用,可以轻松地为单元测试提供虚拟的实现,以便更容易编写和执行测试。
各模块行为不一致,怎么办?
如果各个模块的行为完全不一致,使用接口可能会显得过于累赘,并且不符合设计原则。接口的设计应该基于模块之间的共享行为和约定。那么对于各模块行为都不一致的,可能需要使用不同设计模式:
- 继承和多态:使用继承和多态的概念,可以针对不同的模块创建不同的类或对象,并根据其特定的行为来调用相应的方法。这样可以避免强制使用相同的接口。
- 策略模式:使用策略模式可以为不同的模块定义不同的策略类,每个策略类实现特定的行为。然后在运行时选择适当的策略来执行相应的行为。
- 条件判断:如果只有少数几个模块具有不同的行为,可以使用条件判断来处理。根据条件的不同,执行相应的逻辑。
// 比如 NoBehavorRepo 可能没有一些行为,那么通过将IUserRepo作为实体的一部分,然后在执行的使用通过条件判断是否
type NoBehavorRepo struct {
userRepo IUserRepo
}
func (bh NoBehavorRepo) DoListAction() {
if bh.userRepo != nil {
bh.userRepo.List()
} else {
fmt.Println("没有指定行为")
}
}
使用 New 函数
进一步优化,使用 New 函数封装实体。这种方式可以将实例化对象的细节封装起来,这样调用者就无需了解对象创建过程。后续在对象创建修改和扩展,调用者也无需关心,降低来代码的耦合性等等还有很多的好处。
type userRepo struct {
Db *gorm.DB
}
func NewUserRepo(db *gorm.DB) IUserRepo {
return &userRepo{
Db: db,
}
}
使用 New 函数隐藏了userRepo对象的实现细节,而且如果需要增加 userRepo 属性时,上层调用者都无需关注。这种方式不仅提高代码的封装性和抽象性,而且让系统更容易进行扩展和维护。这也是一种常见的对象创建模式。
使用依赖注入
参数注入
使用New函数后在,接下来就可以在每层之间就可以使用依赖注入方式。通过注入所需要的依赖项完成相关功能。从而使得代码和具体的实现解耦。比如 service 层需要 注入 repo 层的实体。
// service 层定义了结构体,并将依赖的repo作为其
type UserService struct {
userRepo repository.IUserRepo
roleRepo repository.IRoleRepo
}
func NewUserService(userRepo UserRepo, roleRepo RoleRepo) *UserService {
return &UserService{
userRepo,
roleRepo,
}
}
New函数手动实例化
如果依赖项很多的话,就会导致 NewUserService 接收到的参数很多。所以我使用在 NewUserService 函数中手动实例化依赖项。这种会增加代码的复杂性。
func NewUserService2() *UserService {
db := initDB()
userRepo := repository.NewUserRepo(db)
roleRepo := repository.NewRoleRepo(db)
return &UserService{
userRepo,
roleRepo,
}
}
Options 模式
所以可以考虑使用 Options 模式进一步优化。Options 模式通过使用可选参数对象来传递参数,使函数调用更具可读性并提供了更多灵活性。
// 定义一个 Options 结构体,该结构体包含函数的可选参数:
type UserServiceOptions struct {
UserRepo UserRepo
RoleRepo RoleRepo
}
type UserServiceOption func(*UserServiceOptions)
// 在 NewUserService 函数中接受可选参数,并将其应用于 UserService 结构体:
func NewUserService(options ...UserServiceOption) *UserService {
// 默认参数
opts := &UserServiceOptions{
UserRepo: nil,
RoleRepo: nil,
}
// 应用可选参数
for _, opt := range options {
opt(opts)
}
// 创建 UserService 并设置依赖项
userService := &UserService{
userRepo: opts.UserRepo,
roleRepo: opts.RoleRepo,
}
return userService
}
// 定义一些可选参数函数,用于设置 Options 结构体中的字段:
func WithUserRepo(userRepo UserRepo) UserServiceOption {
return func(opts *UserServiceOptions) {
opts.UserRepo = userRepo
}
}
func WithRoleRepo(roleRepo RoleRepo) UserServiceOption {
return func(opts *UserServiceOptions) {
opts.RoleRepo = roleRepo
}
}
// 具体使用时就可以选择性传递参数
userService := NewUserService(
WithUserRepo(userRepo),
WithRoleRepo(roleRepo),
)
记录一切努力而又美好的经历, 我是小雄Ya!!!
转载自:https://juejin.cn/post/7245838232012587068