DDD在Gin中的工程实践;(有人看嘛?)欢迎留言讨论
DDD在Gin中的工程实践;(有人看嘛?)
如题:楼主使用DDD规范在Gin中进行开发,有朋友一起看一下吗?目前这个流程我也不算太熟悉,准备放出来让大家参考,如果我的设计思路有问题你也可以帮我指出我也愿意学习。
目前做了不少了,准备放Github。
目录结构划分
大概介绍一下我的层级划分:主要参考 G-V-A、大明老师的小微书。
---config//主配置文件 ---log//日志存放 log.txt systemLog.txt config.go ---global//全局对象:config|gorm|redis|log…… config.go ---initialize//初始化引导文件 init.go mysql.go redis.go router.go ---internal//内部包 ---api//Controller层 ---v1 ---domain//领域层 ---integration //集成测试 ---model//模型层 ---repository//存储层 ---cache ---dao ---router//router层:集成全部路由 ---service//service层 ---shared//共享层: ---crontask//定时任务 ---e//全局使用的错误规范 code.go error.go msg.go ---enum.go ---public.go value_object.go ---middle//中间件 ---script//脚本 ---utils//工具 .gitgnore go.mod main.go README.md
结构图
领域拆分
进行领域划分前,先对我们的表结构进行分析进而找到合适的边界。
1.模型介绍
数据表:User
、Category
、Article
、Comment
、UserLike
User
package model
type User struct {
ID uint64 `gorm:"primaryKey,autoIncrement"`
UserName string `gorm:"size:50"` // 账号
Password string `gorm:"size:100"` // 密码
NickName string `gorm:"size:30"` // 昵称
Email string `gorm:"size:100"` // 邮箱
Phone string `gorm:"size:11"` // 手机号
Avatar string // 头像
Comments []Comment `gorm:"foreignKey:UserID;references:ID;"` // User : Comment -> 1 : N
LikeArticles []UserLikeArticle `gorm:"foreignKey:UserID;references:ID;"` // User : LikeArt -> 1 : N
Ctime int64 // 创建时间,毫秒作为单位
Utime int64 // 更新时间,毫秒作为单位
}
Article
package model
// Article 直接对应到表结构
type Article struct {
ID uint64 `gorm:"primaryKey;autoIncrement;comment:帖子ID"`
Title string `gorm:"size:50;comment:帖子标题"`
Content string `gorm:"type:longtext;comment:帖子内容"`
CommentCount uint64 `gorm:"comment:评论总数"`
Status uint8 `gorm:"comment:帖子状态 0:保存、1:待审、2:审核通过、3:删除"`
UserID uint64 `gorm:"comment:作者ID"`
CategoryID uint64 `gorm:"comment:所属板块ID"`
NiceTopic uint8 `gorm:"comment:精选话题"`
BrowseCount uint64 `gorm:"comment:浏览量"`
ThumbsUP uint64 `gorm:"comment:点赞数"`
Comments []Comment `gorm:"foreignKey:ArticleID;references:ID;"` // Article : Comment -> 1:N
UserLikes []UserLikeArticle `gorm:"foreignKey:ArticleID;references:ID;"` // Article : Comment -> 1:N
Tags []Tag `gorm:"many2many:article_tag"` // Tag : Article -> N:N 暂时未使用
// 预加载模型
User User
Ctime int64 // 创建时间,毫秒作为单位
Utime int64 // 更新时间,毫秒作为单位
}
Category
package model
// Category 主题表
type Category struct {
ID uint64 `gorm:"primaryKey;autoIncrement;comment:板块ID"`
Name string `gorm:"size:30;comment:板块名称"`
Description string `gorm:"size:200;comment:板块描述"`
ArticleCount uint64 `gorm:"comment:板块文章数量"`
State uint8 `gorm:"comment:状态:0:禁用|1:启用"`
Articles []Article `gorm:"foreignKey:CategoryID;references:ID"` // Category : Article -> 1 : N
Ctime int64 // 创建时间,毫秒作为单位
Utime int64 // 更新时间,毫秒作为单位
}
Comment
package model
// Comment 评论表
type Comment struct {
ID uint64 `gorm:"primaryKey;autoIncrement;comment:评论ID"`
Content string `gorm:"type:longtext;comment:评论内容"`
UserID uint64 `gorm:"comment:评论用户ID"`
ArticleID uint64 `gorm:"comment:[外键]文章ID"`
ParentID uint64 `gorm:"index;not null;comment:父级评论ID"`
Floor uint32 `gorm:"index;not null;comment:评论楼层"`
State uint8 `gorm:"comment:该评论状态"`
// 预加载模型
User User // 评论所属的用户信息,通过预加载获取
Ctime int64 // 创建时间,毫秒作为单位
Utime int64 // 更新时间,毫秒作为单位
}
UserLikeArticle
package model
type UserLikeArticle struct {
ID uint64 `gorm:"primaryKey;autoIncrement;not null;comment:用户点赞表"`
UserID uint64 `gorm:"comment:点赞用户ID"`
ArticleID uint64 `gorm:"comment:点赞文章ID"`
LikeState bool `gorm:"comment:点赞状态# 0禁用,1启用"`
// 预加载模型
User User
Article Article
Ctime uint64 `gorm:"comment:创建时间"`
Utime uint64 `gorm:"comment:修改时间"`
}
2.概念认知
学习DDD前,有很多基础概念需要掌握:领域、子域、核心域、通用域、支撑域、实体、值对象、聚合、聚合根、通用语言、限界上下文、事件风暴、领域事件、领域服务、应用服务、工厂、资源库。
2.1寻找领域
因为本篇文章主要是一个小型论坛为题材的案例并没有复杂的使用和服务场景,所以这里我的主要目标是寻找
核心域
,同样我的理解是:领域是一类业务问题的集合,也是我们始终需要围绕解决问题的中心。领域划分的前提取决于需求。
用户领域(User Domain)
用户领域:包含了用户相关的所有业务逻辑,包括用户的注册、登录、认证、权限管理等。用户表归属于这个领域。
主题领域(Category Domain)
主题领域:处理论坛主题相关的业务逻辑,包括主题的创建、编辑、删除等。主题表归属于这个领域
文章领域(Article Domain)
文章领域:负责管理用户发表的文章,包括文章的保存、编辑、发布、删除等。文章表归属于这个领域。
评论领域(Comment Domain)
评论领域:这个领域管理用户对文章的评论,包括评论的添加、删除等。评论表归属于这个领域。
点赞领域(Like Domain)
点赞领域:这个领域管理用户对文章的点赞操作,包括点赞的添加、取消等。点赞表归属于这个领域。
2.2实体和值对象
- 实体 = 唯一身份标识 + 可变性【状态 + 行为】
- 值对象 = 将一个值用对象的方式进行表述,来表达一个具体的固定不变的概念。
实体列表
在领域驱动设计(DDD)中,实体(Entities)是一种具有标识的领域对象,它们具有生命周期、状态和行为。实体是领域中的核心概念,用于表示业务概念和业务规则。
- User Domain -> User Model(用户实体)
- Category Domain -> Category Model(主题实体)
- Article Domain -> Article Model(文章实体)
- Comment Domain -> Comment Model(评论实体)
- Like Domain -> UserLike Model(点赞实体)
个人理解:所谓实体就是数据模型在各自所对应的领域中保持了唯一身份(唯一ID)不论业务如何变化它们都具有唯一性。
值对象列表
在领域驱动设计(DDD)中,值对象是一种没有独立标识的对象,它的相等性是根据其属性值来确定的。值对象通常用于表示实体的特定属性或行为,而不具有自己的生命周期。
- User Model(昵称、头像)
- Category Model(主题名称、主题描述)
- Article Model(文章标题、文章内容)
- Comment Model(评论文本、评论作者)
- UserLike Model(点赞状态)
2.3聚合和聚合根
聚合
聚合:我们把一些关联性极强、生命周期一致的实体、值对象放到一个聚合里。聚合是领域对象的显式分组,旨在支持领域模型的行为和不变性,同时充当一致性和事务性边界。
例如Article Domain: 实体为Article值对象为用户昵称和用户头像。
聚合根
- 用户领域(User Domain):
- 聚合根:User(用户)
- 主题领域(Topic Domain):
- 聚合根:Topic(主题)
- 文章领域(Article Domain):
- 聚合根:Article (文章)
- 评论领域(Comment Domain):
- 聚合根:Article(文章)(可能以主题或文章为聚合根,评论作为实体)
- 点赞领域(Like Domain):
- 聚合根:Article(文章)(根据点赞的具体对象)
2.4领域服务
当一些逻辑不属于某个实体时,可以把这些逻辑单独拿出来放到领域服务中
- 执行一个显著的业务操作
- 对领域对象进行转换
- 以多个领域对象作为输入参数进行计算,结果产生一个值对象
例如:用户鉴权服务、文章敏感词检测、评论敏感词检测……
2.5领域事件
领域事件 = 事件发布 + 事件存储 + 事件分发 + 事件处理。
事件发布:构建一个事件,需要唯一标识,然后发布;
事件存储:发布事件前需要存储,因为接收后的事建也会存储,可用于重试或对账等;
事件分发:服务内直接发布给订阅者,服务外需要借助消息中间件,比如Kafka,RabbitMQ等;
事件处理:先将事件存储,然后再处理。
例如:高并发评论模块,先把本次评论信息存储Redis,再通过RabbitMQ发布消息、并通过事前准备好的Consumer进行消费再把消费信息存入MySQL。
3.领域划分
3.1用户领域
package domain
import "time"
type User struct {
ID uint64
UserName string // 账号
Password string // 密码
NickName string // 昵称
Email string // 邮箱
Phone string // 手机号
Avatar string // 头像
Token string
Ctime time.Time
Utime time.Time
}
3.2主题领域
package domain
type Category struct {
ID uint64
Name string
Description string
ArticleCount uint64
State bool
Articles []Article
Ctime int64
Utime int64
}
3.3文章领域
package domain
import "time"
type Article struct {
ID uint64 // 帖子ID
Title string // 帖子标题
Content string // 帖子内容
CommentCount uint64 // 评论数量
Status uint8 // 帖子状态
Author uint64 // 作者
CategoryID uint64 // 所属板块
NiceTopic uint8 // 精选话题
BrowseCount uint64 // 浏览量
ThumbsUP uint64 // 点赞数
User User
Comments []Comment
UserLikes []UserLikeArticle
Ctime time.Time
Utime time.Time
}
3.4评论领域
package domain
type Comment struct {
ID uint64 // 评论ID
Content string // 评论内容
UserID uint64 // 评论用户ID
ArticleID uint64 // 文章ID
ParentID uint64 // 父级评论ID
Floor uint32 // 评论楼层
State uint8 // 该评论状态 0:正常,1:删除
Ctime int64 // 创建时间,毫秒作为单位
Utime int64 // 更新时间,毫秒作为单位
}
3.5点赞领域
package domain
type UserLikeArticle struct {
ID uint64
UserID uint64
ArticleID uint64
LikeState bool
Ctime uint64
Utime uint64
}
案例参考
凭空对业务需求进行抽象比较困难,也容易出现问题,所以需要寻找借鉴来源,这里我寻找的目标参考有B站的设计。
B站的消息通知领域结构
Request结构
Response结构
对应领域拆分结构
思考
通过上面的三个截图,可以大体上有一个感知:这个核心域拥有多个通用域,而通用域下有多个支撑域。
大家对这个层级划分有疑问或者看法的请写下你的评论。 :smirk_cat:
貌似这个开发模式,实际普遍用得很少。
error 从底层一直抛到上层,想想就酸爽
最关键的 domain 内部结构没有体现出来呀,业务如何,领域如何划分,边界如何确定,这些问题才是关键吧。 业务增加,复杂的也主要是 domain。其它层怎么来其实都还好。
期待继续展开
DDD需要一个领域专家可以掌控全局,用通用语言去做沟通,否则设计出来的东西弊大于利,再加上人员成本,别说小公司了一般的的大型公司项目都用不上。
看了 domain-driven-design-laravel 的书 也在最近的项目中用到了 用起来一言难尽