Java 设计模式系列(二)简单工厂模式和工厂方法模式

时间:2023-09-18 23:00:02

设计模式之美 - 工厂模式

设计模式之美目录:https://www.cnblogs.com/binarylei/p/8999236.html

工厂模式实现了创建者和调用者的分离。工厂模式可分为三种类型:简单工厂、工厂方法、抽象工厂。不过,在 GoF 的《设计模式》一书中,它将简单工厂模式看作是工厂方法模式的一种特例,所以工厂模式只被分成了工厂方法和抽象工厂两类。实际上,前面一种分类方法更加常见。

首先,我们要明确的是使用工厂模式并不能减少代码行数,只是将合适的代码放在合适的类中,这其实也适用于其它所有的设计模式。不使用工厂模式的什么缺陷呢?最大的问题是对象创建逻辑(if-else)和业务代码耦合在一起,代码可读性可维护性都很差。

  • 简单工厂:也叫静态方法工厂,根据不同的条件创建不同的对象,if-else 逻辑在这个工厂类中。
  • 工厂方法:一个工厂方法只创建一类对象。一般先用简单工厂类来得到某个工厂方法,再用这个工厂方法来创建对象,if-else 逻辑在简单工厂类中。
  • 抽象工厂:复杂对象的创建,一般用于产品簇的创建。相对于前两种工厂模式,使用比较少。

当然,工厂类的创建对象也不全部是 if-else 逻辑,也可以根据参数拼凑出类名,然后使用反射创建对象。如 Jdk 中创建 URL 协议的工厂类 sun.misc.Launcher.Factory 就是根据参数 protocol 拼凑类名 sun.net.www.protocol.${protocol}.Handler。

下面分别介绍一下这三种工厂模式,重点关注它们的使用场景。

1. 简单工厂(Simple Factory)

1.1 场景分析

需求分析:配置文件的解析类 IRuleConfigParser 有 xml、yaml、properteis、json 等不同格式的解析,使用时需要根据文件的后缀名获取不同的解析器进行解析。

Java 设计模式系列(二)简单工厂模式和工厂方法模式

在 v1 版本中,我们的实现方案简单粗暴,代码实现如下:

public RuleConfig load(String ruleConfigFilePath) {
String fileExtension = getFileExtension(ruleConfigFilePath);
IRuleConfigParser parser = null;
if ("json".equalsIgnoreCase(fileExtension)) {
parser = new JsonRuleConfigParser();
} else if ("xml".equalsIgnoreCase(fileExtension)) {
parser = new XmlRuleConfigParser();
} else if ("yaml".equalsIgnoreCase(fileExtension)) {
parser = new YamlRuleConfigParser();
} else if ("properties".equalsIgnoreCase(fileExtension)) {
parser = new PropertiesRuleConfigParser();
} else {
throw new InvalidRuleConfigException(
"Rule config file format is not supported: " + ruleConfigFilePath);
} String configText = "";
// 从 ruleConfigFilePath 文件中读取配置文本到 configText 中
RuleConfig ruleConfig = parser.parse(configText);
return ruleConfig;
}

说明: 很明显,v1 版本最大的问题是解析器的创建和业务耦合在一起,影响代码的可读可维护性,也不符合单一职责原则。这样就有了 v2 版本,在 v2 版本中将解析器的创建过程提取出成一个单独的方法。

public RuleConfig load(String ruleConfigFilePath) {
String fileExtension = getFileExtension(ruleConfigFilePath);
IRuleConfigParser parser = createParser(fileExtension); String configText = "";
RuleConfig ruleConfig = parser.parse(configText);
return ruleConfig;
} // 提出创建解析器的方法
private IRuleConfigParser createParser(String fileExtension) {
IRuleConfigParser parser = null;
if ("json".equalsIgnoreCase(fileExtension)) {
parser = new JsonRuleConfigParser();
}
...
return parser;
}

说明: 在 v2 版本中将解析器的创建过程单独抽象成一个单独的方法。

1.2 简单工厂

简单工厂也叫静态工厂方法模式(Static Factory Method Pattern),这是因为其中创建对象的方法是静态的。

在 v2 版本中已经将解析器的创建过程单独抽象成一个单独的方法,不过,为了让类的职责更加单一、代码更加清晰,我们还可以进一步将 createParser() 函数剥离到一个独立的类中,让这个类只负责对象的创建。而这个类就是我们现在要讲的简单工厂模式类 v3。具体的代码如下所示:

