
工厂模式这个名字刚接触的时候容易让人觉得高深其实它在 JavaScript 项目里出现的频率远超你的想象。你写过的第一个根据类型返回不同对象的函数本质上就是在用工厂思想。很多同学在业务代码里天天写if (type a) return new A()这种逻辑只是还没意识到这玩意儿有个正式名字也不知道怎么把它组织得更优雅而已。我打算用一篇文章把这个事儿讲透先用大白话拆解工厂模式的本质然后带你把一段真实的if-else 地狱代码一步步重构为工厂实现再聊聊它在业务里的高频玩法、和其他模式的搭配最后把容易踩的坑一次性说完。不管你是刚入门的前端新人还是已经被业务代码折磨许久的同学看完应该都能直接上手用起来。1. 工厂模式到底解决了什么问题工厂模式的核心本质只有一句话把对象的创建和使用分离。调用方只需要告诉工厂我要什么不需要关心这个东西到底怎么 new 出来它内部依赖了哪些乱七八糟的组件。1.1 没有工厂的日子长什么样想象一下你的项目里有一个消息推送的需求短信、邮件、站内信三种渠道。没工厂的时候业务代码长这样function sendMessage (type, content, target) { if (type sms) { const sms new SmsSender({ apiKey: config.sms.apiKey }) sms.send(content, target) } else if (type email) { const email new EmailSender({ host: config.email.host, user: config.email.user, pass: config.email.pass }) email.send(content, target) } else if (type inbox) { const inbox new InboxSender() inbox.send(content, target) } else { throw new Error(Unknown message type: ${type}) } }这段代码最大的问题不是长而是调用方知道得太多了。sendMessage这个函数本来只想发个消息结果它得认识SmsSender的构造函数、得知道短信服务需要apiKey、得知道邮件服务需要 host/user/pass。假如哪天短信服务商换了新 SDKSmsSender的构造函数多了一个secret参数你就得跑到所有调用sendMessage的地方去改或者至少改这一大片逻辑。更麻烦的是这个函数会被持续堆代码。今天加个钉钉机器人明天加个企业微信后天加个 App 推送全往这里塞最后变成一个几百行的巨型函数看着就头疼。1.2 工厂的本质把创建细节关进小黑屋工厂模式做的事情就是把上面那段判断逻辑收编到一个专门的工厂函数里。调用方改成这样const sender MessageSenderFactory.create(sms) sender.send(content, target)调用方不再需要关心SmsSender怎么构造它只需要知道两件事工厂能给我一个发短信的发送器这个发送器有send方法。至于这个发送器内部是直连运营商接口还是走第三方网关调用方一概不知。这就是工厂模式的核心价值——解耦。它把创建什么和怎么使用划分成了两个独立的区域。创建区域的复杂度由工厂统一管理使用区域只需要面对一个稳定的接口。另外工厂模式还有一个隐藏的好处统一收敛创建逻辑。比如所有 sender 创建的时候都要打一条日志、都要做参数校验、都要埋点统计这些横切逻辑可以统一放在工厂里而不是散落在每个调用点。这种集中管控的能力是后期做监控、做灰度、做降级的基石。2. 三种工厂形态一次分清很多人学工厂模式容易懵就是因为市面上资料把简单工厂、工厂方法、抽象工厂这三个概念混在一起讲。我帮大家把区分它们的那条线拉直。2.1 简单工厂一个函数包打天下简单工厂是最朴素的形态就一个函数根据参数返回不同的实例。上面的MessageSenderFactory.create本质就是一个简单工厂。class MessageSenderFactory { static create (type) { switch (type) { case sms: return new SmsSender(...) case email: return new EmailSender(...) case inbox: return new InboxSender() default: throw new Error(Unknown type: ${type}) } } }注意简单工厂的简单体现在它是一个集中的创建入口。它适合产品线不算太复杂、创建逻辑相对稳定的场景。缺点是违反开闭原则——每加一种类型你都得改这个函数。2.2 工厂方法把创建权交给子类工厂方法模式的思路是把工厂本身也抽象成接口每一种产品对应一个专门的工厂子类。调用方依赖的是工厂接口具体用哪个工厂子类来创建由上层决定。class SmsSenderFactory { create () { return new SmsSender(...) } } class EmailSenderFactory { create () { return new EmailSender(...) } } // 调用方 const factory getCurrentFactory() // 可能从配置里拿 const sender factory.create() sender.send(content, target)它的核心是让创建逻辑可以被继承、被扩展而不需要改动已有代码。缺点是类数量会膨胀JS 里面相对少见这种严格的类层级写法更常见的变体是用注册表来模拟这种灵活性下一章会讲。2.3 抽象工厂创建一族相关的对象抽象工厂解决的是创建一族产品的问题。比如一个 UI 组件库既要生产暗色主题的按钮、输入框、弹窗又要生产亮色主题的对应组件。你不能只换按钮不换输入框主题必须配套。class DarkThemeFactory { createButton () { return new DarkButton() } createInput () { return new DarkInput() } createModal () { return new DarkModal() } } class LightThemeFactory { createButton () { return new LightButton() } createInput () { return new LightInput() } createModal () { return new LightModal() } } // 使用 const themeFactory theme dark ? new DarkThemeFactory() : new LightThemeFactory() const button themeFactory.createButton()抽象工厂保证了产品族内部的一致性不会出现按钮是暗色的、输入框却是亮色的情况。但在前端业务代码里抽象工厂的使用频率相对较低更多出现在框架底层和组件库设计中。2.4 对 JS 项目来说怎么选我的个人判断是简单工厂最常用工厂方法的变体最实用抽象工厂看场景。JavaScript 作为动态语言很多重模式都会被打薄。你在 TS 项目里写一个type联合类型 switch的简单工厂就已经覆盖 80% 的需求。只有当创建逻辑本身变得很重、需要独立扩展时才考虑变成注册表或者工厂方法。3. 实战重构从 if-else 地狱到注册表工厂说了这么多概念是时候来点硬货了。我挑一个非常经典的前端场景数据导出器。用户在前端勾选数据选择导出格式CSV / JSON / Excel系统生成对应文件。3.1 初始实现每个调用点都在做判断// 页面A function handleExport(data) { const format getSelectedFormat() if (format csv) { const exporter new CsvExporter() const result exporter.export(data) download(result, data.csv) } else if (format json) { const exporter new JsonExporter() const result exporter.export(data) download(result, data.json) } else if (format excel) { const exporter new ExcelExporter() exporter.exportWithSheet(data) // 注意这个接口还不一样 } }这段代码的问题已经很典型了ExcelExporter的方法名不一样exportWithSheet导致分支里没法统一调用。等第三个页面也要做导出时你大概率会把这段 if-else 复制粘贴过去然后噩梦就开始了。3.2 第一步统一产品接口要让工厂模式生效必须先让所有产品实现同一个接口。这里我给三个导出器都统一成export(data)方法返回文件内容或 Blobclass CsvExporter { export (data) { // 转换成 CSV 文本 return csvText } } class JsonExporter { export (data) { return JSON.stringify(data, null, 2) } } class ExcelExporter { export (data) { return this._generateWorkbook(data) // 内部逻辑可以继续保持复杂 } }统一接口的意思是它们对外暴露的调用方式必须一致。这样上层代码才能用同一种姿势去使用不同的产品这也是面向接口编程的基石。3.3 第二步抽一个简单工厂class ExportFactory { static create (format) { switch (format) { case csv: return new CsvExporter() case json: return new JsonExporter() case excel: return new ExcelExporter() default: throw new Error(Unsupported export format: ${format}) } } }调用方立刻清爽了很多function handleExport(data, format) { const exporter ExportFactory.create(format) const result exporter.export(data) download(result, data.${format}) }到这一步调用方不再关心具体的导出类只关心工厂给我一个能导出数据的对象。但注意这个switch还是得在新增格式时改动有没有办法做到完全不用改有就是用注册表。3.4 第三步用注册表消灭 switch注册表模式的思路非常简单把类型 - 创建函数的映射关系放到一个 Map 里支持动态注册class ExportFactory { static registry new Map() static register (format, creator) { ExportFactory.registry.set(format, creator) } static create (format) { const creator ExportFactory.registry.get(format) if (!creator) { throw new Error(No exporter registered for format: ${format}) } return creator() } } // 每种导出器自己完成注册 ExportFactory.register(csv, () new CsvExporter()) ExportFactory.register(json, () new JsonExporter()) ExportFactory.register(excel, () new ExcelExporter())这样ExportFactory本身保持稳定后续新增一种xml导出只需要写一个XmlExporter并在模块加载时调用ExportFactory.register(xml, () new XmlExporter())即可一行都不用改已存在的代码。这就是开闭原则的标准示范对扩展开放对修改关闭。注册表模式之所以适合 JS是因为函数是一等公民。Map 直接把类型字符串和创建函数关联起来比建立一整套工厂类层级要轻得多、直觉得多。3.5 注册表生产环境升级按需加载上面这个注册表还有一个隐患如果ExcelExporter是一个体积庞大的依赖比如引入了xlsx库在页面加载时就注册会导致首屏包体积变大。这时候可以用异步工厂 动态 import 来做按需加载ExportFactory.register(excel, async () { const module await import(./exporters/ExcelExporter) return new module.ExcelExporter() }) // create 也要支持异步 class ExportFactory { static async createAsync (format) { const creator ExportFactory.registry.get(format) if (!creator) throw new Error(No exporter registered for format: ${format}) return await creator() } } // 使用 const exporter await ExportFactory.createAsync(excel) const result exporter.export(data)这样xlsx库只会在用户真正点击导出 Excel时才加载大大优化首屏性能。这种延迟注册 懒加载的组合在大型中后台系统里非常吃香。4. 工厂模式在业务项目中的 4 个高频场景光看代码还不够我结合平时在项目里看到的实际场景把工厂模式最常出现的几个地方列出来方便你遇到类似需求时第一时间想到它。4.1 多通道消息通知这是我在第 1 章举的例子。短信、邮件、站内信、App 推送、企微机器人……每种通道的初始化参数和 API 完全不同。用工厂统一管理后上层只需要维护一个通道类型字段就能拿到对应发送器。进一步可以配合配置文件const channelConfig { sms: { weight: 10, retryCount: 3 }, email: { weight: 5, retryCount: 2 }, inbox: { weight: 1, retryCount: 0 } } function createNotifyManager () { const channelMap Object.entries(channelConfig).map(([type, config]) { const sender NotifyFactory.create(type) return { type, sender, ...config } }) // 后续可以基于 channelMap 做加权路由、降级等逻辑 }这个场景里工厂的价值不只是创建对象更是为上层策略提供统一入口。后面做消息路由、失败重试、流量控制都变得很顺手。4.2 数据解析与接口适配大家在对接第三方 API 时一定遇到过这种场景同样一个用户信息概念A 系统的字段叫userNameB 系统的字段叫nameC 系统的字段叫nick。如果业务层直接面向这些差异代码会遍布补丁。工厂模式给我们一种优雅的解法为每一种数据源创建一个解析器工厂负责根据当前数据源类型返回对应解析器class UserParserFactory { static create (sourceType) { switch (sourceType) { case systemA: return new SystemAUserParser() case systemB: return new SystemBUserParser() default: return new DefaultUserParser() } } } // 业务层 const parser UserParserFactory.create(sourceType) const normalizedUser parser.parse(rawData)这个场景下工厂不生产业务对象而是生产解析策略对象。它隔离了数据源差异让业务层只认normalizedUser这一种结构。我的经验是但凡你的项目里要对接两个以上结构不同的外部数据源这个模式就能派上大用场。4.3 UI 弹窗组件体系前端组件库很喜欢用工厂模式。典型的需求是根据不同状态弹出不同的提示框——成功、失败、警告、确认删除。class DialogFactory { static create (type, options) { switch (type) { case success: return new SuccessDialog(options) case error: return new ErrorDialog(options) case warning: return new WarningDialog(options) case confirm: return new ConfirmDialog(options) default: return new Dialog(options) } } } // 调用 DialogFactory.create(confirm, { title: 确定删除这条记录吗, onConfirm: () deleteRecord() }).show()这里我更推荐配合注册表 异步加载把每种弹窗做成独立组件按需注册避免在一个文件里堆太多逻辑。组件库的扩展性就是这么一点点抠出来的。4.4 表单校验器构建表单校验是一个很容易被写成一坨 if-else 的地方。字段的校验规则根据场景搭配每个字段可能有必填、长度限制、正则匹配、自定义校验等校验器工厂可以根据字段配置创建对应的校验链class ValidatorFactory { static create (rules) { const validators rules.map(rule { switch (rule.validator) { case required: return new RequiredValidator() case maxLength: return new MaxLengthValidator(rule.max) case regex: return new RegExpValidator(rule.pattern) case custom: return rule.customValidator default: return null } }).filter(Boolean) return new ValidatorChain(validators) } }这种写法让校验配置化、可复用新增加一种校验规则时只需要新增一个 validator 类并在工厂里注册不用改动任何已有表单的校验代码。配置驱动 工厂创建是表单类中后台系统的绝佳搭配。5. 工厂模式的黄金搭档与容易混淆的邻居学习设计模式时最大的困惑往往是模式之间的区别。这里我挑两个最容易和工厂搞混的模式来说清楚。5.1 工厂 单例保证全局只有一个实例有些对象在一个应用生命周期里只需要存在一份比如前端的状态管理 store、配置管理器、日志上报器。但它的创建过程可能很复杂我们希望保持一个统一的工厂入口同时保证创建出来的一定是同一份实例。解决方式简单粗暴在工厂里做判断已经创建过就直接返回缓存实例。class StoreFactory { static storeMap new Map() static get (name) { if (!StoreFactory.storeMap.has(name)) { const store new Store(name) StoreFactory.storeMap.set(name, store) } return StoreFactory.storeMap.get(name) } }这就是常说的单例工厂。工厂负责创建单例策略负责去重。两者互不冲突组合使用后既能保证创建入口统一又能控制实例数量。5.2 工厂 vs 策略模式创建和执行的差别工厂模式和策略模式在结构上很像都有一组策略类 一个选择器。很多人会搞混。其实一句话就能说清工厂模式管理的是创建策略模式管理的是执行。工厂模式的产物是对象本身调用方拿到这个对象之后关心的是怎么用它。策略模式的产物是算法调用方不关心算法内部怎么跑只关心输入输出。以消息通知为例MessageSenderFactory负责创建发送器对象对象内部可能有自己独立的send逻辑而如果我们要在多种发送策略之间切换比如优先发短信失败转邮件这个策略选择逻辑适合放在策略模式里。注意这里的策略模式通常用send算法来建模而工厂负责生产这些策略。两者其实经常搭档出现不冲突。我在项目里见过的最常见的错误是把策略模式的执行逻辑写进工厂里工厂既要管创建又要管策略选择最后代码变得难以维护。原则是——创建归工厂执行归策略各司其职。6. 实战中容易踩的坑一次说完6.1 常见问题速查表症状可能原因解决办法工厂函数被疯狂传参参数列表长达七八个创建逻辑太复杂工厂承载了不该承载的责任把参数封装成配置对象考虑拆成多个工厂工厂内出现大量 if-else 且判断条件不是简单的 type工厂的职责扩大到业务逻辑判断了只保留根据类型创建的职责业务判断移到上层新增产品类型后工厂类代码被频繁修改用了硬编码方式而非注册表改为注册表模式产品自己负责注册工厂返回的对象类型不确定调用方经常做类型判断产品接口没统一先统一产品接口再考虑工厂项目到处是工厂找不到真正的业务逻辑在哪追求设计模式过度工程化按实际需求使用不为模式而模式6.2 别做为工厂而工厂有一种声音说工厂模式是代码洁癖的产物。我理解这个观点但它只说对了一半。如果你创建一个对象只需要new Foo()后面也没有扩展的迹象那确实没必要绕一层工厂。但一旦出现以下信号就应该考虑引入创建逻辑开始变复杂构造函数需要根据环境变量、配置等做很多准备工作。同一类型的对象有多种变体且变体数量还在不断增加。希望降低调用方对具体类的依赖方便后期替换实现或做单元测试 mock。如果以上一个都没中那就别折腾。我见过不少初学者把 API 请求也包了一层工厂一个getUser功能拆出七八个文件结果维护成本反而更高。设计模式的价值是让代码变简单不是让它变复杂。6.3 一个锦囊命名规范要统一工厂模式用不好很多时候是命名混乱导致的。我建议团队内约定工厂类统一以Factory结尾工厂创建方法统一叫create或get单例场景用get更贴切。产品类统一实现相同的接口文件放在同一目录下。这样一看目录结构任何人都能快速定位创建逻辑在哪里。提示如果项目里用的是 TypeScript工厂模式的体验会更好。类型联合 映射类型可以把工厂能创建哪些类型约束得死死的写错类型名直接编译报错比运行时抛异常友好得多。我在实际开发里还有一个习惯给工厂加上单元测试尤其是新增类型挂接注册表之后一定要验证create方法能否正确返回对应实例。这个测试写起来非常简单但能防止哪天同事改注册表时不小心把映射写错。7. 最后分享一点个人体会做前端这几年我逐渐意识到设计模式不是拿来背的而是用来解决真实痛点的。工厂模式之所以被我频繁使用就是因为它精准地吃掉了创建对象时的那堆 if-else让我能把目光集中在业务流上。如果你刚学完这个模式我的建议是回自己项目里找一段根据参数创建不同对象的代码尝试用注册表模式重构一遍。不用大改特改Colocation 式的渐进式重构就很好——先抽一个工厂函数再把 switch 换成 Map最后把注册器拆出去按模块加载。每一步都是可独立合并的改动风险很低收益却立竿见影。等你在两三个场景里跑通了这套流程再遇到类似需求身体会自然地产生这里应该用工厂的直觉。工厂模式真正带给我们的是一种思维方式的转变从调用方事事躬亲到工厂统筹负责创建调用方专注使用。这个转变一旦发生你再看那些动辄几百行的大函数思路会清晰很多。