news 2026/5/4 8:34:27

设计模式中的工厂模式业务实战-生产环境实际在用的

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
设计模式中的工厂模式业务实战-生产环境实际在用的
当创建一个对象非常复杂的时候,可以使用工厂模式来应付

我这边的真实场景是连锁餐饮/零售里最传统的一件事:门店每天要做库存盘点。只要是连锁店,门店店员基本都是天天拿着盘点单挨个数货,目的就是把系统里的物料库存矫正回来,这是整个进销存链路里非常核心的一次动作——盘点做错了,后面的订货、生产、报表全部跟着跑偏。

这篇文章就围绕这条“门店盘点”链路里的一张盘点单,展开讲讲我们为什么用工厂模式来创建它,以及这段代码是如何在生产环境里长期稳定跑着的。

顺带交代一下背景:目前我司是按 领域驱动设计(DDD)来搭建微服务的,盘点单是一整个“聚合”,有自己的 聚合根、 实体和 值对象,也有对应的仓储接口和实现。

如果你对 DDD 里的“聚合根”“实体”“值对象”等概念还不太熟,建议先简单读一读相关的入门资料,脑子里先有个大致轮廓,再回来看下面的代码会更顺。

不讲那些“工厂模式的定义”“简单工厂 vs 抽象工厂”的八股,而是老老实实拆一段真实业务代码:

  • 这段代码为什么必须用工厂模式?
  • 工厂到底帮我们收拾了哪些“对象创建地狱”?
  • 它在生产上到底解决了什么实际问题?
说明:文中的类名做了简单脱敏,保留结构和关键字段,逻辑与线上代码一致,只是为了方便公开分享和对外交流。

盘点单的真实长相:远比一个 DTO 复杂

先把业务场景说清楚,否则很难体会到“为什么创建一个对象会这么难”。

我们要解决的是门店库存盘点的问题,每次盘点会生成一张“盘点单”,大致长这样:

  • 盘点单主信息(主表)
    • 门店 ID / 名称
    • 盘点模板 ID / 类型(每日 / 每周 / 临时)
    • 单据编号code
    • 状态status(暂存 / 已审核)
    • ERP 同步状态erpStatus
    • 盘点日期createDate
    • 开始时间 / 结束时间
    • 创建人 / 更新人 / 用户 ID
  • 盘点明细行(子表)
    • 物料 ID / 编码 / 名称 / 规格
    • 盘点单位unit
    • 盘点数量qty
  • 模板规则(模板聚合根里)
    • 某些模板支持“跨日盘点”(比如凌晨 2 点算前一天)
    • 某些模板只允许一天一次