public RuleConfig load(String ruleConfigFilePath) {
String fileExtension = getFileExtension(ruleConfigFilePath);
IRuleConfigParser parser = RuleConfigParserFactory_v1.createParser(fileExtension); String configText = "";
RuleConfig ruleConfig = parser.parse(configText);
return ruleConfig;
} // 简单工厂:负责解析器的创建,在简单工厂模式中,会根据条件创建不同的解析器
public class RuleConfigParserFactory_v1 {
public static IRuleConfigParser createParser(String configFormat) {
IRuleConfigParser parser = null;
if ("json".equalsIgnoreCase(fileExtension)) {
parser = new JsonRuleConfigParser();
} else if ("xml".equalsIgnoreCase(configFormat)) {
parser = new XmlRuleConfigParser();
}
...
return parser;
}
}

说明: 在 v3 版本中,解析器的创建由单独的工厂类负责,也就是简单工厂模式。在简单工厂模式中,工厂类会根据不同的条件创建不同的解析器对象。你可能会说,如果新增加一种格式的解析器,不是也需要修改这个简单工厂类吗?是的,但我认为这种修改,我们是可以接受的。

在很多场景中,我们会提前将解析器初始化完成,放到缓存中,使用时直接取出即可,这有点类似 "简单工厂 + 单例模式"。代码如下:

public class RuleConfigParserFactory_v2 {
private static final Map<String, IRuleConfigParser> cachedParsers = new HashMap<>(); static {
cachedParsers.put("json", new JsonRuleConfigParser());
cachedParsers.put("xml", new XmlRuleConfigParser());
cachedParsers.put("yaml", new YamlRuleConfigParser());
cachedParsers.put("properties", new PropertiesRuleConfigParser());
} public static IRuleConfigParser createParser(String configFormat) {
if (configFormat == null || configFormat.isEmpty()) {
// 返回 null 还是 IllegalArgumentException 全凭你自己说了算
return null;
}
IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
return parser;
}
}

大部分工厂类都是以 "Factory" 这个单词结尾的,但也不是必须的,比如 Java 中的 DateFormat、Calender。除此之外,工厂类中创建对象的方法一般都是 create 开头,比如代码中的 createParser(),但有的也命名为 getInstance()、createInstance()、newInstance(),有的甚至命名为 valueOf()(比如 Java String 类的 valueOf() 函数)等等,这个我们根据具体的场景和习惯来命名就好。

2. 工厂方法模式(Factory Method)

工厂方法模式比起简单工厂模式更加符合开闭原则。我们可以为工厂类再创建一个简单工厂,也就是工厂的工厂,用来创建工厂类对象。

一般有以下两种场景下,我们需要使用工厂方法模式:

  1. 高扩展性。简单工厂模式扩展性差,新增加一种实现都需要修改源码。
  2. 对象创建复杂。如果每个解析器对象创建过程都很复杂,需要好几个步骤,那么我更推荐使用工厂方法,将复杂的对象创建过程封装起来。

2.1 工厂方法典型实现

在 v3 版本中,如果我们需要扩展一种新的 ini 格式的解析器,就需要修改简单工厂类,有没有一种方式,不需要修改工厂类呢?答案就是工厂方法。

Java 设计模式系列(二)简单工厂模式和工厂方法模式

在 v4 版本中,我们提供了一个 IRuleConfigParserFactory#createParser 接口来创建对应的解析器,不同的解析器工厂只要实现这个接口即可。

public RuleConfig load(String ruleConfigFilePath) {
String fileExtension = getFileExtension(ruleConfigFilePath);
IRuleConfigParserFactory parserFactory = null;
if ("json".equalsIgnoreCase(fileExtension)) {
parserFactory = new JsonRuleConfigParserFactory();
} else if ("xml".equalsIgnoreCase(fileExtension)) {
parserFactory = new XmlRuleConfigParserFactory();
}
...
IRuleConfigParser parser = parserFactory.createParser();
return parser.parse(...);
}

说明: 在 v4 版本中,我们先需要获取工厂方法的工厂类,再通过这个工厂类创建解析器。此时,获取工厂方法的逻辑又和业务逻辑耦合,为此可以再使用一个简单工厂来创建工厂方法。

// 通过简单工厂创建工厂方法,再通过工厂方法创建对象。
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());
cachedFactories.put("properties", new PropertiesRuleConfigParserFactory());
} public static IRuleConfigParserFactory getParserFactory(String type) {
if (type == null || type.isEmpty()) {
return null;
}
IRuleConfigParserFactory parserFactory = cachedFactories.get(type.toLowerCase());
return parserFactory;
}
}

说明: 在 v5 版本中,我们通过简单工厂创建工厂方法,再通过工厂方法创建对象,这是工厂方法最常用的方式。你可能会说这不又回到 if-else 模式了吗?每增加一种解析器的实现不也是要修改这个简单工厂吗?事实上,if-else 的逻辑我们是逃不掉的,只能将最将合适的代码放在合适的类中。当然我们也有一些手段来避免修改简单工厂类,最核心的思想其实就是外部化配置。

