Go语言基于 DDD(Domain Driven Design)领域驱动设计架构实现备忘录 todolist
写在前面
基于 DDD(Domain Driven Design)
领域驱动设计 架构实现todolist,在此之前也查阅了很多资料,似乎每个人都有自己所理解的DDD。
这里我也只说我自己理解的 DDD,如何和你所理解的有出入,那一定是你对,我理解是错的。
功能
代码都在github上:https://github.com/CocaineCong/todolist-ddd
非常简单的功能,主要是介绍DDD的整体架构,可以把 docs 下的 json 文件导入postman中,进行请求即可。
- 用户模块:注册、登陆
- 备忘录模块:创建、更新、列表、删除、详情
- 其他模块:日志打印、jwt鉴权、cors跨域、docker启动环境
架构
MVC
在讲DDD之间,我们要先简单回顾一下传统的MVC架构。相比于DDD,MVC的架构被更多人所熟知。MVC分别代表着:
- View:视图层,处理请求和响应
- Controller:控制所有的业务逻辑
- Model:具体的数据交互,也就是dao层
我平时写Go语言多一点,我项目的架构图一般如下,并不是传统意义上的MVC,但也是MVC的思想:
- Controller层只做三件事:
- 处理上游请求的参数,做参数校验之类的,过滤掉一些无用的请求
- 转发请求参数到service
- 获取service的响应,转发给上游
- Service:复杂的业务逻辑操作
- Model:就是真正的与数据打交道的一层。持久层
DDD
todolist的整体目录结构图如下:
./todolist-ddd
├── application // 应用层: 做domain编排
│ ├── task // task 应用层模块
│ └── user // user 应用层模块
├── cmd // 启动入口
├── conf // 配置文件
├── consts // 常量定义
├── docs // 接口文档
├── domain // 领域层:
│ ├── task // task 领域层模块
│ │ ├── entity // task 实体定义及充血对象
│ │ ├── repository // task 实体的数据持久化接口
│ │ └── service // task 具体业务逻辑
│ └── user // user 领域层模块
│ ├── entity // user 实体定义及充血对象
│ ├── repository // user 实体的数据持久化接口
│ └── service // user 具体业务逻辑
├── infrastructure // 基础架构层: 提供数据来源和基础服务能力
│ ├── auth // 鉴权认证服务
│ ├── common // 公共服务
│ │ ├── context // context 上下游管理
│ │ └── log // log 服务
│ ├── encrypt // 加密 服务
│ └── persistence // 持久层
│ ├── dbs // db数据连接
│ ├── task // task 的dao层 访问task数据库
│ └── user // user 的dao层 访问user数据库
├── interfaces // 接口层: 对接不同的端进行适配转化
│ ├── adapter // 适配器
│ │ └── initialize // Web 路由初始化
│ ├── controller // controller 层
│ ├── midddleware // 中间件
│ └── types // 类型
└── logs // 日志文件存储
整个DDD的大体分成了四层:
- interface 接口层: 对接不同的端进行适配转化成对应的函数输入到项目中。
- application 应用层: 做domain层的业务编排。
- domain 领域层: 纯业务逻辑,定义各种dao,entity充血模型。
- infrastructure 基础架构层: 提供数据来源和基础服务能力,相当于真正进行操作的dao层。
domain 领域层
这一层是做具体的业务逻辑。我们先重点说一下这一层,怎么划分领域我们不过多赘述,我们只说领域层里面应该是怎么样的。
首先领域层与领域层之间是不能互相调用的,domain自己是独立的一层。
其次domain和infra两者的关系看上去是domain依赖infra,因为domain需要infra的数据来源。但其实domain是不依赖infra的,而是infra依赖domain,这就是依赖倒置
。
比如会在user domain层中定义业务所需要的interface,包括创建user,查询用户等等,对应项目路径:domain/user/repository/user.go
type UserBase interface {CreateUser(ctx context.Context, user *entity.User) (*entity.User, error)GetUserByName(ctx context.Context, username string) (*entity.User, error)GetUserByID(ctx context.Context, id uint) (*entity.User, error)
}
在 user domain 中,就可以直接使用这些定义好的业务interface,比如对于项目的路径:domain/user/service/user.go
func (u *UserDomainImpl) GetUserDetail(ctx context.Context, id uint) (*entity.User, error) {return u.repo.GetUserByID(ctx, id)
}
这时候你可能会有疑惑,这不就是虚空interface吗?真正实现落库的在哪?不就是infra吗?
是的,真正实现的查询的其实是在infra里面,对应项目路径:infrastructure/persistence/user/repo.go
func (r *RepositoryImpl) GetUserByID(ctx context.Context, id uint) (*entity.User, error) {var u *Usererr := r.db.WithContext(ctx).Model(&User{}).Where("id = ?", id).Find(&u).Errorif err != nil {return nil, err}if u.ID == 0 {return nil, errors.New("user not found")}return PO2Entity(u), nil
}
所以这就引入了一个概念叫注入依赖
。我们的domain是这样进行创建的,domain/user/service/user.go
type UserDomainImpl struct {repo repository.Userencrypt repository.PwdEncrypt
}
func NewUserDomainImpl(repo repository.User, encrypt repository.PwdEncrypt) UserDomain {return &UserDomainImpl{repo: repo, encrypt: encrypt}
}
而依赖是在一开始创建的时候便已经注入进去了,项目的infrastructure/container/init.go
func LoadingDomain() {repos := persistence.NewRepositories(dbs.DB)jwtService := auth.NewJWTTokenService()pwdEncryptService := encrypt.NewPwdEncryptService()// user domainuserDomain := userSrv.NewUserDomainImpl(repos.User, pwdEncryptService)userApp.GetServiceImpl(userDomain, jwtService)...
}
所以我们可以看到Domain层只专注于业务逻辑,并且是依赖抽象接口,而不是具体实现
interface 接口层
这一层的作用主要和上游做交互:
- 接受上游传送的参数,并进行校验。
- 请求数据参数转发到application应用层。
- 数据参数转发给上游。
func UserLoginHandler() gin.HandlerFunc {return func(ctx *gin.Context) {var req types.UserReqerr := ctx.ShouldBind(&req)if err != nil {log.LogrusObj.Infoln(err)ctx.JSON(http.StatusOK, types.RespError(err, "bind req"))return}entity := types.UserReq2Entity(&req)resp, err := user.ServiceImplIns.Login(ctx, entity)if err != nil {ctx.JSON(http.StatusOK, types.RespError(err, "login failed"))return}ctx.JSON(http.StatusOK, types.RespSuccessWithData(resp))}
}
application 应用层
这一层的作用是对各种domain层的函数方法进行编排。
func (s *ServiceImpl) Login(ctx context.Context, entity *entity.User) (any, error) {user, err := s.ud.FindUserByName(ctx, entity.Username)if err != nil {return nil, err}// 检查密码err = s.ud.CheckUserPwd(ctx, user, entity.Password)if err != nil {return nil, errors.New("invalid password")}// 生成tokentoken, err := s.tokenService.GenerateToken(ctx, user.ID, user.Username)if err != nil {return nil, err}return LoginResponse(user, token), nil
}
infrastructure 基础架构层
这一层是各种外部数据来源服务或者功能服务
,比如db、redis、rpc或者各种jwt,加解密服务等等。
⚠️ 有几个很重要的概念:
- 充血模型:entity 需要增加对应的
业务逻辑方法
,而不只是单纯的数据载体。 - 依赖倒置:可能从MVC的角度,我们看起来是 domain 依赖 infrastructure,因为domain需要infra的数据源来做业务操作。但其实domain只是定义了interface,真正实现这些interface是从infra中实现,而我们一开始启动的时候就会将所有依赖都注入到domain中,所以是一个
依赖倒置的
关系。 - 注入依赖:我个人不太倾向这种注入依赖的方式。虽然说技术实现和语言无关,但对于go而言,我个人比较倾向
函数式编程
,而不是面向对象编程。