工厂方法模式:让对象的创建“延迟”到子类
工厂方法模式,说白了就是一句话:定义一个创建对象的接口,让子类决定实例化哪一个类。嗯,这句话听起来有点绕,我换个说法——你写一个“工厂接口”,但具体生产什么产品,交给子工厂去决定。
我记得刚入行那会儿,写代码特别喜欢用 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 写了上百行,每次改都提心吊胆。
什么时候用工厂方法?
不是所有地方都需要工厂方法。我总结了几条经验:
- 当客户端不知道它需要什么具体类时——比如框架代码,由子类决定实例化哪个类
- 当创建对象需要复杂逻辑时——比如需要读取配置、连接池、权限校验等
- 当你想让子类扩展产品类型时——这是最典型的场景
避坑指南:我曾经在一个小工具里强行使用工厂方法,结果产品只有两种,工厂类却写了五个。过度设计比没有设计更可怕。记住:模式是为了解决问题,不是为了炫技。
工厂方法模式的结构图
下面这张图展示了工厂方法模式的核心结构,我把它画成了 SVG,方便你理解各个角色之间的关系。
从图中可以看到,抽象工厂和抽象产品处于上层,具体工厂和具体产品在下层。每个具体工厂只负责创建对应的具体产品,互不干扰。这就是“让子类决定实例化哪一个类”的直观体现。
实际项目中的应用
工厂方法模式在真实项目中随处可见。我举几个例子:
- JDBC 驱动:
DriverManager.getConnection()本质上就是一个工厂方法,不同的数据库驱动返回不同的Connection实现 - 日志框架:SLF4J 的
LoggerFactory.getLogger()会根据配置返回不同的日志实现 - Spring 容器:
BeanFactory就是工厂方法模式的经典应用,根据配置创建不同的 Bean
我记得有一次接手一个遗留系统,里面全是 new 关键字,想换一个缓存实现得改几十个文件。后来我花了两天时间,用工厂方法模式重构了缓存创建部分。改完之后,新增一种缓存只需要写一个类和对应的工厂,其他代码完全不动。那种感觉,就像把一团乱麻理成了整齐的线缆。
总结一下
工厂方法模式不是什么高深的东西。它解决的核心问题就是:当创建对象变得复杂或需要扩展时,把创建逻辑封装起来。
你想想看,写代码就像盖房子。如果你每次需要一扇窗户都现场烧玻璃、做窗框,那效率得多低?工厂方法就是帮你把“做窗户”这件事标准化,你需要什么窗户,告诉工厂就行,不用关心窗户是怎么造出来的。
嗯,最后说一句:模式是工具,不是教条。用之前先问问自己——这里真的需要工厂方法吗?如果答案是否定的,那就别用。我见过太多为了用模式而用模式的代码,反而把简单问题搞复杂了。
公众号:蓝海资料掘金营,微信deep3321