工厂方法模式:让对象的创建“延迟”到子类

工厂方法模式,说白了就是一句话:定义一个创建对象的接口,让子类决定实例化哪一个类。嗯,这句话听起来有点绕,我换个说法——你写一个“工厂接口”,但具体生产什么产品,交给子工厂去决定。

我记得刚入行那会儿,写代码特别喜欢用 new 关键字。看到需要对象,直接 new XXX() 就完事了。后来项目越来越大,每次新增一种产品类型,就得改一堆代码。那种滋味,就像你装修房子,结果发现每换一盏灯都得重新布线——累不累?

为什么需要工厂方法?

先看一个反面教材。假设你有个日志系统,需要记录到文件、数据库、或者控制台。新手写法可能是这样:

public class LoggerService {
    public void log(String message, String type) {
        if (type.equals("file")) {
            FileLogger logger = new FileLogger();
            logger.write(message);
        } else if (type.equals("db")) {
            DatabaseLogger logger = new DatabaseLogger();
            logger.write(message);
        } else if (type.equals("console")) {
            ConsoleLogger logger = new ConsoleLogger();
            logger.write(message);
        }
    }
}

这段代码有什么问题?我告诉你,问题大了去了。

  • 违背开闭原则:每新增一种日志类型,就得改 LoggerService
  • 耦合度高:高层模块直接依赖了具体日志类
  • 测试困难:想测试日志逻辑,得把所有实现都加载进来

我曾经在一个电商项目中见过类似的代码,当时要接入第三种支付渠道,结果改了一整天,还引入了两个 bug。从那以后,我对这种“硬编码创建对象”的做法就特别敏感。

工厂方法模式的核心结构

工厂方法模式把“创建对象”这件事抽象出来。它包含四个角色:

角色 名称 职责
抽象产品 Product 定义产品的公共接口
具体产品 ConcreteProduct 实现产品接口的具体类
抽象工厂 Creator 声明工厂方法,返回产品对象
具体工厂 ConcreteCreator 实现工厂方法,创建具体产品

你想想看,这个结构其实很自然。就像一家汽车公司,总部只规定“我们要造车”,但具体是造轿车还是 SUV,由各个分厂决定。总部不需要知道每个车型的细节,它只关心“能开就行”。

核心思想:将对象的创建和使用分离。客户端只依赖抽象工厂和抽象产品,不依赖具体实现。

用代码说话

还是那个日志系统的例子,我们用工厂方法模式重构一下。

先定义产品接口:

// 抽象产品
public interface Logger {
    void write(String message);
}

// 具体产品
public class FileLogger implements Logger {
    @Override
    public void write(String message) {
        System.out.println("写入文件: " + message);
    }
}

public class DatabaseLogger implements Logger {
    @Override
    public void write(String message) {
        System.out.println("写入数据库: " + message);
    }
}

public class ConsoleLogger implements Logger {
    @Override
    public void write(String message) {
        System.out.println("控制台输出: " + message);
    }
}

再定义工厂接口和具体工厂:

// 抽象工厂
public abstract class LoggerFactory {
    public abstract Logger createLogger();
    
    // 这里可以加一些公共逻辑
    public void log(String message) {
        Logger logger = createLogger();
        logger.write(message);
    }
}

// 具体工厂
public class FileLoggerFactory extends LoggerFactory {
    @Override
    public Logger createLogger() {
        // 可以在这里做初始化工作
        return new FileLogger();
    }
}

public class DatabaseLoggerFactory extends LoggerFactory {
    @Override
    public Logger createLogger() {
        return new DatabaseLogger();
    }
}

public class ConsoleLoggerFactory extends LoggerFactory {
    @Override
    public Logger createLogger() {
        return new ConsoleLogger();
    }
}

客户端使用:

public class Client {
    public static void main(String[] args) {
        LoggerFactory factory = new FileLoggerFactory();
        factory.log("用户登录成功");
        
        // 想换数据库日志?改一行就行
        factory = new DatabaseLoggerFactory();
        factory.log("订单创建成功");
    }
}

