ddd中领域服务和应用服务

什么时候服务需要放到领域层,又什么时候需要放到应用层?

#type / howto #status / evergreen

[!info] related notes

ddd中领域服务和应用服务

在 DDD 架构中,区分 领域服务 (Domain Service)应用服务 (Application Service) 是最容易混淆的地方。

直接回答你的问题: Registration (注册)Account Cancellation (注销/销户) 非常适合作为领域服务; Login (登录) 通常也是领域服务; Logout (登出) 则视情况而定,往往更多是应用层逻辑。

为了让你通过“是否应该放入领域服务”的考试,请使用以下判断标准:


1. 核心判断标准

  • 领域服务 (Domain Service)
  • 职责:处理业务规则
  • 特征:逻辑涉及多个聚合根,或者逻辑无法自然地放在某个实体中(例如检查“邮箱唯一性”,因为一个用户实体无法知道其他用户的邮箱)。
  • 是否必须:如果逻辑属于“核心业务知识”,不论是用 HTTP 还是命令行调用都必须执行,那就是领域服务。
  • 应用服务 (Application Service)
  • 职责:处理用例流程 (Orchestration)
  • 特征:不包含业务规则,只负责“指挥”。它负责从数据库取数据、调用领域服务、发送邮件、记录日志、提交事务。
  • 是否必须:它是对外的接口层(Facade)。

2. 具体场景分析

✅ 1. Registration (注册) -> 典型的领域服务

注册不仅仅是“保存一个对象”,它通常包含核心业务规则:

  • 业务规则
  • 邮箱必须唯一(需要查询 Repository)。
  • 密码必须加密(需要调用 Hasher)。
  • 可能会根据邀请码分配初始权限。
  • 为什么是领域服务User 实体自己无法检查“邮箱是否被别人用了”,这需要跨越聚合的视野。
  • 代码位置libs/modules/auth/domain/services/registration.service.ts

✅ 2. Login (登录) -> 典型的领域服务

登录是核心的身份验证逻辑。

  • 业务规则
  • 密码匹配逻辑(调用 Hasher 比对)。
  • 锁定策略(连续输错 5 次锁定账号)。
  • 记录“最后登录时间”。
  • 为什么是领域服务:它涉及跨组件协作(Identity + PasswordHasher + LockingPolicy),且这是业务的核心安全性要求。
  • 代码位置libs/modules/auth/domain/services/login.service.ts

❓ 3. Logout (登出) -> 通常是应用服务,除非…

登出通常只是“状态管理”,业务含义较弱。

  • 情况 A:纯应用层逻辑(大多数情况)
  • 如果登出只是前端删掉 Token,或者后端 Redis 删掉 Session Key。这完全没有改变数据库里的业务数据。
  • 归属应用服务(直接在 Application Service 里调 Redis 或返回指令)。
  • 情况 B:涉及业务状态变更(你的情况)
  • 如果你需要在数据库记录“登出时间”、“在线时长”,或者更新 isOnline = false 字段。
  • 归属领域服务(因为你修改了聚合根 AuthIdentity 的状态)。

✅ 4. Account Cancellation (注销/销户) -> 强领域服务

销户是极其复杂的业务。

  • 业务规则
  • 前置检查:用户是否有未完成的订单?是否有未偿还的欠款?(这需要调用 OrderModuleBillingModule)。
  • 数据处理:是物理删除(DELETE)还是软删除(status = DELETED)?
  • 关联处理:需要匿名化处理历史评论吗?
  • 为什么是领域服务:这包含了大量的“能不能删”和“怎么删”的业务判断。

3. 代码结构示例:如何配合?

最佳实践是:应用服务 (Application Service) 作为入口,调用 领域服务 (Domain Service) 干活。

示例:注册流程 (Registration)

1. 应用服务 (Orchestrator) *位置:libs/application-server/src/modules/auth/commands/register.handler.ts*

export class RegisterHandler {
  constructor(
    private registrationService: RegistrationService, // 注入领域服务
    private emailSender: EmailSender,                 // 注入基础设施
    private logger: Logger
  ) {}

  async execute(dto: RegisterReq) {
    // 1. 调用领域服务执行核心逻辑 (创建、校验、存库)
    const user = await this.registrationService.registerByEmail(dto.email, dto.password);

    // 2. 处理副作用 (非核心业务)
    // 发送欢迎邮件这种事,通常不放在领域服务里,因为发邮件失败不应该导致注册回滚
    await this.emailSender.sendWelcome(user.email);
    
    // 3. 记录审计日志
    this.logger.info(`User registered: ${user.id}`);
  }
}

2. 领域服务 (Business Logic) *位置:libs/modules/auth/domain/services/registration.service.ts*

export class RegistrationService {
  constructor(
    private repo: IAuthIdentityRepository,
    private hasher: IPasswordHasher
  ) {}

  async registerByEmail(email: string, pass: string): Promise<AuthIdentity> {
    // 1. 核心规则检查:唯一性
    if (await this.repo.exists(email)) throw new Error('Exists');

    // 2. 核心状态构建
    const user = AuthIdentity.create(email, pass, this.hasher);

    // 3. 核心数据持久化
    await this.repo.save(user);

    return user;
  }
}

总结建议

操作推荐位置理由
Registration领域服务涉及唯一性校验、密码加密策略,是核心规则。
Login领域服务涉及凭证比对、安全策略(锁定/解封)。
Logout应用服务 (大部分)只要不改动 Entity 状态,只是清 Token,就是应用层逻辑。如果你要记录日志到 DB,那是领域逻辑。
Cancellation领域服务涉及复杂的“能否删除”校验规则(如:有余额不能注销)。

一句话心法: 如果把这个逻辑删了,业务就错了(比如重名也能注册了),那就是领域服务。 如果把这个逻辑删了,只是流程断了(比如注册成功了但没发欢迎邮件),那就是应用服务

创建于 2026/2/3 更新于 2026/5/27