1
0

工厂模式三兄弟:简单工厂、工厂方法、抽象工厂

new 一个对象有什么错?问题不在 new,而在于把"用哪个类"这个决定硬编码进了调用方。工厂模式就是把"创建"这件事从"使用"里拆出来——这篇文章把三个最容易混淆的"工厂"讲清楚:简单工厂(不在 GoF 23 里,却最常用)、工厂方法、抽象工厂,并给出可运行的单文件 Java 示例、UML 类图与选型判断。

一、为什么需要工厂:new 到底错在哪

假设下单后要发起支付:

public void checkout(String type, BigDecimal amount) {
    if ("alipay".equals(type)) {
        new AliPay().pay(amount);
    } else if ("wechat".equals(type)) {
        new WeChatPay().pay(amount);
    } else if ("paypal".equals(type)) {
        new PayPalPay().pay(amount);
    }
}

这段代码能跑,但它的三处病根很典型:

  1. 耦合:调用方(业务逻辑)依赖了所有具体实现类。加一种支付方式,就要改这段业务代码。
  2. 重复:这种 if-else 会在下单、退款、对账……每个入口复制一遍。
  3. 职责混乱:业务方法本该只关心"收钱",却在操心"怎么造出支付对象"(可能还要读配置、初始化 SDK、处理线程池)。

工厂模式的一句话概括:把"创建哪个具体类"的决策,从调用方搬到工厂里,让调用方只面向接口编程。

二、三个"工厂"是什么关系

名称关注点是否 GoF 23 模式一句话记忆
简单工厂(静态工厂)一个工厂造多种产品❌ 不在(习惯叫法)一个柜台,报名字给货
工厂方法一个工厂造一种产品,交给子类决定具体类型✅一条产品线一个工厂
抽象工厂一个工厂造一族(多个相关)产品✅成套供货,同风格不混搭

这里有个关键区分:"产品等级结构"和"产品族"——记住这两个词,后面就再也不会混。

  • 产品等级结构:Button 是抽象产品,WinButton、MacButton 属于同一等级结构的实现。
  • 产品族:WinButton + WinCheckbox + WinTextBox 这一整套属于"Windows 风格"族。

工厂方法解决"一个产品等级结构的创建延迟到子类";抽象工厂解决"多个产品等级结构成套创建、且要保证同一族"。

三、简单工厂:最常用,也最容易被误用

它不藏在复杂的抽象里,核心只有一个静态方法。

diagram-1-simple-factory

/** 简单工厂:一个方法决定造哪个具体类 */
public class SimpleFactoryDemo {

    interface Payment {                       // 抽象产品
        void pay(java.math.BigDecimal amount);
    }

    static class AliPay implements Payment {  // 具体产品 A
        public void pay(java.math.BigDecimal amount) {
            System.out.println("支付宝支付 " + amount);
        }
    }

    static class WeChatPay implements Payment { // 具体产品 B
        public void pay(java.math.BigDecimal amount) {
            System.out.println("微信支付 " + amount);
        }
    }

    /** 工厂:集中创建逻辑,调用方只认接口 */
    static class PaymentFactory {
        static Payment create(String type) {
            switch (type.toLowerCase()) {
                case "alipay": return new AliPay();
                case "wechat": return new WeChatPay();
                default: throw new IllegalArgumentException("不支持的支付方式: " + type);
            }
        }
    }

    public static void main(String[] args) {
        Payment p = PaymentFactory.create("alipay");
        p.pay(new java.math.BigDecimal("19.90"));
        PaymentFactory.create("wechat").pay(new java.math.BigDecimal("5.00"));
    }
}

优点:简单直接、调用方零耦合、把创建逻辑(含初始化、异常处理)集中到一处。

缺点:新增产品必须修改工厂类(那个 switch),违背开闭原则;工厂类会随着产品增多而膨胀。

什么时候用它:产品种类少且稳定、不会频繁增加;创建逻辑需要统一收口(校验、缓存、默认值)。

JDK 里的简单工厂:Integer.valueOf(int)、Boolean.valueOf(boolean)、Calendar.getInstance()、Charset.forName("UTF-8")、Executors.newFixedThreadPool(n)。这些都是静态方法返回对象,和你自己写的工厂本质相同。

