工厂模式
本文内容
前言
除了上一章讲的单例模式外,还有一个比较常用的创建型模式:工厂模式。
工厂模式可以分为三种类型:简单工厂、工厂方法和抽象工厂。在本章中,除了了解工厂模式的原理和实现之外,更重要的是搞清楚下面两个问题:
- 什么时候应该使用工厂模式?
- 相对于直接 new 来创建对象,工厂模式有什么好处?
1. 简单工厂
简单工厂模式 又称为 静态方法模式(因为工厂类定义了一个创建对象的静态方法),简单工厂模式可以理解为 一个专门负责生产对象的 “工厂类”。
简单工厂有两种实现方式,分别是常规的实现和结合单例的实现。
1.1 常规的简单工厂
下面用一个配置文件解析器的例子来说明,比如可以根据配置文件的后缀(json、xml、yaml 等),选择不同的解析器(JsonRuleConfigParser、XmlRuleConfigParser……),将存储在文件中的配置解析成内存对象 RuleConfig。那么我们可以用如下代码来实现:
public class RuleConfigSource {
public RuleConfig load(String ruleConfigFilePath) {
String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
IRuleConfigParser parser = null;
if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {
parser = new JsonRuleConfigParser();
} else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {
parser = new XmlRuleConfigParser();
} else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {
parser = new YamlRuleConfigParser();
} else {
throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
}
String configText = "";
// ...从 ruleConfigFilePath 文件中读取配置文本到 configText 中...
RuleConfig ruleConfig = parser.parse(configText);
return ruleConfig;
}
private String getFileExtension(String filePath) {
//...解析文件名获取扩展名,比如 rule.json,返回 json
return "json";
}
}
上面这段代码是一个原始的实现,实际上,我们应该 把创建对象的步骤提取到一个独立的类中,让这个类只负责对象的创建,这样才能使 类的职责更单一、代码更清晰。
我们 将只负责创建对象的这个类称为工厂类,里面提供一个创建解析器的静态方法,这就是 简单工厂模式。具体实现如下:
// 工厂类
public class RuleConfigParserFactory {
public static IRuleConfigParser createParser(String configFormat) {
IRuleConfigParser parser = null;
if ("json".equalsIgnoreCase(configFormat)) {
parser = new JsonRuleConfigParser();
} else if ("xml".equalsIgnoreCase(configFormat)) {
parser = new XmlRuleConfigParser();
} else if ("yaml".equalsIgnoreCase(configFormat)) {
parser = new YamlRuleConfigParser();
}
return parser;
}
}
这样,我们在获取解析器的时候,就可以直接调用工厂类中的 createParser()
创建了:
public class RuleConfigSource {
public RuleConfig load(String ruleConfigFilePath) {
String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
// 从工厂类中获取解析器
IRuleConfigParser parser = RuleConfigParserFactory.createParser(ruleConfigFileExtension);
if (parser == null) {
throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
}
String configText = "";
// ...从 ruleConfigFilePath 文件中读取配置文本到 configText 中...
RuleConfig ruleConfig = parser.parse(configText);
return ruleConfig;
}
private String getFileExtension(String filePath) {
//...解析文件名获取扩展名,比如 rule.json,返回 json
return "json";
}
}
大部分工厂类都是以 "Factory" 结尾,但也不是必须的,比如 Java 中的 DateFormat、Calender 等。
而工厂类中创建对象的方法一般都是以 "create" 开头,但有的也命名为
getInstance()
、newInstance()
,甚至命名为valueOf()
(Java String 类的valueOf()
方法)。
类图如下:
1.2 结合单例的简单工厂
在上面的代码中,我们每次调用 RuleConfigParserFactory 的 createParser()
时,都会创建一个新的对象返回。但是如果 Parser 可以复用,那么我们就可以先将 Parser 缓存起来,而不用每次需要时都去新创建一个 Parser 对象了。
这有点类似于 单例模式和简单工厂模式的结合,具体代码实现如下:
public class RuleConfigParserFactory {
private static final Map<String, RuleConfigParser> cachedParsers = new HashMap<>();
static {
cachedParsers.put("json", new JsonRuleConfigParser());
cachedParsers.put("xml", new XmlRuleConfigParser());
cachedParsers.put("yaml", new YamlRuleConfigParser());
}
public static IRuleConfigParser createParser(String configFormat) {
if (configFormat == null || configFormat.isEmpty()) {
return null; // 返回 null 还是 IllegalArgumentException 全凭你自己说了算
}
IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
return parser;
}
}
类图如下:
1.3 小结
在上面的两种简单工厂的实现方式中,如果需要添加新的 Parser,那就需要修改 RuleConfigParserFactory 工厂类中的代码(第一种需要添加 if-else 分支,第二种需要在容器中添加新的 Parser),这不是违背了开闭原则呢?实际上,如果不是需要频繁的添加新的 Parser,偶尔修改一下工厂类中的代码,也是可以接受的。
此外,还发现在第一种实现中,RuleConfigParserFactory 中有一组 if-else 分支判断,是否应该用多态或其他设计模式来代替呢?实际上,如果 if-else 分支不是很多,也是可以接受的。而如果使用多态或其他设计模式来代替,虽然提高了代码的扩展性,更符合开闭原则,但也随之增加了类的个数,牺牲了代码的可读性。后面马上就会讲到。
所以,我们需要根据实际情况进行设计,不是非要一味地追求极致,我们需要 在扩展性和可读性之间做权衡。
2. 工厂方法
2.1 工厂方法的引出
在上面的简单工厂实现中,有很多 if-else 分支,如果想要去掉,比较经典的方法就是利用多态。
按照多态思想实现如下:
public interface IRuleConfigParserFactory {
IRuleConfigParser createParser();
}
public class JsonRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new JsonRuleConfigParser();
}
}
public class XmlRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new XmlRuleConfigParser();
}
}
public class YamlRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new YamlRuleConfigParser();
}
}
}
这就是工厂方法模式的典型实现。这样当我们新增一种 Parser 时,只需要新增一个实现 IRuleConfigParserFactory 接口的 Factory 类即可。
类图如下:
简单工厂的创建对象逻辑都在同一个工厂类中,而工厂方法则将不同对象的创建逻辑放在了不同的子工厂类中,所以工厂方法模式比简单工厂模式更加符合开闭原则。
工厂方法模式,又称为多态工厂模式,它的定义是:通过定义一个工厂父类来负责定义创建对象的公共接口,而子类则负责生成具体的对象。
2.2 工厂方法存在的问题
从上面的实现来看,好像解决了工厂类中的开闭原则问题。但是,我们如何使用上面的那些工厂类呢?下面来看看:
public class RuleConfigSource {
public RuleConfig load(String ruleConfigFilePath) {
String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
IRuleConfigParserFactory parserFactory = null;
if ("json".equalsIgnoreCase(ruleConfigFileExtension)) {
parserFactory = new JsonRuleConfigParserFactory();
} else if ("xml".equalsIgnoreCase(ruleConfigFileExtension)) {
parserFactory = new XmlRuleConfigParserFactory();
} else if ("yaml".equalsIgnoreCase(ruleConfigFileExtension)) {
parserFactory = new YamlRuleConfigParserFactory();
} else {
throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
}
IRuleConfigParser parser = parserFactory.createParser();
String configText = "";
// ...从 ruleConfigFilePath 文件中读取配置文本到 configText 中...
RuleConfig ruleConfig = parser.parse(configText);
return ruleConfig;
}
private String getFileExtension(String filePath) {
// ...解析文件名获取扩展名,比如 rule.json,返回 json
return "json";
}
}
我们发现,工厂类的对象创建逻辑又耦合进了更上一层的使用类中,所以工厂方法并没有实际解决掉这个问题,反而让类的设计变更复杂了。
想要解决这个问题,我们可以 为工厂类再创建一个简单工厂,用于创建工厂类对象。
因为各工厂类只包含方法,不包含成员变量,完全可以复用,不需要每次都创建新的工厂类对象。所以,我们使用结合单例的简单工厂模式更加合适。
具体来说,我们定义一个 RuleConfigParserFactoryMap 类作为创建工厂对象的工厂类,getParserFactory()
用于返回缓存好的单例工厂对象,实现如下:
public class RuleConfigSource {
public RuleConfig load(String ruleConfigFilePath) {
String ruleConfigFileExtension = getFileExtension(ruleConfigFilePath);
IRuleConfigParserFactory parserFactory = RuleConfigParserFactoryMap.getParserFactory(ruleConfigFileExtension);
if (parserFactory == null) {
throw new InvalidRuleConfigException("Rule config file format is not supported: " + ruleConfigFilePath);
}
IRuleConfigParser parser = parserFactory.createParser();
String configText = "";
// ...从 ruleConfigFilePath 文件中读取配置文本到 configText 中...
RuleConfig ruleConfig = parser.parse(configText);
return ruleConfig;
}
private String getFileExtension(String filePath) {
// ...解析文件名获取扩展名,比如 rule.json,返回 json
return "json";
}
}
// 工厂的工厂
public class RuleConfigParserFactoryMap {
private static final Map<String, IRuleConfigParserFactory> cachedFactories = new HashMap<>();
static {
cachedFactories.put("json", new JsonRuleConfigParserFactory());
cachedFactories.put("xml", new XmlRuleConfigParserFactory());
cachedFactories.put("yaml", new YamlRuleConfigParserFactory());
}
public static IRuleConfigParserFactory getParserFactory(String type) {
if (type == null || type.isEmpty()) {
return null;
}
IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());
return parserFactory;
}
}
可以发现,这样的实现方式其实就是再抽象出一个单例的简单工厂,把 if-else 分支使用一个 Map 来代替了。
类图如下:
上面这个配置文件解析器的例子只是为了帮助我们学习工厂模式,实际对于这个例子而言,简单工厂模式比工厂方法模式更加合适,因为我们这里 对每个 Factory 类只是做简单的 new 操作,功能非常单薄,没必要设计成单独的类,增加代码的复杂性。
2.3 什么时候使用工厂方法,而非简单工厂呢?
我们知道,如果要将某个代码块抽取出来,独立为一个函数或类,是因为这个代码块的逻辑过于复杂,抽取后能让代码逻辑更清晰、更加可读和可维护。但是,如果代码块本身就不复杂,就几行代码,那我们就完全没必要将它抽取成单独的函数或类了。
将上面的思想类比到工厂模式中来,可以得出:当对象的创建逻辑比较复杂(例如需要组合其他类对象,做各种初始化操作),那就推荐使用工厂方法模式,将复杂的创建逻辑拆分到多个工厂类中,而如果使用简单工厂模式,将所有的创建逻辑都放到一个工厂类中,就会导致这个工厂类过于复杂。
3. 抽象工厂
抽象工厂模式的应用场景有些特殊,没有前两种常用,下面简单的了解一下。
如果 类有两种分类方式,比如上个例子中,我们除了根据配置文件格式(Json、Xml、Yaml......)进行分类,如果还要根据解析的对象(Rule 规则配置、System 系统配置)进行分类,那么就会对应下面 6 种 Parser 类:
- 针对规则配置的解析器,基于接口 IRuleConfigParser:
- JsonRuleConfigParser
- XmlRuleConfigParser
- YamlRuleConfigParser
- 针对系统配置的解析器,基于接口 ISystemConfigParser:
- JsonSystemConfigParser
- XmlSystemConfigParser
- YamlSystemConfigParser
针对这种特殊的场景,如果使用工厂方法实现,就要针对每个 Parser 都编写工厂类,那么就会有 6 各工厂类。而如果后续还要继续增加根据业务配置的解析器(如 IBizConfigParser),那就要再对应地增加 3 个工厂类。类过多就会导致系统难以维护。
这时候,就要有请抽象工厂出场了。我们可以让一个工厂负责创建多个不同类型的对象(IRuleConfigParser、ISystemConfigParser 等),而不是只创建一种 Parser 对象,这样就可以减少工厂类的个数。具体代码实现如下:
// IConfigParserFactory 工厂,负责创建不同类型的对象
public interface IConfigParserFactory {
IRuleConfigParser createRuleParser();
ISystemConfigParser createSystemParser();
// 此处可以扩展新的 parser 类型,比如 IBizConfigParser
}
// Json 类型的解析器工厂,里面提供创建不同类型的对象
public class JsonConfigParserFactory implements IConfigParserFactory {
@Override
public IRuleConfigParser createRuleParser() {
return new JsonRuleConfigParser();
}
@Override
public ISystemConfigParser createSystemParser() {
return new JsonSystemConfigParser();
}
}
// Xml 类型的解析器工厂
public class XmlConfigParserFactory implements IConfigParserFactory {
@Override
public IRuleConfigParser createRuleParser() {
return new XmlRuleConfigParser();
}
@Override
public ISystemConfigParser createSystemParser() {
return new XmlSystemConfigParser();
}
}
// 省略 YamlConfigParserFactory
在使用时,就可以直接根据不同解析对象的类型和不同的配置文件格式来创建不同的解析器了。
类图如下:
有没有发现,抽象工厂其实就是:如果不只是一种分类时,可以在接口中定义第二个分类维度,从而减少需要创建的工厂类。
4. 总结
本章主要讲了三种工厂模式,它们的实现方式在上面的代码和类图中都已经表达得非常清楚了,那么下面就来总结一下它们的使用场景。
如果 创建逻辑比较复杂,那么就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用分离开来。判断创建逻辑是否复杂,可以根据如下依据:
代码中存在 if-else 分支判断,需要动态地根据不同类型创建不同的对象:
在这种情况下,如果每个对象的创建逻辑都比较简单,那么直接使用简单工厂模式,将多个对象的创建逻辑都放到一个工厂类中即可。相反,若每个对象的创建逻辑都比较复杂时,为了避免一个工厂类过于庞大,推荐使用工厂方法模式,将创建逻辑拆分得更细,每个对象的创建逻辑独立到各自的工厂类中。
尽管不需要根据不同类型创建不同对象,但 单个对象本身的创建过程比较复杂时,也可以考虑使用工厂模式,将对象的创建过程封装到工厂类中:
在这种情况下,由于单个对象本身的创建逻辑就比较复杂,因此直接使用工厂方法模式即可。
除此之外,如果创建对象的逻辑并不复杂,那我们直接通过 new 来创建对象即可,不必使用工厂模式。
最后再来总结一下 工厂模式的作用:
- 封装变化:创建逻辑可能会发生变化,封装成工厂模式后,创建逻辑的变化对调用者更透明;
- 代码复用:创建对象的逻辑抽取到独立的工厂类后,可以进行复用;
- 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何具体的创建对象;
- 控制复杂度:将创建对象的逻辑抽取出来,让原本的函数或类职责更单一,代码更简洁。