10. 工厂模式:从简单到抽象,再到实战
工厂模式,说实话,是Java里最常用、也最容易“用错”的设计模式之一。我见过太多项目,一开始图省事,new得飞起,结果后期需求一变,改代码改到想哭。工厂模式的核心思想就一句话:把创建对象的逻辑抽出来,交给专门的“工厂”去管。
今天咱们就把它讲透。从最简单的简单工厂,到工厂方法,再到抽象工厂,最后来个实战——数据库连接工厂。嗯,一步步来。
10.1 简单工厂:最朴素的“代工厂”
简单工厂,说白了就是一个类负责创建所有相关的对象。你给它一个参数,它返回你要的东西。
举个例子。假设我们要做一个日志系统,支持文件日志和数据库日志。
// 日志接口
public interface Logger {
void log(String message);
}
// 文件日志实现
public class FileLogger implements Logger {
@Override
public void log(String message) {
System.out.println("写入文件: " + message);
}
}
// 数据库日志实现
public class DatabaseLogger implements Logger {
@Override
public void log(String message) {
System.out.println("写入数据库: " + message);
}
}
// 简单工厂
public class LoggerFactory {
public static Logger createLogger(String type) {
if ("file".equalsIgnoreCase(type)) {
return new FileLogger();
} else if ("database".equalsIgnoreCase(type)) {
return new DatabaseLogger();
}
throw new IllegalArgumentException("未知的日志类型: " + type);
}
}
使用起来很简单:
Logger logger = LoggerFactory.createLogger("file");
logger.log("这是一条日志");
优点:把创建逻辑集中管理,客户端不用关心具体实现类。
缺点:每次新增类型,都要改工厂类的if-else。违反了“开闭原则”。
我个人习惯:简单工厂适合产品种类少、变化不频繁的场景。比如工具类、常量类。一旦超过5种,我就考虑升级了。
10.2 工厂方法:把“选择权”交给子类
简单工厂的痛点在于“改代码”。工厂方法模式就是来解决这个问题的。它把工厂抽象化,每个具体产品对应一个具体工厂。
还是日志的例子,我们用工厂方法重写:
// 抽象工厂
public interface LoggerFactory {
Logger createLogger();
}
// 文件日志工厂
public class FileLoggerFactory implements LoggerFactory {
@Override
public Logger createLogger() {
return new FileLogger();
}
}
// 数据库日志工厂
public class DatabaseLoggerFactory implements LoggerFactory {
@Override
public Logger createLogger() {
return new DatabaseLogger();
}
}
客户端使用:
LoggerFactory factory = new FileLoggerFactory();
Logger logger = factory.createLogger();
logger.log("工厂方法模式");
核心变化:新增一种日志类型,只需要新增一个产品类和一个工厂类,不用改已有代码。完美符合开闭原则。
我曾经在一个项目中,团队用了简单工厂管理20多种支付渠道。每次新增渠道,都要改工厂类,改完还要回归测试所有渠道。后来重构为工厂方法,虽然类多了,但维护成本直线下降。嗯,这就是代价与收益的权衡。
10.3 抽象工厂:创建“产品族”
工厂方法解决的是“单个产品”的创建。但如果你的产品有多个维度呢?比如不同操作系统下的按钮和文本框。
抽象工厂就是用来创建“产品族”的。它定义了一个工厂接口,这个接口可以创建多个相关的产品。
// 抽象产品:按钮
public interface Button {
void render();
}
// 抽象产品:文本框
public interface TextField {
void display();
}
// Windows风格实现
public class WindowsButton implements Button {
@Override
public void render() {
System.out.println("渲染Windows按钮");
}
}
public class WindowsTextField implements TextField {
@Override
public void display() {
System.out.println("显示Windows文本框");
}
}
// Mac风格实现
public class MacButton implements Button {
@Override
public void render() {
System.out.println("渲染Mac按钮");
}
}
public class MacTextField implements TextField {
@Override
public void display() {
System.out.println("显示Mac文本框");
}
}
// 抽象工厂
public interface UIFactory {
Button createButton();
TextField createTextField();
}
// Windows工厂
public class WindowsUIFactory implements UIFactory {
@Override
public Button createButton() {
return new WindowsButton();
}
@Override
public TextField createTextField() {
return new WindowsTextField();
}
}
// Mac工厂
public class MacUIFactory implements UIFactory {
@Override
public Button createButton() {
return new MacButton();
}
@Override
public TextField createTextField() {
return new MacTextField();
}
}
客户端使用:
UIFactory factory = new WindowsUIFactory();
Button button = factory.createButton();
TextField textField = factory.createTextField();
button.render();
textField.display();
适用场景:当你的系统需要支持多套“产品族”(比如不同数据库、不同UI风格、不同云平台),并且这些产品之间有关联时,抽象工厂是首选。
你想想看,如果不用抽象工厂,你要怎么保证Windows按钮和Windows文本框一起出现?靠if-else硬编码?那代码会变成一团乱麻。
10.4 实战:数据库连接工厂
好了,理论讲完了。咱们来点实际的。假设你要做一个支持多种数据库的系统,比如MySQL、PostgreSQL、Oracle。每个数据库的连接方式不同,但使用方式应该统一。
这里我选择工厂方法模式,因为数据库类型相对稳定,但每种数据库的连接细节可能变化。
// 数据库连接接口
public interface DatabaseConnection {
void connect();
void disconnect();
void executeQuery(String sql);
}
// MySQL实现
public class MySQLConnection implements DatabaseConnection {
private String url;
private String username;
private String password;
public MySQLConnection(String url, String username, String password) {
this.url = url;
this.username = username;
this.password = password;
}
@Override
public void connect() {
System.out.println("连接到MySQL: " + url);
// 实际连接逻辑
}
@Override
public void disconnect() {
System.out.println("断开MySQL连接");
}
@Override
public void executeQuery(String sql) {
System.out.println("MySQL执行: " + sql);
}
}
// PostgreSQL实现
public class PostgreSQLConnection implements DatabaseConnection {
// 类似实现...
}
// 抽象工厂
public interface DatabaseFactory {
DatabaseConnection createConnection(String url, String username, String password);
}
// MySQL工厂
public class MySQLFactory implements DatabaseFactory {
@Override
public DatabaseConnection createConnection(String url, String username, String password) {
return new MySQLConnection(url, username, password);
}
}
// PostgreSQL工厂
public class PostgreSQLFactory implements DatabaseFactory {
@Override
public DatabaseConnection createConnection(String url, String username, String password) {
return new PostgreSQLConnection(url, username, password);
}
}
客户端使用:
// 通过配置决定使用哪种数据库
String dbType = config.getProperty("db.type");
DatabaseFactory factory;
if ("mysql".equalsIgnoreCase(dbType)) {
factory = new MySQLFactory();
} else if ("postgresql".equalsIgnoreCase(dbType)) {
factory = new PostgreSQLFactory();
} else {
throw new IllegalArgumentException("不支持的数据库类型");
}
DatabaseConnection connection = factory.createConnection(url, username, password);
connection.connect();
connection.executeQuery("SELECT * FROM users");
connection.disconnect();
这里有个小技巧:工厂的创建本身也可以封装起来。比如用一个“工厂的工厂”或者配置文件来管理。我习惯用Spring的@Bean来管理这些工厂实例,这样连if-else都省了。
10.5 三种工厂模式对比
| 模式 | 核心思想 | 适用场景 | 开闭原则 |
|---|---|---|---|
| 简单工厂 | 一个工厂类创建所有产品 | 产品种类少,变化不频繁 | 违反 |
| 工厂方法 | 每个产品对应一个工厂 | 产品种类多,但产品等级结构单一 | 符合 |
| 抽象工厂 | 一个工厂创建一组相关产品 | 需要创建产品族,产品之间有约束 | 符合 |
注意:不要为了用模式而用模式。如果项目只有一种数据库,直接new就完了,别搞什么工厂。过度设计比没有设计更可怕。
10.6 知识体系图
这张图把三种工厂模式的关系和特点梳理清楚了。从左到右,复杂度递增,灵活性也递增。实战中,我建议从简单工厂开始,当发现if-else越来越多时,果断重构为工厂方法。至于抽象工厂,等你真的需要管理“产品族”时再用。
核心总结:工厂模式的核心就是“封装变化”。把对象的创建逻辑从业务代码中剥离出来,让代码更清晰、更易维护。记住,模式是工具,不是目的。用对了,事半功倍;用错了,画蛇添足。
公众号:蓝海资料掘金营,微信deep3321