写在前面

基于 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 *User
	err := r.db.WithContext(ctx).Model(&User{}).Where("id = ?", id).Find(&u).Error
	if 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.User
	encrypt 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 domain
	userDomain := userSrv.NewUserDomainImpl(repos.User, pwdEncryptService)
	userApp.GetServiceImpl(userDomain, jwtService)
	...
}

所以我们可以看到Domain层只专注于业务逻辑,并且是依赖抽象接口,而不是具体实现

interface 接口层

这一层的作用主要和上游做交互:

  1. 接受上游传送的参数,并进行校验。
  2. 请求数据参数转发到application应用层。
  3. 数据参数转发给上游。
func UserLoginHandler() gin.HandlerFunc {
	return func(ctx *gin.Context) {
		var req types.UserReq
		err := 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")
	}
	// 生成token
	token, 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,加解密服务等等。

在这里插入图片描述

⚠️ 有几个很重要的概念:

  1. 充血模型:entity 需要增加对应的业务逻辑方法,而不只是单纯的数据载体。
  2. 依赖倒置:可能从MVC的角度,我们看起来是 domain 依赖 infrastructure,因为domain需要infra的数据源来做业务操作。但其实domain只是定义了interface,真正实现这些interface是从infra中实现,而我们一开始启动的时候就会将所有依赖都注入到domain中,所以是一个依赖倒置的关系。
  3. 注入依赖:我个人不太倾向这种注入依赖的方式。虽然说技术实现和语言无关,但对于go而言,我个人比较倾向函数式编程,而不是面向对象编程。
Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