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 (注销/销户) -> 强领域服务
销户是极其复杂的业务。
- 业务规则:
- 前置检查:用户是否有未完成的订单?是否有未偿还的欠款?(这需要调用
OrderModule或BillingModule)。 - 数据处理:是物理删除(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 | 领域服务 | 涉及复杂的“能否删除”校验规则(如:有余额不能注销)。 |
一句话心法: 如果把这个逻辑删了,业务就错了(比如重名也能注册了),那就是领域服务。 如果把这个逻辑删了,只是流程断了(比如注册成功了但没发欢迎邮件),那就是应用服务。