
先聊个场景。我做消息推送系统的时候第一版代码里到处是new EmailSender()、new SmsSender()后来需求一改要往EmailSender构造方法里塞一个 SMTP 配置我当时人还在开会临时让同事先顶一下结果他打开代码一看十几个地方都在 new每个地方都要改参数当场就翻了。也就是从那次之后我意识到“创建对象”和“使用对象”这两件事如果不拆开代码迟早要爆炸。简单工厂模式Simple Factory Pattern解决的就是这么个问题把对象的创建逻辑集中到某一个独立的类里调用方只告诉工厂“我想要什么”工厂负责把对象创建好再交给你。整个过程里调用方完全不接触具体类的构造过程看不到任何创建细节。它严格来说不是 GoF 23 种设计模式里的一员更像是一种编程习惯但因为这个习惯太常见、太好用几乎所有讲设计模式的书都会把它放在第一篇来讲。这篇文章我会从一段没有工厂的“裸奔代码”讲起逐步拆解简单工厂的核心角色、实现细节、适用边界再顺着“创建对象”这件事把 Java 对象从类加载到内存分配的完整链路也串一遍。最后分享几个我在真实项目里踩过的坑。适合刚接触设计模式的初学者也适合那些已经在写业务代码、但总觉得代码越来越难维护的同学。1. 简单工厂到底在解决什么问题从一段没有工厂的代码说起1.1 没有工厂时创建逻辑散落在哪里先看一段很典型的业务代码。假设你要做一个多渠道通知系统支持邮件、短信、App 推送三种渠道。很多人的第一版代码长这样public class NotificationService { public void send(String type, String content) { if (email.equals(type)) { EmailSender sender new EmailSender(); sender.setSmtpHost(smtp.example.com); sender.setSmtpPort(465); sender.setUsername(no-replyexample.com); sender.setPassword(secret); sender.setSsl(true); sender.send(content); } else if (sms.equals(type)) { SmsSender sender new SmsSender(); sender.setGatewayUrl(http://sms.example.com/api); sender.setAppKey(appKey123); sender.setAppSecret(appSecret456); sender.send(content); } else if (push.equals(type)) { PushSender sender new PushSender(); sender.setApiToken(token_abc); sender.setAppId(app_001); sender.send(content); } else { throw new IllegalArgumentException(unsupported type: type); } } }这段代码有几个很明显的问题。第一创建逻辑完全暴露给调用方。NotificationService的send方法要做的事是“发送通知”但它被迫知道了每个渠道的配置参数、初始化顺序、默认值这些跟“发送”这件事本身毫无关系。第二修改任何一个产品的构造方式都要改所有调用处。比如三个渠道的配置要挪到配置文件里或者某个Sender的构造方法要加一个参数你就得把所有写了new的地方全部找出来改一遍。一个项目里可能还不止一个NotificationService到时候改起来就是一场灾难。第三测试非常难写。你想对NotificationService做单元测试结果它内部 new 了真的EmailSender测试的时候真的去连 SMTP 服务器一跑就超时。正确的做法应该是对创建过程做隔离但最终代码里根本看不到替换的口子。1.2 引入简单工厂之后变成什么样把创建逻辑抽取到一个独立的工厂类里代码就变成这样public class NotificationSenderFactory { public static NotificationSender create(String type) { if (email.equals(type)) { EmailSender sender new EmailSender(); sender.setSmtpHost(smtp.example.com); sender.setSmtpPort(465); sender.setUsername(no-replyexample.com); sender.setPassword(secret); sender.setSsl(true); return sender; } if (sms.equals(type)) { SmsSender sender new SmsSender(); sender.setGatewayUrl(http://sms.example.com/api); sender.setAppKey(appKey123); sender.setAppSecret(appSecret456); return sender; } if (push.equals(type)) { PushSender sender new PushSender(); sender.setApiToken(token_abc); sender.setAppId(app_001); return sender; } throw new IllegalArgumentException(unsupported type: type); } }调用方的代码一下子清爽了public class NotificationService { public void send(String type, String content) { NotificationSender sender NotificationSenderFactory.create(type); sender.send(content); } }这个例子是我在实际项目里做过的简化版核心变化就是“创建”这个动作被收拢到了工厂类里业务代码只面对一个统一的入口。1.3 四个角色划分谁负责什么谁不负责什么拆开看简单工厂模式一共涉及四个角色。产品抽象Product定义产品的统一接口或抽象类也就是代码里的NotificationSender。它只声明send方法不关心具体实现。具体产品Concrete Product实现了产品接口的具体类比如EmailSender、SmsSender、PushSender。它们各自完成真正的业务逻辑。工厂类Factory负责创建具体产品也是整个模式的“大脑”。它接收一个参数类型、枚举、字符串等通过判断返回对应的产品实例。客户端Client也就是调用方只依赖产品抽象和工厂不直接依赖具体产品类。这种职责划分最核心的一个点在于具体产品类对客户端不可见。客户端不知道EmailSender存在不知道它的构造方法需要哪些参数它只知道“传一个 email拿回来一个能发通知的东西”。提示判断一个实现是不是合格的简单工厂就看客户端除了工厂和产品接口之外是否 import 了任何具体产品类。如果 import 了就说明创建逻辑还是暴露了。2. 手写一个简单工厂核心实现与关键技术细节2.1 第一步定义产品抽象别把接口设计成大杂烩产品接口是工厂的返回类型也是客户端唯一依赖的类型接口设计得合不合理直接影响整个模式的好坏。很多初学者会把接口设计得非常大把各种渠道独有的方法都塞进去。比如EmailSender有setSmtpHostSmsSender有setGatewayUrl那就把这两个方法都写到NotificationSender接口里。这样做的后果是客户端的代码必须处理一堆用不到的方法而且每个产品类都要实现跟自己无关的方法。正确的做法是接口只保留所有产品共有的业务方法。我的经验是如果一个方法只有某个具体产品有它就不应该出现在接口里。比如通知场景里所有渠道都可以做“发送”那就只定义send(String content)各渠道特殊的配置方法放在各自的具体类里由工厂内部去调用客户端不关心。public interface NotificationSender { void send(String content); }接口的方法名和参数也要尽量稳定。后面所有具体产品都要实现这个接口如果方法签名经常变所有实现类都要跟着改工厂和客户端也会受影响改造成本会指数级上升。2.2 第二步实现具体产品每个类只关心自己的逻辑具体产品类的实现相对直接。以EmailSender为例public class EmailSender implements NotificationSender { private String smtpHost; private int smtpPort; private String username; private String password; private boolean ssl; public void setSmtpHost(String smtpHost) { this.smtpHost smtpHost; } public void setSmtpPort(int smtpPort) { this.smtpPort smtpPort; } // 其他 setter 省略 Override public void send(String content) { // 这里才是真正的邮件发送逻辑 System.out.println(Sending email via smtpHost : smtpPort : content); } }具体产品类不要自己去加载配置、解析参数这些初始化工作应该由工厂完成。好好理解这个分工产品类专注“怎么发送”工厂专注“怎么创建和配置”。一旦产品类既管业务又管初始化就又回到了耦合的老路。2.3 第三步编写工厂类这五个细节直接影响代码质量工厂类是简单工厂模式里最重要的一个类实现的时候有五个细节我建议认真处理。参数类型用枚举或常量不要用裸字符串。直接传email这种字符串在写代码时很容易手滑打成emial编译器还不会报错运行期才抛异常。用枚举可以在编译期就避免这个问题IDE 也有自动补全。public enum SenderType { EMAIL, SMS, PUSH }create 方法内部要处理未知类型。无论用if-else还是switch都必须有一个默认分支抛异常。我的习惯是抛IllegalArgumentException异常信息里带上传入的非法参数方便排查问题。工厂方法的返回值类型必须是产品接口不能是具体类。一旦返回EmailSender客户端就又被拉回了依赖具体类的老路工厂等于白写了。区分静态方法和实例方法。大多数简单工厂用静态方法因为调用简单NotificationSenderFactory.create(...)一行就完了。但静态方法有测试上的缺陷——没法在测试里替换工厂实现。如果你的项目里需要 mock 工厂比如测试NotificationService时不想真的创建EmailSender更推荐把它设计成实例方法然后通过构造方法注入public class NotificationService { private final NotificationSenderFactory factory; public NotificationService(NotificationSenderFactory factory) { this.factory factory; } public void send(String type, String content) { NotificationSender sender factory.create(type); sender.send(content); } }考虑对象复用的可能。有些产品对象的创建成本很高比如要建立网络连接、加载配置、初始化线程池每个对象都从头创建一次很浪费。工厂可以做一层缓存同类型的对象用同一个实例。但要注意线程安全问题如果产品实现不是线程安全的就不能乱复用。综合这些点一个相对完整的工厂实现长这样public class NotificationSenderFactory { public NotificationSender create(SenderType type) { switch (type) { case EMAIL: return createEmailSender(); case SMS: return createSmsSender(); case PUSH: return createPushSender(); default: throw new IllegalArgumentException(unknown sender type: type); } } private EmailSender createEmailSender() { EmailSender sender new EmailSender(); sender.setSmtpHost(smtp.example.com); sender.setSmtpPort(465); sender.setUsername(no-replyexample.com); sender.setPassword(secret); sender.setSsl(true); return sender; } // 其他创建方法省略 }2.4 客户端视角创建逻辑真正做到“不可见”完成之后客户端代码只有一个关注点拿到一个产品实例调用它的方法。创建过程里所有的判断逻辑、参数装配、异常分支全部被工厂吞掉了。public class Application { public static void main(String[] args) { NotificationSenderFactory factory new NotificationSenderFactory(); NotificationSender sender factory.create(SenderType.EMAIL); sender.send(订单支付成功感谢您的购买。); } }这种“隐藏”不是通过访问修饰符强制实现的而是通过设计约定实现的。EmailSender、SmsSender这些具体类还在那里但约定是除了工厂其他代码不准 new 它们。团队协作时这个约定靠命名和代码评审来保障等后续用了 Spring 这类框架才能从框架层面真正禁止手动创建。3. 什么时候该用什么时候别硬用从真实项目里总结的判断标准3.1 适合使用简单工厂的三种典型场景不是所有代码都值得套一个工厂。我总结了三个“看到就可以考虑上工厂”的信号。第一产品类型不多且短期内不会频繁增加。如果你负责的是一个内部工具渠道就邮件和短信两种也没有规划要接入新渠道那简单工厂非常合适。一个小 switch 就能搞定代码清晰维护成本低。第二产品对象的创建过程包含大量初始化逻辑。比如每个产品都要从配置文件读参数、要组装依赖、要做格式转换。这些逻辑如果放到业务代码里业务代码会变得很臃肿。工厂把这些细节收纳起来业务代码就干净了。第三调用方只需要“用产品”不关心“怎么造产品”。这个信号要从客户端代码里去闻味道如果一个类里出现了几种具体产品的构造方法并且有if-else或switch在判断用哪个这个东西八成可以抽取成一个工厂。3.2 不适合的情况别把简单工厂当成万能药简单工厂最大的毛病是违反开闭原则。新增一个产品类别必须修改工厂类的代码在switch或者if-else里加一个分支。如果产品类别很多、扩展频繁工厂类会变成一个不断膨胀的“上帝类”每次加需求都要动它非常容易出错。判断标准很简单如果你的产品每周都在变或者已经出现“工厂里全是 case”的情况就该考虑工厂方法模式了。工厂方法模式把工厂本身抽象成一个接口每种产品对应一个子工厂新增产品时不需要改已有代码只需要新增一个工厂子类和一个产品类。3.3 一张表看懂简单工厂、工厂方法、抽象工厂的取舍很多初学者容易把这三种模式搞混。它们的核心区别在边界上我用一张表来总结维度简单工厂工厂方法抽象工厂核心思想一个工厂类通过参数判断返回不同产品一个产品对应一个工厂由子工厂决定创建哪个产品一个工厂对应一组相关产品保证产品族一致性扩展方式修改工厂类代码新增工厂子类和产品类符合开闭原则新增工厂子类和一组产品类符合开闭原则客户端依赖工厂类 产品接口抽象工厂 产品接口抽象工厂 多个产品接口复杂度最低中等较高适用场景产品少、创建简单、扩展不频繁产品扩展频繁、每个产品创建逻辑差异大存在多个产品族、必须成套使用产品我的经验是先在简单工厂里待着等它真的疼了再升级到工厂方法。设计模式不是越复杂越好过早引入抽象反而会让代码难懂。4. “创建对象”这件事背后从 Java 对象创建过程到内存布局4.1 无论你怎么 newJVM 都在做这五件事“创建对象”这四个字在 Java 里并不是一个简单的动作。调用方写一行new EmailSender()JVM 在底层做了一整套流程。理解这个过程有助于理解为什么“集中创建逻辑”值得单独做一个模式。类加载检查JVM 先检查EmailSender类是否已经被加载、解析、初始化过。如果还没有先触发类加载流程通过类加载器把.class文件加载到方法区。分配内存类确认可用的前提下JVM 在堆内存里为EmailSender对象划分一块固定大小的内存区域。分配方式有指针碰撞和空闲列表两种取决于堆内存是否规整而堆是否规整又取决于垃圾收集器是否带压缩整理功能。内存空间初始化JVM 把这块内存区域清零。这一步很关键它保证了对象的实例字段即使没有显式赋值也有默认值。比如int smtpPort会变成 0String username会变成 null。设置对象头JVM 在对象头里记录必要信息包括这个对象是哪个类的实例类型指针、对象的哈希码、GC 分代年龄、锁状态标志等。执行构造方法JVM 调用init方法也就是你在代码里写的构造器、实例初始化块、实例变量的默认值赋值。这一步执行完之后一个真正意义上“可使用”的对象才诞生。简单工厂的出现不会改变这五个步骤它改变的是“什么时候执行这五步”“在哪里执行”的决定权。没有工厂时每个业务类都可以触发这个过程你可以选择在 30 个地方各 new 一个EmailSender有工厂时触发点只有一个也就是工厂的create方法。4.2 对象在内存里长什么样以及调用方如何“找到”它创建完之后对象在堆里的布局通常是三块区域对象头Object Header、实例数据Instance Data、对齐填充Padding。对象头存放 Mark Word 和类型指针。Mark Word 记录了对象的运行时数据比如哈希码、GC 分代年龄、锁状态类型指针指向方法区的类元数据告诉 JVM 这个对象属于哪个类。实例数据存放的是真正业务相关的字段比如smtpHost、port。对齐填充不是必选的它只是为了满足 JVM 要求对象起始地址是 8 字节的整数倍。调用方拿到对象引用之后怎么通过引用访问到这个对象HotSpot 虚拟机主要用直接指针的方式引用里直接存储对象地址访问对象的实例数据一步到位。还有一种方式是句柄池引用先指向句柄池中的句柄句柄里再存对象地址好处是对象移动时只需要改句柄引用不变坏处是多一次指针定位。对理解设计模式来说你不需要把内存布局背得滚瓜烂熟但要建立一个概念对象创建不是一个透明、廉价的动作它背后有完整的链路。把这条链路的触发点收拢到工厂本质上是在说“创建有成本、有规则的不能允许业务代码随意触碰”。4.3 创建逻辑“不暴露”的本质把变化关在门内把简单工厂和对象创建链路放在一起看会得出一个更深的结论简单工厂做的不只是“封装 new 语句”它封装的是整条“对象生产流水线”。没有工厂时业务代码要想创建SmsSender它必须知道 SmsSender 存在、知道它的构造器、知道它的配置怎么填、知道配置从哪来。这些知识散落在业务代码里一旦变化就是连锁反应。有工厂时所有这些知识被集中在一个点。业务代码只需要知道一个稳定的入口factory.create(SenderType.SMS)。产品类的变化被困在工厂内部产品实现想怎么改、配置想怎么调、依赖想怎么换外边都不感知。“不暴露创建逻辑”的本质就是把变化最大、最频繁的因子关在一扇门内门外的代码只面对稳定的接口。5. 实战高频问题与排查实录那些文档里不会讲的坑5.1 常见的六个问题以及对应的处理方案我在很多项目里都见到过简单工厂的错误用法以下六个问题出现频率最高整理成一张速查表问题典型现象原因解决方案工厂方法返回具体类型客户端直接 new 出EmailSender再赋值没理解返回类型的作用返回类型改成产品接口工厂出现大量 if-else工厂类几百行全是判断分支产品过多且不断新增考虑工厂方法模式或引入注册表字符串类型拼写错误运行时抛IllegalArgumentException用裸字符串传参改用枚举或常量类测试时真的创建产品对象单测跑到真实网络请求静态工厂无法替换工厂改成实例方法并依赖注入工厂内部重复创建昂贵对象每次创建都建立新网络连接没考虑对象复用增加缓存或池化客户端绕过工厂代码里直接new EmailSender()靠约定约束力不够代码评审把关后续引入容器5.2 用反射和配置消灭 if-else进阶玩法当产品类型比较多时一套反射 配置文件的写法可以替代工厂里的“一堆 case”这个套路我在项目里用过多次效果不错。public class NotificationSenderFactory { private static final MapString, Class? REGISTRY new HashMap(); static { // 实际项目里这些映射关系可以来自配置文件 REGISTRY.put(email, EmailSender.class); REGISTRY.put(sms, SmsSender.class); REGISTRY.put(push, PushSender.class); } public NotificationSender create(String type) { Class? clazz REGISTRY.get(type); if (clazz null) { throw new IllegalArgumentException(unknown sender type: type); } try { return (NotificationSender) clazz.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new RuntimeException(create sender failed: type, e); } } }好处是新增产品的时候只需要把新类放进注册表不需要动create方法本身。坏处是反射创建对象有轻微性能开销而且如果具体产品类没有默认构造器反射那一下会失败。我的建议是不是所有场景都用反射产品类多到一定程度、且配置化需求明显时再用。5.3 从简单工厂到更广阔的天地Spring 容器其实就是一个大工厂讲到简单工厂的进阶不得不提 Spring。Spring 的ApplicationContext从本质上说就是一个超级工厂你告诉它“我想要哪个 Bean”它负责创建、初始化、注入依赖然后把一个可用的对象交给你。业务代码里常见的Autowired、Resource背后都是这个逻辑。这也是为什么很多人说“用了 Spring 之后手动写简单工厂的机会变少了”——因为创建逻辑已经被容器接管了。但我要提醒一点框架替你做不等于你可以不懂这层逻辑。当你需要自己实现一个插件机制、一个多租户扩展点或者一个策略分发器时简单工厂的思路依然是最快的解法。比如策略分发器我最近在一个支付对接项目里就用到了不同支付渠道微信、支付宝、银联的支付请求参数差异很大但接口的返回结构被统一了。我用一个简单工厂根据支付类型返回对应的渠道处理器再统一调用。代码非常干净后续接新渠道时只需要加一个处理器类在工厂里加一个分支改动面非常小。5.4 我踩过的三个坑分享给正在动手的你第一个坑是工厂返回类型的误用。早期我写过这样的代码工厂的create方法返回EmailSender结果业务代码里直接引用了EmailSender独有的方法工厂形同虚设。后来我在 code review 里会专门检查“有没有人 import 了具体产品类”一旦发现就打回。第二个坑是对“创建逻辑”的理解过于狭窄。一开始我以为简单工厂只是把new换了个位置后来才发现真正的价值在于把“参数从哪来、依赖怎么组装、异常怎么处理”也一并收拢。如果工厂只是返回一个裸的new EmailSender()那加了跟没加一个样该暴露的创建逻辑还是没有真正闭起来。第三个坑是过早引入复杂模式。有段时间我特别喜欢“抽象”看到工厂里有两个 if 就想着换工厂方法模式结果产品根本没那么多种反而给团队增加了理解成本。后来我学乖了设计模式的引入要跟着业务复杂度走不是跟着代码审美走。简单工厂能解决问题的时候就不要急着上更复杂的方案。结尾关于设计模式我个人一直坚持的看法做了这么多年开发真正把设计模式用到“化境”的人很少会把模式名字挂在嘴边。简单工厂也好工厂方法也好本质上都是在管理一个东西变化。把变化集中到一个点让其他代码不受波及这就是它存在的最大的意义。细想一下Spring 为什么把 IoC 容器放在那么核心的位置Docker 为什么能把环境的一致性做到位背后的思想跟简单工厂如出一辙——把“怎么创建”的复杂性收起来对外只露出一个稳定的接口。你把这个思想弄通了设计模式这层窗户纸也就捅破了大半。最后再分享一个我个人的操作习惯每次写完一个工厂类我会在代码评审时问自己三个问题。第一客户端是否还依赖了具体产品类第二新增一个产品改动范围是否只有一个点第三工厂内部是否仍然“简单”——没有过度设计没有为了抽象而抽象三个问题的答案都是肯定的我才觉得这个工厂写到位了。如果你想练手我建议别去抄那些“形状 Shape”“动物 Animal”的教科书例子直接从自己的业务里找一个“类型判断 创建 初始化”的地方动手抽一个简单工厂出来。做完你会有一种“代码瞬间顺眼不少”的感觉这种体感是看十篇文章都换不来的。