
1. 从“满地都是new”到想要一个工厂——这个模式到底在替我们解决什么前几天在给某团队做代码评审看到订单模块里一个创建支付单据的方法被人用分支一条条堆到了十来个分支每个分支里做的事基本一个模子创建一个具体的支付服务对象、设置一堆参数、调用接口、返回结果唯一的区别就是服务类型不同。我刚说了句“这块可以考虑收敛一下”对方下意识回了一句“能用就行改了怕出错”。这句话我太熟了几乎每个从“能用就行”走向失控的系统里都飘着这么一句。我们扪心自问那些if-else连锁切换看起来直白、好加但为什么新需求一来心里就发虚因为每一次新增类型都要动老代码动一次就要把整个分支链条重新捋一遍漏掉一个细节线上就给你颜色看。更让人窒息的是这种重复不像复制粘贴那么容易被发现它藏在每一个需要创建对象的角落你改完这一处转头发现另一处还有一个同款分支整个项目像一张到处结痂的网越补越乱。工厂模式就是在这样的背景下被反复搬上台面的。它要做的用一句话就能讲完把“创建对象”这步从使用方手里抽出来不让业务代码在关键路径上随手去new一个具体类。听起来特别朴素甚至让人怀疑“这也算模式”但恰恰因为它看着简单能用对、用出价值的人反而不多。我想用大白话把这件事彻底讲透。先记住一个生活画面你去餐厅吃饭只需要对着服务员说“我要一份宫保鸡丁”你没指望跑进后厨自己切肉、调汁、颠勺、装盘。服务员和后厨之间有条清晰的边界你关心的是能不能吃到菜后厨关心的是怎么做出来。工厂模式干的就是这件事——把“点菜”和“做菜”解耦。业务代码扮演客人工厂扮演服务员和后厨的交接者具体实现类是后厨里的师傅各有各的炒法但客人永远不需要进厨房。三种工厂对应三种服务形式简单工厂像前厅服务员后厨就一个点什么他喊一嗓子工厂方法像连锁店的中央厨房川菜有川菜的小队、粤菜有粤菜的小队菜单一发下来每个小队自己张罗自己的做法抽象工厂更像给整个品牌供菜品的供应商体系你选了这套供应链配菜、调料、餐盒就得整套配套来不能东拼西凑。理解了这个画面下面三种模式的代码细节就全都能对号入座。2. 三种工厂的完整拆解2.1 简单工厂最朴素也最容易被人看低的写法绝大多数人接触工厂模式第一个碰到的就是“简单工厂”。严格讲它并不在古典设计模式的正册里因为它的实现太直白了甚至给人一种“这也能叫模式”的错觉。它的做法就是用一个类集中负责根据参数创建不同类型的对象。用一个会员折扣的例子来演示。假设电商系统里有一套折扣体系现在有普通会员、高级会员、无会员三种折扣策略它们都实现同一个折扣接口public interface Discount { double apply(double originalPrice); } public class NormalDiscount implements Discount { Override public double apply(double price) { return price * 0.95; } } public class VipDiscount implements Discount { Override public double apply(double price) { return price * 0.85; } } public class NoDiscount implements Discount { Override public double apply(double price) { return price; } }如果没有工厂业务代码里会出现一把if分支而且往往不止出现在一处。今天在结算方法里出现一次明天在购物车展示里又来一次后天在促销计算器里再来一次。每一处都重复着“判断类型、new具体类”的逻辑。我见过一个项目里同样的折扣判断写了六七遍每次加会员等级改得精疲力尽。简单工厂做的事情就是把“根据等级创建折扣对象”这件事单独收拢到一个类里public class DiscountFactory { public static Discount create(String memberLevel) { if (normal.equals(memberLevel)) { return new NormalDiscount(); } if (vip.equals(memberLevel)) { return new VipDiscount(); } return new NoDiscount(); } }改造之后业务代码只剩一句Discount discount DiscountFactory.create(memberLevel);从表面看我们只是把移到了另一个类里并没有消灭它。但它的意义在于选择的职责彻底从业务逻辑中剥离了。以后新增会员等级即使有七个地方都在用折扣我也只需要改动工厂这一个类而不是去翻那七处业务方法。变更的安全范围被压缩到了一个点排查问题同理怀疑折扣选错不用再去业务代码里漫无目的地翻直接打开工厂类看映射关系就行。当然简单工厂身上背着一个著名的批评违反开闭原则每加一个类型都要打开工厂类来改。这个批评在理论上是成立的但在实际小团队、小项目里这个代价并不可怕。开闭原则追求的是“对扩展开放、对修改关闭”可如果扩展频率很低、分支数量可控集中式的修改反而比分散式修改更安全。不要被教科书上的原则吓住务实一点分支不超过五六个、创建逻辑也不复杂的前提下简单工厂完全够用而且它最大的优点——直观、好读、新人一看就懂——常常被人忽略。2.2 工厂方法把选择权放回子类的口袋里简单工厂确实有个让人头疼的升级场景当创建不同对象的流程本身开始出现差异简单工厂就吃不消了。比如同样是创建折扣对象普通会员只需要new一个类高级会员却要先读积分再扣减额度还在创建时要远程调一次营销系统。这时候把这么多“创建动作”全部塞进一个工厂方法里那个方法会迅速膨胀成一个大泥球。工厂方法模式想了一个新招把“创建对象”的职责拆散下放到抽象的每一个具体子类里。这里的关键不是用一个类去选择实现而是定义一个抽象的创建方法让子类各自决定自己该创建谁。还是折扣场景我们定义一个抽象的折扣处理器它对外提供一个标准流程但具体怎么创建折扣对象交给子类public abstract class DiscountProcessor { /** 子类提供各自创建折扣对象的方式 */ protected abstract Discount createDiscount(MemberContext context); /** 公共流程创建对象、应用折扣、记录结果 */ public final double checkout(MemberContext context, double price) { Discount discount createDiscount(context); // 这里可以统一做埋点、校验、日志 return discount.apply(price); } } public class NormalMemberProcessor extends DiscountProcessor { Override protected Discount createDiscount(MemberContext context) { // 普通会员没什么花活直接创建 return new NormalDiscount(); } } public class VipMemberProcessor extends DiscountProcessor { Override protected Discount createDiscount(MemberContext context) { // 高级会员要经过一次规则引擎判断 VipRuleEngine engine new VipRuleEngine(); int ruleVersion engine.fetchLatestRuleVersion(); return new VipDiscount(ruleVersion); } }注意一个细节NormalMemberProcessor和VipMemberProcessor都只关心自己的创建逻辑互不干扰。调用方呢它拿到的是DiscountProcessor这个抽象引用只需要调用checkout方法根本不关心底下的对象是哪来的。工厂方法和简单工厂的区别表面看只是把“选择”从外部挪到了层级内部但带来的代码组织方式完全变了。简单工厂像前台接线员所有电话都转接给他由他判断该把电话接到哪个部门判断逻辑全在这个前台上。工厂方法则是给每条产品线都配了独立的小组小组自己接待自己部门的电话。小组成长起来之后前台的判断就越来越轻每个小组各自深耕自己的业务。这里还需要解释一个容易迷糊的点工厂方法模式里的“工厂”其实是由抽象父类定义的createXxx方法它本身不一定有完整实现甚至可能就是抽象方法。整个模式的精髓是让父类定义骨架流程让子类填充具体创建行为。这个思路和模板方法模式很亲近只是模板方法通常不限制创建的对象。说得再直接点工厂方法就是“模板方法 创建型意图”的合体。2.3 抽象工厂当你需要一整个家族一起出现抽象工厂比前两个都重解决的问题从“创建一个对象”变成了“创建一整套互相关联的对象”。举一个我在某报表系统里见过的例子同样的报表在Web端要渲染成网页图表在移动端要渲染成小程序图表。图表工具、导出工具、水印工具、样式主题这些东西往往成套出现。你不能Web端用A风格的工具却给移动端配置B风格的导出逻辑那会把整个产品线的视觉和格式都搞得四不像。抽象工厂的做法是先定义一个工厂接口里面声明多个创建方法每个方法负责创建一族产品中的一种。然后用不同的具体工厂实现去配对不同的产品族public interface ReportUIFactory { ChartTool createChartTool(); ExportTool createExportTool(); WatermarkTool createWatermarkTool(); } public class WebReportUIFactory implements ReportUIFactory { Override public ChartTool createChartTool() { return new WebChartTool(); } Override public ExportTool createExportTool() { return new WebExportTool(); } Override public WatermarkTool createWatermarkTool() { return new WebWatermarkTool(); } } public class MobileReportUIFactory implements ReportUIFactory { Override public ChartTool createChartTool() { return new MobileChartTool(); } Override public ExportTool createExportTool() { return new MobileExportTool(); } Override public WatermarkTool createWatermarkTool() { return new MobileWatermarkTool(); } }使用方在构造报表页面时持有的是ReportUIFactory抽象引用。页面要创建图表工具就用factory.createChartTool()要导出就用factory.createExportTool()。由于同一个具体工厂实例保证所有产品来自同一个族Web端永远配Web端的整套套件移动端永远配移动端的整套套件一致性不用靠人肉约束而是靠结构天然保证。抽象工厂之所以叫“抽象”是因为它关心的不是某一个对象的具体类而是整个产品族的搭配规则。它在跨平台、主题系统、ORM多数据库适配这类场景下特别好使。但也要承认抽象工厂的类数量是所有工厂模式里最多的接口、实现类、产品族一多阅读成本立马上升。所以我在实际项目里会克制地用抽象工厂只有当“成套产品必须配套出现”这个约束成立时才会考虑它否则单件创建的场景用简单工厂或工厂方法更合适。3. 支付渠道重构实录从改一处蹦三处的泥潭里爬出来理论讲完了我带你看一个真实做过的重构。背景是某商城模拟项目的结算模块。收银台一开始支持三种支付方式钱包余额支付、银行卡快捷支付、积分抵扣支付。代码结构是很典型的“中心化if-else”所有渠道在一个方法里排队每加一个渠道就往后面续一个分支。重构前的代码长这样public class CheckoutService { public PayResult pay(String channel, double amount, PayContext ctx) { if (wallet.equals(channel)) { WalletService ws new WalletService(); ws.setToken(ctx.getToken()); ws.setVersion(v2); if (ws.balance() amount) { return new PayResult(false, 钱包余额不足); } return ws.deduct(amount); } else if (bankcard.equals(channel)) { BankCardService bs new BankCardService(); bs.setCardNo(ctx.getCardNo()); bs.setCvv(ctx.getCvv()); return bs.pay(amount); } else if (points.equals(channel)) { PointsService ps new PointsService(); ps.setUserId(ctx.getUserId()); int points ps.queryPoints(); if (points amount * 10) { return new PayResult(false, 积分不足); } return ps.pay(amount); } return new PayResult(false, 渠道不存在); } }这种写法有几个糟糕的地方。第一三个渠道虽然类型不同但公共动作被打断了比如参数校验、异常兜底、日志记录这些逻辑要么在分支里反复复制要么漏掉某条渠道没做。第二新增渠道时要碰核心方法代码评审里对这块尤其敏感大家都怕改出线上故障。第三不同渠道的初始化方式差异很大有的要读Token有的要查卡号有的要拿用户ID这些细节全绞在一个大方法里可读性已经接近冰点。我印象最深的一次线上事故新接入一个渠道时同事顺手复制了钱包分支的代码改了参数以为就完事了结果银行卡分支里的重试机制根本没被复制进来。某天上游支付网关超时新渠道直接失败用户看着转圈圈后台一点重试日志都没有排查了大半天才找到原因。这类问题不是靠细心能避免的而是结构本身在逼你犯错。重构的第一步定义一个统一的支付渠道接口把不同渠道的差异收敛进去public interface PayChannel { PayResult pay(double amount, PayContext ctx); String channelName(); }第二步把“根据渠道名创建渠道服务”的逻辑抽到专门工厂里。这里我选择了简单工厂的变体注册表驱动。明明三种渠道、扩展节奏不快为什么不用if-else因为我见过if版本工厂在渠道多起来之后同样会腐化。用注册表的好处是新增渠道只需要往Map里加一行工厂本身永远保持不变形public class ChannelFactory { private static final MapString, SupplierPayChannel REGISTRY new HashMap(); static { REGISTRY.put(wallet, WalletPayChannel::new); REGISTRY.put(bankcard, BankCardPayChannel::new); REGISTRY.put(points, PointsPayChannel::new); } public static PayChannel getChannel(String channel) { SupplierPayChannel supplier REGISTRY.get(channel); if (supplier null) { throw new UnsupportedOperationException(渠道不存在: channel); } return supplier.get(); } }第三步把门面方法瘦身让公共逻辑只保留一份public PayResult pay(String channel, double amount, PayContext ctx) { PayChannel payChannel ChannelFactory.getChannel(channel); return payChannel.pay(amount, ctx); }每个渠道独立成类比如钱包渠道public class WalletPayChannel implements PayChannel { Override public PayResult pay(double amount, PayContext ctx) { WalletService ws new WalletService(); ws.setToken(ctx.getToken()); ws.setVersion(v2); if (ws.balance() amount) { return new PayResult(false, 钱包余额不足); } return ws.deduct(amount); } }改造完成之后CheckoutService从一百多行的大方法变成一个薄薄的门面。半年后再看收益会真切地浮出来排查问题不用在分支里跳来跳去测试用例可以按渠道独立编写新增渠道的改动面从整条方法缩小到“新建一个类 注册表加一行”。这类重构不会带来肉眼可见的性能提升但它让后续每一次需求变更的风险都变得可控。4. 三种工厂之间到底怎么选——我的判断套路很多人学完三种工厂最关心的问题只有一个我现在写代码到底该用哪种这问题比想象中更常见我也被问了很多次。干脆把我自己的判断逻辑总结成一张表照着权衡就行场景特征推荐模式理由类型少、创建逻辑简单、分支集中简单工厂结构直白改动集中在一个入口类型多、创建流程差异大、子类需要自己掌控初始化工厂方法把差异下沉到子类父类只留骨架一整套产品需要配套出现、强调风格/版本一致性抽象工厂用同一个工厂保证产品族不混搭类型极多、希望动态扫描注册简单工厂 注册表/反射避免工厂类自身膨胀但牺牲一些显式可读性光看表格还不够。我实际判断时会额外关注“新增一种类型我要改动几个文件”这个指标。如果只改一个文件哪怕里面有七八个分支我也能接受如果新增一个类型要同时改三五个文件比如业务方法、配置类、初始化器、测试夹具都要跟着动那我会停下来认真考虑引入工厂或其他抽象手段。这个指标比单纯数“我有几个实现类”要实用得多因为它直接度量了变更风险和改动面。还有一个常见的纠结明明简单工厂够用有人非要套上抽象工厂美其名曰“预留扩展空间”。结果代码里凭空多出五六个类和接口阅读成本直线上升扩展空间却从来没等来。我个人的立场很明确项目里的抽象应该以现实的扩展节奏为前提而不是以“万一以后要怎样”为前提。如果业务一年都没出新渠道那你就不需要那条“预留”之路。抽象是有维护成本的这个成本由整个团队每天阅读代码时共同承担。反过来也有一个需要警惕的信号当你的工厂里开始出现一大段创建流程而不只是几行映射时说明创建逻辑的复杂已经超出了简单工厂的承受范围。这时如果不切换到工厂方法你会忍不住把越来越多的初始化逻辑塞进工厂类最后得到一个“大工厂反模式”——名义上叫工厂实际上退化成了if-else集中营。这个信号非常好识别打开工厂类如果发现一个方法超过二三十行每行都在描述“怎么配置某个具体对象”那就是该重构的时候了。另外要提一下和依赖注入容器的关系。现在主流依赖注入框架都能自动创建对象很多时候并不需要手写工厂。但两者不是非此即彼依赖注入擅长管理“无参数或简单参数”的Bean而工厂擅长处理“需要运行时参数才能确定创建方式”的场景。我在实际项目里的做法是静态、无状态、创建参数固定的对象交给注入容器需要根据上下文字段动态选择、动态配置的交给专门的工厂。框架是工具模式是思想没必要为了一种放弃另一种。5. 工厂模式与可测试性的纠缠——写单测时它到底帮了什么忙聊工厂模式如果不聊测试就少了一大块实用价值。很多人没意识到工厂模式对提升代码可测试性的贡献甚至比它对扩展性的贡献还大。先看最简单的场景没有工厂时一个业务方法里直接new了一个具体类测试想替换这个具体类就很尴尬。要么用Mock工具去拦截构造器要么老老实实把真实依赖也初始化一遍而真实依赖往往还要连数据库、调外部接口测试既慢又脆。一旦改用工厂业务方法只依赖抽象接口测试时就可以轻轻松松给业务方法注入一个替身实现。举个例子。前面那个支付门面改造后变成public PayResult pay(PayChannel payChannel, double amount, PayContext ctx) { return payChannel.pay(amount, ctx); }测试时我可以直接传入一个假的PayChannelPayChannel fakeChannel (amount, ctx) - new PayResult(true, 模拟成功); CheckoutService service new CheckoutService(); PayResult result service.pay(fakeChannel, 100, new PayContext());不需要准备真实的钱包账号、银行卡号不需要启动容器也没有任何外部依赖测试跑起来飞快。这正是工厂模式的意义之一让依赖都变成抽象依赖让测试能够随意替换实现。但有一个细节很多人会忽略工厂类本身也需要测试吗我的答案是看工厂内部有没有逻辑。如果只是一个参数映射字典或者一行行简单的映射那它出错的风险很低不值得为它写额外测试你每写一个渠道的集成测试时自然会覆盖到它。但如果工厂内部有复杂的条件判断、有分支嵌套、有异常处理那它就该有自己的独立测试。判断标准同样朴素这里有分支吗有分支就要有对应分支的测试。还有一个测试上的经验之谈用工厂之后测试夹具可以做得更干净。比如测试“钱包余额不足”这个场景在没有工厂的年代你得创建一个真实钱包服务还得往里面放一笔钱再扣除逻辑绕来绕去有了工厂你可以注册一个“余额不足版钱包渠道”到测试专用的工厂里测完引用就完了。这里的核心就是工厂给了测试一个稳定的替换点让边界条件能够快速构造出来。我在重构那类烂if代码时通常会在开工之前先给现有方法补上几个基础测试再动手抽象工厂。等重构完测试全部变绿心里才踏实。如果你的项目里那块代码压根没有测试别急着顺手重构先补测试再动手这是我自己踩过无数回坑之后学到的规矩。6. 工厂模式的高阶玩法与翻车现场6.1 工厂模式与依赖注入框架的恩怨讨论到工厂模式几乎每次都会被问到“框架里的Bean创建算不算工厂模式”我一般会回答容器负责创建和装配对象这个行为本身和工厂有很强的亲缘关系但依赖注入的重要之处不只是创建对象它更强调“创建出来之后怎么把依赖送到该去的地方”。工厂模式管的是“如何创建”依赖注入管的是“创建完怎么装配”。二者也不冲突甚至经常合作。在一个用了依赖注入框架的项目里你照样可以写工厂。把工厂本身注册成一个单例让它持有类型映射关系业务侧仍然面向工厂调用。这种写法常见于策略类场景根据运行时的标识符动态选择一个具体的策略实现。容器负责把各种策略实现备好工厂负责把它们组织起来业务侧做选择。6.2 用反射和注解让工厂“隐形”前面支付渠道的例子用了注册表这已经比if-else整洁不少。但随着渠道数量增加注册表也是一个需要手工维护的地方。进阶一点的玩法是用注解加反射让类自己“报名”成为渠道成员工厂里不再需要维护一行注册代码。Retention(RetentionPolicy.RUNTIME) Target(ElementType.TYPE) public interface ChannelMeta { String name(); } ChannelMeta(name wallet) public class WalletPayChannel implements PayChannel { // ... }配合包扫描或者框架提供的扫描能力启动时把带ChannelMeta注解的类收集起来按name放进Map工厂调用时就彻底看不到注册表了。这个写法的优点是新渠道类创建后自动生效不用碰任何注册逻辑代价也很大首先启动扫描有成本其次代码的可读性反而变差了新人看到工厂类里空荡荡的Map根本不知道渠道从哪来的得去查注解扫描配置才能明白。我对反射工厂的态度是能用但慎用。要不要用取决于团队是否已经养成了约定式编码的习惯。如果团队本身就在大量使用配置化、注解化开发反射工厂会很顺手如果团队里的成员更依赖显式代码阅读那维护一张注册表反而更安全、更好理解。6.3 工厂模式和策略模式到底谁是谁还有一个经常被混在一起讨论的模式策略模式。这两个模式确实长得像都要定义一个接口、都有多个实现类、都经常配合工厂去调用。但它们解决的问题本质不同工厂模式的意图是“对象该由谁创建、怎么创建”解决的是实例化时的选择问题策略模式的意图是“算法该选哪个、怎么替换”解决的是运行时的行为替换问题。一句话概括工厂管出生策略管干活。这俩完全可以协同工作。前面折扣例子里Discount接口和它的实现们其实是在做策略模式而DiscountFactory是负责为策略“生产”合适实现的工厂。两者不冲突甚至在设计上是黄金搭档。实际项目中常见的一种组合是策略接口定义行为工厂负责按标识选择策略业务侧一行代码触发策略执行。这也是为什么很多人把它们混在一起——它们在用的时候总是成对出现确实很难完全切开。7. 团队协作、面试和“过度设计”的边界聊到最后我想说说工厂模式在面试和团队协作里的那些观察毕竟学这个模式不只是为了写代码更是为了在代码评审、面试和架构讨论中把自己的设计理由说清楚。面试官爱问工厂模式通常不是为了考你能不能背出三种模式的类图而是想听你怎么判断场景。只答出“简单工厂、工厂方法、抽象工厂有什么区别”算及格更值钱的回答是结合自己的项目经历讲某处为什么从if-else重构为工厂重构前后测试代码怎么变化新增一种业务类型时改动点在哪几个文件。你讲得越具体越能证明是真实用过的而不是面试前临时背的。我自己面试别人时最反感听到的答案就是“这个应该用抽象工厂因为将来扩展性好”但问一句“你上个项目里抽象工厂新增一种产品族要改几个文件”对方就沉默了。在团队协作里我见过不少用工厂模式翻车的案例。最典型的就是“假工厂”有人把一堆创建逻辑藏进一个工厂方法里以为这样算面向扩展实际上工厂内部照样是几十行if-else调用方拿着工厂孤零零地调用扩展时依然要改工厂内部。这种写法比直接写if-else多绕了一圈维护成本更高。所以我在评审时常常对团队强调工厂模式的价值核心是“创建逻辑集中、变化点隔离、调用方依赖抽象”这三件事。任何一个不满足这个工厂就只是个形式不如不写。另外还有一个关于“过度设计”的老生常谈但值得反复拿出来讲对于只有一两个分支的小case直接写if-else一点都不丢人。我看到过不少新人学完设计模式之后像拿了新玩具一样恨不得把所有条件判断都抽象成工厂。结果代码里全是抽象层Debug时要跨越五六个类才能看到真正的逻辑。这种东西看上去很“架构”实际上很费人。设计模式的取舍应该服务于一个朴素的目标让下一次改动更安全、更快、改动面更小。如果引入工厂带来的复杂度大于它减少的复杂度那就不该用。我还有一个很个人化的操作习惯已经用习惯了决定要不要引入工厂几乎不看“这个类有几个实现”更看重“新增一个类型时我会改动几个文件”。如果只改一个文件哪怕分支多点我也愿意先留着如果改三五个文件我就会停下来认真考虑抽象。这个判断方式不承认绝对的答案做的是风险感知代码结构的好坏最终要用“变更的代价”来衡量。工厂模式本质上不是什么高深莫测的技术它是编程界对“重复、耦合、变化点”的一种经典归纳。真正拉开差距的从来不是会不会背定义而是能不能在自己的代码里看见那些值得被工厂处理的信号。这篇文章给的也不是什么奇技淫巧而是我在真实项目里的判断逻辑和踩坑经验。希望你在面对“到底要不要加一层”的纠结时能少一点犹豫多一点底气。