写软件的时候,你很快会发现一个现象:到处都是 new。new 这个类,new 那个类,控制器里 new,Service 里 new,工具类里也 new。刚开始你可能觉得没什么,反正 new 很简单,可是系统一旦复杂起来,你会发现这些 new 带来的问题越来越多:
-
想换一个实现,需要全局搜索替换;
-
想给对象创建过程加点额外逻辑(比如初始化配置、打日志、做缓存),每个 new 的地方都要改;
-
单元测试也很难写,到处都是硬编码的具体类依赖。
leader 看你在项目里到处 new,终于忍不住提醒你:"对象的创建本身也是一种职责,不要乱塞到业务逻辑里。可以考虑用工厂模式,把创建对象这件事单独抽出来管理。"
你于是开始接触"工厂"这一类设计模式,慢慢从"直接 new"进化到"找工厂要东西"。
一、从"直接 new"开始的困境
假设你现在要做一个简单的日志接口,不想绑定具体实现,于是先定义了一个抽象接口:
public interface Logger {
void info(String msg);
void error(String msg);
}
先写一个最简单的控制台实现:
public class ConsoleLogger implements Logger {
@Override
public void info(String msg) {
System.out.println("INFO: " + msg);
}
@Override
public void error(String msg) {
System.err.println("ERROR: " + msg);
}
}
业务代码里,你开始到处这么用:
Logger logger = new ConsoleLogger();
logger.info("用户登录成功");
后来项目上线了,leader 说要把日志切到文件里,并且以后还有可能接别的日志系统。你只好再写一个 FileLogger:
public class FileLogger implements Logger {
@Override
public void info(String msg) {
// 写入文件
}
@Override
public void error(String msg) {
// 写入文件
}
}
问题来了:你现在想切换到 FileLogger,就得把所有 new ConsoleLogger() 的地方一一改成 new FileLogger()。如果调用的地方少还好,但在真实项目中,Logger 很可能被使用在大量位置。
leader 看你愁眉苦脸,笑着说:"别直接 new 具体类了,先包一层工厂,后续要切换实现,就从工厂下手就行了。"
二、简单工厂模式:先集中管理"创建哪一种"
你第一步做的事情,就是写了一个"简单工厂":
public class LoggerFactory {
// 简单用一个类型值来区分
public static Logger createLogger(String type) {
if ("console".equalsIgnoreCase(type)) {
return new ConsoleLogger();
} else if ("file".equalsIgnoreCase(type)) {
return new FileLogger();
} else {
throw new IllegalArgumentException("未知日志类型: " + type);
}
}
}
业务代码里,你不再直接 new:
Logger logger = LoggerFactory.createLogger("console");
logger.info("用户登录成功");
当你要切到文件日志时,只需要在配置里把 "console" 换成 "file",或者在工厂内部做一点策略逻辑,就可以完成"全局切换":
// 伪代码:根据配置决定到底给谁
public static Logger createLogger() {
String type = readFromConfig();
if ("file".equalsIgnoreCase(type)) {
return new FileLogger();
} else {
return new ConsoleLogger();
}
}
这就是最入门、也最常用的一种工厂模式:**简单工厂模式**(有些书上称为"静态工厂方法")。
它解决的问题很明确:
-
把"创建哪一种具体产品"的逻辑集中到一个地方;
-
调用方只要要一个 Logger,不关心实际返回的是 Console 还是 File。
但是 leader 很快又指出了一个问题:"如果以后再加一种 CloudLogger 呢?每次加新类型,你就要改 LoggerFactory,这其实违反了开闭原则。"
你这才知道,还有一个"工厂方法模式"等着你。
三、工厂方法模式:让子类决定"创建哪一种"
leader 让你把 Logger 的创建逻辑再抽象一层:
// 抽象工厂
public interface LoggerFactory {
Logger createLogger();
}
每种 Logger 对应一个具体工厂:
// 控制台日志工厂
public class ConsoleLoggerFactory implements LoggerFactory {
@Override
public Logger createLogger() {
return new ConsoleLogger();
}
}
// 文件日志工厂
public class FileLoggerFactory implements LoggerFactory {
@Override
public Logger createLogger() {
// 这里还可以顺便做一些初始化工作,比如创建文件目录等
return new FileLogger();
}
}
客户端代码变为:
public class Client {
public static void main(String[] args) {
LoggerFactory factory = new ConsoleLoggerFactory();
Logger logger = factory.createLogger();
logger.info("用户登录成功");
}
}
当你要换成 FileLogger,只需要把工厂换掉:
LoggerFactory factory = new FileLoggerFactory();
这就是**工厂方法模式**的典型做法:**把"创建具体产品对象"的职责延迟到子类中去实现**,这样新增产品时,只需要新增一个工厂实现,而不用去修改已有工厂逻辑。
和简单工厂相比:
-
简单工厂把所有判断逻辑写在一个大 if / else 里,新增产品要改这个类;
-
工厂方法是"一种产品,对应一个工厂子类",新增产品只需新增工厂,不动原来的代码,更符合开闭原则。
四、再看一个更接近实际业务的例子:支付工厂
假设你在做一个支付模块,支持微信支付、支付宝支付、银联支付。你一开始可能会这样写:
public interface PayService {
void pay(double amount);
}
public class WechatPayService implements PayService {
@Override
public void pay(double amount) {
System.out.println("微信支付:" + amount);
}
}
public class AlipayService implements PayService {
@Override
public void pay(double amount) {
System.out.println("支付宝支付:" + amount);
}
}
如果你在业务代码里到处这么干:
PayService payService = new WechatPayService();
payService.pay(100);
那当以后要切到支付宝、或者增加银联、Apple Pay 时,你又得满世界搜索替换。
leader 提醒你:"这就是典型的工厂方法模式使用场景,把'要哪种支付'这件事交给工厂。"
你可以这么设计:
public interface PayFactory {
PayService createPayService();
}
public class WechatPayFactory implements PayFactory {
@Override
public PayService createPayService() {
return new WechatPayService();
}
}
public class AlipayFactory implements PayFactory {
@Override
public PayService createPayService() {
return new AlipayService();
}
}
客户端:
public class PayClient {
public static void main(String[] args) {
// 根据配置或用户选择,决定使用哪个工厂
PayFactory factory = new WechatPayFactory();
PayService payService = factory.createPayService();
payService.pay(100);
}
}
以后你要增加银联支付,只需要:
-
新增一个 UnionPayService 实现 PayService;
-
新增一个 UnionPayFactory 实现 PayFactory。
原来的代码无需改动。
五、工厂模式的正式定义
你会发现,"工厂模式"其实是一族模式的统称,最常见的是:
-
简单工厂(不在 GoF 23 种之内,但非常常用);
-
工厂方法(GoF 之一);
-
抽象工厂(你前面已经单独写过了)。
如果聚焦在"工厂方法模式",可以给它一个比较严谨的定义:
工厂方法模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类。工厂方法使一个类的实例化延迟到其子类。
关键点有两个:
-
"定义创建对象的接口":抽象工厂接口(比如 LoggerFactory、PayFactory);
-
"具体实例化逻辑交给子类":ConsoleLoggerFactory、FileLoggerFactory 等。
六、工厂模式的优点和缺点
leader 还是那句话:"设计模式不是背定义,是要知道啥时候用、用了能带来什么好处。"
优点:
-
**去掉了客户端对具体类的依赖**:客户端只依赖抽象接口和工厂,不直接依赖具体实现,降低耦合度。
-
**符合开闭原则**:新增一种产品时,只需要新增一个具体工厂和对应产品类,不必改原有代码。
-
**集中管理创建逻辑**:对象创建过程中的复杂逻辑(参数校验、默认配置、缓存复用等)可以集中放在工厂里。
缺点:
-
**类数量增多**:一种产品往往要对应一个工厂类,对小项目来说可能略显繁琐。
-
**结构复杂度上升**:相比"简单 new 一下",工厂模式引入了一层间接,会增加理解成本。
-
**易与其他创建型模式混淆**:尤其是和抽象工厂、建造者,经常被混用,使用时需要分清关注点。
leader 给你的建议是:**当"创建对象"这件事本身变复杂、或者产品族不断扩展时,就可以考虑引入工厂模式,把"怎么创建"抽离出来管理。**
七、工厂模式与抽象工厂、建造者的关系
你已经写过抽象工厂和建造者的文档,难免会觉得这些模式都在"创建对象",有点傻傻分不清。
leader 帮你梳理了一下思路:
-
**简单工厂 / 工厂方法**:关注"选择哪一种产品"。比如要 ConsoleLogger 还是 FileLogger,要微信支付还是支付宝支付。
-
**抽象工厂**:关注"一整套相关产品"的组合。比如同一系列的 Button + Input + ListView,一起切换。
-
**建造者模式**:关注"复杂产品内部如何一步步组装"。比如一台电脑的 CPU、内存、硬盘、显卡如何搭配。
一句话概括:
-
工厂方法:帮你选"哪一款成品";
-
抽象工厂:帮你选"一整套成品组合";
-
建造者:帮你一步步"配置出一款定制成品"。
八、工厂模式的典型应用场景
最后,你结合自己的经验,总结了一些工厂模式特别适合的场景:
- **经常需要根据不同条件创建不同实现时**
比如不同环境(开发 / 测试 / 生产)用不同的配置加载器、不同的日志实现。
- **系统对"创建对象的过程"有额外要求时**
比如对象创建前要读配置、做缓存复用、注入依赖、打监控日志等。
- **产品族会不断扩展时**
比如支付方式、存储引擎、消息中间件适配等,会随着业务发展增加越来越多实现。
- **希望对上层屏蔽具体类细节时**
比如你对外只暴露统一的 `ImageReader` 接口,具体到底用的是 PNGReader 还是 JPGReader 由工厂内部决定。
你这才真正理解 leader 最开始说的那句话:
"当你发现 new 出来的不是'普通小物件',而是以后极有可能不断扩展、更换实现的'关键角色'时,就别急着直接 new 了,给它配一个工厂,长远来看你会省下很多麻烦。"
工厂方法模式:定义一个用于创建对象的接口,让子类决定实例化哪一个类,使一个类的实例化延迟到其子类。
备注:以上内容基于AI生成