「设计模式与范式」系列 Day10
写在前面
简单工厂、工厂方法、抽象工厂,光看名字很容易被带偏------好像后面的一定比前面的"高级",写代码就该一步到位上抽象工厂。这篇用一个规则配置解析器的真实需求走一遍:不同格式(JSON/XML/YAML/Properties)的配置文件要用不同的 Parser 去解析,看看这三种工厂到底是怎么演进出来的,以及为什么"更复杂"不等于"更好"。
一、是什么:三种工厂各自解决的问题
| 模式 | 解决的问题 | 对象的分类维度 |
|---|---|---|
| 简单工厂 | 一个工厂类,按参数创建对应的实现类 | 一个维度(比如按文件格式) |
| 工厂方法 | 用多态代替 if-else,每种类型对应一个工厂类 | 一个维度,但用继承体系表达 |
| 抽象工厂 | 一个工厂负责创建一组相关的对象 | 两个及以上维度 |
三者共同的目标没有变过:把"根据什么条件创建哪个对象"这件事从业务代码里挪出去。区别只在于对象的分类维度有多少个、以及创建逻辑复杂到什么程度。
二、为什么:从简单工厂演进到抽象工厂,每一步都是被具体问题推着走的
起点:规则配置文件要按格式选择解析器
java
public class RuleConfigSource {
public RuleConfig load(String filePath) {
String extension = getFileExtension(filePath); // json/xml/yaml/properties
IRuleConfigParser parser = RuleConfigParserFactory.createParser(extension);
if (parser == null) {
throw new InvalidRuleConfigException("unsupported: " + filePath);
}
String configText = ""; // 从文件读取
return parser.parse(configText);
}
}
createParser 该怎么实现,取决于创建逻辑复不复杂:如果只是简单的按格式 new 一个对应的 Parser,用简单工厂 就够了;如果创建过程本身很重,才需要往工厂方法升级。
简单工厂:够用就不要多加抽象
java
public class RuleConfigParserFactory {
public static IRuleConfigParser createParser(String format) {
if ("json".equalsIgnoreCase(format)) return new JsonRuleConfigParser();
if ("xml".equalsIgnoreCase(format)) return new XmlRuleConfigParser();
// yaml、properties 同理
return null;
}
}
这个 if-else 版本每次调用都要重新 new 一个 Parser 对象。如果 Parser 本身没有成员变量、可以安全复用,还有一种更省资源的写法------预先创建好、缓存在 Map 里:
java
public class RuleConfigParserFactory {
private static final Map<String, IRuleConfigParser> cachedParsers = new HashMap<>();
static {
cachedParsers.put("json", new JsonRuleConfigParser());
cachedParsers.put("xml", new XmlRuleConfigParser());
// yaml、properties 同理
}
public static IRuleConfigParser createParser(String format) {
if (format == null || format.isEmpty()) return null;
return cachedParsers.get(format.toLowerCase());
}
}
两种写法都是简单工厂,选哪种只取决于 Parser 是否无状态、能不能复用,跟"该不该用简单工厂"这个问题无关。
工厂方法:创建逻辑变复杂时,把工厂本身也拆开
如果某种 Parser 的创建过程不再是一个 new 能打发的------比如要组合别的对象、要做额外的初始化------继续把所有创建逻辑塞进一个工厂类,这个类会越来越臃肿。工厂方法模式用多态把每种类型的创建逻辑拆到各自的工厂类里:
java
public interface IRuleConfigParserFactory {
IRuleConfigParser createParser();
}
public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory {
public IRuleConfigParser createParser() { return new JsonRuleConfigParser(); }
}
// XmlRuleConfigParserFactory、YamlRuleConfigParserFactory 同理
但这样一来,load() 里又多了一段新的 if-else------用来判断该 new 哪个工厂类:
java
IRuleConfigParserFactory factory =
"json".equalsIgnoreCase(ext) ? new JsonRuleConfigParserFactory() : /* ... */ null;
创建对象的逻辑被挪走了,创建工厂 的逻辑又耦合回了 load()。解法很直接:再套一层简单工厂,去创建这些工厂对象,工厂类本身通常不含状态,用 Map 缓存复用即可:
java
public class RuleConfigParserFactoryMap { // 工厂的工厂
private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();
static {
cachedFactories.put("json", new JsonRuleConfigParserFactory());
cachedFactories.put("xml", new XmlRuleConfigParserFactory());
}
public static IRuleConfigParserFactory getParserFactory(String type) {
return type == null ? null : cachedFactories.get(type.toLowerCase());
}
}
load() 只需要调用 RuleConfigParserFactoryMap.getParserFactory(ext).createParser(),不再关心具体是哪个工厂类。
抽象工厂:当分类维度不止一个
前面的场景里,Parser 只有"文件格式"这一个分类维度。假设现在配置文件不只有规则配置(Rule),还有系统配置(System),两个维度交叉起来,用工厂方法就要写 Json/Xml/Yaml/Properties × Rule/System 一共 8 个工厂类。抽象工厂就是为了应对这种场景:让一个工厂负责创建一组相关的对象,而不是一种。
java
public interface IConfigParserFactory {
IRuleConfigParser createRuleParser();
ISystemConfigParser createSystemParser();
}
public class JsonConfigParserFactory implements IConfigParserFactory {
public IRuleConfigParser createRuleParser() { return new JsonRuleConfigParser(); }
public ISystemConfigParser createSystemParser() { return new JsonSystemConfigParser(); }
}
// XmlConfigParserFactory 同理,8个工厂类收敛成了4个
三种工厂的调用结构对比:
三、怎么用:判断标准和容易混淆的边界
什么时候该引入工厂模式
对照 Day06 的判断标准,出现下面任意一种情况,才值得把创建逻辑抽到工厂类里,而不是提前套用:
- 代码里存在 if-else 分支,需要根据条件动态创建不同的对象
- 单个对象本身的创建过程就很复杂(要组合其他对象、要做额外初始化),哪怕不需要按类型区分
引入之后能拿到的收益,也可以反过来当作判断依据:封装变化 (创建逻辑变了,调用方无感知)、代码复用 (创建逻辑抽成独立类后能被多处复用)、隔离复杂性 (调用方不用了解创建细节)、控制复杂度(原来的函数/类职责更单一了)。四条里一条都不占,多半就是过度设计。
简单工厂 vs 工厂方法:别看名字选,看创建逻辑复不复杂
创建逻辑简单(判断类型、直接 new)用简单工厂;创建逻辑复杂到值得拆分(一个工厂类会因此变得臃肿)才用工厂方法。工厂方法不是简单工厂的"升级版",是简单工厂扛不住的场景下的下一个台阶,两者没有谁比谁更正确。
工厂模式和 DI 容器不是一回事
Spring 这类 DI 容器的底层设计思路确实是基于工厂模式的------本质上是一个"大工厂",在应用启动时根据配置创建好所有需要的对象,用的时候直接从容器里取。但两者管的范围不一样:一个工厂类只负责某个类或某一组相关类的创建;DI 容器负责整个应用所有类对象的创建,还要管配置解析和对象生命周期(比如 Spring 里 scope=singleton/prototype、lazy-init、init-method/destroy-method 这些配置)。工厂类的创建逻辑是写死在代码里的,DI 容器的创建逻辑是从配置动态读出来的,这也是为什么工厂类的代码量会随着类的个数线性增长,而 DI 容器不会------它靠反射机制在运行时动态加载类、创建对象。
四、面试追问
Q1:简单工厂、工厂方法、抽象工厂,本质上的区别是什么?
简单工厂用一个工厂类按参数创建对应的实现类,适合创建逻辑简单的场景;工厂方法用多态把每种类型的创建逻辑拆到各自的工厂类里,适合单个对象创建过程本身就复杂、放一起会让工厂类臃肿的场景;抽象工厂应对的是对象有两个及以上分类维度的场景,让一个工厂负责创建一组相关的对象,避免维度交叉导致工厂类数量爆炸。三者共同的目标都是把"按什么条件创建哪个对象"从业务代码里挪出去。
Q2:工厂方法模式一定比简单工厂模式更好吗?
不是。工厂方法不是简单工厂的升级版,而是简单工厂在某类场景下会失效时的下一个台阶。创建逻辑简单时,硬套工厂方法只会多出一堆只有一个实现的工厂类,属于过度设计;只有当创建逻辑复杂到会让单个工厂类变得臃肿时,才值得拆分成多个工厂类。
Q3:引入工厂方法之后,为什么又出现了 "工厂的工厂"?它解决了什么问题?
工厂方法把对象的创建逻辑拆到了多个工厂类里,但调用方要先决定该 new 哪个工厂类,这段判断逻辑又耦合回了业务代码。"工厂的工厂"是再套一层简单工厂,专门负责创建这些工厂对象------因为工厂类通常不含状态,可以用 Map 缓存好直接复用,调用方就完全不用关心具体是哪个工厂类了。
Q4:什么时候该考虑引入工厂模式?
两种典型信号:一是代码里出现 if-else 分支,需要根据条件动态创建不同的对象;二是即便不需要按类型区分,单个对象本身的创建过程就足够复杂(组合其他对象、做额外初始化)。判断是否真的该用,还可以对照四个收益------封装变化、代码复用、隔离复杂性、控制复杂度------一条都占不上,大概率是过度设计。
Q5:工厂模式和 DI 容器有什么区别?
DI 容器的底层思路确实基于工厂模式,可以理解为一个管理整个应用所有对象的"大工厂",但两者管辖范围不同:工厂类只负责某个类或某一组相关类的创建,创建逻辑写死在代码里;DI 容器负责整个应用所有对象的创建、配置解析和生命周期管理,创建逻辑是从配置动态读取的,并且通过反射在运行时动态加载类,代码量不会随类的个数线性增长。
下一篇预告
Day11 建造者模式:什么时候构造函数该退休,交给 Builder。