TypeScript 实战复盘:巧用建造者模式落地 CQRS 架构

TypeScript 实战复盘:巧用建造者模式落地 CQRS 架构

在复杂业务系统里,CQRS(命令查询职责分离)能够显著提升模型的清晰度与扩展性,但落地时常常卡在命令和查询对象的构建上。一个命令对象可能涉及十多个字段、必填校验、默认值、审计信息以及领域语义约束,如果直接散落在服务层用 new 加 setter 赋值,很容易出现“半初始化”和重复代码。本文结合一次 TypeScript 项目实战,复盘如何用建造者模式系统性地解决这些问题,让 CQRS 落地更干净、更可控。

一、CQRS 落地中的对象构建痛点

CQRS 的核心是把写模型和读模型分开,命令对象负责表达意图,查询对象负责描述筛选条件。二者在规模变大后,构建逻辑会迅速膨胀。很多团队在落地时只关注了 Handler 的拆分,忽略了对象创建本身的复杂度,最终导致服务层充满字段赋值和条件判断。

1.1 命令对象容易“半初始化”

命令对象通常需要携带用户 ID、操作类型、业务主体 ID、时间戳、来源渠道等元数据。如果直接 new Command() 再逐个 set,很容易漏掉某个字段,尤其是新同事接手时,不知道哪些字段是必须的、哪些有默认值。TypeScript 的类型系统虽然能避免类型错误,却无法阻止业务上的不完整构造。

1.2 查询条件拼装散落在服务层

查询对象往往包含分页、排序、过滤条件、关键字等可选参数。不同接口对查询条件的组合要求不同,如果每个 Controller 都自己拼装 Query 对象,就会出现大量相似又不完全一致的代码。例如“未传 pageSize 时默认 20”这个规则,可能在十几个地方被重复实现,一旦默认值调整,改动面非常大。

二、建造者模式如何支撑 CQRS

建造者模式的核心思想是把复杂对象的构建过程与表示分离,让同一个构建过程可以创建不同表示。在 CQRS 中,命令和查询对象都属于“复杂对象”,它们有大量可选字段、默认值规则和校验逻辑,非常适合用建造者模式封装。具体来说,建造者模式在 CQRS 落地中带来三个直接好处。

  • 链式调用提升可读性:通过 builder.fieldA(...).fieldB(...).build() 的方式,调用方可以清晰看到当前命令或查询使用了哪些字段,避免构造参数列表过长。
  • 集中校验与默认值:build() 方法内可以统一执行必填校验、默认值填充以及领域规则检查,保证返回的对象始终是完整、合法的。
  • 支持不可变结果:建造者完成后返回的对象可以设计为只读,命令和查询一旦创建就不允许被随意修改,降低并发和副作用风险。

三、TypeScript 中的落地要点

TypeScript 提供了强大的类型系统,和建造者模式结合后,可以进一步减少运行时错误,并让 IDE 的自动提示成为开发体验的一部分。以下是实践中总结的几个关键步骤。

3.1 用泛型约束建造步骤

很多场景下,命令对象的某些字段是互斥的,或者某个字段出现后另一个字段才允许设置。可以通过泛型建造器,将“当前已设置的状态”建模为类型参数,从而在编译期阻止非法链式调用。例如 builder.withType('A') 之后,类型上就不再暴露只适用于 B 类型的方法。

3.2 私有构造器限制直接实例化

为了强制调用方走建造者流程,可以把命令或查询类的构造函数设为私有,只允许通过静态的 builder() 方法创建建造器。这样既能防止外部直接 new 一个不完整对象,也能把构建逻辑集中在一个文件里,避免散落到业务代码中。

3.3 与 Command/Query Handler 自然衔接

在 CQRS 中,命令通过 CommandBus 派发到对应的 Handler。建造者创建出的命令对象可以作为统一的数据载体,Handler 只依赖命令对象的不可变属性。查询侧同样如此,QueryBuilder 创建出的查询对象可以直接传给 QueryHandler,Handler 内部无需关心分页默认值或字段是否缺失,因为这些已经在建造阶段处理完毕。

四、复盘总结:优势与常见误区

从项目复盘结果看,引入建造者模式后,命令和查询相关代码的可维护性明显提升。原来分散在 Controller、Service、Handler 中的组装逻辑被收敛到各自 Builder 中,单元测试也更聚焦。团队新成员可以通过 Builder 的链式提示快速了解可用字段,不必翻阅大量接口定义。但这并不意味着建造者模式可以无节制使用,实践中有几个误区需要警惕。

  • 优势一:构建逻辑内聚:字段校验、默认值、元数据填充集中在一处,修改成本低。
  • 优势二:类型提示友好:结合 TS 泛型后,链式调用具备编译期约束,减少低级错误。
  • 优势三:对象状态更稳定:不可变命令和查询对象减少了运行期被意外修改的风险。

常见误区主要集中在过度设计。比如只包含两三个字段的简单查询对象,用普通工厂函数即可,不必强行引入 Builder;还有些团队为每个命令都创建一个包含全部生命周期方法的 Builder,导致 Builder 类数量激增,反而增加维护成本。另外,建造者模式与工厂模式的边界要区分清楚:工厂模式关注创建哪个对象,建造者模式关注如何一步步构建一个复杂对象。在 CQRS 中,如果命令类型差异大,可以先用工厂选择正确的 Builder,再由 Builder 完成具体构建。

五、结语

CQRS 落地不是简单地把命令和查询拆成两个模型,更重要的是让这些模型能够被安全、清晰地创建和使用。TypeScript 的建造者模式恰好提供了一种结构化方案,把构建复杂性从业务代码中剥离出来。通过复盘实践可以看到,合理的建造者设计能让 CQRS 架构在 TypeScript 项目中更易落地、更易维护,同时避免过度设计带来的负担。下一步可以在团队中沉淀统一的 Builder 基类或工具类型,让命令与查询的构建风格保持一致,进一步提升工程效率。