You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DDD中需访问基础设施层的业务逻辑分层最佳实践

核心结论

你当前在应用层直接调用仓储做校验的写法不合理,最优方案是通过领域服务封装需要访问持久层的校验逻辑,既不破坏DDD分层依赖规则,也能避免业务逻辑泄漏、从根源上防止非法领域对象生成。


先澄清几个常见认知误区

  • 误区1:领域层不能调用仓储。DDD的依赖规则是领域层不依赖基础设施层的具体实现,只要在领域层定义仓储的抽象接口(比如ICountryCodeRepository),由基础设施层实现这个接口,依赖方向永远是基础设施指向领域,完全不违反分层规则。
  • 误区2:值对象里可以调用仓储。非常不推荐。值对象的设计原则是无副作用、纯内存运算,创建时的校验应该是同步、无IO的格式/规则校验(比如alpha-3编码必须是3位大写字母这类固定规则),一旦引入IO依赖,值对象就失去了可测试性、轻量性,本质是把领域服务的职责强行塞给了值对象。
  • 误区3:少量逻辑泄漏到应用层无所谓。这只是无法优雅收敛逻辑时的妥协方案,长期运行必踩坑:只要有一个用例漏写校验,就会生成非法领域对象,脏数据入库后的修复成本极高。只要能通过合理分层做到逻辑内聚,完全没必要承担这类风险。

具体落地方案

把需要访问持久层的校验逻辑收敛到领域服务中,应用层只负责流程编排,不编写任何业务校验规则:

第一步:拆分值对象的校验职责

值对象CountryAlpha3Code的create方法只做纯内存的格式校验,不引入任何外部依赖:

// 领域层的值对象,无外部IO依赖
public static create(value: string): Result<CountryAlpha3Code> {
  // 仅做固定规则校验:非空、长度3、全大写字母、无不可见字符
  if (!value || value.length !== 3 || !/^[A-Z]{3}$/.test(value)) {
    return Result.fail('国家编码必须是3位大写alpha-3格式');
  }
  return Result.ok(new CountryAlpha3Code({ value }));
}

第二步:领域层定义仓储接口+校验领域服务

在领域层目录下定义仓储的抽象接口,再编写专门的领域服务承载持久化相关的校验逻辑:

// 领域层定义的仓储抽象接口,不依赖任何基础设施实现
export interface ICountryCodeRepository {
  existsByAlpha3Code(code: CountryAlpha3Code): Promise<boolean>;
}

// 领域层的校验领域服务,仅依赖抽象仓储接口
export class CountryCodeValidationService {
  constructor(private readonly countryCodeRepo: ICountryCodeRepository) {}

  async validateCodeExists(code: CountryAlpha3Code): Promise<Result<void>> {
    const exists = await this.countryCodeRepo.existsByAlpha3Code(code);
    if (!exists) {
      return Result.fail(`国家编码${code.value}不存在`);
    }
    return Result.ok();
  }
}

第三步:应用层仅做流程编排

应用层注入领域服务、仓储,只负责按顺序调用组件、处理DTO转换、返回结果,不编写业务校验规则:

export default class UseCaseClass implements IUseCaseInterface {
  constructor(
    private readonly _repo: IRepo,
    // 注入领域层定义的接口,由基础设施层做实现注入
    private readonly countryValidationService: CountryCodeValidationService
  ) {}

  async execute(request: dto): Promise<dtoResponse> {
    // 1. 先执行纯内存的VO/实体格式校验
    const someOtherKeyorError = KeyEntity.create(request.someOtherDtoKey);
    const countryOrError = CountryAlpha3Code.create(request.country);
    const dtoResult = Result.combine([someOtherKeyorError, countryOrError]);
    if (dtoResult.isFailure) {
      return left(Result.fail<void>(dtoResult.error)) as dtoResponse;
    }

    const countryCode = countryOrError.getValue();
    try {
      // 2. 调用领域服务执行需要访问持久层的校验
      const validationResult = await this.countryValidationService.validateCodeExists(countryCode);
      if (validationResult.isFailure) {
        return left(new ValidCountryCodeError.CountryCodeNotValid(countryCode.value)) as dtoResponse;
      }

      // 3. 所有校验通过后再创建实体、持久化
      const dataOrError = MyEntity.create({
        ...request,
        key: someOtherKeyorError.city.getValue(),
        country: countryCode,
      });
      const commandResult = await this._repo.save(dataOrError.getValue());
      return right(Result.ok<any>(commandResult));
    } catch (err: any) {
      return left(new AppError.UnexpectedError(err)) as dtoResponse;
    }
  }
}

其余疑问的针对性解答

  • 关于用领域事件实现同步校验:完全没必要。领域事件天生适合异步、最终一致的场景(比如发通知、生成统计数据),同步校验用领域服务是最直接、可测试性最强、调试成本最低的方案,硬用事件实现同步只会增加不必要的复杂度。
  • 关于如何避免漏校验:可以把实体的构造函数设为私有,仅暴露经过完整校验的静态工厂方法,甚至把校验逻辑内置到工厂方法中,强制所有创建实体的路径都必须走校验流程,从根源上避免非法对象生成。
  • 高并发场景兜底:如果是邮箱全局唯一性、库存扣减这类强一致要求的场景,不要只靠应用层/领域层的校验,必须在数据库层加唯一索引、乐观锁/悲观锁做兜底,避免并发场景下的脏数据。

内容的提问来源于stack exchange,提问作者Mr X

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 16:39:13