Skip to content

工厂方法模式 (Factory Method) —— 各自为政的作坊

前言

在深入探讨工厂方法模式之前,想向大家推荐一个非常棒的开源项目:design-patterns-23。这个项目用最现代的技术栈重写了 23 种设计模式,非常适合实战学习,还配套了在线交互演示站,可以边读文章边动手玩。本文的实战案例灵感也来源于此。

1. 工厂方法模式是什么?

工厂方法模式(Factory Method Pattern)定义了一个创建对象的接口,但由子类决定要实例化的类是哪一个。工厂方法让类把实例化推迟到子类。

通俗解释

  • 简单工厂(Simple Factory):就像一个大杂烩工厂。你想买鞋,阿迪、耐克都在这一个工厂里造。如果想造新百伦,还得改这个大工厂的流水线。
  • 工厂方法(Factory Method):就像品牌专卖店。阿迪有专门的阿迪工厂,耐克有专门的耐克工厂。你想买阿迪,就去阿迪工厂;想买新百伦?没问题,开一家新百伦工厂就行,完全不用去动阿迪的工厂。

1.1 为什么我们需要它?

核心痛点违反开闭原则 (Open/Closed Principle)。 在简单工厂模式中,工厂类集中了所有产品的创建逻辑。一旦要增加新产品,就必须修改工厂类的 if-elseswitch-case 代码。

烂代码示例(简单工厂)

java
public class SimpleFactory {
    public static Product createProduct(String type) {
        if ("A".equals(type)) {
            return new ProductA();
        } else if ("B".equals(type)) {
            return new ProductB();
        } else {
            return null;
        }
    }
}

如果我要加个 ProductC,我必须去改 SimpleFactory 的代码。这在大型项目中是危险的,因为修改已有代码可能会破坏原有的稳定性。

2. Java 实战演练:日志记录器系统

假设我们要开发一个日志记录器,支持记录到文件(FileLogger)和记录到数据库(DatabaseLogger)。未来可能还会支持记录到云端。

2.1 定义产品接口 (Logger)

这是所有日志记录器的共同规范。

java
public interface Logger {
    void writeLog();
}

2.2 具体产品实现

文件日志记录器

java
public class FileLogger implements Logger {
    @Override
    public void writeLog() {
        System.out.println("把日志写入文件...");
    }
}

数据库日志记录器

java
public class DatabaseLogger implements Logger {
    @Override
    public void writeLog() {
        System.out.println("把日志写入数据库...");
    }
}

2.3 定义工厂接口 (LoggerFactory)

注意,这里不再是一个具体的类,而是一个接口。它只规定了“工厂应该能生产日志记录器”,具体怎么生产,由子类决定。

java
public interface LoggerFactory {
    Logger createLogger();
}

2.4 具体工厂实现

文件日志工厂:只负责生产文件日志记录器。

java
public class FileLoggerFactory implements LoggerFactory {
    @Override
    public Logger createLogger() {
        // 这里可以包含复杂的初始化逻辑,比如打开文件流等
        return new FileLogger();
    }
}

数据库日志工厂:只负责生产数据库日志记录器。

java
public class DatabaseLoggerFactory implements LoggerFactory {
    @Override
    public Logger createLogger() {
        // 这里可以包含连接数据库的逻辑
        return new DatabaseLogger();
    }
}

2.5 客户端调用

客户端不再依赖具体的 FileLogger,而是依赖抽象的 LoggerFactory

java
public class Client {
    public static void main(String[] args) {
        // 1. 想要文件日志,就找文件工厂
        LoggerFactory factory = new FileLoggerFactory();
        Logger logger = factory.createLogger();
        logger.writeLog();

        // 2. 想要数据库日志,就换成数据库工厂
        // 注意:这里只需要改这一行代码,后续逻辑都不用动
        factory = new DatabaseLoggerFactory();
        logger = factory.createLogger();
        logger.writeLog();
    }
}

3. 进阶思考:开闭原则的胜利

现在,老板说要加一个“云端日志记录器”(CloudLogger)。我们需要改现有代码吗? 完全不需要! 我们只需要:

  1. 新建 CloudLogger 实现 Logger 接口。
  2. 新建 CloudLoggerFactory 实现 LoggerFactory 接口。

老的 FileLoggerFileLoggerFactory 代码连碰都不用碰。这就完美符合了开闭原则:对扩展开放,对修改关闭。

3.1 简单工厂 vs 工厂方法 vs 静态工厂方法