顺手升级:用"注册表"替掉 switch

switch 的问题在于"新增产品要动工厂"。用 EnumMap + Supplier(Java 8+)可以把"注册"和"创建"分开,工厂再也不用改:

/** 注册表式工厂:新增产品只注册,不改工厂 */
public class RegistryFactoryDemo {

    interface Payment { void pay(java.math.BigDecimal amount); }
    static class AliPay implements Payment {
        public void pay(java.math.BigDecimal a) { System.out.println("支付宝 " + a); }
    }
    static class WeChatPay implements Payment {
        public void pay(java.math.BigDecimal a) { System.out.println("微信 " + a); }
    }

    static class PaymentFactory {
        private static final java.util.Map<String, java.util.function.Supplier<Payment>>
                REGISTRY = new java.util.HashMap<>();

        static void register(String type, java.util.function.Supplier<Payment> supplier) {
            REGISTRY.put(type.toLowerCase(), supplier);
        }
        static Payment create(String type) {
            java.util.function.Supplier<Payment> s = REGISTRY.get(type.toLowerCase());
            if (s == null) throw new IllegalArgumentException("未注册的支付方式: " + type);
            return s.get();
        }
    }

    public static void main(String[] args) {
        PaymentFactory.register("alipay", AliPay::new);     // 注册
        PaymentFactory.register("wechat", WeChatPay::new);
        PaymentFactory.create("alipay").pay(new java.math.BigDecimal("9.90"));
    }
}

这其实是很多框架的做法:SPI、ServiceLoader、Spring 的 BeanFactory 都是"注册 + 按名取"。简单工厂的形态,注册表的骨架。

四、工厂方法:把"用哪个类"交给子类

典型场景:物流公司要发货,但运输方式由具体业务线决定(陆运算卡车、海运算轮船)。

diagram-2-factory-method

/** 工厂方法:抽象工厂定义"造什么"的接口,由子类决定具体产品 */
public class FactoryMethodDemo {

    interface Transport {                    // 抽象产品
        void deliver();
    }
    static class Truck implements Transport {
        public void deliver() { System.out.println("卡车陆运,走高速"); }
    }
    static class Ship implements Transport {
        public void deliver() { System.out.println("轮船海运,走港口"); }
    }

    /** 抽象工厂:包含"业务方法"和"工厂方法" */
    abstract static class Logistics {
        protected abstract Transport createTransport();   // 工厂方法

        public void planDelivery() {                      // 业务方法:只依赖抽象产品
            Transport t = createTransport();
            System.out.println("规划路线…");
            t.deliver();
        }
    }

    static class RoadLogistics extends Logistics {        // 具体工厂 A
        protected Transport createTransport() { return new Truck(); }
    }
    static class SeaLogistics extends Logistics {         // 具体工厂 B
        protected Transport createTransport() { return new Ship(); }
    }

    public static void main(String[] args) {
        new RoadLogistics().planDelivery();
        new SeaLogistics().planDelivery();
    }
}

diagram-3-factory-method-sequence

与简单工厂的本质区别:简单工厂的工厂类自己判断并创建;工厂方法把 createTransport() 声明成抽象方法,由子类实现——新增一种运输方式,只需要新增一个工厂子类,不用改任何已有代码。

代价:类数量翻倍(每个产品配一个工厂)。所以它适合"产品种类会持续增加"的场景。

JDK 里的工厂方法:Collection.iterator()——ArrayList 返回自己的迭代器,LinkedList 返回自己的,接口只声明"能造迭代器";再比如 URLStreamHandlerFactory、NumberFormat.getInstance() 在子类体系中的实现。

五、抽象工厂:成套供货,保证同族一致

最经典的例子是跨平台 UI:一个界面要同时用"按钮 + 复选框 + 文本框",它们必须是同一风格,不能 Windows 按钮配 Mac 复选框。

diagram-4-abstract-factory

/** 抽象工厂:一个工厂负责一个产品族(同风格的整套组件) */
public class AbstractFactoryDemo {

    interface Button { void paint(); }        // 产品等级结构 1
    interface Checkbox { void paint(); }      // 产品等级结构 2