看到区别了吗?客户端不再直接 new 具体日志类,而是通过工厂来获取。新增一种日志类型时,只需要新增一个产品类和对应的工厂类,已有的代码一行都不用改

个人习惯:我一般会在抽象工厂里加一个默认的 log() 方法,把“获取日志器 + 写日志”这两个步骤封装起来。这样客户端调用更简洁,也避免了忘记调用 createLogger() 的尴尬。

工厂方法 vs 简单工厂

很多人容易把这两个搞混。简单工厂其实是一个“伪模式”,它把所有创建逻辑放在一个类里。而工厂方法把创建逻辑分散到各个子类中。

举个例子:

  • 简单工厂:一个厨师做所有菜,你点啥他做啥。厨师累死,菜单变了还得改厨师。
  • 工厂方法:每个菜系一个厨师,川菜找川菜师傅,粤菜找粤菜师傅。新增菜系就新增一个师傅。

我个人更倾向于工厂方法,尤其是当产品种类可能频繁变化时。简单工厂虽然写起来快,但后期维护成本高。我见过一个项目,简单工厂里的 switch-case 写了上百行,每次改都提心吊胆。

什么时候用工厂方法?

不是所有地方都需要工厂方法。我总结了几条经验:

  1. 当客户端不知道它需要什么具体类时——比如框架代码,由子类决定实例化哪个类
  2. 当创建对象需要复杂逻辑时——比如需要读取配置、连接池、权限校验等
  3. 当你想让子类扩展产品类型时——这是最典型的场景

避坑指南:我曾经在一个小工具里强行使用工厂方法,结果产品只有两种,工厂类却写了五个。过度设计比没有设计更可怕。记住:模式是为了解决问题,不是为了炫技。

工厂方法模式的结构图

下面这张图展示了工厂方法模式的核心结构,我把它画成了 SVG,方便你理解各个角色之间的关系。

抽象工厂 +createLogger(): Logger 文件日志工厂 +createLogger(): FileLogger 数据库日志工厂 +createLogger(): DatabaseLogger 控制台日志工厂 +createLogger(): ConsoleLogger 抽象产品 +write(message) 文件日志 数据库日志 控制台日志 继承/实现 创建关系

从图中可以看到,抽象工厂和抽象产品处于上层,具体工厂和具体产品在下层。每个具体工厂只负责创建对应的具体产品,互不干扰。这就是“让子类决定实例化哪一个类”的直观体现。

实际项目中的应用

工厂方法模式在真实项目中随处可见。我举几个例子:

  • JDBC 驱动DriverManager.getConnection() 本质上就是一个工厂方法,不同的数据库驱动返回不同的 Connection 实现
  • 日志框架:SLF4J 的 LoggerFactory.getLogger() 会根据配置返回不同的日志实现
  • Spring 容器BeanFactory 就是工厂方法模式的经典应用,根据配置创建不同的 Bean

我记得有一次接手一个遗留系统,里面全是 new 关键字,想换一个缓存实现得改几十个文件。后来我花了两天时间,用工厂方法模式重构了缓存创建部分。改完之后,新增一种缓存只需要写一个类和对应的工厂,其他代码完全不动。那种感觉,就像把一团乱麻理成了整齐的线缆。

总结一下

工厂方法模式不是什么高深的东西。它解决的核心问题就是:当创建对象变得复杂或需要扩展时,把创建逻辑封装起来

你想想看,写代码就像盖房子。如果你每次需要一扇窗户都现场烧玻璃、做窗框,那效率得多低?工厂方法就是帮你把“做窗户”这件事标准化,你需要什么窗户,告诉工厂就行,不用关心窗户是怎么造出来的。

嗯,最后说一句:模式是工具,不是教条。用之前先问问自己——这里真的需要工厂方法吗?如果答案是否定的,那就别用。我见过太多为了用模式而用模式的代码,反而把简单问题搞复杂了。


公众号:蓝海资料掘金营,微信deep3321