这也是面试中容易混淆的概念。

  1. 简单工厂 (Simple Factory):

    • 不是 GoF 23 种设计模式之一,更像是一种编程习惯。
    • 优点:简单,不需要定义一堆工厂类。
    • 缺点:违背开闭原则,扩展困难。
  2. 工厂方法 (Factory Method):

    • GoF 23 种标准模式之一。
    • 优点:符合开闭原则,扩展性好。
    • 缺点:代码量增加。
  3. 静态工厂方法 (Static Factory Method):

    • 这指的是 Integer.valueOf("123") 这种写法。
    • 它不是一种设计模式,而是一种代码惯例(Idiom),通常用来替代构造函数。
    • 优点
      • 有名字(构造函数必须和类名一样,无法表达含义)。
      • 不需要每次都创建新对象(比如 Boolean.valueOf(true) 返回的是缓存对象)。
      • 可以返回子类型。

3.2 依赖注入 (Dependency Injection) 的降维打击

在现代 Spring 开发中,你可能会发现我们很少手写工厂类了。为什么? 因为 Spring 容器本身就是一个超级工厂

当我们在 XML 或注解中配置 <bean id="myService" class="com.example.MyServiceImpl"/> 时,Spring 容器负责了创建对象、装配依赖的过程。

  • @Autowired:相当于调用了工厂获取产品。
  • @Component / @Bean:相当于注册了产品及其创建逻辑。

Spring 的 DI (Dependency Injection) 机制,在很大程度上取代了手写工厂方法模式的需求,让开发者专注于业务逻辑。

4. 源码中的工厂方法模式

JDK 和 Spring 中到处都是工厂方法模式的影子:

  1. java.util.Collection 接口: iterator() 方法就是一个工厂方法。

    java
    public interface Collection<E> extends Iterable<E> {
        // 工厂方法,要求子类必须返回一个 Iterator 对象
        Iterator<E> iterator();
    }

    ArrayList 返回 ItrHashSet 返回 KeyIterator。客户端只知道返回的是 Iterator,不关心具体是谁。

  2. java.net.URLStreamHandlerFactory: 用于根据协议(http, ftp)创建 URLStreamHandler。

  3. Spring 的 FactoryBean 接口: 这是 Spring 框架的核心接口之一。

    java
    public interface FactoryBean<T> {
        T getObject() throws Exception;
        Class<?> getObjectType();
        boolean isSingleton();
    }

    当你实现这个接口并注册到 Spring 容器时,Spring 就会调用 getObject() 来获取你真正想要生成的 Bean。这完全就是工厂方法模式的应用。典型的例子是 SqlSessionFactoryBean (MyBatis) 或 LocalSessionFactoryBean (Hibernate)。

  4. SLF4J 的 ILoggerFactory: SLF4J 只是一个门面(Facade),它通过 ILoggerFactory 接口来适配不同的日志实现(Logback, Log4j)。

    java
    public interface ILoggerFactory {
        Logger getLogger(String name);
    }

    Logback 的 LoggerContext 实现了这个接口,负责创建 Logback 的 Logger 对象。

5. 优缺点与适用场景总结

优点

  • 解耦:客户端只需要知道具体工厂的名称,不需要知道具体产品的类名,更不需要知道产品是如何创建的。
  • 符合开闭原则:增加新产品时,只需要增加相应的具体产品类和工厂类,无需修改现有代码。
  • 屏蔽复杂性:如果产品的创建过程非常复杂(比如需要读配置、建立连接),工厂方法可以将这些逻辑封装在工厂内部,客户端只需调用 create() 即可。

缺点

  • 类爆炸:每增加一种产品,就需要增加一个具体产品类和一个具体工厂类。如果有 100 种产品,就得写 200 个类,增加了系统的复杂度。
  • 抽象层增加了理解难度:引入了抽象层,对于简单的对象创建,反而显得繁琐。

6. 实验实操

design-patterns-web 项目中,左侧展示了非工厂模式的紧耦合做法,你需要分别点击 new Car(), new Truck(), new Bike() 来创建对象,客户端必须知道具体的类名。 右侧展示了工厂方法模式,你只需要选择产品类型(Car/Truck/Bike),然后点击“Order from Factory”。工厂会负责创建具体对象,客户端无需关心实例化的细节,实现了创建者与具体产品类的解耦。 截图建议:截取左右两侧对比,左侧显示手动创建的多种对象,右侧显示通过工厂统一创建的对象。

适用场景

  1. 不知道具体类名:一个类不知道它所需要的对象的类:客户端不需要知道具体产品类的类名,只需要知道所对应的工厂即可。
  2. 扩展性要求高:需要提供给第三方库或框架使用,允许使用者扩展具体实现。
  3. 初始化逻辑复杂:对象的创建包含复杂的逻辑(如依赖注入、配置读取),不适合直接 new

6. 结语

工厂方法模式是多态在对象创建过程中的极致体现。它通过将实例化的责任推迟到子类,解决了简单工厂模式无法灵活扩展的弊端。虽然它带来了类数量的增加,但在大型系统中,这种“用空间换灵活性”的交易通常是值得的。

在面试中,能清晰地区分简单工厂工厂方法抽象工厂(下文会讲),并能结合 Spring FactoryBean 谈出深度,是加分项。