2.2 外部化配置

在 v5 版本中,工厂方法的获取是通过简单工厂 RuleConfigParserFactoryMap 创建的,其中使用了大量的 if-else,在程序升级过程中,也需要修改这个简单工厂。虽然,这个简单工厂类代码修改非常简单,但还是违反了开闭原则。

那有没有一种方法,只增加对应的工厂方法扩展类,不修改这个简单工厂类呢?答案是有的,而且还是有多种方式,但核心的思想都是外部化配置。

  • SPI:Service Provider Interface,是 JDK 提供的一种外部化配置手段。
  • Spring IoC:通过外部化配置,向容器中直接注入工厂方法实例。
  • 契约编程:通过参数获取工厂方法的实现类名称,再通过 JDK 反射创建工厂方法实例。如 "URL 协议扩展"
  • 自定义外部化配置:如 "Dubbo 自适应扩展"。

2.2.1 SPI

此时,简单工厂通过 JDK SPI 机制 ServiceLoader 加载工厂方法实例,而不是通过 if-else。

在 META-INF/services 下,配置 com.binarylei.design.factory.IRuleConfigParserFactory 文件:

com.binarylei.design.factory.JsonRuleConfigParserFactory

2.2.2 Spring IoC

通过 Config 类,直接向容器中注入工厂方法实例,本质还是外部配置思想。

2.2.3 契约编程

通过参数获取工厂方法的实现类名称,再通过 JDK 反射创建工厂方法实例。 以 URL 协议扩展为例。

当实例化 URL 时,会获取参数 url 对应的协议 protocol,再通过协议 protocol 获取其对应的工厂方法。我们看一下这个工厂类 sun.misc.Launcher.Factory,Factory 类会通过协议获取对应的工厂方法名称,再通过反射创建工厂方法实例。代码非常简单,实际上是通过契约编程的方式,规避 if-else。这样扩展时,只需要实现对应的协议的实现类即可。

new URL("http://www.baidu.com").openConnection()

// 通过契约编程的方式,规避 if-else
private static class Factory implements URLStreamHandlerFactory {
private static String PREFIX = "sun.net.www.protocol";
public URLStreamHandler createURLStreamHandler(String protocol) {
String name = PREFIX + "." + protocol + ".Handler";
Class<?> c = Class.forName(name);
return (URLStreamHandler)c.newInstance();
}
}

2.2.4 自定义

自定义外部配置,这样可以读取配置文件,通过参数获取工厂方法名称。以 Dubbo 自适应扩展为例。

Dubbo 协议会在 META-INF/dubbo/internal 下配置 org.apache.dubbo.rpc.Protocol 文件

dubbo=org.apache.dubbo.rpc.protocol.dubbo.DubboProtocol

这样,我们就可以通过 Dubbo URL 中的 protocol 参数查找对应的工厂方法名称。

3. 抽象工厂(Abstract Factory)

抽象工厂模式的应用场景比较特殊,没有前两种常用,所以不是我们本节课学习的重点,简单了解一下就可以了。

如果对象由多个组件组成,如 IRuleConfigParser 和 ISystemConfigParser,这时候不可能针对每个组件都编写一个工厂类,也不可能让用户来组装最终的对象,这时就需要用到抽象工厂模式。

Java 设计模式系列(二)简单工厂模式和工厂方法模式

4. 什么时候使用工厂模式

当创建逻辑比较复杂,是一个 "大工程" 的时候,我们就考虑使用工厂模式,封装对象的创建过程,将对象的创建和使用相分离。何为创建逻辑比较复杂呢?我总结了下面两种情况。

  1. 对象创建存在 if-else 分支判断,动态地根据不同的类型创建不同的对象。针对这种情况,我们就考虑使用简单工厂模式,对象创建逻辑和业务逻辑分离。简单工厂模式的扩展性比较差。
  2. 如果代码扩展性要求高,或单个对象创建比较复杂,我们也可以考虑使用工厂方法模式。
  3. 如果对象由多个组件组成,每个组件都有不同的创建方式,这里往往就是抽象工厂。

现在,我们上升一个思维层面来看工厂模式,它的作用无外乎下面这四个。这也是判断要不要使用工厂模式的最本质的参考标准。

  1. 封装变化:创建逻辑有可能变化,封装成工厂类之后,创建逻辑的变更对调用者透明。
  2. 代码复用:创建代码抽离到独立的工厂类之后可以复用。
  3. 隔离复杂性:封装复杂的创建逻辑,调用者无需了解如何创建对象。

每天用心记录一点点。内容也许不重要,但习惯很重要!