项目只实现了简单的个人的登录注册修改功能
目录结构
想参考github.com/golang-standards/project-layout,但是没整明白。在解耦的时候模拟了java-springboot的方案。
项目根目录
├── cmd // 放指令
│ └── main.go
├── configs // 配置信息和配置文件
│ ├── app_dev.yaml
│ ├── config.go
│ └── db.go
├── db // 放数据库的迁移文件,但是我没用到所以这里直接放了表的创建语句
│ ├── manager.sql
│ └── session.sql
├── go.mod
├── go.sum
├── internal // 放逻辑代码,一般不被其他包引用
│ ├── global // 放全局变量
│ │ └── global.go
│ ├── handlers // controller层
│ │ ├── main_test.go
│ │ ├── manager_handler.go
│ │ ├── server.go
│ │ └── token_handler.go
│ ├── middleware // 中间件
│ │ ├── authenticate.go
│ │ └── logger.go
│ ├── models // 实体
│ │ ├── manager.go
│ │ └── session.go
│ ├── repositories // 数据库层
│ │ ├── manager.go
│ │ ├── session.go
│ │ └── transaction.go
│ ├── services // 逻辑层
│ │ ├── manager_service.go
│ │ └── manager_service_test.go
│ └── utils // 工具类
│ ├── logger.go
│ ├── password.go
│ ├── token.go
│ └── token_test.go
├── makefile
├── mock // mock生成文件
│ ├── manager_mock.go
│ ├── session_mock.go
│ └── transaction_mock.go
├── router // 路由配置文件
│ └── router.go
解耦
项目结构
首先在写一个接口的时候,如果把所有代码都放在一个函数里面虽然说没有什么互相依赖的问题,但是这样做代码会很长,不好维护,其次如果切换了框架,那么所有代码有需要重写一遍。
为了解决这个问题,就需要将一个接口的实现分成:models、repositories、services、handlers。
models存在实体以及接口的接受体和相应体,repositories存放关于数据库表的操作代码(对应java的mapper),services存放接口的逻辑处理代码并调用repositoies与数据库交互,handlers负责接收返回请求并调用services代码完成请求处理(对应java-controller)。
models独立出来非常有必要,这会避免包跟包之间的互相依赖问题。(dto表示解析后的请求体,vo表示请求响应体)在我还没有独立出models时,我直接将对应的models放在handlers中,当handlers解析完请求体后需要将解析后的dto直接传给services处理,那么services就要依赖于handlers,这就导致包跟包之间的互相依赖问题。那为什么handlers解析处dto后将dto的值逐项传给services呢?这样确实可以避免这个问题,但是这样子做属实不优雅,一旦业务复杂一些,函数的参数就会很多,看着有点恶心。如果独立出models那么上面这两个问题就可以完美解决。
那为什么要分成handlers、services、repositories呢?当程序收到一个请求时,首先要解析请求体,然后将请求信息进行逻辑处理,处理好后中间要进行数据库的操作,当逻辑处理完毕后,再将结果写入响应体返回给客户端。这里我们就可以讲这个过程分成三部分,handlers(解析请求体、将结果写入响应体),services(逻辑处理),repositoires(封装数据库操作)。handlers调用services,services调用repositoires,这样逻辑清晰简单。如果repositories不独立出来是当业务负责时,要进行多表的操作,同样的数据库操作代码必将在service反复去写,麻烦且不优雅,代码没得复用。
抽象接口
只是修改项目结构还是不能够完全解决代码耦合性强的问题。下面是几个例子:
场景1:原本项目是使用gorm框架操作数据库,现在觉得gorm框架不符合项目的需求,要换成golang的标准库。问题:如果说在没有将数据库操作抽象出接口时,那么要解决上面这个场景的需求,就不仅要修改repositories中的代码,还要修改services的数据库相关逻辑,因为当你切换框架时,可能存在因为框架不同导致入参出参不同的问题。这一看就会发现代码耦合性还是比较强的,不易修改。
//manager_service.go
type ManagerService struct {
managerRepository repositories.ManagerRepositoryInterface
}
func (service ManagerService) RegistryManager(ctx *gin.Context, managerDTO models.ManagerRegistryDTO) (models.ManagerBaseVO, error) {
tx := service.transaction.Begin()
hashedPassword, err := utils.HashedPassword(managerDTO.Password)
if err != nil {
service.transaction.Rollback(tx)
return models.ManagerBaseVO{}, fmt.Errorf("encode password failed: %w", err)
}
manager := models.Manager{
Name: managerDTO.Name,
Account: managerDTO.Account,
Password: hashedPassword,
Role: CommonManagerCode,
}
err = service.managerRepository.TxCreateManager(tx, &manager)
if err != nil {
service.transaction.Rollback(tx)
return models.ManagerBaseVO{}, fmt.Errorf("create manager failed: %w", err)
}
session := &models.Session{
SessionID: sessionID,
UserID: manager.ID,
IP: ctx.ClientIP(),
UserAgent: ctx.Request.UserAgent(),
ExpiresAt: time.Now().Add(global.Config.Server.SessionDuration),
}
err = service.sessionRepository.TxCreateSession(tx, session)
if err != nil {
service.transaction.Rollback(tx)
return models.ManagerBaseVO{}, fmt.Errorf("create session failed: %w", err)
}
service.transaction.Commit(tx)
return convertManagerToVO(manager), nil
}
//manager_repository.go
type GormManagerRepository struct {
db *gorm.DB
}
func (g *GormManagerRepository) GetManagerByAccount(account string, manager *models.Manager) error {
return g.db.Where("account = ?", account).First(&manager).Error
}
比如这里要切换为golang的标准库,就要重新定义所有Repository对象,并且要手动去修改services较多的代码。
解决方案
为了解决这个问题,我们可以将通用的函数抽象成接口,让services层代码不在乎repositories如何实现。这里以上面的代码给出解决方案
//manager_service.go
type ManagerService struct {
managerRepository repositories.ManagerRepositoryInterface
}
func (service ManagerService) RegistryManager(ctx *gin.Context, managerDTO models.ManagerRegistryDTO) (models.ManagerBaseVO, error) {
tx := service.transaction.Begin()
hashedPassword, err := utils.HashedPassword(managerDTO.Password)
if err != nil {
service.transaction.Rollback(tx)
return models.ManagerBaseVO{}, fmt.Errorf("encode password failed: %w", err)
}
manager := models.Manager{
Name: managerDTO.Name,
Account: managerDTO.Account,
Password: hashedPassword,
Role: CommonManagerCode,
}
err = service.managerRepository.TxCreateManager(tx, &manager)
if err != nil {
service.transaction.Rollback(tx)
return models.ManagerBaseVO{}, fmt.Errorf("create manager failed: %w", err)
}
session := &models.Session{
SessionID: sessionID,
UserID: manager.ID,
IP: ctx.ClientIP(),
UserAgent: ctx.Request.UserAgent(),
ExpiresAt: time.Now().Add(global.Config.Server.SessionDuration),
}
err = service.sessionRepository.TxCreateSession(tx, session)
if err != nil {
service.transaction.Rollback(tx)
return models.ManagerBaseVO{}, fmt.Errorf("create session failed: %w", err)
}
service.transaction.Commit(tx)
return convertManagerToVO(manager), nil
}
//manager_repository.go
// 定义所有Manager接口
type ManagerRepositoryInterface interface {
GetManagerByAccount(string, *models.Manager) error
}
func NewManagerRepository(db *gorm.DB) *GormManagerRepository{
return &GormManagerRepository{db:db}
}
type GormManagerRepository struct {
db *gorm.DB
}
func (g *GormManagerRepository) GetManagerByAccount(account string, manager *models.Manager) error {
return g.db.Where("account = ?", account).First(&manager).Error
}
当我们将数据库操作封装成一个接口类型时,切换数据库框架不再需要修改services的代码,因为services根本就不在乎实现,只要框架能够实现对应的接口就可以了。
总结:
只要是重复使用的代码就独立成函数
只要是被不同包同时依赖的代码,就应该将其独立成包
使用接口和依赖注入的方式减少服务之间的耦合。这样可以灵活地替换某个模块或层的实现,而不需要修改其他地方。