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

结构图

DDD在Gin中的工程实践;(有人看嘛?)欢迎留言讨论


领域拆分

进行领域划分前,先对我们的表结构进行分析进而找到合适的边界。

1.模型介绍

数据表:UserCategoryArticleCommentUserLike

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前,有很多基础概念需要掌握:领域、子域、核心域、通用域、支撑域、实体、值对象、聚合、聚合根、通用语言、限界上下文、事件风暴、领域事件、领域服务、应用服务、工厂、资源库。

参考链接领域驱动设计:从理论到实践,一文带你掌握DDD! - 掘金 (juejin.cn)

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值对象为用户昵称和用户头像。

聚合根

  1. 用户领域(User Domain)
    • 聚合根:User(用户)
  2. 主题领域(Topic Domain)
    • 聚合根:Topic(主题)
  3. 文章领域(Article Domain)
    • 聚合根:Article (文章)
  4. 评论领域(Comment Domain)
    • 聚合根:Article(文章)(可能以主题或文章为聚合根,评论作为实体)
  5. 点赞领域(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结构

Request请求结构

Response结构

Response响应结构
对应领域拆分结构

NotificationDomain 大体结构

思考

通过上面的三个截图,可以大体上有一个感知:这个核心域拥有多个通用域,而通用域下有多个支撑域。

本帖已被设为精华帖!
本帖由系统于 1年前 自动加精
讨论数量: 14

大家对这个层级划分有疑问或者看法的请写下你的评论。 :smirk_cat:

1年前 评论
goStruct

貌似这个开发模式,实际普遍用得很少。

1年前 评论
mengxin666 (楼主) 1年前

error 从底层一直抛到上层,想想就酸爽

1年前 评论
mengxin666 (楼主) 1年前

最关键的 domain 内部结构没有体现出来呀,业务如何,领域如何划分,边界如何确定,这些问题才是关键吧。 业务增加,复杂的也主要是 domain。其它层怎么来其实都还好。

1年前 评论
mengxin666 (楼主) 1年前

期待继续展开

1年前 评论
yourself

DDD需要一个领域专家可以掌控全局,用通用语言去做沟通,否则设计出来的东西弊大于利,再加上人员成本,别说小公司了一般的的大型公司项目都用不上。

1年前 评论
mengxin666 (楼主) 1年前
CR 9个月前
mengxin666 (楼主) 9个月前

看了 domain-driven-design-laravel 的书 也在最近的项目中用到了 用起来一言难尽

1年前 评论
mengxin666 (楼主) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!