这些字段分散在不同地方:

  • 前端传来的SubmitCheckingDocsCommand(用户输入 + 一些基础信息)
  • 数据库里的盘点模板CheckingTemplateAggregateRoot
  • 模板规则解析辅助类CheckingHelper
  • 单据号生成服务DocsDayIdGeneratorDomainService
  • 历史盘点单(更新时要沿用原来的code

从数据来源看,构造一张盘点单至少要拼接4类信息:

  • 用户提交的命令对象
  • 模板的主数据 + 规则
  • 单据号生成器输出
  • 历史单据(更新场景)

如果把这些逻辑散落在 Controller / Service 里,后期维护基本是灾难。


调用链拆开看:工厂藏在这条“提交流水线”的中段

这是提交盘点单时的大致调用链:

真正负责“把一堆数据拼成完整聚合根”的,正是中间的CheckingDocsFactory

  • Controller 负责 HTTP / 参数校验
  • ApplicationService 负责跨服务编排(比如校验门店、调用库存中心)
  • DomainService 负责并发控制、业务校验(今天是否已盘点)
  • Factory 负责“复杂对象的构造”
  • Repository 负责持久化

在这条调用链上,只有一层关心“盘点单内部到底有多少字段、这些字段从哪来”——就是工厂。这样一来,修改构造逻辑时只需要看一个类,而不是满工程搜索new CheckingDocsAggregateRoot


代码主角:盘点单工厂的两条构造路径

这个工厂类有两个核心方法,对应读写两条路径:

  • factoryOf(CheckingDocsCreator, List<CheckingDocsItemCreator>)
    • 用于从数据库 PO 还原聚合根(读侧)
  • factoryOf(SubmitCheckingDocsCommand)
    • 用于从前端命令创建 / 更新聚合根(写侧)

先看写侧,这才是“对象创建地狱”真正爆发的地方

public class CheckingDocsFactory { private final DocsDayIdGeneratorDomainService docsDayIdGenerator; private final CheckingTemplateRepository templateRepository; private final CheckingHelper checkingHelper; // 读侧:从数据库 Creator + ItemCreator 还原聚合根 public CheckingDocsAggregateRoot factoryOf(CheckingDocsCreator creator, List<CheckingDocsItemCreator> itemCreators) { CheckingDocsMainEntity main = convertToMainEntity(creator); List<CheckingDocsItemEntity> items = convertToItemEntities(itemCreators); return CheckingDocsAggregateRoot.builder() .checkingDocsMainEntity(main) .checkingDocsItemEntityList(items) .build(); } // 写侧:从提交命令创建 / 更新聚合根 public CheckingDocsAggregateRoot factoryOf(SubmitCheckingDocsCommand command) { List<CheckingDocsItemEntity> items = buildItemsFromCommand(command); NameTypeAndDate info = resolveNameTypeAndDate(command); String code = resolveCode(command); CheckingDocsMainEntity main = buildMainEntity(command, info, code); return CheckingDocsAggregateRoot.builder() .checkingDocsMainEntity(main) .checkingDocsItemEntityList(items) .build(); } //补充说明,下面这个不是真实方法,是写本文时,用来说明创建对象的复杂步骤的。真实是使用上面两个factoryOf去创建对象的 public CheckingDocsAggregateRoot create(SubmitCheckingDocsCommand cmd) { // 1)组装明细 List<CheckingItem> items = cmd.getMaterialList().stream() .map(m -> new CheckingItem(m.getId(), m.getCode(), m.getQty(), m.getUnit())) .toList(); // 2)根据模板或临时盘点决定名称、类型、盘点日期 String name = cmd.getName() != null ? cmd.getName() : "临时盘点任务"; String type = cmd.getTemplateTypeCode(); LocalDate checkingDate = resolveCheckingDate(cmd.getTemplateId()); // 3)更新沿用旧单号,新建生成新单号 String code = cmd.getDocsId() != null ? loadOldCode(cmd.getDocsId()) : generateNewCode(); // 4)构造聚合根 return new CheckingDocsAggregateRoot(code, name, type, checkingDate, items); } }

这段工厂代码把“盘点单构造过程里所有会变的东西”都集中在了一个地方:

  • 明细行构造 + 数量合法性校验
  • 基于模板的名称 / 类型 / 盘点日期决策
  • 单号生成策略(新建 vs 更新)
  • 状态 / ERP 状态 / 开始结束时间处理

如果没有工厂,这些逻辑极大概率会被填进 Controller / ApplicationService / DomainService 里,最后谁都不敢碰。


调用方视角:DomainService 只关心“要不要提交”,不关心“怎么构造”

再往上看一层,领域服务CheckingDocsDomainServiceImpl.submitDocs是怎么用工厂的:

public class CheckingDocsDomainService { private final CheckingDocsFactory factory; private final CheckingDocsRepository repository; public CheckingDocsAggregateRoot submit(SubmitCheckingDocsCommand cmd) { // 领域服务更关心“能不能提交”,而不是“对象怎么 new” checkRepeatChecking(cmd); CheckingDocsAggregateRoot root = factory.create(cmd); return repository.saveOrUpdate(root); } }

在这里,领域服务关注的是:

  • 并发控制:用 Redisson 锁避免同一门店并发提交
  • 业务约束:今天是否已经做过模板盘点
  • 提交意图:是“暂存”还是“已审核”

它完全不关心“盘点单内部怎么拼起来”——那是工厂的工作。

这个分工非常干净:

  • DomainService 负责“要不要创建 / 更新”“什么时候允许创建”“怎么控制并发”
  • Factory 负责“怎么构造这个复杂对象

这正好踩中了工厂模式最适合的点:当创建一个对象非常复杂时,用工厂把复杂度收进去,让调用方只关心“我要一个什么对象”,而不是“这个对象细节怎么拼”。


读侧也走工厂:读写同一口径,出问题好排查

这套工厂不仅管写入时的创建,还负责从数据库还原聚合根。

看仓储实现里的读逻辑(核心部分):

public class CheckingDocsRepository { public CheckingDocsAggregateRoot findById(Long id, boolean withItems) { // 这里的关键点是:从数据库查出的原始数据,不直接 new 聚合根 CheckingDocsCreator creator = loadMainRecord(id); List<CheckingDocsItemCreator> itemCreators = withItems ? loadItems(id) : List.of(); return factory.create(creator, itemCreators); } }

这样设计有几个直接好处:

  • 写入:SubmitCheckingDocsCommand → factoryOf(command)
  • 读取:PO → Creator → factoryOf(creator, items)

读写两条路径在“如何构造聚合根”这一步完全共用同一个工厂,提高了一致性:

  • 新增字段时,只要工厂多处理一个字段,读写两侧立刻保持一致
  • 排查某个字段错误时,只需要看工厂里那一处逻辑
  • 自动化测试可以只围绕工厂写用例,覆盖读写两侧的构造流程


为什么说“创建一个对象非常复杂时,就该用工厂”?

结合这段代码,回头看那句非常朴素的话:

当创建一个对象非常复杂的时候,可以使用工厂模式。

落到盘点单这个场景,“复杂”具体体现在哪些点?

  • 数据来源多样
    • 命令对象:前端传参
    • 模板聚合根:名称、类型、规则
    • 时间辅助类:盘点日期的计算
    • 单号生成服务:按业务模块生成流水号
    • 历史单据:更新时沿用code
  • 分支条件多
    • 临时盘点 vs 模板盘点:
      • 名称来源不同
      • 盘点类型不同
      • 盘点日期算法不同
    • 新建 vs 更新:
      • 单号生成策略不同
    • 状态不同:
      • 是否需要写endTime
  • 构造阶段就需要做业务校验
    • 每一行明细的数量必须 ≥ 0
    • 盘点单位不能为空
    • 模板必须存在且能够推导出盘点日期

如果把这些 if/else、校验、查询库、调用辅助类的逻辑都堆在 Controller 或 Service 里,最后代码会变成:

  • 不敢重构
  • 不敢复用
  • 不敢给新人改

而工厂模式在这里做的事情很简单:

  • 把所有“如何构造聚合根”的逻辑归拢到一个类里
  • 调用方只传入上下文(命令 / Creator),只关心“我要一个完整的盘点单”
  • 一旦字段或规则变化,只需要修改工厂,不需要满工程找 new

这是我更愿意在团队里强调的“工厂模式”:不是 UML 图上的模式,而是解决工程里“对象构造复杂度爆表”的那块砖。


这套工厂在生产上的实际收益

说几个我在生产环境里感受最明显的收益。

1. 改字段的心理负担小了

典型场景:

业务说:“盘点单要加一个字段 X,这个值要根据模板 + 门店某个配置计算出来,还得兼容老数据。”

之前的做法(没有工厂时)大概是:

  • Controller 里加一段 if/else
  • ApplicationService 再加一段
  • Repository 读出来时再补一段

现在有工厂之后:

  • 直接在CheckingDocsFactory里接入 X 的构造逻辑
  • Creator / Command 衔接好之后,读写两边自动共用这一套构造

维护成本明显下降,代码的“风险集中点”也更清晰。

2. 线上排查路径变短了

遇到“盘点单某个字段为什么不对”这种问题,我现在基本这么查:

  1. 看数据库里的值
  2. 在工厂里搜索这个字段名
  3. 看它是从哪一个数据源拿到的,逻辑是否走到了

如果是写入错:

  • 要么是命令对象没传;
  • 要么是模板数据本身就有问题;
  • 要么是工厂里拼装逻辑写错。

不会再出现那种“Controller 改了一段、Service 改了一段、Repository 又补了一段”的拼图式排查。

3. 与 DDD 聚合根契合得很好

在这套盘点系统里,CheckingDocsAggregateRoot是一个标准聚合根:

  • 聚合主实体 + 明细实体 + 值对象
  • 包含领域行为(删除、提交、同步 ERP 成功 / 失败)
  • 需要维护业务不变量(状态合法、数量合法等)

在这种情况下,“工厂 + 聚合根”的组合非常自然

  • 工厂负责从各种上下文构造出一个“合法的初始聚合根”
  • 聚合根内部方法负责后续状态变更
  • 仓储只关心持久化,不管内部构造细节


如果你想在自己的项目里落地类似的工厂,可以从这几步开始

我自己在团队里推工厂模式,基本按这个 Checklist 来:

  • 先识别“构造足够复杂的对象”
    • 构造时需要访问3个以上的上下文(命令、配置、数据库、第三方服务)
    • Service 里充斥着各种new+ if/else 去拼对象
    • 经常有人问:“这个字段到底谁来赋值?”
  • 把“构造阶段”的逻辑收拢进工厂
    • 只处理:字段来源决策、复杂构造、构造阶段的校验
    • 不处理:后续业务行为、事务控制、跨聚合协作
  • 读写统一走工厂
    • 写入:命令对象 → 工厂 → 聚合根 → 仓储
    • 读取:PO → Creator → 工厂 → 聚合根
  • 循序渐进迁移
    • 先画出“这个对象现在是在哪几段代码里被拼的”
    • 把这些拼装逻辑平移到工厂,调用方改成只调工厂
    • 保留旧逻辑一小段时间做对比验证,再彻底删掉

这套做法我在盘点模块里反复实践过几次,没遇到过任何事故,主要原因很简单:构造逻辑集中在一个类里,测试和验证都更可控。


最后:别为“用模式而用模式”,而是为“减少痛苦而用模式”

回头看CheckingDocsFactory这段代码,它并不“花哨”,也没有玩什么高阶语法,甚至可以说是很“土”的几百行 if/else + Builder。

但正是这一层工厂,把原本散落在各处的构造逻辑收到了一个地方,让:

  • 新字段有了唯一的落点
  • 线上问题有了清晰的排查入口
  • 读写路径有了统一的构造口径

我现在脑子里对工厂模式的印象,其实就一句话:

当你开始害怕在业务代码里写new的时候,就该给它建一个工厂了。
版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/2 23:36:36

大模型学习路线图:从数学基础到AGI应用全解析_AI大模型学习路线(2025最新)神仙级大模型教程分享

文章提供了大模型学习的七个阶段路线图&#xff1a;从数学编程基础、机器学习、深度学习到NLP和大模型应用&#xff0c;最后是进阶学习。文章介绍了大模型行业前景、薪资水平和学习资源&#xff0c;强调掌握AI技术的重要性&#xff0c;并提供免费学习资料包&#xff0c;帮助小白…

作者头像 李华
网站建设 2026/5/3 13:57:09

Open-AutoGLM API接口实战手册(从入门到高并发优化)

第一章&#xff1a;Open-AutoGLM API接口实战手册&#xff08;从入门到高并发优化&#xff09;快速接入API服务 要开始使用Open-AutoGLM API&#xff0c;首先需获取有效的认证密钥。注册开发者账户后&#xff0c;在控制台生成API Key&#xff0c;并通过HTTP Header传递进行身份…

作者头像 李华
网站建设 2026/5/3 0:24:56

卡帕西2025大模型回顾解读:AI的脑、手、场进化与2026前瞻

最近AI圈有一篇重磅文章——安德烈卡帕西&#xff08;Andrej Karpathy&#xff09;的2025年大模型回顾。 卡帕西的行业影响力毋庸置疑&#xff1a;2016年加入OpenAI&#xff0c;是早期最核心的研究员之一&#xff1b;2017-2022年担任特斯拉人工智能高级总监。他的观点横跨软件端…

作者头像 李华
网站建设 2026/5/2 15:41:03

告别文本低效协作:潜在空间多智能体系统(LatentMAS)全面解析

LatentMAS是一种革命性的多智能体协作框架&#xff0c;通过在潜在空间中直接共享智能体的内部"思维状态"&#xff0c;而非传统文本交流&#xff0c;实现了高效、无损的信息交换。这种"心灵感应"式协作基于推理表达力、通信保真度和协作复杂度三大支柱&…

作者头像 李华
网站建设 2026/4/30 11:37:42

在 GeckoCIRCUITS 上开发新工具模块的方法

简介 最近在使用开源电力电子仿真软件 GeckoCIRCUITS 进行仿真时&#xff0c;我想使用离散时间控制器&#xff0c;但是 GeckoCIRCUITS 中没有这个模块&#xff0c;需要使用工具栏中自带的 JAVA 模块添加算法代码。但是我想把类似这样常用的模块固定在工具栏里&#xff0c;方便使…

作者头像 李华
网站建设 2026/4/23 10:09:53

9个降aigc工具推荐!继续教育学生高效避坑指南

9个降aigc工具推荐&#xff01;继续教育学生高效避坑指南 AI降重工具&#xff0c;让论文更自然、更安全 在继续教育的学习过程中&#xff0c;论文写作是不可避免的一环。然而&#xff0c;随着人工智能技术的广泛应用&#xff0c;许多学生发现自己的论文出现了明显的“AI痕迹”…

作者头像 李华