1. 为什么我要单独拎出简单工厂模式来讲做Java开发这些年设计模式来来回回翻过好几轮23种模式里真正在业务代码里高频出现的其实就那么几个简单工厂模式绝对算一个。很多人一提到工厂模式就直奔工厂方法、抽象工厂去了觉得简单工厂太低级、不够面向对象、违反开闭原则面试的时候都不好意思说。但我自己的体会恰恰相反——在中小型项目里简单工厂是投入产出比最高的创建型模式没有之一。这篇文章要聊的就是简单工厂模式在一个设计模式示例项目里的完整落地形态。我会把代码结构、每个类的职责、为什么这么拆、实际写的时候容易踩什么坑全部摊开讲清楚。适合谁看如果你已经会写Java、能看懂接口和继承但对什么时候该抽工厂、工厂里到底放什么逻辑还比较模糊那这篇就是给你准备的。哪怕你之前完全没接触过设计模式跟着代码走一遍也能明白个七八成。先把核心概念摆出来简单工厂模式英文叫Simple Factory有时候也叫静态工厂方法模式。它的本质是把对象的创建过程从调用方手里拿走交给一个专门的工厂类来负责。调用方不再关心new哪个具体实现、构造参数怎么传只需要告诉工厂我要什么类型工厂把造好的对象递给你。听起来简单但它解决的问题很实在——创建逻辑的集中管理和调用方与具体实现类的解耦。我在实际项目里见过太多这样的代码一个方法里散落着七八个new每个new后面跟着一堆if-else判断参数改一个实现类要翻遍整个工程找调用点。这种时候简单工厂就是最直接的解药。它不像工厂方法那样需要为每个产品建一个工厂类也不像抽象工厂那样面向产品族它就是一把螺丝刀简单、直接、够用。2. 项目整体结构与设计思路拆解2.1 这个示例项目的目录长什么样先看整体骨架。这个设计模式示例项目里简单工厂相关的代码通常放在一个独立的包下命名上一般会体现simplefactory或者factory.simple这样的层级。我按常见的组织方式来还原一下结构src/main/java/ └── com/example/patterns/ └── simplefactory/ ├── Product.java // 产品接口 ├── ProductA.java // 具体产品A ├── ProductB.java // 具体产品B ├── ProductC.java // 具体产品C ├── SimpleFactory.java // 工厂类 └── Client.java // 调用方示例这个结构不是随便摆的每一层都有明确意图。Product是抽象产品角色定义所有产品共有的行为契约ProductA/B/C是具体产品角色各自实现接口SimpleFactory是工厂角色扛下所有创建逻辑Client是使用方只跟接口和工厂打交道。四个角色职责分明这就是简单工厂模式的标准四件套。我特别想强调一点包名和类名的语义化非常重要。很多示例代码喜欢用Factory、Product这种泛泛的名字放在教学里没问题但真实项目里你最好带上业务语义比如PaymentChannelFactory、NotificationSender。名字起对了后来的人一眼就知道这个工厂造的是什么不用点进去猜。2.2 为什么是接口 工厂而不是直接new有人会问就三个产品类直接new ProductA()不就完了搞个工厂是不是过度设计这个问题问得好答案取决于变化的方向。直接new的问题在于调用方必须知道具体类名。一旦产品家族要扩展、要替换、要按配置动态选择调用方就得跟着改。举个生活化的例子——你去餐厅吃饭直接new相当于你自己跑进后厨炒菜你得知道灶台在哪、调料放哪而工厂模式相当于你对着菜单点菜后厨怎么做的你不用管菜单上有什么你就能点什么。餐厅换厨师、加菜品你的点菜方式完全不用变。从代码层面看接口的存在让调用方依赖的是抽象而非具体。Client里写的是Product p SimpleFactory.create(A)而不是ProductA p new ProductA()。前者在编译期只依赖Product接口后者死死绑定了ProductA。这个差别在单元测试里尤其明显——你可以轻松用工厂返回一个mock实现而直接new的话测试替身就不好塞进去。2.3 简单工厂与工厂方法、抽象工厂的边界在哪这是最容易混淆的地方我用自己的话划一下界限。简单工厂是一个工厂类管所有产品工厂方法是每个产品配一个专属工厂抽象工厂是一个工厂管一族相关产品。三者的复杂度是递增的。简单工厂的优势是结构最轻一个类搞定学习成本几乎为零。劣势也明显工厂类里通常有一坨if-else或者switch每加一个产品就要改工厂类这确实违反了开闭原则。但我想说句实在话——开闭原则不是教条。如果你的产品类型是有限且稳定的比如支付渠道就那么几种、文件解析格式就那么几类那点if-else带来的维护成本远低于为每个产品建工厂类的复杂度。反过来如果产品类型会频繁增加、且增加时不想动任何已有代码那才该上工厂方法。我在项目里的判断标准很简单产品种类在可预见范围内不超过十来个、且变更频率低就用简单工厂产品会持续扩展、或者需要按插件方式动态注册才考虑工厂方法。这个经验帮我省了很多不必要的抽象。3. 核心角色逐个拆解与代码实现3.1 抽象产品接口契约先行先看Product接口。它是整个模式的基石定义了所有产品必须提供的能力public interface Product { String getName(); void doSomething(); }别看只有两个方法设计上有讲究。getName()返回产品标识方便日志和调试doSomething()是业务行为的占位真实项目里会换成具体方法比如pay()、parse()、send()。接口方法要克制只放所有产品共有的行为不要把某个产品特有的方法塞进来否则其他实现类就得写空实现那是坏味道。注意接口里不要定义构造相关的逻辑也不要把工厂方法塞进产品接口。产品和创建产品的职责要分开这是单一职责原则的基本要求。3.2 具体产品实现各司其职三个具体产品类分别实现接口各自提供差异化行为public class ProductA implements Product { Override public String getName() { return ProductA; } Override public void doSomething() { System.out.println(ProductA is working...); } }ProductB、ProductC结构一致只是行为不同。这里有个实操细节具体产品类尽量保持无状态或者状态简单。如果产品对象需要携带大量运行时状态那创建逻辑会变复杂工厂方法的参数也会膨胀这时候要考虑是不是该用建造者模式配合而不是硬塞进简单工厂。另外产品类的构造器我建议显式声明哪怕是无参的。默认构造器在反射、序列化场景下偶尔会出幺蛾子显式写出来更稳。如果产品初始化有耗时操作比如加载配置、建立连接千万别放在构造器里应该延迟到首次使用时否则工厂一调用就卡住。3.3 工厂类创建逻辑的唯一入口工厂类是重头戏先看最朴素的字符串匹配版本public class SimpleFactory { public static Product create(String type) { if (type null) { throw new IllegalArgumentException(product type must not be null); } switch (type.toUpperCase()) { case A: return new ProductA(); case B: return new ProductB(); case C: return new ProductC(); default: throw new IllegalArgumentException(unknown product type: type); } } }这段代码有几个我特意加进去的防御性设计。第一type为null直接抛异常而不是返回null让调用方去猜第二toUpperCase()做大小写归一避免调用方传a就找不到第三未知类型抛IllegalArgumentException并带上原始值排查问题时日志里能直接看到传了什么。为什么用static方法因为简单工厂的工厂类通常不需要维护状态静态方法调用最方便不用先new工厂再调方法。这也是静态工厂方法模式这个名字的由来。但要注意静态方法不利于mock如果你的测试需要替换工厂行为可以改成实例方法配合依赖注入这是取舍。3.4 调用方只认接口和工厂Client的写法体现了模式的价值public class Client { public static void main(String[] args) { Product product SimpleFactory.create(A); System.out.println(created: product.getName()); product.doSomething(); } }调用方全程没有出现ProductA这个具体类名只依赖Product接口和SimpleFactory。这意味着将来把A对应的实现从ProductA换成别的类Client一行都不用改。这就是解耦带来的实际收益。4. 工厂实现的进阶写法与参数设计4.1 用枚举替代字符串杜绝拼写错误字符串传参最大的问题是编译器帮不了你写错一个字母运行时才炸。用枚举就稳多了public enum ProductType { A, B, C } public class SimpleFactory { public static Product create(ProductType type) { if (type null) { throw new IllegalArgumentException(product type must not be null); } switch (type) { case A: return new ProductA(); case B: return new ProductB(); case C: return new ProductC(); default: throw new IllegalArgumentException(unsupported: type); } } }枚举的好处是类型安全、IDE能自动补全、switch覆盖不全时部分IDE还会提示。代价是产品类型被编译期固定了如果产品需要从配置文件动态加载枚举就不合适得回到字符串或者用注册表方案。我一般是这样权衡产品类型是代码里写死的业务分支用枚举产品类型来自外部配置或插件用字符串加注册表。4.2 注册表式工厂把if-else干掉当产品多到十几个switch会变得很长。这时候可以用Map注册表把创建逻辑分散出去public class RegistryFactory { private static final MapString, SupplierProduct REGISTRY new HashMap(); static { REGISTRY.put(A, ProductA::new); REGISTRY.put(B, ProductB::new); REGISTRY.put(C, ProductC::new); } public static Product create(String type) { SupplierProduct supplier REGISTRY.get(type); if (supplier null) { throw new IllegalArgumentException(unknown product type: type); } return supplier.get(); } public static void register(String type, SupplierProduct supplier) { REGISTRY.put(type, supplier); } }这个版本用SupplierProduct把怎么造抽象成函数注册表负责造哪个。新增产品只需register一行工厂核心逻辑不动某种程度上缓解了开闭原则的问题。register方法还留了口子允许运行时动态注册适合插件化场景。提示注册表用ConcurrentHashMap更稳妥如果存在运行时注册的可能普通HashMap在并发下会出问题。静态初始化块里的put在类加载时执行单线程安全但后续的register调用要考虑并发。4.3 带参数的创建工厂方法签名怎么定真实产品往往需要构造参数比如数据库连接需要url、用户名密码。工厂方法签名要能容纳这些参数public static Product create(String type, MapString, Object params) { switch (type) { case A: return new ProductA((String) params.get(name)); case B: return new ProductB((Integer) params.get(size)); default: throw new IllegalArgumentException(unknown: type); } }用Map传参灵活但类型不安全强转可能抛ClassCastException。更严谨的做法是为每类产品定义专门的参数对象或者干脆拆成多个重载方法。我的经验是参数少于三个且类型简单用重载参数多且因产品而异用参数对象实在要通用用Map但务必在工厂内部做类型校验并给出清晰异常信息。5. 实操过程中的坑与排查实录5.1 常见问题速查表问题现象可能原因排查与解决工厂返回null导致NPE未知类型时返回了null而非抛异常改为抛IllegalArgumentException调用方无需判空传a找不到产品大小写未归一工厂内统一toUpperCase()或toLowerCase()新增产品后老代码报错switch漏了分支或枚举未更新用枚举IDE检查或注册表方案并发下注册表丢数据用了非线程安全Map换ConcurrentHashMap产品构造耗时导致工厂卡顿构造器里做了重活构造器只赋值重活延迟到首次使用单元测试无法替换产品工厂用静态方法硬编码改为实例方法依赖注入或提供setter5.2 我踩过的两个真实坑第一个坑是工厂里偷偷做了业务判断。早期我在工厂的create方法里根据参数决定返回哪个产品判断逻辑越加越多最后工厂变成了一个几百行的大杂烩。后来我把它拆开工厂只负责根据类型造对象选哪个类型由调用方或者一个独立的策略类决定。工厂保持纯粹才好维护。第二个坑是产品类之间共享了可变静态状态。两个产品类都往同一个静态List里塞数据测试时互相污染排查了半天才发现。教训是产品类尽量无状态必须共享的东西要明确生命周期和清理时机。5.3 什么时候不该用简单工厂不是所有创建都值得抽工厂。如果某个对象全项目只new一次、且永远不会换实现那直接new最省事。如果创建逻辑简单到只有一行new抽工厂反而是噪音。我的判断标准是创建逻辑是否复杂到值得集中、调用方是否多到值得解耦、产品是否可能替换三个里满足两个才动手。6. 简单工厂在实际项目中的落地建议6.1 与配置中心结合做动态选择实际项目里产品类型经常来自配置。比如消息发送配置里写channelsms代码就根据这个值去工厂拿对应的发送器。这种场景下工厂的输入是配置字符串输出是产品接口调用方完全不知道背后是短信还是邮件。配置变更时只需重启或热刷新代码零改动。这是简单工厂最实用的落地形态之一。6.2 配合单元测试的写法测试工厂本身要覆盖三类用例正常类型返回正确产品、未知类型抛异常、null输入抛异常。测试调用方时如果工厂是静态的不好替换可以把工厂包装成一个可注入的实例测试时注入返回mock产品的假工厂。这样调用方逻辑和产品实现彻底隔离测试跑得又快又稳。6.3 代码评审时的检查点我评审别人写的简单工厂会重点看几件事工厂方法有没有对未知输入做明确处理、产品接口是否足够抽象、工厂里有没有混入业务逻辑、新增产品时改动范围是否可控。这几条过了基本就是个合格的工厂实现。我个人在实际操作中的体会是简单工厂的价值不在于它多高级而在于它用最小的结构成本换来了创建逻辑的集中和调用方的解耦。把它用在对的地方代码会清爽很多用错了地方就是给自己加了一层没必要的间接。判断标准始终是变化的方向和频率而不是模式本身的名气。后续如果产品家族真的膨胀到需要动态扩展再平滑迁移到工厂方法或者注册表方案也不迟简单工厂完全可以作为起点。