工厂模式详解:从简单工厂到抽象工厂的实战演进 我见过最让人头大的对象创建代码是在一个报表导出模块里一个Service方法内联了七八个new每个new后面跟三行长参数。后来业务加了一个按部门定制页脚的需求差不多全组人花了两个下午一个个去排查构造器参数到底该传什么、哪里该改。那会儿我就想要是早一点把创建对象这件事交给专门的机制去管代码库不会这么脆弱。这种机制就是设计模式里的工厂模式。工厂模式听起来玄乎其实本质就一句话创建对象的活不让你直接new而是交给一个专门的组件去决定最终实例化谁。它解决的不是怎么写代码的问题而是代码该怎么组织的问题。这篇文章我按自己的理解把工厂模式的三种形态捋一遍简单工厂、工厂方法、抽象工厂用真实的日志系统演进过程做例子最后说说我平时判断该不该上工厂的四条标准。不管你是刚接触设计模式的初学者还是写了两三年业务代码想重构的老手这篇都能给你一点能直接落地的东西。1. 硬编码new的灾难那种改一处崩三处的绝望怎么来的1.1 需求变更为什么总在new的地方连环爆炸先还原一下最典型的事故现场。某个报表模块早期只有一种PDF导出方式代码里到处是new PdfReportExporter(configA, configB, configC)。随着业务发展出现了Word导出、Excel导出、按模板定制导出于是有人加了if判断有人复制粘贴改参数几十个调用点各写各的。等到按部门定制页脚这个需求下来问题就炸了因为每个调用点都直接依赖了PdfReportExporter的具体构造器只要构造器签名一变所有new的地方全都编译报错。表面上你只是改一个导出类的构造参数实际上你是在改全项目所有引用它的地方。这种情况我见过太多次根因不是需求变态而是调用方和具体实现之间的耦合太深。这种耦合的正式说法叫依赖倒置被违反高层业务代码本该依赖一个稳定的抽象接口结果直接绑死在了底层具体类上。用大白话讲调用方不仅知道我想要一个能导出的东西还得知道这玩意儿是用PDF还是Word实现的、构造时要传哪些参数。知道得越多改起来就越痛。1.2 硬编码创建还要你赔上可测试性除了改代码痛硬编码new还有一个容易被忽略的代价测试难写。假设某个支付对账服务里直接写了new FileBasedDataSource(/etc/pay/config.ini)你在单元测试里想验证对账逻辑就不得不真的去准备一个配置文件、真的让代码去读磁盘。想换成内存版数据源对不起new已经写死在方法里了你动不了。正确做法是让业务逻辑依赖一个DataSource接口具体实例由外部传入或者通过工厂拿到。测试的时候你只需要用一个实现了DataSource接口的假对象替换进去整个测试就轻量了。工厂模式在这种场景里最大的价值不是让代码看起来更设计模式而是让依赖可以被替换、被Mock、被控制。这是硬编码new永远给不了的自由。1.3 一个类比点单和进后厨开火的区别我经常给团队里新同学打一个比方没有工厂的时候业务代码就像客人去餐厅吃饭得自己跑进后厨自己打开冰箱找食材、自己起锅烧油炒菜。你当然能炒出一盘菜但你从此被绑死在后厨了——冰箱进新货、燃气灶换了、师傅换了配方你都受影响。工厂模式要做的事就是让客人坐在餐桌前看着菜单说一句我要宫保鸡丁然后由后厨统一决定用哪块鸡胸肉、放多少花生米。客人不需要关心食材怎么来的、锅在哪、火候怎么控制只需要拿到最终那盘菜。代码里的菜单就是工厂接口后厨就是工厂实现一盘菜就是返回的对象。这个类比还能解释工厂的另一个好处多个食材的搭配关系被集中管理了。比如宫保鸡丁必须配花生米如果你每次自己进后厨炒可能有人忘了放花生米有人放了腰果产品就乱套了。工厂把整套搭配规则锁死在一个地方谁也别想乱来。2. 三种工厂一条线简单工厂、工厂方法、抽象工厂的演进逻辑2.1 简单工厂把new集中到一个收银台很多人以为三种工厂是三个并列选项其实不是。它们更像一条需求催生出来的升级路线从集中管理到开放扩展再到产品成套。首先是简单工厂也叫静态工厂。它用一个类里面的一两个方法集中处理所有创建逻辑。调用方传一个类型参数工厂内部用switch或if来判断到底new哪个对象。public class LoggerFactory { public static Logger create(LoggerType type) { switch (type) { case CONSOLE: return new ConsoleLogger(); case FILE: return new FileLogger(); case DATABASE: return new DatabaseLogger(); default: throw new IllegalArgumentException(未知日志类型: type); } } }这段代码的好处在哪所有new都收拢到一个入口了调用方再也不用关心具体类名和构造参数。缺点也很明显每新增一种日志类型你得打开LoggerFactory改这个switch加一个case。它违反了开闭原则的一半——对扩展不算友好对修改倒是相当友好因为所有修改都集中在一个文件里不容易漏。简单工厂适合变体不多、变化不频繁的场景。如果你确定未来半年都不会有新类型用它最省事。一上来就上抽象工厂属于拿大炮打蚊子。2.2 工厂方法把选谁下沉到子类简单工厂的问题在于创建逻辑被if/switch写死无法在不动已有代码的前提下扩展。工厂方法就是来解决这个问题的。工厂方法的核心思想是定义一个创建对象的接口让子类决定实例化哪个类。也就是说选谁这个决策从工厂类里挪出来下沉到各个子类。public abstract class LoggerCreator { // 工厂方法延迟到子类实现 public abstract Logger createLogger(); // 模板方法使用Logger的公共逻辑 public void log(String message) { Logger logger createLogger(); logger.write(message); } } public class FileLoggerCreator extends LoggerCreator { Override public Logger createLogger() { return new FileLogger(); } } public class ConsoleLoggerCreator extends LoggerCreator { Override public Logger createLogger() { return new ConsoleLogger(); } }现在想新增一种数据库日志你只需要新建一个DatabaseLoggerCreator子类完全不用碰已有的FileLoggerCreator和ConsoleLoggerCreator。这就是真正的对扩展开放对修改关闭。工厂方法也有代价每增加一种产品就要增加一个对应的工厂子类。类数量会膨胀结构也会变复杂。如果你的产品类型只有两三种且基本固定工厂方法带来的收益不一定抵得过类数量的成本。2.3 抽象工厂为成套产品立规矩抽象工厂解决的是另一个层面的问题不是单个对象的创建而是一组相关对象的创建。它允许你创建一个产品族并保证这个产品族内部的配套关系是固定的。举个例子。一个日志系统如果只是控制台还是文件简单工厂就够了。但如果我们还要求日志的格式必须是JSON、序列化方式必须是批量压缩、上报通道必须是消息队列而且这三个东西必须成套更换——测试环境用普通文本直接写入本地文件一套生产环境用JSON批量压缩MQ另一套——这时候就需要抽象工厂了。public interface LoggingFactory { LogFormatter createFormatter(); LogCompressor createCompressor(); LogTransport createTransport(); } public class ProductionLoggingFactory implements LoggingFactory { Override public LogFormatter createFormatter() { return new JsonFormatter(); } Override public LogCompressor createCompressor() { return new BatchCompressor(); } Override public LogTransport createTransport() { return new MqTransport(); } }客户端只依赖LoggingFactory这个抽象接口不依赖任何具体实现拿到一个工厂实例后通过它拿到的Formatter、Compressor、Transport天然就是配套的不会出现JSON格式化配了一个本地文件直写这种组合错误。抽象工厂的缺点是新增一个产品维度比如增加一个日志脱敏器所有既有工厂实现都得跟着改。所以它适合产品族结构稳定、但族与族之间切换频繁的场景。2.4 三者的差异对照表维度简单工厂工厂方法抽象工厂封装粒度一个方法/一个类一个工厂接口 多个子类一组产品接口 多个工厂实现核心机制调用方传类型参数子类决定具体实例工厂决定整套产品族扩展方式修改工厂内部判断新增工厂子类新增一整套工厂实现是否必然违反开闭原则会基本不违反不违反但新增产品维度成本高适合场景变体少、变化不频繁单条产品线频繁扩展产品族强配套、成组切换复杂度低中高我见过不少团队在选型时陷入纠结到底用哪一种我的经验是不要一开始就选最复杂的先从简单工厂开始当你在某个switch里开始积累超过五六种case并且预感到下一步还要加的时候再顺势升级到工厂方法。如果出现的是配套产品要整体切换的需求再考虑抽象工厂。让模式跟着需求走而不是让需求迁就模式的华丽。3. 一个日志系统走完三种工厂每一步重构都在解决什么3.1 第一版用简单工厂统一日志来源为了讲明白三种工厂到底在什么场景下登场我用一个真实的演进过程来串。某系统早期日志功能很简单就两种输出控制台和文件。当时的代码并不复杂但问题在于每个业务模块都自己new有的写new ConsoleLogger()有的写new FileLogger()还有的为了统一自己封装了一个工具方法散落各处。第一步重构先引入简单工厂。所有调用方统一走LoggerFactory.create(LoggerType.FILE)创建逻辑集中到一处。这次重构没什么技术含量纯粹是把散落的new收拢。收益立刻出现后来要统一给所有Logger增加一个初始化参数比如日志前缀、默认级别只需要在工厂内部改一处不用全项目搜索替换。3.2 第二版工厂方法让创建行为随环境切换再往后系统需要部署到不同环境测试环境打控制台日志生产环境写文件并异步刷新。如果继续用简单工厂你得在create()里写if (env TEST)这样的判断。分支越来越多工厂内部开始变成一个环境配置分发器这已经超出了简单工厂该干的活。于是升级成工厂方法定义EnvironmentLoggerCreator抽象类测试环境有TestLoggerCreator生产有ProductionLoggerCreator各自实现createLogger()。业务代码只依赖抽象类具体创建行为由当前环境的工厂实例决定。此时你会发现增加的类虽然变多了但每个类都极其干净职责单一改测试环境不影响生产扩展新环境也只是新增一个文件。3.3 第三版抽象工厂处理规范序列化上报的组合约束真正的转折点出现在合规需求。某次安全审计要求所有生产日志必须是JSON格式、需要脱敏、需要批量压缩后上报到统一的日志采集通路。而测试环境的日志可以保持普通文本、直接写入本地文件。说白了我们有了两套完全不同的产品族。如果继续用工厂方法只能保证Logger怎么创建但没法保证Logger依赖的Formatter、Compressor、Transport也是配套的。你可能会不小心在测试环境用了JSON格式或者在生产环境忘了加脱敏器。这时候抽象工厂的价值就体现了把LogFormatter、LogCompressor、LogTransport三个产品绑定在同一个LoggingFactory下TestLoggingFactory和ProductionLoggingFactory各自产出成套的组件谁也别想破坏组合规则。public class TestLoggingFactory implements LoggingFactory { Override public LogFormatter createFormatter() { return new PlainTextFormatter(); } Override public LogCompressor createCompressor() { return new NoOpCompressor(); } Override public LogTransport createTransport() { return new LocalFileTransport(); } }这次升级之后业务代码完全不需要知道生产环境用的什么压缩算法只需要在启动时装配好ProductionLoggingFactory剩下的组件都通过抽象接口拿。3.4 重构记录里最值得回看的三个坑这次日志系统的重构前前后后踩了三个坑写出来给你避一避。第一个坑是万能工厂倾向。第一版重构时我一度想在一个工厂里把所有对象都创建出来包括数据库连接、缓存客户端、配置中心客户端。结果工厂类越来越大什么都要管最后变成了个上帝类。后来忍痛拆开只让工厂负责日志相关对象的创建。记住工厂也应该遵循单一职责它不是一个杂物间。第二个坑是字符串参数地狱。最早写简单工厂的时候用字符串当类型参数调用方传file、db拼错了一个字母编译期完全无感运行期才炸。后来改成枚举LoggerType错误提前暴露。再后来升级到工厂方法直接把类型参数彻底消掉了因为每个子类对应一种产品根本不需要传参。能少传一个参数就少一类错误。第三个坑是组合约束没有提前考虑。我们最开始只有控制台还是文件这一个维度完全没考虑Formatter和Transport将来也会跟着变。等到合规需求来了简单工厂和工厂方法都没法优雅地表达这三样必须成套只能硬着头皮重构到抽象工厂。如果早期就预见到日志格式、压缩方式、上报通道大概率要整体切换一开始直接上抽象工厂反而省事。但话说回来大多数项目一开始看不清这些我也理解关键是重构的时候要果断不要因为代码丑就一直将就。4. 工厂与单例、建造者、原型的边界别把所有创建活都塞给工厂4.1 单例工厂经常要和它签一份合作契约很多人在创建型模式里容易混淆以为工厂模式就是管创建的那单例、建造者、原型不也管创建吗到底用哪个其实这几个模式关注点完全不同。单例解决的是实例数量问题保证一个类全局只有一个实例并提供全局访问点。工厂解决的是实例化逻辑谁来负责的问题。两者经常一起出现——比如日志工厂本身通常是单例因为工厂内部可能持有配置缓存、连接池等状态多实例没有必要但工厂创建的Logger对象本身又往往是多实例的每个业务模块一个。单例管工厂自己工厂管产品对象各司其职。4.2 建造者负责怎么拼工厂负责给谁建造者模式解决的是对象构造过程太复杂的问题。一个对象有十几个可选参数、必填参数如果直接写构造器调用方根本分不清哪个参数是干嘛的代码可读性极差。建造者把组装过程拆成一步步的withXxx()调用让代码读起来像自然语言。工厂和建造者不是替代关系而是合作关系。一个典型的组合是工厂决定返回哪种对象建造者负责这个对象怎么一步步组装。比如ExporterFactory.createPdfExporter()内部可以返回一个new PdfExporter.Builder().withHeader(...).withFooter(...).build()。工厂管方向建造者管拼装。4.3 原型需要的是复制而不是新建原型模式解决的是如何高效地获取一个已有对象的副本。如果一个对象的创建开销很大或者需要大量相似对象用clone()复制比重新new再设置一遍属性更划算。它不是替代工厂而是和工厂并列的另一种创建策略。实际选型时可以这样想如果每次要的对象内部状态差异很大、组装路径不一样工厂更合适如果每次拿到的都是同一个模板的变体只是个别字段不同原型更合适。比如报表模板几百个部门可能共用一套基础模板只是页脚不同这时候从原型复制再改改比每次从头组装高效得多内存和CPU都能省一笔。4.4 四种模式的分工速查表模式核心问题典型回答和工厂的关系简单工厂/工厂方法/抽象工厂创建逻辑谁来负责专门的工厂组件负责实例化本身单例实例数量是否唯一全局只有一个工厂经常做成单例建造者复杂对象如何优雅组装步骤化连缀式构造工厂内部可调用建造者原型如何低成本得到相似对象复制已有实例替代新建的一种策略这张表对我选型的作用很大先问我到底愁什么。愁的是调用方不该知道具体类——上工厂愁的是全局到处new同一个对象太浪费——上单例愁的是构造参数太多写得像天书——上建造者愁的是每次新建太重了——上原型。搞混了需求模式就变成了负担。5. 工厂不是万灵药上帝工厂和过度抽象的四条翻车记录5.1 翻车记录一上帝工厂把所有创建权限揽到自己身上我见过一个项目所有对象都从一个ObjectFactory.createXxx()方法里取从数据库连接池到DTO从工具类到业务服务全塞在一起。刚开始确实方便所有创建逻辑都集中了嘛。后来这个工厂的代码超过两千行任何人改需求都要动这个类频繁的冲突让人抓狂没人说得清某个方法到底依赖哪些配置。这个翻车的根子在于把创建集中管理理解成了所有对象全部集中管理。工厂应该分领域、分模块地存在订单模块有订单模块的工厂日志模块有日志模块的工厂它们各管各的。集中管理一旦超出边界就变成单点故障了。5.2 翻车记录二参数里到处飞字符串类型保护全丢了另一个常见问题是工厂的入参设计太随意直接用字符串或整数标识类型。前面日志系统踩过一次这个坑。用字符串的后果是编译器帮你兜不住错误哪天拼错一个fireLogger直接到运行期才炸。改枚举能好一点但枚举本质上还是让调用方去告诉工厂要什么。真正解决这个问题的是工厂方法不需要任何类型参数子类本身已经代表了该创建谁。所以我在第二次重构日志系统时第一时间把字符串参数全部删掉让每个调用点不再关注传什么类型只关注用哪个工厂。少了参数就少了出错面。5.3 翻车记录三只有一种实现也硬套工厂有些团队为了显得规范即使某类只有一个实现也硬套一层工厂。举个例子系统里只有一个DefaultCacheManager没有第二个实现也没有任何替换的迹象却引入了一个CacheManagerFactory。这带来的纯粹是负面成本调用方多跳一层、阅读代码多一处转折、IDE搜索路径变长而收益趋近于零。过度抽象是工厂模式最容易被吐槽的点。我的态度很明确如果只有一个实现且看不到第二个就老老实实new。等出现第二个变体再上工厂修复重构成本并不高。别为想象中的未来付今天的利息。5.4 翻车记录四把工厂的复杂度甩给了调用方还有一类翻车表现在抽象工厂设计得太碎。比如一个工厂接口里定义了十几个创建方法调用方为了拿到一个日志对象得先createFormatter()、再createCompressor()、再createTransport()自己拼装。这等于把工厂该干的组装活又甩给了调用方只是让new变成了调工厂方法没有本质改善。正确做法是工厂对外暴露的业务方法应该尽量少最好一个方法就能拿到一个组装好的可用对象。如果实在需要组合多个组件也应该由工厂内部再提供一个高层的createBundle()之类的组合方法而不是让每个调用方重复拼装。工厂存在的意义是简化调用方而不是重新把复杂性转移回去。6. 用四条标准判断该不该上工厂我平时的决策习惯6.1 变体数量与增长趋势一个产品还是十几个产品我判断该不该用工厂第一条标准就是看现在和可预期的未来这个产品到底有几个变体。只有一个实现且一年都没变过直接new不要搞工厂。有两三个变体但几乎不增长一个简单工厂就够。变体数量开始朝五六个以上走且你观察到业务方几乎每季度都在提新类型工厂方法就该上了。产品族要整体切换才考虑抽象工厂。这条标准的核心是趋势比现状重要。我曾经因为现在只有一个实现就没上工厂结果三个月内连续加了三种数据源每次都要到各个调用点改构造器改得头皮发麻。后来复盘当时只要多问一句这个数据源未来会不会有新的类型就能预判到风险。6.2 构造过程的复杂度三行参数和三十行装配是两种难度第二条看构造过程。一个对象的创建如果只需要new加两三个简单参数不值得上工厂。但如果创建它需要读配置、建连接、设默认值、做校验前后几十行那一定要把这段逻辑收拢进工厂或者建造者否则每个调用点都是一份重复的定时炸弹。举个例子我们系统里的消息客户端创建时要加载TLS证书、设置连接池参数、注册心跳回调整套流程三十多行。早期每个调用方都自己初始化结果有三处忘记设超时时间生产环境出了好几个怪问题。后来把初始化逻辑全部收进一个工厂类问题立刻消失。创建过程越复杂工厂的边际收益越高。6.3 是否需要把创建权交给调用方或测试代码第三条标准来自测试体验。如果你在单元测试里频繁想替换某个依赖但发现依赖的new被埋在一个方法深处替换非常痛苦这就说明该有工厂介入了。有了工厂之后测试代码可以通过工厂注入一个假实现整个测试的难度瞬间下降一个量级。反过来如果业务代码本来就通过构造函数或依赖注入拿到了依赖根本没有自己new那也不需要工厂因为你已经有了比工厂更优雅的依赖管理方式。工厂只在调用方确实需要自己获取对象的时候才有用武之地。6.4 结合我最近一次重构的最终决策拿最近一次报表导出的重构收个尾。那个模块符合三条标准变体已经有PDF、Word、Excel三种且明确知道还要加HTML构造过程极其复杂涉及模板加载、数据填充、样式设置测试里我需要频繁替换不同导出实现做对比。三个条件齐了我最终选用工厂方法再叠加建造者处理复杂构造参数。重构完成后业务代码从原来每个导出点几十行配置代码缩到三五行只需要从工厂拿到一个Exporter接口然后调用export()。后来加HTML变体时只新增了一个HtmlExporter和一个HtmlExporterCreator没碰任何既有代码。说实话这个需求下来的时候我心里是很爽的因为这就是工厂模式该有的样子变化来了代码稳稳接住。我个人的体会是工厂模式不是一道必须每一步都遵循的八股题而是一种把创建这件事变得可维护、可替换、可测试的思路。它解决的真实痛点永远是那些你在重构时才会切身体会到的东西。如果你也遇到类似的味道——改构造器改到怀疑人生、测试里替换一个依赖比登天还难——那大概率就是该认真考虑上工厂的时候了。