Skip to content

项目复盘:Gin 框架下的领域驱动设计(DDD)

1. 项目背景:我们是否在滥用 Gin?

Gin 是一个性能卓越、API 简洁的 Go Web 框架,深受开发者喜爱。在项目初期,我们通常会采用经典的 MVC 模式快速开发。一个典型的 Handler (等同于 Controller) 可能是这样的:

go
func CreateUser(c *gin.Context) {
    // 1. 解析和校验请求
    var req CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // 2. 复杂的业务逻辑
    // 检查用户名是否重复、密码是否符合强度要求、生成默认头像...
    // ... 大量业务代码 ...

    // 3. 操作数据库
    user := &models.User{Username: req.Username, Password: hash(req.Password)}
    if err := db.Create(user).Error; err != nil {
        c.JSON(http.StatusInternalServerError, gin.H{"error": "数据库错误"})
        return
    }

    // 4. 返回响应
    c.JSON(http.StatusOK, gin.H{"message": "用户创建成功"})
}

随着业务变得复杂,这种模式的弊病逐渐显现:

  • 臃肿的 ControllerHandler 中混杂了协议解析、业务校验、数据库操作等多种职责,变得越来越难以维护。
  • 贫血的领域模型models.User 结构体通常只包含字段,没有任何业务行为,沦为单纯的数据载体。
  • 测试困难:要测试 CreateUser,我们必须启动一个完整的 Gin 引擎,模拟一个 HTTP 请求,并连接到真实的数据库。这使得单元测试变得异常困难和缓慢。

我们意识到,我们正在将业务逻辑与 Web 框架过度耦合。为了解决这个问题,我们决定引入领域驱动设计(DDD)的分层架构思想进行重构。

2. 架构演进:引入分层架构

我们借鉴了 DDD 的经典分层思想,并结合 Go 的特点,将项目划分为四个明确的层次:

webapp/
├── cmd/         # main.go 入口
├── domain/      # 领域层
│   ├── model/
│   └── repository/
├── application/ # 应用层
│   └── service/
├── infrastructure/ # 基础设施层
│   └── persistence/
└── interfaces/   # 接口层 (或称 Presentation 层)
    └── handler/
  • Domain (领域层):项目的核心,包含纯粹的领域模型(Entities, Value Objects)和业务规则。它不依赖于任何其他层repository 子目录定义了仓储接口,用于解耦领域层与数据库的实现。
  • Application (应用层):负责编排领域对象来完成业务用例(Use Case)。它调用领域模型的方法,并通过仓储接口来持久化数据。它依赖于领域层
  • Interfaces (接口层):负责与外部系统交互,如处理 HTTP 请求。它包含 Gin 的 Handler。它的职责是解析请求、调用应用层服务,并将结果转换为外部系统能理解的格式(如 JSON)。它依赖于应用层
  • Infrastructure (基础设施层):提供通用的技术能力,如数据库访问、日志、消息队列等。它负责实现领域层定义的仓储接口。它依赖于领域层(为了实现接口)

这种架构的核心是依赖倒置原则:上层模块不依赖于下层模块的具体实现,而是依赖于抽象。领域层和应用层定义了"需要什么"(接口),而基础设施层负责"如何做"(实现)。

3. 核心实现:用户注册流程重构

让我们看看重构后的用户注册流程。

3.1. Domain Layer

domain/model/user.go:

go
// User 是一个实体,包含了业务逻辑
type User struct {
    ID       uint
    Username string
    PasswordHashed string
}

// NewUser 是一个工厂函数,封装了创建用户的业务规则
func NewUser(username, plainPassword string) (*User, error) {
    if len(username) < 4 {
        return nil, errors.New("用户名长度不能少于4位")
    }
    // ... 其他校验规则
    
    hashedPassword, err := hashPassword(plainPassword)
    if err != nil {
        return nil, err
    }

    return &User{Username: username, PasswordHashed: hashedPassword}, nil
}

domain/repository/user_repository.go:

go
// UserRepository 定义了用户仓储需要实现的接口
type UserRepository interface {
    Save(user *model.User) error
    FindByUsername(username string) (*model.User, error)
}

3.2. Application Layer

application/service/user_service.go:

go
type UserService struct {
    userRepo repository.UserRepository
}

func NewUserService(repo repository.UserRepository) *UserService {
    return &UserService{userRepo: repo}
}

// Register 封装了完整的注册用例
func (s *UserService) Register(username, password string) error {
    // 检查用户是否已存在
    if _, err := s.userRepo.FindByUsername(username); err == nil {
        return errors.New("用户名已存在")
    }
    
    // 创建领域对象
    user, err := model.NewUser(username, password)
    if err != nil {
        return err
    }

    // 持久化
    return s.userRepo.Save(user)
}

3.3. Infrastructure Layer

infrastructure/persistence/user_gorm.go:

go
// GormUserRepository 是 UserRepository 接口基于 GORM 的实现
type GormUserRepository struct {
    db *gorm.DB
}

// ... NewGormUserRepository ...

func (r *GormUserRepository) Save(user *model.User) error {
    return r.db.Create(user).Error
}

// ... FindByUsername ...

3.4. Interfaces Layer

interfaces/handler/user_handler.go:

go
type UserHandler struct {
    userService *service.UserService
}

// ... NewUserHandler ...

func (h *UserHandler) Register(c *gin.Context) {
    var req CreateUserRequest
    if err := c.ShouldBindJSON(&req); err != nil {
        c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
        return
    }

    // 只调用应用层服务,不关心内部实现
    err := h.userService.Register(req.Username, req.Password)
    
    if err != nil {
        c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
        return
    }

    c.JSON(http.StatusOK, gin.H{"message": "注册成功"})
}

4. 复盘与反思

通过这次重构,我们收获了巨大的回报:

  • 清晰的职责:每一层都有明确的单一职责。Handler 只做协议转换,Service 只做业务编排,Domain Model 只包含核心业务规则。
  • 极高的可测试性:测试 UserService 时,我们只需要实现一个 UserRepository 的 Mock 接口,完全不需要依赖 Gin 和 GORM。测试可以并行运行,速度极快,这极大地提升了我们的开发信心和效率。
  • 灵活的技术选型:由于核心业务逻辑与框架和数据库解耦,未来如果我们想将 Gin 更换为其他 Web 框架,或者将 GORM 替换为 sqlx,需要修改的仅仅是 Interfaces 层和 Infrastructure 层,DomainApplication 层几乎不受影响。

当然,DDD 并非银弹。它引入了更多的抽象和代码分层,对于非常简单的 CRUD 应用来说,可能会显得"过度设计"。但对于业务逻辑复杂、需要长期演进的系统而言,这套架构所带来的可维护性和可测试性收益,将远远超过其初期投入的成本。这次重构是一次非常有价值的投资。