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 MapString, 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 MapString, SupplierSender 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) { SupplierSender 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这些渠道名都收进枚举工厂注册表使用MapChannelType, RefundServiceFactory作为存储结构。这样写的好处一方面把渠道范围收敛到一个明确集合里传参时编译器就能帮你拦截错误另一方面在 IDE 里也很好找想加新渠道时顺着枚举一目了然。这个习惯我在几个中大型项目中都已经固化下来了效果非常稳定。
阅读完成 · 觉得有帮助?