1. 工厂模式到底是什么
在项目开发里干得久了,你会发现很多代码问题不是技术难度有多高,而是对象创建这个看似不起眼的环节没有处理好。业务方今天要一种实现,明天又换一种配置,后天再加一个新类型,结果你满脑子都是new来new去,调优没做多少,改改改倒是没停过。这个痛点,正是工厂模式要解决的问题。
工厂模式是一种创建型设计模式,核心思想非常朴素:把对象的创建过程和使用过程分离开,由工厂统一负责“造什么、怎么造”,调用方只负责“我要什么,给我什么”。这就好比你去餐厅吃饭,你只需要看菜单点菜,不需要关心后厨怎么洗菜切菜炒菜,更不用自己去开火。菜单是统一接口,后厨是工厂,菜品是对象。
很多初学者觉得工厂模式不过就是多写了几个类、多绕了一层,没必要。但一旦业务复杂起来,这“多绕的一层”反而成了救命的缓冲带。举个例子,某公司有一个消息推送系统,原本只支持短信,后来要加邮件,再加App站内信。如果所有业务代码都是直接new SmsSender()、new EmailSender(),那么每次加一个渠道,就得把所有调用点翻一遍。而如果用上工厂模式,新增渠道只需要扩展工厂,调用方代码一行都不用动。
这篇内容我按设计模式系列的方式来写,重点聊三种最常用的形态简单工厂、工厂方法、抽象工厂,以及它们之间的差异、代码实现和实战选型思路。适合刚学设计模式的初学者,也适合想在实际项目中把工厂模式用对、用活的开发者。
2. 三种工厂形态的适用边界
工厂模式并不是一个孤零零的写法,围绕“如何把创建对象这件事做好”,业界沉淀了三套递进式的方案,分别解决不同复杂度的问题。理解清楚它们的边界,做技术选型时才不会走偏。
2.1 简单工厂:最直接、最省事
简单工厂其实不是一个正式的设计模式,更像是一种编程习惯。它通常把创建逻辑集中到一个静态方法里,根据传入参数的不同返回不同类型的实例。
public class SenderFactory { public static Sender create(String type) { if ("sms".equals(type)) { return new SmsSender(); } else if ("email".equals(type)) { return new EmailSender(); } else if ("app".equals(type)) { return new AppSender(); } throw new IllegalArgumentException("未知发送类型: " + type); } }调用方拿到的是统一的Sender接口,无需关心具体实现。这种方式优点是简单直观,几个条件判断就能解决问题;缺点是每增加一个类型就得修改工厂方法,违反了开闭原则。所以在类型数量较少、变化不频繁的模块里,简单工厂非常顺手。
2.2 工厂方法:把创建逻辑下沉到子类
当类型数量开始膨胀,或者不同产品的构造流程差异比较大时,简单工厂里的那个静态方法会逐渐变成一个大杂烩,塞满if和switch。这时候就需要把创建逻辑从单一的工厂里拆出来,交给各个子工厂去做,这就是工厂方法模式。
public interface SenderFactory { Sender create(); } public class SmsSenderFactory implements SenderFactory { @Override public Sender create() { // 可以在这里处理SmsSender特有的初始化逻辑 return new SmsSender(); } } public class EmailSenderFactory implements SenderFactory { @Override public Sender create() { // 可以在这里配置邮箱服务器的参数 return new EmailSender(); } }这种形态的核心价值,是让新增产品类型时不必改动已有工厂。想加一个 AppSender,只需要新增AppSenderFactory实现SenderFactory接口即可,原有代码完全不用动,这才是真正符合开闭原则的写法。
2.3 抽象工厂:面向产品族
抽象工厂比工厂方法又进了一步,解决的问题是“一组相互关联的对象如何统一创建”。打个比方,如果发送消息和记录消息日志是配套出现的,短信配短信日志、邮件配邮件日志,那么直接用工厂方法就得分别创建两个工厂,调用方容易搞混。抽象工厂把这组相关联的产品放在同一个工厂接口里约定好,保证它们始终是配套的。
public interface MessageFactory { Sender createSender(); Logger createLogger(); } public class SmsFactory implements MessageFactory { @Override public Sender createSender() { return new SmsSender(); } @Override public Logger createLogger() { return new SmsLogger(); } }说白了,工厂方法关注的是“同一产品等级”的扩展,抽象工厂关注的是“产品族”之间的组合约束。实际项目中,抽象工厂用得相对少一些,因为大多数业务还没复杂到必须用产品族来约束的程度。
3. 从需求出发完成工厂模式的落地
理论讲完之后,最关键的还是动手写一遍。我拿一个完整的业务场景来做拆解,从需求分析到代码实现,把工厂模式完整的落地过程展示出来。
3.1 需求场景与选型判断
假设要做一个订单售后系统,支持三种退款方式:原路退回、支付宝退回、银行卡退回。每种退款方式的参数不一样,有的需要对接支付网关,有的需要走银行接口,有的只是内部账户操作。开发时如果到处new RefundService(),一旦支付渠道方调整了接口,改动量会很吓人。
在这个场景下,我首先判断的就是产品类型是否稳定、扩展频率高不高。三种退款方式是明确的业务形态,短期内大概率会新增渠道,比如微信支付、礼品卡余额等。基于这个判断,我选择了工厂方法模式,而不是简单工厂。理由很简单:工厂方法能保证后续扩展时不动现有代码,这在对接外部支付渠道的场景里非常重要,毕竟每一次改动都意味着回归测试。
3.2 代码实现与关键细节
先把配置和结构搭起来,我习惯把接口、具体实现、工厂分层放,避免后面找代码时在包结构里头晕。
public interface RefundService { RefundResult refund(RefundRequest request); }然后是实现类,每个渠道各自处理自己的逻辑。
public class OriginalRefundService implements RefundService { @Override public RefundResult refund(RefundRequest request) { // 原路退回的逻辑 return RefundResult.success(); } } public class AlipayRefundService implements RefundService { @Override public RefundResult refund(RefundRequest request) { // 调用支付宝退款接口 return RefundResult.success(); } }接下来是工厂接口和具体工厂:
public interface RefundServiceFactory { RefundService createRefundService(); } public class OriginalRefundFactory implements RefundServiceFactory { @Override public RefundService createRefundService() { return new OriginalRefundService(); } } public class AlipayRefundFactory implements RefundServiceFactory { @Override public RefundService createRefundService() { return new AlipayRefundService(); } }然后还需要一个工厂的注册中心,让调用方通过一个入口拿到对应的工厂:
public class RefundFactoryRegistry { private static final Map<String, RefundServiceFactory> FACTORIES = new HashMap<>(); static { FACTORIES.put("original", new OriginalRefundFactory()); FACTORIES.put("alipay", new AlipayRefundFactory()); } public static RefundServiceFactory getFactory(String channel) { RefundServiceFactory factory = FACTORIES.get(channel); if (factory == null) { throw new IllegalArgumentException("不支持的退款渠道: " + channel); } return factory; } }调用方的代码就非常简洁了:
RefundServiceFactory factory = RefundFactoryRegistry.getFactory(channel); RefundService refundService = factory.createRefundService(); refundService.refund(request);看到这里你应该感受到工厂模式的价值了:调用方完全不感知具体实现类,只依赖RefundService接口。如果哪天新增了“礼品卡退款”,只需要新建GiftCardRefundService和GiftCardRefundFactory,然后往注册中心里加一行注册代码,之前的调用方代码一行都不用改。
3.3 一些容易被忽略的实操细节
有几个点我认为非常值得单独拎出来强调,都是在实际开发中踩过坑才明白的。
第一,工厂层级不要设计太深。有些开发者为了套概念,把工厂又抽象出几层,结果新同事接手时根本看不明白对象到底是谁创建出来的。工厂的价值在于隔离复杂性,而不是制造复杂性。一般能保证“接口 + 一组实现 + 一组工厂”这个平铺结构就够了,除非代码规模真的大到需要额外分层。
第二,装配参数不要硬编码在工厂里。如果每个渠道的退款需要不同的超时时间、重试次数,建议把配置外置到配置文件或者注册中心里,工厂在创建对象时从配置中心读取。否则渠道一多,工厂内部全是魔法数字,维护成本直接起飞。
第三,注意线程安全问题。如果工厂本身是无状态的,完全可以使用单例模式配合静态注册中心;但如果工厂持有了一些可变状态(比如缓存了一些连接信息),就必须做好并发控制,否则高并发场景下容易出现线程安全隐患。
4. 工厂模式在项目中的融合应用
工厂模式单独使用的场景并不少见,但真正发挥威力的时候,往往是和其他设计模式配合起来用。这部分我聊聊实践中比较常见的组合方式,以及工厂模式和依赖注入框架之间的关系。
4.1 与策略模式的搭配
工厂模式和策略模式经常是一对黄金搭档。策略模式的核心是定义一组可互相替换的算法,工厂模式可以负责在运行时根据条件选择具体的策略实现。还是拿退款场景来说,不同渠道的退款就是一种不同的退款策略,工厂根据传入的渠道参数返回对应的退款策略对象,调用方再执行统一的退款流程。这时候,工厂模式承担了策略对象的创建与分发职责,策略模式承担了算法的统一抽象,两者配合让系统既灵活又清晰。
4.2 与单例模式的配合
很多工厂对象本身不需要重复创建,所以工厂常和单例模式配合使用。比如上述的RefundFactoryRegistry,所有线程直接共享同一个注册表即可,没必要每次调用都new一个。配合方式也简单,可以是静态字段初始化,也可以用枚举实现单例,反正目的是保证工厂实例唯一。
4.3 现代框架对工厂模式的影响
现在很多项目都在用 Spring 这类依赖注入框架,容器本身就自带了工厂能力。你在 Spring 里定义的各种 Bean,实际上就是通过容器这个“大工厂”来创建的。Spring 的ApplicationContext本身就是一个抽象工厂的典型实现。
但这不代表工厂模式就失去了意义。在框架基础上,我们仍然需要业务层面的工厂来管理“同类接口的不同实现”。比如一个接口在 Spring 里有多个实现类,需要根据运行时条件动态选择注入哪个,这时候可以结合ApplicationContext的能力,写一个业务工厂,从容器中按名称或类型获取对应的 Bean。这种做法的好处是:对象的生命周期管理交给框架,而业务创建策略由我们自己控制,两全其美。
实际融合的示例大概是这样的:
@Component public class RefundFactoryBean { @Autowired private ApplicationContext context; public RefundService getRefundService(String channel) { String beanName = channel + "RefundService"; return context.getBean(beanName, RefundService.class); } }这种方式在 Spring 项目中非常实用,既利用了容器的优势,又保留了工厂模式的灵活语义。不过要注意,使用ApplicationContext直接按名称取 Bean,如果 Bean 名称拼写错了,运行时会报NoSuchBeanDefinitionException,排查起来需要看日志上下文,建议把 Bean 名称的映射统一收敛到一个枚举或者常量类中,降低出错概率。
5. 常见问题与排查技巧实录
在设计模式的问答区和论坛里,工厂模式相关的问题一直都有热度。很多问题不是模式本身难,而是使用场景没拿捏好,实现细节没考虑周全。我整理了实战中常遇到的几个问题,附带我的排查思路和处理建议。
5.1 工厂类和业务类之间是不是必须一一对应
不是。工厂方法模式里,工厂类通常和产品类一一对应,比如SmsSenderFactory对应SmsSender。但在简单工厂的写法里,一个工厂可以创建多个产品类。而在某些复杂场景下,一个工厂也可能创建几个产品类,比如抽象工厂里一个工厂对应一组产品。所以不要死板地认为工厂和产品必须一比一。
5.2 工厂模式解决了什么问题,带来了什么问题
它解决的问题是解耦,让调用方不依赖具体类,新增类型时尽量不用改旧代码。它带来的问题也很明显——如果过度使用,会让系统类数量激增,一个简单的创建动作被拆成好几层,阅读和维护成本反而上升。
我的建议是:当创建逻辑稳定、产品类型少、几乎不会扩展时,直接用new就好;只有当创建逻辑确实复杂,或者产品类型变化频繁时,才值得引入工厂模式。模式是为代码可维护性服务的,不是为了凑概念才存在的。
5.3 简单工厂里那些 if-else 怎么消除最优雅
简单工厂最常见的毛病就是if-else或switch堆积。我个人比较推荐用注册表加函数式接口的方式来替代,特别是当判断条件很多、每个分支只是返回不同的对象时,用 Map 可以大幅减少分支代码量。
public class SenderFactory { private static final Map<String, Supplier<Sender>> CREATORS = new HashMap<>(); static { CREATORS.put("sms", SmsSender::new); CREATORS.put("email", EmailSender::new); CREATORS.put("app", AppSender::new); } public static Sender create(String type) { Supplier<Sender> supplier = CREATORS.get(type); if (supplier == null) { throw new IllegalArgumentException("未知发送类型: " + type); } return supplier.get(); } }这种写法的好处很明显,新增类型时只需要CREATORS.put一行代码,而且不需要进入复杂的嵌套判断逻辑。对于 Java 8 及以上的项目是个不错的优化方向。
5.4 工厂创建出来的对象需要销毁,资源管理怎么做
这个问题在普通对象创建上不太起眼,但一旦工厂管理的是数据库连接、网络客户端、文件句柄这类有限资源时,就变得很关键。工厂模式本身不管对象生命周期,所以必须配合使用方或者容器来统一管理资源释放。
一个稳妥的思路是让工厂返回的对象实现统一的关闭接口,比如 Java 里的AutoCloseable,然后在调用方使用try-with-resources来确保释放。如果对象生命周期较长,可以结合对象池技术,工厂只负责从池子里借出对象,使用完成后归还,而不是每次新建销毁。
5.5 什么时候可以考虑放弃工厂模式,改用抽象工厂模式
实际项目中,工厂方法和抽象工厂并没有一条绝对清晰的分界线。我的判断标准是:当产品之间存在隐式的配套约束时,就要考虑用抽象工厂。举一个典型的场景,一个跨平台 UI 组件库,在 Windows 平台和 macOS 平台上分别有对应的按钮、文本框和下拉框,如果你允许 Windows 按钮和 macOS 文本框混用,界面的风格就会乱套。抽象工厂正好通过一个工厂接口把同一平台的一组组件绑定在一起,从机制上杜绝了这种混用。
如果在日常业务开发中并没有这种产品族的概念,那就不要硬上抽象工厂,工厂方法通常已经够用了。
5.6 工厂模式会不会影响单元测试
会,而且影响还不小。如果你在业务代码里直接调用工厂的静态方法拿对象,测试时会发现没法轻松替换成 mock 对象,因为静态方法无法被常规方式 mock。这在简单工厂写法中尤其明显。
处理方案有几种:一种是把工厂对象注入到业务类中,而不是在业务类里直接调用静态方法,这样测试时可以注入一个 mock 工厂;另一种是依赖 Spring 的依赖注入能力,在测试配置里替换具体的 Bean。个人更推荐第一种,因为它无论是配合单元测试还是配合代码审查,都更直观,也不会因为全局静态状态导致测试之间互相干扰。
6. 个人经验与编码建议
模式这个东西,理论学习一个月可能就掌握了,但真正能在项目里用得恰到好处,需要时间的打磨。我个人的体会是:不要因为某个模式“高级”就用,也不要因为某种写法“简单”就不用,一切以当前代码的可读性和可维护性为决策依据。
在做技术选型时,我通常会先画一张简单的表格,把场景的关键因素列出来,再决定用哪种工厂形态:
| 场景特征 | 推荐方案 | 理由 |
|---|---|---|
| 产品类型少、创建逻辑简单 | 直接new或简单工厂 | 避免过度设计,代码直观 |
| 产品类型多、扩展频繁 | 工厂方法 | 新增类型不改旧代码,符合开闭原则 |
| 产品之间存在配套约束 | 抽象工厂 | 从类型上保证产品组合的正确性 |
| 项目已使用 DI 容器 | 容器 + 业务工厂 | 结合框架能力,简化管理 |
最后再分享一个小技巧。在写工厂注册表时,我几乎总是会用枚举来约束 key,而不是直接用字符串常量。比如定义一个ChannelType枚举,把original、alipay、wechat这些渠道名都收进枚举,工厂注册表使用Map<ChannelType, RefundServiceFactory>作为存储结构。这样写的好处,一方面把渠道范围收敛到一个明确集合里,传参时编译器就能帮你拦截错误;另一方面,在 IDE 里也很好找,想加新渠道时顺着枚举一目了然。这个习惯我在几个中大型项目中都已经固化下来了,效果非常稳定。