    static class WinButton implements Button {
        public void paint() { System.out.println("渲染 Windows 风格按钮"); }
    }
    static class WinCheckbox implements Checkbox {
        public void paint() { System.out.println("渲染 Windows 风格复选框"); }
    }
    static class MacButton implements Button {
        public void paint() { System.out.println("渲染 macOS 风格按钮"); }
    }
    static class MacCheckbox implements Checkbox {
        public void paint() { System.out.println("渲染 macOS 风格复选框"); }
    }

    /** 抽象工厂:声明一族产品的创建方法 */
    interface GUIFactory {
        Button createButton();
        Checkbox createCheckbox();
    }
    static class WinFactory implements GUIFactory {
        public Button createButton() { return new WinButton(); }
        public Checkbox createCheckbox() { return new WinCheckbox(); }
    }
    static class MacFactory implements GUIFactory {
        public Button createButton() { return new MacButton(); }
        public Checkbox createCheckbox() { return new MacCheckbox(); }
    }

    /** 客户端只依赖抽象工厂 + 抽象产品,不知道具体风格 */
    static class Application {
        private final Button button;
        private final Checkbox checkbox;

        Application(GUIFactory factory) {
            this.button = factory.createButton();
            this.checkbox = factory.createCheckbox();
        }
        void render() { button.paint(); checkbox.paint(); }
    }

    public static void main(String[] args) {
        String os = System.getProperty("os.name").toLowerCase();
        GUIFactory factory = os.contains("mac") ? new MacFactory() : new WinFactory();
        new Application(factory).render();
    }
}

它换来了什么:新增一套风格(比如 Linux 风格)只需新增一个工厂 + 两个产品类,客户端代码不动;而且不同族的组件绝不会混搭——这个一致性保证是抽象工厂独有的价值。

它的软肋(面试常问):新增"产品种类"(比如要加 TextBox)要改所有工厂接口和所有实现。也就是说:

  • 维度是"族"(换风格)→ 抽象工厂符合开闭原则;
  • 维度是"种类"(加组件)→ 抽象工厂违背开闭原则,这时候要拆成更细的工厂方法或改用注册表。

框架里的抽象工厂:javax.xml.parsers.DocumentBuilderFactory(不同 XML 解析器实现)、JDBC 的 Driver → Connection → Statement 一整套(换数据库只换驱动)、Spring 的 FactoryBean 也算半个。

六、三者对比与选型

维度简单工厂工厂方法抽象工厂
抽象层次无(具体工厂)抽象工厂 + 具体工厂抽象工厂 + 具体工厂
产品数量一个等级结构,多个实现一个等级结构,延迟到子类多个等级结构(产品族)
新增产品实现❌ 要改工厂✅ 新增工厂子类✅ 新增族(但要改接口才能加种类)
类数量少多(产品数 × 2)最多(族数 × 种类数)
典型场景支付/日志/解析器按名创建一个业务线一种实现跨平台 UI、多数据库套装

diagram-5-selection-flow

判断口诀:一个维度用工厂方法,两个(以上)维度用抽象工厂;产品少而稳定,简单工厂最实用。

七、什么时候不要用工厂

设计模式的价值在于隔离变化,如果没有变化点,它就是纯粹的复杂度:

  • 只有一个实现、短期也没有第二个:直接 new,或者用依赖注入把它注入进来。
  • 创建逻辑就是一句 new:包一层工厂只是把 new 换了个地方,没有任何收益。
  • 已经有 DI 容器:Spring 里 @Component + 构造器注入基本取代了手写工厂;ApplicationContext 本身就是个"超级工厂"(BeanFactory 是它的顶层接口)。这时候你要写的是 @Configuration + @Bean,而不是自定义工厂类。
  • 只是想"按名字取对象":EnumMap / Map<String, Supplier<T>> 的注册表比一整套继承体系轻得多(见第三节的注册表版)。

八、参考资料

  • GoF《设计模式:可复用面向对象软件的基础》——Factory Method、Abstract Factory 两章
  • Joshua Bloch《Effective Java》Item 1:静态工厂方法替代构造器(讲的是"工厂"的另一种形态与它的取舍)
  • Spring Framework 文档:The IoC Container / BeanFactory 与 ApplicationContext 的关系

评论