资讯动态

软件工厂设计模式:用蓝图、部件与装配器搭建代码生成流水线

发布时间:2026/9/1 4:05:51 来源:尧图企业网站定制
软件工厂设计模式听起来像是对工厂方法模式的又一次包装实际上它解决的问题完全不同。它不关心某个对象由哪个类创建而是关心一个软件产物如何由一组可组合部件按蓝图装配出来。项目脚手架、配置生成、编译校验、测试执行、部署产物生成甚至多 Agent 编排中的子任务调用都可以放进同一个组合模型。这篇文章用一套最小接口和一个 TypeScript 示例把这种模式拆开讲清楚并给出可以复现的运行结果、常见坑和生产落地清单。如果你正在写代码生成器、CI/CD 流水线、脚手架工具或 Agent 编排框架这篇文章的思路可以直接落到自己的项目里。1. 软件工厂设计模式到底是什么1.1 从制造业工厂到软件工厂的映射“工厂”这个词容易让人想到制造业但这并不是普通意义上的工厂。软件工厂设计模式的核心不是“用机器替代程序员”而是把软件开发过程中大量重复、可标准化、可组合的环节变成一条可以按规格执行的装配线。装配线上流动的不是零件而是 Blueprint蓝图、Part部件、Context上下文和 Artifact产物。蓝图描述要构建什么部件描述每一步做什么上下文保存整个装配过程的输入、输出和日志产物是最终生成的代码、配置文件、测试结果或部署包。这个叫法不是新造的。软件工程很早就提出过 Software Factory 的思路核心就是把项目中可复用的模式、框架、工具和配置组合起来形成一条相对稳定的软件构建流程。和传统工厂方法的区别在于前者关心“如何创建对象”后者关心“如何组装一个软件项目或一套交付物”。制造工厂和软件工厂可以做一张非常直接的映射制造工厂软件工厂图纸Blueprint构建规格描述零件Part可执行的功能部件工位Stage一个装配环节流水线控制Assembler按顺序调用部件质检Validation / Quality Gate原料仓部件注册表、配置库成品库Artifact Store产物和日志存储这个映射的价值在于它让“软件构建”本身成为可设计、可测试、可追踪的过程。过去很多项目写脚手架时都是一个人打开编辑器手工新建目录、复制文件、改配置。软件工厂设计模式希望把这一套流程变成“输入一份蓝图执行一条装配线得到稳定结果”。1.2 和 GoF 工厂方法、抽象工厂的区别接触过设计模式的人很容易把“软件工厂设计模式”和 GoF 的 Factory Method 混在一起。它们都叫工厂但关注点完全不同。GoF 的工厂方法解决的是对象创建问题调用方不需要直接new具体类而是通过工厂方法返回某个接口或基类实例。抽象工厂则更进一步解决一族相关对象的创建问题比如数据库连接、事务、日志等配套对象的统一创建。软件工厂设计模式解决问题的是“软件产物的组装”而不是“内存对象的创建”。它可以是更高层级的编排思路内部可以继续使用 GoF 工厂方法。例如某个 Part 在创建配置对象时完全可以内部使用工厂方法来决定生成哪种配置解析器。三者的区别可以整理成一张表维度GoF 工厂方法GoF 抽象工厂软件工厂设计模式关注对象单个对象一族相关对象软件产物和交付流程产物用例创建对象实例创建配套对象组脚手架、代码、配置、测试、部署产物扩展点子类重写工厂方法新增具体工厂注册新 Part、新增 Blueprint运行结果返回对象返回一族对象返回产物、日志和运行上下文是否关心顺序不关心不关心非常关心所以软件工厂设计模式更多是一种“组合架构模式”它把部件之间的调用关系、版本关系、执行顺序和异常处理显性化。很多团队把生成器做成一个巨大的脚本本质上也是软件工厂只不过没有把部件边界拆清楚。1.3 它能解决什么问题哪些场景不适合软件工厂设计模式适合以下场景需要反复创建结构一致的服务模块比如微服务项目脚手架。需要把配置注入、依赖安装、测试执行、质量校验串成一条流水线。需要做代码生成器但生成规则的维护成本已经超过收益。需要编排多个 AI Agent 子任务并且希望子任务像工具一样可复用、可观测。团队里有多个语言、多个仓库想要统一规范但又不希望写死一套固定模板。不适合的场景也很明显一次性临时脚本不需要组合和复用。极度简单的 CRUD 项目直接手工创建比引入软件工厂更快。项目还在频繁探索阶段需求每天变化过早把流程固化成装配线反而增加维护成本。软件工厂的核心价值不是自动化本身而是让“重复的部分标准化变化的部分参数化组合的逻辑显性化”。2. 先设计一套可组合部件模型2.1 部件模型里的几个角色要把软件工厂落地不能只写一个很大的Factory类。合理的模型至少要包含以下几个角色Blueprint一份声明式的构建规格描述项目名称、版本、输入参数、要执行的 Stage 列表。Part一个可执行的功能部件必须有自己的名字、版本和运行函数。Context装配过程共享的上下文包括工作目录、输入参数、输出结果和日志。Assembler装配器负责按 Blueprint 里的顺序调用 Part捕获异常并继续向调用方返回结果。Registry部件注册表通过 name 找到对应的 Part 实现。Quality Gate一个可选的校验阶段通常是普通 Part但职责更明确只检查不修改。这里最容易犯的错是把 Assembler 写成一个超大类里面直接写大量文件操作、命令执行和字符串拼接。正确做法是Assembler 只负责流程控制真正的文件生成、配置写入、命令执行逻辑都放在 Part 内部。这样新增一个环节时不需要修改 Assembler只需要新增一个 Part 并注册进去。2.2 可组合的标准不是接口而是边界很多人以为只要定义一个接口部件就可以自由组合。实际上组合是否顺利取决于三个边界条件第一输入输出是否清晰。一个 Part 必须能明确说明它需要哪些输入会生成哪些输出。如果两个部件都直接改全局目录里的文件互相之间没有明确的产物连接那么它们无法安全组合。第二副作用是否可控。软件工厂里的 Part 通常要读写文件、执行命令、调用网络接口。每个 Part 都应该记录自己做了什么并且尽量做到可以重复执行。如果一次运行会污染下次运行就需要在流程开始前清理工作目录或者使用可覆盖的写入策略。第三依赖顺序是否可见。蓝图里列出 Stage 的顺序实际上就是部件间依赖关系。后一个部件可以从 Context 的 outputs 中读取前一个部件的结果这样依赖关系才不是靠隐式全局变量维持的。一句话总结接口只能解决类型匹配边界才能解决组合安全。2.3 一个 YAML 蓝图示例Blueprint 用 YAML 描述是为了让非开发人员也能看懂构建规格。下面是一个最小示例name: user-service version: 1.0.0 inputs: module: user port: 8080 stages: - name: scaffold part: scaffolder with: module: user - name: add-config part: config-injector with: values: name: user-service port: 8080 - name: validate part: validator with: file: application.yml这份蓝图表达了三件事项目叫什么、输入参数是什么、按什么顺序执行哪些部件。每个stage里的part是在注册表中能找到的部件名with是传给当前部件的配置。这样蓝图本身就可以存储在 Git 仓库里随着项目版本一起演进。3. 用 TypeScript 实现一个最小可运行工厂3.1 项目结构为了让你能直接复现下面用一个非常小的 TypeScript 项目演示。示例会生成一个名为user的目录写入一份application.yml再校验文件是否存在。核心代码只有几十行但它已经包含软件工厂设计模式的最小闭环。项目结构如下factory-demo/ ├─ blueprints/ │ └─ user-service.yaml ├─ src/ │ ├─ types.ts │ ├─ registry.ts │ ├─ factory.ts │ ├─ parts/ │ │ ├─ scaffolder.ts │ │ ├─ configInjector.ts │ │ └─ validator.ts │ └─ run.ts └─ package.json初始化命令mkdir factory-demo cd factory-demo npm init -y npm install typescript tsx yaml types/node这里使用tsx只是为了本地快速运行生产环境建议编译成 JavaScript 后运行。3.2 定义公共类型在src/types.ts中定义 Blueprint、Stage、Context 和 PartDefinitionexport interface Blueprint { name: string; version: string; inputs?: Recordstring, unknown; stages: Stage[]; } export interface Stage { name: string; part: string; with?: Recordstring, unknown; } export interface PartContext { workspace: string; inputs: Recordstring, unknown; outputs: Recordstring, unknown; logs: string[]; } export interface PartDefinition { name: string; version: string; run: ( context: PartContext, config: Recordstring, unknown ) PromisePartContext; }这套类型是整个工厂模型的骨架。PartContext是部件之间传递数据的通道outputs用来承接上一个部件的结果logs用来记录运行轨迹。3.3 实现部件注册表在src/registry.ts中实现注册表import type { PartDefinition } from ./types; const registry new Mapstring, PartDefinition(); export function registerPart(part: PartDefinition): void { registry.set(part.name, part); } export function getPart(name: string): PartDefinition { const part registry.get(name); if (!part) { throw new Error(part not found: ${name}); } return part; }注册表的意义在于工厂本身不直接依赖具体 Part而是通过名称查找部件。新增一个部件时不需要改动factory.ts只需要注册一下。3.4 实现三个可组合部件第一个部件负责创建目录。在src/parts/scaffolder.ts中import { mkdir } from node:fs/promises; import path from node:path; import type { PartDefinition } from ../types; export const scaffolder: PartDefinition { name: scaffolder, version: 1.0.0, async run(context, config) { const moduleName String(config.module ?? context.inputs.module ?? demo); const targetDir path.join(context.workspace, moduleName); await mkdir(targetDir, { recursive: true }); context.outputs.projectDir targetDir; context.logs.push(scaffolder created ${targetDir}); return context; }, };第二个部件负责写入配置文件。在src/parts/configInjector.ts中import { writeFile } from node:fs/promises; import path from node:path; import type { PartDefinition } from ../types; const yamlContent (values: Recordstring, unknown) server: port: ${values.port ?? 8080} app: name: ${values.name ?? demo} ; export const configInjector: PartDefinition { name: config-injector, version: 1.0.0, async run(context, config) { const projectDir context.outputs.projectDir as string; const configFile path.join(projectDir, application.yml); const values (config.values as Recordstring, unknown) ?? {}; await writeFile(configFile, yamlContent(values), utf-8); context.outputs.configFile configFile; context.logs.push(config-injector wrote ${configFile}); return context; }, };第三个部件负责校验产物是否存在。在src/parts/validator.ts中import { access } from node:fs/promises; import path from node:path; import type { PartDefinition } from ../types; export const validator: PartDefinition { name: validator, version: 1.0.0, async run(context, config) { const projectDir context.outputs.projectDir as string; const file config.file ? String(config.file) : application.yml; const target path.join(projectDir, file); try { await access(target); } catch { throw new Error(validator expected file not found: ${target}); } context.logs.push(validator checked ${target}); return context; }, };这三个部件的结构完全一致从context和config中读取输入执行自己的任务把结果写入context.outputs并记录日志。后面再增加新部件时只需要照这个模式写。3.5 实现装配器在src/factory.ts中实现assembleimport type { Blueprint, PartContext } from ./types; import { getPart } from ./registry; export async function assemble( blueprint: Blueprint, initial: PartContext ): PromisePartContext { let context initial; context.logs.push(factory ${blueprint.name} v${blueprint.version}: start); for (const stage of blueprint.stages) { const part getPart(stage.part); context.logs.push(stage ${stage.name}: run ${stage.part}); try { context await part.run(context, stage.with ?? {}); } catch (error) { const message error instanceof Error ? error.message : String(error); throw new Error( stage ${stage.name} (${stage.part}) failed: ${message} ); } } context.logs.push(factory ${blueprint.name}: done); return context; }装配器的职责非常单一解析蓝图按顺序调用部件异常时把失败信息包装成“哪一个 stage、哪一个 part 失败”。这里没有写任何具体文件生成逻辑因此装配器可以被不同项目复用。3.6 编写运行入口在src/run.ts中读取 YAML注册部件然后执行装配import { readFile } from node:fs/promises; import { parse } from yaml; import { assemble } from ./factory; import { registerPart } from ./registry; import { scaffolder } from ./parts/scaffolder; import { configInjector } from ./parts/configInjector; import { validator } from ./parts/validator; import type { Blueprint, PartContext } from ./types; async function main() { const blueprintText await readFile( blueprints/user-service.yaml, utf-8 ); const blueprint parse(blueprintText) as Blueprint; registerPart(scaffolder); registerPart(configInjector); registerPart(validator); const context: PartContext { workspace: .work, inputs: blueprint.inputs ?? {}, outputs: {}, logs: [], }; const result await assemble(blueprint, context); console.log(result.logs.join(\n)); console.log(outputs:, result.outputs); } main().catch((error) { console.error(error); process.exitCode 1; });运行命令npx tsx src/run.ts预期输出factory user-service v1.0.0: start stage scaffold: run scaffolder scaffolder created .work/user stage add-config: run config-injector config-injector wrote .work/user/application.yml stage validate: run validator validator checked .work/user/application.yml factory user-service: done outputs: { projectDir: .work/user, configFile: .work/user/application.yml }验证文件确实生成ls -R .work cat .work/user/application.yml到这里最小闭环已经成立。你可以在blueprints目录新增一个 YAML再注册新的 Part就能扩展出新的软件产物。学习环境下这样的实现足够说明模式的核心。4. 把同一模型扩展到多 Agent 编排主从模式的本质4.1 多 Agent 编排里的主从模式本质上是“部件调用”如果你的开发场景已经进入 AI Agent 编排会发现设计软件工厂时遇到的所有问题都会再次出现。最新的多 Agent 设计里编排者-子代理模式比较常见很多人把它称为主从模式。它的基本结构是一个主 Agent 负责任务拆解多个子 Agent 各自处理一个子任务最后主 Agent 汇总结果。这里有一个关键判断在主从模式中子 Agent 并不是一个不可拆分的独立产品它本质上可以看作一个另类的 Tool。Tool 通常有名称、描述、输入参数和输出结果主 Agent 决定什么时候调用它。子 Agent 也一样它接收某个子任务的输入执行完后返回输出。只要子 Agent 的输入输出契约清晰它就可以被注册进软件工厂的部件注册表像其他 Part 一样被调用。因此软件工厂设计模式可以很自然地延伸到 Agent 编排场景。一个 Blueprint 里的 Stage 可以是“调用代码生成子 Agent”另一个 Stage 可以是“执行校验工具”。只要你把子 Agent 的调用方式封装成 PartDefinition装配器根本不需要关心它调用的是普通函数、外部服务还是 AI 模型。4.2 把子 Agent 封装成 Part 的适配器在 TypeScript 里可以写一个适配器把子 Agent 调用包装成 Partimport type { PartDefinition } from ../types; export type AgentCaller ( input: Recordstring, unknown ) PromiseRecordstring, unknown; export interface AgentPartSpec { name: string; description: string; inputSchema: Recordstring, unknown; outputSchema: Recordstring, unknown; } export function createAgentPart( spec: AgentPartSpec, caller: AgentCaller ): PartDefinition { return { name: spec.name, version: 0.1.0, async run(context, config) { const input { ...context.inputs, ...config }; const output await caller(input); context.outputs[spec.name] output; context.logs.push( agent part ${spec.name} returned ${JSON.stringify(output).slice(0, 200)} ); return context; }, }; }这段代码说明了一个重要思路Agent 是否聪明不改变它在工厂中的位置。它依然是一个有输入、有输出、有日志的部件。无论底层是调用远程模型还是本地推理服务上层装配逻辑都不需要改。从软件工厂角度看多 Agent 编排中的 Tool、子 Agent、普通函数其实只是同类部件的不同实现方式。你可以把它们放进同一个注册表由同一个装配器按蓝图调用。4.3 不是所有子 Agent 都适合当部件把子 Agent 当作 Tool 调用能显著降低编排复杂度但不是所有子 Agent 都适合这样做。适合做部件的子 Agent 有三个特征目标边界清晰、输入输出可描述、副作用可观测。比如“把一段需求描述翻译成接口文档”“根据表结构生成 CRUD 代码”这些都是边界明确的子任务。不适合做部件的子 Agent 也有三个特征目标开放、输出不可控、执行过程会无限制修改外部系统。比如“帮我完善整个项目”或“自由探索所有文件并修改配置”这种子 Agent 不适合放进固定装配线。就算放进去了也需要强校验的 Quality Gate 兜底。可以整理一张对照表维度可组合的子 Agent不可组合的子 Agent输入有明确的输入 Schema输入模糊靠对话临时解释输出有固定的输出结构每次输出格式不固定副作用只修改指定目录或接口可能乱改文件、乱调服务失败隔离可按 Agent 名称跟踪日志失败原因难以定位重跑性支持相同输入重复执行结果随模型状态变化如果你的 Agent 编排正朝着“工具化”方向演进软件工厂设计模式会是一个很好的底座。它不限制你使用多强的模型只要求每个 Agent 部件都有边界。5. 排错从现象倒推到部件还是装配器的问题5.1 坑 1把所有逻辑塞进一个 Factory 类错误写法是建一个Factory类里面直接写fs.mkdir、writeFile、exec然后根据type字段走不同的分支。每新增一种产物类型就要改一遍这个类。问题在于工厂类变成了上帝类既负责流程控制又负责所有具体逻辑。一旦某个生成步骤失败排查时无法快速判断失败在哪个环节。推荐做法是Factory 只读 Blueprint通过注册表查找 Part并负责调用和异常包装。具体业务逻辑永远放在 Part 内部。5.2 坑 2部件依赖隐式路径而不是显式输出错误现象第二个部件需要读第一个部件生成的文件但它直接写死了一个相对路径比如./output/config.yml。一旦工作目录变了或者第一个部件的输出路径调整了第二个部件就会失败。这里的原因是没有使用context.outputs传递产物路径。正确的做法是第一个部件把生成的路径写入context.outputs第二个部件从context.outputs读取而不是自己去猜路径。这样路径变更只影响产生路径的部件不影响消费路径的部件。5.3 坑 3没有考虑幂等、重试和清理软件工厂通常会反复执行。如果脚本每次运行都往同一个目录追加文件、每次都调用外部 Agent、每次都往日志里重复写相同内容运行几次之后结果就不可信了。推荐原则是在装配开始前清理工作目录每个 Part 记录自己的输入摘要和输出结果对外部调用设置超时时间和重试次数。这里的清理命令可以这样跑rm -rf .work npx tsx src/run.tsWindows PowerShell 下可以执行Remove-Item -Recurse -Force .work node dist/run.js5.4 排查链路按日志、目录、蓝图三层检查遇到软件工厂运行失败按下面顺序排查通常比盲改代码更有效。先看运行日志。日志里应该包含factory xxx: start、stage xxx: run xxx、part not found等关键信息。如果日志没有说明日志写得不全需要先补日志。再看工作目录。运行ls -R .work确认前一个部件是否真的生成了文件。如果文件不存在问题出在生成部件如果文件存在但内容不对问题出在配置写入部件。再检查蓝图。逐行对照 Blueprint 里的stage.name、stage.part和with配置确认部件名拼写一致、配置字段是否和 Part 内部读取的键匹配。下面是一张常见问题对照表现象检查点建议part not foundBlueprint 里的 part 名和注册名是否一致统一用常量或注册表打印所有可用部件stage ... failedPart 内部抛出的异常信息是不是清晰在 Factory 层包装时附加 stage 和 part 名称配置文件缺失前一个部件的输出有没有写入 context.outputs后一个部件从 context.outputs 读取生成内容总是旧版依赖包、模板、Blueprint 版本不在同一个版本体系给 Blueprint 增加 version并在日志中打印每次执行结果不一致外部调用没有记录输入输出或没有清理目录执行前清理Part 内部记录输入摘要和响应摘要Agent 调用超时没有设置 timeout、retry、max tokens在 Agent Part 适配器中统一设置超时和重试排查的原则是先把问题定位到“哪一个部件”再定位到“哪一行代码”。如果日志里连 stage 名都没有就要先修日志而不是继续查业务逻辑。6. 生产化落地清单和可复用最佳实践6.1 学习环境和生产环境的差异要分成两套标准本地跑通不算落地生产环境对软件工厂的要求会高很多。下面这张表是关键差异维度学习环境生产环境蓝图存储本地 YAMLGit 仓库、配置中心或版本化制品部件注册代码手动 register通过 SPI、插件目录、服务发现加载Context 状态内存变量持久化到数据库或对象存储日志console.log结构化日志、Trace ID、运行记录密钥写在 YAML 里从 Secret Manager 或环境变量注入校验手动查看文件自动 Quality Gate失败阻断产物发布Agent 调用本地 Mock模型网关、鉴权、配额、超时、审计回滚删除目录重跑保留上一版产物支持一键回滚学习环境可以忽略很多工程细节但

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价