在TypeScript的同一Builder模式中,如何对创建(不需要ID)和更新(必须有ID)实现有条件的运行时校验?

前端开发 2026-07-09

注:下面我用一个Permission实体作为一个简单、通用的示例来说明结构性问题。

我在Node.js(TypeScript)后端实现Builder模式,在把对象传递给仓储层之前对它进行实例化。 我需要这个Builder能在运行时根据是“Create”还是“Update”操作动态地强制执行以下验证规则:

  1. 创建时:id属性缺失。name和 module这两个字段是严格必填的。
  2. 更新时:id必须严格提供。然而,name和 module变为可选,前提是至少提供其中一个。未提供的字段必须保持未定义,以便仓储层知道不覆盖数据库中的相应列。

以下是我当前的实现,它目前未能支持字段的可选性以及对id的条件性要求:

import { CustomError } from "../../../shared";

export class PermissionBuilder {
    private id?: string;
    private name?: string;
    private module?: string;

    constructor() {}

    setId(id: string): PermissionBuilder {
        if (!id || !id.trim()) throw CustomError.badRequest('ID cannot be empty');
        this.id = id;
        return this;
    }

    setName(name: string): PermissionBuilder {
        if (!name || !name.trim()) throw CustomError.badRequest('Name cannot be empty');
        this.name = name;
        return this;
    }

    setModule(module: string): PermissionBuilder {
        if (!module || !module.trim()) throw CustomError.badRequest('Module cannot be empty');
        this.module = module;
        return this;
    }

    // How to implement the runtime validation logic here?
    build() {
        // TODO: Enforce mandatory name/module if ID is missing (Create)
        // TODO: Enforce mandatory ID and at least one field if ID is present (Update)
        return {
            id: this.id,
            name: this.name,
            module: this.module,
        };
    }
}

技术要求: * 给出对build() 方法(或内部状态)的具体重构,确保在不产生碎片化、无效对象状态的前提下完成这些条件性的运行时检查。 * 解释在build() 阶段Builder如何安全地区分这两种状态,或说明该模式是否本质上需要拆分成两个独立的构建方法(例如buildForCreate() 与buildForUpdate())以处理互斥的运行时约束。

解决方案

我会把它拆成两个Builder。

创建与更新之间的验证规则完全不同:

  • 创建需要 namemodule
  • 更新需要 id,但 namemodule 变为可选

在同一个Builder中处理两种流程通常会让 build() 充满条件判断,导致返回类型尴尬。

像下面这样会更清晰:

class CreatePermissionBuilder {
  private name?: string;
  private module?: string;

  setName(name: string) {
    if (!name.trim()) {
      throw CustomError.badRequest("Name is required");
    }

    this.name = name;
    return this;
  }

  setModule(module: string) {
    if (!module.trim()) {
      throw CustomError.badRequest("Module is required");
    }

    this.module = module;
    return this;
  }

  build() {
    if (!this.name || !this.module) {
      throw CustomError.badRequest("Missing required fields");
    }

    return {
      name: this.name,
      module: this.module,
    };
  }
}
class UpdatePermissionBuilder {
  private id?: string;
  private name?: string;
  private module?: string;

  setId(id: string) {
    if (!id.trim()) {
      throw CustomError.badRequest("Id is required");
    }

    this.id = id;
    return this;
  }

  setName(name?: string) {
    this.name = name;
    return this;
  }

  setModule(module?: string) {
    this.module = module;
    return this;
  }

  build() {
    if (!this.id) {
      throw CustomError.badRequest("Id is required");
    }

    if (this.name == null && this.module == null) {
      throw CustomError.badRequest(
        "At least one field must be provided"
      );
    }

    return {
      id: this.id,
      ...(this.name !== undefined && { name: this.name }),
      ...(this.module !== undefined && { module: this.module }),
    };
  }
}

在更新构建器中使用 undefined 字段也让仓储层决定实际应更新哪些列。如果重复开始增多,你可以后续提取共享验证,但我不会尝试把两种用例强行放在一个Builder中。

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章