工厂模式与设计原则:它如何体现开闭原则和依赖倒置原则
聊到工厂模式,很多初学者会把它当成一个“创建对象的工具”。这没错,但有点浅了。我个人习惯把工厂模式看作设计原则的实践载体。说白了,它不是为了创建对象而创建对象,而是为了让代码在面对变化时,尽量少改、甚至不改。
今天我们就来拆解一下,工厂模式到底是怎么体现开闭原则和依赖倒置原则的。这两个原则,你想想看,其实是工厂模式的“灵魂”。
一、开闭原则:对扩展开放,对修改关闭
开闭原则(Open/Closed Principle,OCP)是面向对象设计的基石。它的核心思想是:当系统需要增加新功能时,尽量通过扩展代码来实现,而不是修改已有的代码。
工厂模式是怎么做到这一点的?我举个例子你就明白了。
1.1 没有工厂模式时的痛点
假设我们有一个日志系统,需要支持文件日志和数据库日志。如果直接在业务代码里用 new 关键字创建对象,代码会写成这样:
public class LoggerService {
public void log(String message, String type) {
ILogger logger;
if ("file".equals(type)) {
logger = new FileLogger();
} else if ("database".equals(type)) {
logger = new DatabaseLogger();
}
logger.write(message);
}
}
这段代码有什么问题?每次新增一种日志类型,你都得改这个 if-else 块。我在项目中遇到过类似的情况,一开始只有两种日志,后来客户要求加一个“消息队列日志”,我不得不打开这个类,加一个 else if。改完之后还要重新测试整个流程,生怕改漏了什么。
这明显违反了开闭原则——对修改是开放的,但这不是我们想要的开放。
1.2 引入工厂模式后的改进
现在我们用简单工厂模式重构一下:
public class LoggerFactory {
public static ILogger createLogger(String type) {
if ("file".equals(type)) {
return new FileLogger();
} else if ("database".equals(type)) {
return new DatabaseLogger();
}
throw new IllegalArgumentException("Unknown logger type: " + type);
}
}
public class LoggerService {
public void log(String message, String type) {
ILogger logger = LoggerFactory.createLogger(type);
logger.write(message);
}
}
看起来只是把创建逻辑挪到了工厂类里,对吧?但这里有一个关键变化:业务代码 LoggerService 不再依赖具体的日志类,而是依赖工厂。当新增一种日志类型时,你只需要修改工厂类,业务代码完全不用动。
嗯,这里要注意:简单工厂并没有完全满足开闭原则,因为工厂类本身还是需要修改。真正体现开闭原则的是工厂方法模式和抽象工厂模式。
1.3 工厂方法模式:真正的开闭
工厂方法模式把工厂也抽象化了。每个产品对应一个工厂,新增产品时,只需要新增一个工厂类,不需要修改任何已有代码。
// 抽象工厂
public interface ILoggerFactory {
ILogger createLogger();
}
// 具体工厂
public class FileLoggerFactory implements ILoggerFactory {
@Override
public ILogger createLogger() {
return new FileLogger();
}
}
public class DatabaseLoggerFactory implements ILoggerFactory {
@Override
public ILogger createLogger() {
return new DatabaseLogger();
}
}
// 使用
public class LoggerService {
private ILoggerFactory factory;
public LoggerService(ILoggerFactory factory) {
this.factory = factory;
}
public void log(String message) {
ILogger logger = factory.createLogger();
logger.write(message);
}
}
看到区别了吗?新增一种日志类型时,你只需要写一个新的工厂类和一个新的日志类,已有的 FileLoggerFactory、DatabaseLoggerFactory 和 LoggerService 一行代码都不用改。这就是“对扩展开放,对修改关闭”的典型体现。
核心要点:工厂方法模式通过将对象的创建延迟到子类,使得系统可以在不修改现有代码的情况下,通过增加新的子类来扩展功能。
二、依赖倒置原则:依赖抽象,不依赖具体
依赖倒置原则(Dependency Inversion Principle,DIP)的定义有点绕:高层模块不应该依赖低层模块,两者都应该依赖抽象;抽象不应该依赖细节,细节应该依赖抽象。
说白了就是:面向接口编程,不要面向实现编程。
2.1 违反依赖倒置的典型场景
回到刚才那个没有工厂的例子:
public class LoggerService {
private FileLogger fileLogger = new FileLogger(); // 直接依赖具体类
public void log(String message) {
fileLogger.write(message);
}
}
这段代码里,LoggerService(高层模块)直接依赖了 FileLogger(低层模块的具体实现)。一旦你想换成数据库日志,就得改 LoggerService 的代码。高层模块被低层模块的变更牵着鼻子走,这显然不合理。
2.2 工厂模式如何实现依赖倒置
工厂模式通过引入抽象层,让高层模块和低层模块都依赖抽象接口:
// 抽象接口
public interface ILogger {
void write(String message);
}
// 低层模块实现抽象
public class FileLogger implements ILogger { ... }
public class DatabaseLogger implements ILogger { ... }
// 高层模块通过工厂获取抽象
public class LoggerService {
private ILogger logger;
public LoggerService(ILoggerFactory factory) {
this.logger = factory.createLogger(); // 依赖工厂抽象
}
public void log(String message) {
logger.write(message); // 依赖接口抽象
}
}
这里的关键变化是:
- 高层模块
LoggerService不再依赖具体的FileLogger或DatabaseLogger,而是依赖ILogger接口和ILoggerFactory接口。 - 低层模块
FileLogger也实现了ILogger接口,而不是直接暴露给高层。 - 工厂类 负责将高层和低层“粘合”起来,但两者都不直接依赖对方。
避坑指南:我曾经在一个项目中看到有人把工厂模式用成了“换皮”的 new 关键字——工厂方法返回的是具体类而不是接口。这其实没有真正实现依赖倒置,因为调用方仍然依赖了具体类型。记住:工厂返回的应该是抽象类型,而不是具体类型。
三、一张图看懂工厂模式与设计原则的关系
下面这张 SVG 图展示了工厂模式如何将开闭原则和依赖倒置原则落地到代码结构中:
四、实际项目中的经验总结
我在多个项目中实践过工厂模式,这里分享几个关键体会:
| 设计原则 | 工厂模式如何体现 | 我踩过的坑 |
|---|---|---|
| 开闭原则 | 通过工厂方法模式,新增产品时只需新增工厂子类,不修改已有代码 | 曾经在简单工厂里堆了十几个 if-else,每次新增都要改工厂类,后来重构为工厂方法模式才解决 |
| 依赖倒置原则 | 高层模块依赖工厂接口和产品接口,不依赖具体产品类 | 有一次工厂返回的是具体类而不是接口,导致调用方强依赖具体实现,换产品时还得改调用方代码 |
| 单一职责原则 | 工厂类只负责对象的创建,业务类只负责业务逻辑 | 有人把业务逻辑也塞进工厂里,工厂变得又臭又长,职责混乱 |
注意事项:工厂模式不是万能的。如果你的系统只有一两种产品,而且几乎不会变化,用简单的 new 反而更清晰。过度设计也是设计的大敌。我见过有人为了“以后可能扩展”而引入抽象工厂,结果项目结束了都没用上第二次。记住:为当前需求设计,为可预见的扩展预留接口。
五、总结
工厂模式之所以成为经典,不是因为它能创建对象,而是因为它把“变化”封装在了工厂内部,让系统的其他部分免受变化的影响。开闭原则保证了系统的可扩展性,依赖倒置原则保证了系统的灵活性——这两个原则,通过工厂模式完美地融合在了一起。
你想想看,一个模式能同时支撑两个核心设计原则,这本身就说明了它的价值。下次你在代码里用工厂模式时,不妨多想想:我是在用模式,还是在用原则?