DDD 入门实践

DDD 是 Domain-Driven Design 的缩写,中文翻译为领域驱动设计。DDD 是一种软件设计的方法论。

简单来说,就是通过领域问题驱动模型设计,模型驱动软件设计。
要从领域问题出发,去设计领域模型,来解决它。并设计具体的软件来实现领域模型。
进而得到一个可以解决领域问题的软件系统。

而这里面的领域,实际上指的是业务范围,比如:

  • 电商系统中的订单领域
  • 支付系统中的交易领域
  • 社交系统中的关系领域
  • 用户系统中的认证领域

本文将以一个最常见的场景,用户登录注册,来帮助快速理解 DDD 的基本实践方式。

为什么使用 DDD

以用户登录注册为例:

// service.go
type Service struct {
  repo Repository
}
// ...

// handler.go
type Handler struct {
  service Service
}

func (h *Handler) Register(ctx *gin.Context) {
  // 校验用户输入
  // 检查密码强度
  // 检索数据库
  // 加密密码
  // 存储用户
  // 响应请求
}

func (h *Handler) Login(ctx *gin.Context) {
  // 校验用户输入
  // 查询用户
  // 校验密码
  // 更新登录时间
  // 生成 token
  // 响应请求
}

最初的代码是这样没问题,但随着业务增长,引入各种规则,例如,支持手机号注册;邮箱验证码;OAuth 登录;登录风控等,Service 就会越来越庞大。

而 Service 膨胀的根本原因,是业务规则缺乏明确归属,导致规则被堆积在流程层。

这是因为代码是围绕着功能进行组织的,而不是围绕着业务模型组织的。

而 DDD 恰好提供了一种新的思路,围绕领域模型来组织代码系统。

领域建模

如果从业务角度分析,“用户登录注册”涉及哪些核心概念?

我们很容易识别出几个对象:

首先是用户对象 User,它是系统中的核心业务对象,它拥有:

  • 用户 ID
  • 用户名
  • 密码
  • 状态
  • 注册时间
  • 凭证(Credential)

接着是登录认证相关信息 Credential,例如:

  • 密码哈希
  • 盐值
  • 登录方式

然后是用户状态 UserStatus,用户可能处于:

  • 未激活
  • 正常
  • 冻结
  • 注销

之后是登录行为,登录不是简单查库,它包含业务规则:

  • 校验密码
  • 判断账号状态
  • 更新登录时间
  • 记录登录日志

这时候我们已经在做领域建模了。

聚合根

聚合是 DDD 中的一个重要概念。简单来说,就是一组具备一致性边界的对象,说人话就是这些对象同属一个业务范围。

聚合根就是这组聚合的入口,所有修改都必须经过聚合根,而无法直接操作聚合根内部。

而对于用户系统,User 就是聚合根,因为所有和用户有关的操作都需要经过 User:

  • 登录需要通过它
  • 注册需要创建它
  • 状态变更作用于它
type User struct {
  // ...
}

func (u *User) Register() {}

func (u *User) ChangePassword() {}

func (u *User) ChangeStatus() {}

func (u *User) VerifyPassword() {}

与传统分层架构不同的是,这些逻辑不在 Service 中,而是在 User 内部。

因为这些逻辑是 User 的业务行为,是直接作用于用户状态,需要维护用户业务一致性的行为。

而有关于密码加密,token 签发,风控等逻辑,并不直接作用于用户状态,也不需要维护用户业务一致性的行为,那么就不应该放在 User。

这便是领域模型的核心。

值对象

DDD 强调不是所有的东西都是字符串,register('admin', '123456') 这里的参数语义会很弱,在 DDD 中,会把需要的参数建模为值对象。

var emailRegexp = regexp.MustCompile(`^[a-zA-Z0-9._%+\-]+@[a-zA-Z0-9.\-]+\.[a-zA-Z]{2,}$`)

type Email string

func NewEmail(email string) (Email, error) {
  email = strings.ToLower(strings.TrimSpace(email))

  if email == "" {
    return "", ErrEmailEmpty
  }

  if !emailRegexp.MatchString(email) {
    return "", ErrEmailInvalid
  }

  return Email(email), nil
}
type Password string

