设计模式之美 - 工厂模式

设计模式之美目录: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 等不同格式的解析,使用时需要根据文件的后缀名获取不同的解析器进行解析。

在 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 格式的解析器,就需要修改简单工厂类,有没有一种方式,不需要修改工厂类呢?答案就是工厂方法。

在 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,这时候不可能针对每个组件都编写一个工厂类,也不可能让用户来组装最终的对象,这时就需要用到抽象工厂模式。

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

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

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

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

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

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

Java 设计模式系列(二)简单工厂模式和工厂方法模式的更多相关文章

  1. Java设计模式(二) 工厂方法模式

    本文介绍了工厂方法模式的概念,优缺点,实现方式,UML类图,并介绍了工厂方法(未)遵循的OOP原则 原创文章.同步自作者个人博客 http://www.jasongj.com/design_patte ...

  2. Java 设计模式系列(二十)状态模式

    Java 设计模式系列(二十)状态模式 状态模式,又称状态对象模式(Pattern of Objects for States),状态模式是对象的行为模式.状态模式允许一个对象在其内部状态改变的时候改 ...

  3. Java 设计模式系列(十二)策略模式(Strategy)

    Java 设计模式系列(十二)策略模式(Strategy) 策略模式属于对象的行为模式.其用意是针对一组算法,将每一个算法封装到具有共同接口的独立的类中,从而使得它们可以相互替换.策略模式使得算法可以 ...

  4. Java 设计模式系列(二二)责任链模式

    Java 设计模式系列(二二)责任链模式 责任链模式是一种对象的行为模式.在责任链模式里,很多对象由每一个对象对其下家的引用而连接起来形成一条链.请求在这个链上传递,直到链上的某一个对象决定处理此请求 ...

  5. Java 设计模式系列(十四)命令模式(Command)

    Java 设计模式系列(十四)命令模式(Command) 命令模式把一个请求或者操作封装到一个对象中.命令模式允许系统使用不同的请求把客户端参数化,对请求排队或者记录请求日志,可以提供命令的撤销和恢复 ...

  6. Java 设计模式系列(十)外观模式

    Java 设计模式系列(十)外观模式 门面模式(Facade):外部与一个子系统的通信必须通过一个统一的外观对象进行,为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这 ...

  7. Java 设计模式系列(十一)享元模式

    Java 设计模式系列(十一)享元模式 Flyweight 享元模式是对象的结构模式.享元模式以共享的方式高效地支持大量的细粒度对象. 一.享元模式的结构 享元模式采用一个共享来避免大量拥有相同内容对 ...

  8. Java 设计模式系列(八)装饰者模式

    Java 设计模式系列(八)装饰者模式 装饰模式又名包装(Wrapper)模式.装饰模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案.Decorator 或 Wrapper 一.装饰模 ...

  9. Java 设计模式系列(九)组合模式

    Java 设计模式系列(九)组合模式 将对象组合成树形结构以表示"部分-整体"的层次结构.组合模式使得用户对单个对象的使用具有一致性. 一.组合模式结构 Component: 抽象 ...

  10. Java 设计模式系列(二三)访问者模式(Vistor)

    Java 设计模式系列(二三)访问者模式(Vistor) 访问者模式是对象的行为模式.访问者模式的目的是封装一些施加于某种数据结构元素之上的操作.一旦这些操作需要修改的话,接受这个操作的数据结构则可以 ...

随机推荐

  1. 【网络通信】TCP三次握手和四次挥手的示意图

    三次握手 TCP连接是通过三次握手来连接的. 第一次握手 当客户端向服务器发起连接请求时,客户端会发送同步序列标号SYN到服务器,在这里我们设SYN为m,等待服务器确认,这时客户端的状态为SYN_SE ...

  2. ES6必知必会 (八)—— async 函数

    async 函数 1.ES2017 标准引入了 async 函数,它是对 Generator 函数的改进 , 我们先看一个读取文件的例子: Generator 写法是这样的 : var fs = re ...

  3. streamsets stream selector 使用

    stream selector 就是一个选择器,可以方便的对于不同record 的数据进行区分,并执行不同的处理 pipeline flow stream selector 配置 local fs 配 ...

  4. 项目管理软件选择:redmine or JIRA

    个人理解,这两款软件从本质上说是issue tracking,而不是项目管理. 先说些个人的想法 1)从现阶段情况看,都是够用的,毕竟本来就是小团队 2)从扩展而言,根据现在团队的实际情况(基本都是搞 ...

  5. EXCEL中如何删除透视表的多余汇总

    EXCEL中如何删除透视表的多余汇总 1)如下图,选中字段列,单击鼠标右键,在弹出的菜单中选择[字段设置]选项. 2)弹出[字段设置]对话框. 3)选择“分类汇总和筛选”选项卡,然后勾选“无”选项,单 ...

  6. 代码规范 for node.js with 'npm-coding-style'

    npm-coding-style npm's "funny" coding style Description npm's coding style is a bit unconv ...

  7. umount时目标忙解决办法

    标签(空格分隔): ceph ceph运维 osd 在删除osd后umount时,始终无法umonut,可以通过fuser查看设备被哪个进程占用,之后杀死进程,就可以顺利umount了. [root@ ...

  8. 四.jQuery源码解析之jQuery.fn.init()的参数解析

    从return new jQuery.fn.init( selector, context, rootjQuery )中可以看出 参数selector和context是来自我们在调用jQuery方法时 ...

  9. 在window的IIS中搭配Php的简单方法

    在window的IIS中搭配Php的简单方法.搭配php的时候找到的一个超级简单方法 关键的核心是 PHP Manager for IIS 这是微软开发的一个项目,使用它可以在window下最方便简单 ...

  10. grunt 不是内部或外部命令,也不是可运行的程序或批处理文件

    问题1 grunt 不是内部或外部命令,也不是可运行的程序或批处理文件 解决方法: Grunt和 Grunt 插件是通过 npm 安装并管理的,npm是 Node.js 的包管理器. 安装CLI 在继 ...