工厂模式这名字只要是学过设计模式的人应该都听过。它是23种经典设计模式里最基础、面试最爱问、日常写代码最容易用上的一个。我最早是在做支付模块的时候真正把它用起来的当时被一堆new出来的支付渠道类折磨得够呛改一个对接方式要翻好几个文件。后来重新梳理用工厂模式把实例创建的逻辑收敛到一处代码瞬间清爽了很多。这篇文章不打算讲那些教科书式的概念我直接说人话把工厂模式底层的设计逻辑和它在实际项目里的玩法拆开顺便把常见的坑也一起列出来。这篇内容适合这几类人刚接触设计模式的在校学生、准备期末或者求职面试的人、像我一样在业务代码里摸爬滚打的开发者还有那些想搞清楚工厂模式和依赖注入、和 Spring 之间到底是什么关系的人。看完之后你应该能做三件事自己手写一个工厂能在项目里判断该不该用工厂以及能看出框架源码里哪些地方其实就是在用工厂模式。1. 工厂模式到底在解决什么问题很多教程上来就贴三种工厂的 UML 图最后读者图看懂了代码还是不知道怎么写。我先换个角度从大家天天写的new关键字讲起这才是工厂模式存在的真正理由。1.1 new 为什么是敌人假设你今天在写一个订单模块的支付逻辑简单点支持支付宝和微信两种支付方式。最直接的做法是这样public class OrderService { public void pay(Order order, String channel) { if (alipay.equals(channel)) { } else if (wechat.equals(channel)) { } } }这段代码有很多问题但最大的问题不是if-else多。往深一层看问题在于OrderService直接依赖了支付宝和微信这两个具体的支付类。一旦哪天要加一个银联支付或者悄悄替换掉某个渠道的实现那么所有写过new这些类的地方都要动一遍。这就是所谓的高耦合。我拿咖啡店打个比方。你在柜台点单如果每次都要自己跑到后厨、找到对应材料的货架、亲手冲一杯咖啡那不叫点单那叫打工。现实生活中你只需要跟店员说一杯拿铁后面的事情由店里的人搞定。在这个例子里あなた是调用方店员就是那个工厂。你把怎么制作的细节交给店员自己只需要表达我要什么。工厂模式做的事情和咖啡店一模一样——它把创建对象这件看似简单、实际上到处重复的脏活从业务代码里剥离出来集中到一个地方统一管理。调用方只负责告诉工厂需求工厂负责挑选、创建、返回一个正确的对象。1.2 消除依赖具体类的连锁反应如果前面的支付例子还只是一两个类你可能觉得手动new也没什么问题。真正疼的是下面这种情况。我用宿舍里自己折腾论坛的经历来说吧。刚开始论坛系统只有一个用户类后来加了帖子、加了版块、加了消息通知每个业务类在好几个地方被实例化。某天你想给用户类加一个构造参数比如加一个userType好了CtrlH 全局搜索new User(能搜出十几个调用点。每个点都要小心翼翼地确认需不需要改。稍微漏掉一个编译期不报错运行期直接空指针。这种连环骚动往往会伴随一个通宵。工厂模式可以切断这条链。业务层不再直接去new一个具体的类而是统一调用工厂类的方法。这样构造函数就算变了你只需要改工厂内部那一个地方所有调用方都不用动。这属于设计模式里封装变化思想的入门实践也是我最早从工厂模式里获得成就感的地方。2. 三种典型形态与核心实现2.1 简单工厂入门首选但不是正式模式很多教材会把简单工厂说成是三种之一严格来说它并不是 GoF 里的 23 种模式只是一种编程习惯。它的玩法是用一个静态方法接收一个参数根据参数判断返回不同产品。public class PayFactory { // 简单工厂风格静态方法 switch public static PayChannel create(String channel) { switch (channel) { case alipay: return new AliPayChannel(); case wechat: return new WeChatPayChannel(); default: throw new IllegalArgumentException(不支持的支付渠道 channel); } } }这么做的好处非常直白把创建逻辑收拢到一个方法里。但它的缺点也明显——这个switch依旧存在。以后每加一个支付渠道你还是要在这个方法里加一个case修改了旧代码严格来说违反了开闭原则。所以我一般把它用在两类场景产品种类少且稳定、产品创建逻辑简单还有做作业或者写 demo 的时候。如果你负责一个只对接两三家物流公司的内部系统那简单工厂完全够用硬套工厂方法反而过度设计。2.2 工厂方法把选择权交给子类工厂方法模式在简单工厂的基础上做了一件事把那个巨大的switch拆散交给不同的子类去处理。核心思路是定义一个抽象工厂接口每个具体工厂对应一种产品调用方只和抽象工厂打交道。// 产品抽象 具体产品 public interface PayChannel { void pay(Order order); } public class AliPayChannel implements PayChannel { Override public void pay(Order order) { /* 支付宝逻辑 */ } } // 工厂抽象 具体工厂 public interface PayChannelFactory { PayChannel create(); } public class AliPayChannelFactory implements PayChannelFactory { Override public PayChannel create() { return new AliPayChannel(); } }调用的时候业务代码里不出现任何具体的PayChannel类public class OrderService { private final PayChannelFactory payChannelFactory; public OrderService(PayChannelFactory payChannelFactory) { this.payChannelFactory payChannelFactory; } public void pay(Order order) { PayChannel channel payChannelFactory.create(); channel.pay(order); } }这样做能带来什么实际好处呢新增一种支付渠道时旧代码一个字节都不用动只需要新写一个XXXPayChannel和XXXPayChannelFactory这叫扩展开放、修改封闭。不过代价也很明显类数量翻倍了如果产品只有两三个用这种模式会显得有点臃肿。我自己的体会是工厂方法更适合产品类型会在不同场景下独立扩展的项目比如要写一套支持多种数据库的日志组件。顺便提一句工厂方法模式是模板方法模式的近亲。它的创建方法create()就是一个模板子类实现具体的创建细节两者的血缘关系很近理解一个再去学另一个会轻松不少。2.3 抽象工厂从单个对象到一组产品抽象工厂是三者中最复杂、笔试里最让人头疼的一个。它的出现是因为很多场景下我们需要的不是一个对象而是一组配套的对象。想象一个后台管理系统它需要用户管理功能、订单管理功能和报表管理功能而底层可能需要支持 MySQL 和 Oracle 两套数据库。如果每个功能都单独选择具体实现那可能出现在 MySQL 环境里用到了 Oracle 的报表类这种混乱局面。这时候抽象工厂就派上用场了。它把整组产品打包绑定一个工厂代表一种产品族产品族内部的产品是配套的不能混搭。public interface DatabaseFactory { UserDao createUserDao(); OrderDao createOrderDao(); ReportDao createReportDao(); } public class MySqlDatabaseFactory implements DatabaseFactory { Override public UserDao createUserDao() { return new MySqlUserDao(); } Override public OrderDao createOrderDao() { return new MySqlOrderDao(); } Override public ReportDao createReportDao() { return new MySqlReportDao(); } } // Oracle 同理 public class OracleDatabaseFactory implements DatabaseFactory { Override public UserDao createUserDao() { return new OracleUserDao(); } Override public OrderDao createOrderDao() { return new OracleOrderDao(); } Override public ReportDao createReportDao() { return new OracleReportDao(); } }这样一来只需要在启动配置里决定用哪一个DatabaseFactory后续获取UserDao、OrderDao、ReportDao时就永远不会出现MySQL 的 UserDao 配上 Oracle 的 OrderDao这种错乱。产品族的统一性是这个模式的核心价值。我把三种形态的取舍整理成一个表格方便以后翻阅形态核心思想优点缺点适合场景简单工厂静态方法 参数分支代码量少、易理解违反开闭原则分支会不断膨胀产品种类少且稳定工厂方法接口 延迟到子类创建符合开闭原则扩展性好类数量翻倍结构偏重产品类型独立扩展抽象工厂产品族接口 族内配套保证产品族一致性灵活切换环境新增产品族很麻烦多套环境切换、产品成组出现抽象工厂最经典的例子是跨数据库的持久层框架设计不过现在大部分场景都有 ORM 框架当中间层实际手写抽象工厂的机会不如前两者多。但这并不代表它不重要很多中间件的核心入口依然在用这套思想。3. 实战中的工厂模式设计与选型3.1 从需求倒推具体怎么判断用哪种工厂学了概念之后最大的问题就是到项目里不知道该怎么选。我通常用一套简单的路径来做决定可以参考一下。先看你的产品类型有多少。如果只有一个接口、两种实现且几乎不可能再加第三种那就别用工厂。直接在调用的时候new对应的实现类或者用静态方法来包一层都行。如果产品类型开始多起来而且看得出来将来会持续增加那就用工厂方法。如果不同类型的对象会组成一个套系同时出现且需要在不同套系之间切换那就用抽象工厂。可以先用一句话帮忙判断如果要管的东西是一类用工厂方法如果要管的是一组配套的东西用抽象工厂。这句话在我带新人时很有用他们往往会过两三个月回来说好像确实是这个道理。3.2 把工厂做成单例还是静态方法在实际开发中工厂类本身往往不需要被反复实例化。如果工厂内部没有任何状态直接用静态方法是最省事的做法就像java.util.Collections里的很多方法干的事。如果工厂类内部持有配置项、缓存或其他共享资源那就把工厂做成单例。这里有个很常见的坑很多人会在工厂里缓存创建好的对象却忘了考虑线程安全。比如新手写了一个create()方法里面做双重检查锁代码看着头头是道结果缓存用的是普通 HashMap多线程一跑就偶发脏数据。我在排查这类问题的时候最先看的就是缓存容器是不是用了 ConcurrentHashMap。工厂模式本身不负责并发但既然把对象创建集中管理了并发访问自然要从工厂这里开始治理。3.3 反射加配置换一种方式消灭 switch工厂模式的核心痛点之一是switch分支永远在膨胀。一个常见的优化方案是用配置文件来驱动工厂用反射来实例化类。# pay.properties alipaycom.example.channel.AliPayChannel wechatcom.example.channel.WeChatPayChannel unionpaycom.example.channel.UnionPayChannelpublic class PayFactory { private static final MapString, String map new ConcurrentHashMap(); public static PayChannel create(String channel) throws Exception { String className map.get(channel); if (className null) { throw new IllegalArgumentException(不支持的支付渠道: channel); } Class? clazz Class.forName(className); return (PayChannel) clazz.getDeclaredConstructor().newInstance(); } }这种写法在框架代码里非常常见因为框架不能预先知道用哪些实现类。但对业务项目来说我一般不建议直接这么干。理由是失去了编译期类型检查类名写错了只能运行时报错排查起来反而更痛苦。除非你的类列表本身也是动态变化的否则老老实实写switch可能更安全。这是一个比较反直觉的结论但实战中确实是稳定优先。3.4 工厂模式在框架层是怎么被玩出花的读 Spring 源码的时候总能看到一个非常核心的接口BeanFactory。它干的事情和手工工厂一模一样根据一个名字或者类型帮你取到一个实例。只不过它的配置来源于 Bean 定义背后还带了完整的生命周期管理、代理增强和依赖注入。可以说BeanFactory这个命名就是对工厂模式最直接的一次致敬。Unity 游戏引擎也大量应用工厂模式尤其是复杂的敌人刷新和道具掉落系统。比如一个关卡里需要刷出不同类型的怪物与其在更新逻辑里层层if不如建一个EnemyFactory接口每种敌人对应一款工厂再把创建参数交给外部传入。这样游戏策划想调怪物参数的时候程序员不需要在逻辑里反复找哪里有 new。如果你已经能读懂这些框架的入口设计与工厂模式之间的关系你的阅读能力基本上就已经超越了大多数只在概念层打转的开发者。4. 实践中的坑与排查经验4.1 为了工厂而工厂结果把代码搞复杂了我见过不少项目和作业本来一个需求只用两三个类结果为了表现得有设计感硬是套了三层工厂。结果代码量直接膨胀两倍而且后续维护的人还得先搞懂工厂结构才能改需求。这种叫过度设计。最直接的判断标准是如果产品类型增加的概率极低或者是个人项目里以后随手能改的就直接new。别为了模式而模式。模式是工具不是目的这句话听起来很空但我在代码 Review 里说过不止十遍每次都是真的救了一位同学的大作业。4.2 跨产品族的隐藏式耦合抽象工厂最大的坑不是理解而是后续扩展。假设你已经定义了DatabaseFactory这套接口里面有三个方法。现在新加了一个技术栈你需要新写一个对应的技术栈工厂重写三个方法这倒没什么。真正的麻烦在哪如果产品族里面要多加一种产品比如加了日志审计模块那么抽象工厂接口要先加方法然后所有已经实现的工厂子类全都得跟着改。这可就是违反开闭原则的大改造了。这种问题在真实项目里很难完全避免只能靠提前设计接口时尽量想清楚产品族会如何演化。B站上看视频教程的时候老师不会重点讲这个但实际写代码的时候十有八九会碰上。有取舍的时候我一般会选择接口少但稳定而不是接口全但频繁变动。4.3 并发环境下初始化工厂的陷阱工厂在懒加载场景下最常见的联合错误是缓存 HashMap 没加锁 双重检查锁写得不完整。第一版差劲的写法是这样public class ProductFactory { private static Product product; public static Product get() { if (product null) { synchronized (ProductFactory.class) { if (product null) { product new Product(); } } } return product; } }这段代码的问题在于product不是 volatile存在指令重排的风险某个线程可能拿到一个没有完全初始化的对象。正确做法是给字段加volatile。这种坑大多数情况下十次跑不出一次但线上流量一大偶发问题就极其难查。新手阶段遇到这类问题最容易怀疑数据库、怀疑网络很少有人想到是对象初始化顺序的问题。4.4 常见问题速查表为了方便收藏我把实际踩过的坑和排查方向整理成一个表你以后遇到类似现象可以直接按表操作症状最可能的原因排查方向工厂里新增产品后调用方报空指针忘记在工厂分支里加上新产品的创建逻辑检查switch/if分支是否覆盖所有类型参数多线程下偶发拿到半成品对象懒加载单例缺少 volatile 或锁竞争处理查看字段是否 volatile缓存容器是否是线程安全的产品换了一个实现类但代码没生效工厂还是用硬编码的new没从配置或注册中心读取检查配置加载逻辑是否正确缓存是否过期抛开工厂直接new出来的对象行为不一致调用方绕过工厂导致配置没有注入搜索项目中是否直接new了本应通过工厂创建的对象抽象工厂新增产品后大量子类编译不过接口新增方法后所有实现类都需要同步修改考虑是否真正需要该产品或者调整接口拆分粒度4.5 比工厂模式更宽松的替代方案日常开发里工厂模式并不是唯一的选择。如果你的项目已经引入了 Spring 容器那大量场景直接用依赖注入来解决也可以。依赖注入的核心思想之一本来就是把对象的创建和组装放在容器层业务代码里连工厂都不用写。我对这两者的理解是工厂模式可以在没有任何框架的纯 Java、纯 C 项目里独立使用它不依赖任何外部容器依赖注入则更多是框架能力需要你把类设计交给容器管理。在自己练手的学习项目中优先用工厂模式去体会封装变化比一上来就丢给 Spring 托管要更能理解底层原理。框架能帮你省掉工厂代码不假但理解了自己手写工厂之后你才能真正明白 BeanFactory 里的Factory这几个字母意味着什么。5. 从作业到源码工厂模式该如何学习如果你是在准备设计模式大作业或者期末考试我建议不要把重点放在背 UML 图上而是找一个场景自己从零写一遍。比如设计一个通用的消息通知系统可以支持短信、邮件、站内信然后用简单工厂和工厂方法各写一版对比一下它们的差异。写完之后你会对设计模式有比看十篇教程都更深的体会。如果是想在真实项目里应用工厂模式我的建议是从一个小的、新增的需求入手不要一上来就重构旧代码。旧代码能跑就不要急于动它这是行业里的经验之谈。比如你正在一个老系统里开发一个新的报表导出功能导出格式有 PDF、Excel、CSV那这种新功能就是完美的工厂模式切入点。先写一个导出器接口再写一个工厂类新代码立马清爽。等到下一次有人要在导出器里加新的格式时你会庆幸当初留了一个工厂。再进一步如果你想学会读框架源码建议拿着 Spring 的BeanFactory相关源码当教材。它能告诉你真正的工业级工厂远比教科书复杂但核心骨架和教科书是相通的。理解了这种从概念到工业实现的对应关系你的水平就上了一个层次。我个人在实际学习过程中的一个感受是设计模式不是一个一个单独学的工厂模式往往和单例、策略、模板方法这几个模式联动出现。它们是你日常写代码的组装配件学会搭配比死记硬背某个模式本身重要得多。工厂模式看似简单但它是进入这个体系的好入口。把它弄明白后面学其他模式会顺利很多。