func NewPassword(pw string) (Password, error) {
  l := len(pw)

  if l < minPasswordLength {
    return "", ErrPasswordTooShort
  }

  if l > maxPasswordLength {
    return "", ErrPasswordTooLong
  }

  if PasswordRule&MustHasSymbol != 0 && !strings.ContainsAny(pw, passwordSymbol) {
    return "", ErrPasswordMustHasSymbol
  }

  if PasswordRule&MustHasUppercase != 0 && !strings.ContainsAny(pw, passwordUppercase) {
    return "", ErrPasswordMustHasUppercase
  }

  if PasswordRule&MustHasLowercase != 0 && !strings.ContainsAny(pw, passwordLowercase) {
    return "", ErrPasswordMustHasLowercase
  }

  if PasswordRule&MustHasDigit != 0 && !strings.ContainsAny(pw, passwordDigits) {
    return "", ErrPasswordMustHasDigit
  }

  return Password(pw), nil
}

值对象是不可变的,且 a1 = Email("a@example.com") 一定等同于 a2 = Email("a@example.com"),因为值对象关注的是值,而不是身份。

值对象的意义在于,封装了校验规则,提高表达能力,防止非法状态,此外,如果值对象创建成功,就一定是合法的。

仓储管理持久化

领域对象不会直接操作数据库。

DDD 使用抽象的 Repository 来管理存储。

// user_repository.go
type UserRepository interface {
  Save(user *User) error
  ExistsByEmail(email Email) (bool, error)
  FindByEmail(email Email) (*User, error)
}

// user_repository_postgres.go
var _ UserRepository = (*UserRepositoryPostgres)(nil)

type UserRepositoryPostgres struct {
  // ...
}

func (repo *UserRepositoryPostgres) Save(user *User) error {
  // ...
}

func (repo *UserRepositoryPostgres) ExistsByEmail(email Email) (bool, error) {
  // ...
}

func (repo *UserRepositoryPostgres) FindByEmail(email Email) (*User, error) {
  // ...
}

简单来说就是面向对象的接口模式,不关心具体实现,只关心接口。

服务协调

既然业务逻辑都放在了领域对象,那么还要 Service 做什么?

简单来说,Service 的作用是变为了进行流程编排,而不是接着做业务决策。

它负责调用领域对象,协调仓储并管理事务。
而领域模型则负责业务规则,状态变化和领域行为。

type UserService struct {
  // ...
}

func (s *UserAppService) Register(cmd RegisterCommand) error {
    email, err := NewEmail(cmd.Email)
    if err != nil {
        return err
    }

    password, err := NewPassword(cmd.Password)
    if err != nil {
        return err
    }

    exists, err := s.repo.ExistsByEmail(email)
    if err != nil {
        return err
    }
    if exists {
        return ErrUserAlreadyExists
    }

    user := NewUser(email, s.passwordHasher.Hash(password))

    if err := s.repo.Save(user); err != nil {
        return err
    }

    s.eventBus.Publish(UserRegisteredEvent{
        UserID: user.ID(),
    })

    return nil
}

项目结构

user
|- application        # 流程协调
  |- UserService
  |- Command
|- domain             # 业务核心
  |- model
    |- User
    |- Email
    |- Password
  |- repository
  |- service
|- infrastructure     # 基础实现
  |- persistence
  |- security
|- interfaces         # 对外接口
  |- controller

请求流程

HTTP Request
   ↓
Controller
   ↓
Application Service
   ↓
Domain Model
   ↓
Repository Interface
   ↓
Infrastructure Repository
   ↓
Database

POST /register
  → RegisterController
  → UserAppService.Register()
  → NewEmail()
  → NewPassword()
  → User.New()
  → UserRepository.Save()
  → Response

DDD 的价值

使用 DDD 的好处主要就是业务语义清晰化了。

原先需要 checkPasswordcheckStatusupdateLoginTime 等多段流程的代码,直接被 user.login(dto) 替代。

虽然这看起来只是封装了这些这些流程,不使用 DDD 也能做到,但那样只是有了形,但没有意。

DDD 的核心是通过分析领域问题,从而进行领域建模,随后基于领域建模开发软件,最后得到可以解决领域问题的系统。

因此 DDD 的价值在于迫使团队显式建模业务规则,把“隐含在代码流程中的业务知识”提升为清晰的领域模型。

只有复杂业务系统,核心业务规则频繁变化,多人协作大型项目等才适用 DDD 开发。

简单的 CRUD 后台,一次性工具等并不适合。强行使用反而会增加复杂度。


DDD 入门实践
https://www.inksha.com/archives/ddd-ru-men-shi-jian
作者
inksha
发布于
2026年05月15日
许可协议