GoF 之工厂模式 - 回顾已学过的设计模式

GoF 包含 23 种设计模式,其中有一些设计模式,老杜在之前的课程中(JavaSE JavaWeb MyBatis)中已经讲过。这里做一个简单的回顾。

设计模式优秀博客或文章

单例模式

单例模式是一种创建型设计模式,让你能够保证一个类只有一个实例,并提供一个访问该实例的全局节点。

在韩顺平的 JavaSE 课程中学过单例模式。

常见的实现方式有:懒汉式,饿汉式。

目前最佳实现方式是:单元素的枚举。

简单工厂模式,工厂方法模式,抽象工厂模式

老杜的工厂模式讲解中,工厂方法模式讲得不够完美,讲义中的抽象工厂模式的内容除了开头,后面几乎都是错的,这里略作补充。建议阅读:

工厂模式(上):为什么说没事不要随便用工厂模式创建对象?
抽象工厂模式

对象生产与对象消费的分离,或者说解耦,是工厂模式的最核心设计目的。

简单工厂模式,以相对简单的方式进行实现,来达到对象生产与消费分离的目的。但它的问题很明显:

  • 不满足 OCP 原则,需要新增产品时,必须要修改工厂类。
  • 工厂类是超级工厂,是全能工厂,包含了各种产品的生产逻辑。如果产品的生产逻辑比较简单,产品的类型不多,那么问题还不大;可如果产品的生产逻辑很复杂,或者产品很多,工厂类的代码会非常臃肿。

工厂方法模式,是对简单工厂模式的改进。
它当然也满足对象生产与消费分离的目的,此外它还成功解决了简单工厂模式的两个问题。
怎么解决的?将工厂类角色拆分为抽象工厂角色和具体工厂角色,一个具体工厂角色对应生产一个具体产品。

  • 新增产品时,只需要额外再新增相应的具体工厂类即可。对客户端使用而言,已经满足了 OCP 原则。
  • 不同产品的生产逻辑被分离到相应的具体工厂类中,能够适应产品生产逻辑复杂,或者产品类型较多的场景。

工厂方法模式虽然解决了简单工厂模式的问题,但也引入了一些新的问题。其中最主要的问题就是类爆炸问题:每增加一个新的产品类型,就需要增加两个类。当产品类型特别多的时候,系统中的类数量爆炸。

抽象工厂模式,是对工厂方法模式的改进。
它当然也满足对象生产与消费分离的目的,此外它还成功解决了工厂方法模式的类爆炸问题。
怎么解决的?让一个具体工厂角色能够生产多种具体产品。
需要注意的细节是,一个具体工厂角色所生产的多种具体产品中,每种具体产品的父类或者说接口应该是不同的,也即属于不同的产品族。

老杜讲义中的举例是有些问题的,明明是 WeaponFactory 却拥有 getFruit() 方法,这是很矛盾的,而且 WeaponFactory 中的 getWeapon 是传入武器类型获取相应具体的 Weapon,这并不是经典的抽象工厂模式!

抽象工厂模式 的举例就好多了,有 Chair Sofa Coffe Table 三种产品族,这三种产品族都属于家具产品,每个具体工厂是家具工厂,都能够生产这三族产品,只是所生产的家具的风格不同,比如 Modern­Furniture­Factory 所生产的是现代风格的 Modern­Chair Modern­Sofa Modern­Coffee­Table

回到工厂方法模式,在该模式下,具体工厂的创建与对象消费是耦合的。在大部分情况下,这并没有什么问题,客户端往往需要的是一个具体的对象,此时创建具体工厂来生产即可。

但有些时候,客户端并不关心对象的具体类型是什么,只关心对象的方法,即客户端希望完全面向接口来消费对象。这就与工厂方法模式有矛盾了,客户端要消费对象,就必须先创建具体工厂,既然是创建具体工厂,客户端就不可能是完全面向接口的。

工厂模式(上):为什么说没事不要随便用工厂模式创建对象?中提到了相应的问题场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
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 if ("properties".equalsIgnoreCase(ruleConfigFileExtension)) {
parserFactory = new PropertiesRuleConfigParserFactory();
} 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";
}
}

客户端提供一个配置文件路径,并希望获得一个解析器。客户端并不关心解析器是 Json 解析器还是 XML 解析器,而只希望所获得的解析器,能够通过 parse() 方法完成解析操作。

工厂方法模式,具体工厂的创建与对象消费的耦合,决定了客户端不能完全面向接口,即客户端必须要关心解析器创建的细节:上面的代码中,客户端需要获取配置文件路径的后缀,然后根据后缀来创建不同格式的解析器工厂,这显然给客户端造成了使用上的负担,这是另一种生产与消费的耦合。

老杜的讲课中也提到了这个问题,并给出了和工厂模式(上):为什么说没事不要随便用工厂模式创建对象?一样的解决方案?引入简单工厂模式,创建一个生产具体工厂的简单工厂。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
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());
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;
}
}

这样的做法,显然会重新违背 OCP 原则。当需要增加一种新格式的解析器时,除了创建新的具体解析器类,新的具体解析器工厂类以外,还需要修改简单工厂 RuleConfigParserFactoryMap

虽然重新违背了 OCP 原则,但是考虑到:

  • 一般不会频繁添加新格式解析器
  • 工厂类的创建一般比较简单(往往一行 new 即可),不像产品的创建那么复杂,简单工厂 RuleConfigParserFactoryMap 的改动不会很大
    此时稍微不符合开闭原则,也是可以接受的。

另外,同样由于工厂类的创建比较简单,简单工厂 RuleConfigParserFactoryMap 在解析器不断增多的情况下,也能较好地控制住类的体积。

有没有一种完美的解决方案呢?对工厂方法模式引入简单工厂后,又不至于重新违背 OCP 原则?那就只能想到利用配置文件加反射机制了。简单说一下就是:

  • 增加一种新格式的解析器时,除了创建新的具体解析器类,新的具体解析器工厂类以外,还需要在配置文件中进行注册(指明新的具体解析器工厂的全类名)
  • 修改简单工厂 RuleConfigParserFactoryMap 的代码,在静态代码块中,完成对配置文件的解析,以反射的方式创建配置文件中所有注册的具体解析器工厂,并放到缓存中。

代理模式

代理模式是一种结构型设计模式,让你能够提供目标对象的替代品或其占位符。代理控制着对于目标对象的访问,并允许在将请求提交给对象前后进行一些处理。

代理模式可细分为静态代理和动态代理。

JavaSE 中的 Thread 就是代理模式中,静态代理的经典案例。

Mybatis 中的 sqlSession.getMapper() 接口代理机制就是代理模式中,动态代理的经典案例。

装饰器模式

在 JavaSE 的 IO 类库中学过装饰器模式,比如 BufferedInputStream 就是对 FileInputStream 进行装饰(或者说包装),从而额外提供缓存读取的支持。建议阅读:

装饰器模式:通过剖析Java IO类库源码学习装饰器模式

适配器模式

适配器模式是一种结构型设计模式,它能使接口不兼容的对象能够相互合作。

在 Servlet 中讲到 GenericServlet 时提到过。

Servlet 规范接口有 5 个方法,如 initdestroygetServletConfiggetServletInfoservice。这 5 个方法中,service 方法是最常用的最核心的,一般情况下,我们往往只关注于对 service 方法提供具体实现。但是,如果是实现 Servlet 接口,即使我们只关心 service 方法,却还是同时需要实现其他方法(哪怕是实现为空方法)。

想象一下,一个 UserServlet 类实现了 Servlet 接口,该 UserServlet 类中存在一堆重写方法(基本都是空实现),但是只有 service 方法是真正有意义,真正需要进行重写的。这样的 UserServlet 类内部充斥了大量无意义的内容,可不可以让 UserServlet 中只实现 service 方法就可以了。

可以,我们不让 UserServlet 去直接实现 Servlet 接口,而是去继承 GenericServlet 类。GenericServlet 是一个抽象类,该类实现了 Servlet 接口,对其中 service 以外的所有方法提供了默认实现,唯独 service 方法被声明为 abstract。这样,继承了 GenericServletUserServlet 就只需要关注于提供 service 的实现了。

GenericServlet 可以视为对原接口 Servlet 的一个适配器,它相对原接口更加易用。

其实 Servlet 中 GenericServlet 算是对适配器模式的最简单应用,它的主要作用是减少方法的 Override。更多的时候,适配器是需要做一些转换工作的。比如可以阅读:

适配器模式

享元模式

在 JavaSE 的 Integer 的缓存以及 String 的字符串常量池中学过。建议阅读:

享元模式(上):如何利用享元模式优化文本编辑器的内存占用?

享元模式(下):剖析享元模式在Java Integer、String中的应用

策略模式

策略模式是一种行为设计模式,它能让你定义一系列算法,并将每种算法分别放入独立的类中,以使算法的对象能够相互替换。

策略模式的思想非常朴素:一个接口(问题)下有多个实现类(策略)。建议阅读:

策略模式(上):如何避免冗长的if-else/switch分支判断代码?

策略模式(下):如何实现一个支持给不同大小文件排序的小程序?

观察者模式

观察者模式是一种行为设计模式,允许你定义一种订阅机制,可在对象事件发生时通知多个“观察”该对象的其他对象。

MVC 架构模式,是符合观察者模式的,而 Servlet Listener 是观察者模式的经典应用。

在老杜的 JavaWeb 课程中,学到 MVC 架构模式和 Servlet Listener 时,其实并没有讲解观察者模式相关的内容。

至于 MVC 架构模式与观察者模式的关系,深入理解MVC - 陈大侠的文章 - 知乎 中提到:

对应到 MVC 中,Model 是被观察的对象,View 是观察者,Model 层一旦发生变化,View 层即被通知更新

经典的观察者模式中,被观察者内部除了维护一个状态变量外,还维护了一个观察者的列表,当被观察者的状态发生更新后,会遍历通知所有观察者。

如果将 Model 视为经典观察者模式中的被观察者,显然:

  • Model 内部并没有维护一个观察者的列表。
  • Model 内部也没有提供观察者的注册方法,以及通知观察者的方法。

实际上这些工作,都被转移到 Controller 中了:

  • Model 与 View 的关系是在 Controller 层编码写死的,因此不存在一个显式的观察者列表,也不存在注册观察者的方法。
  • 通知 View 的逻辑是在 Controller 层直接定义的。Controller 在收到 Model 返回的数据后,通知 View 进行更新。

因此,我更倾向于这样理解 MVC 架构模式与经典观察者模式的关系:Model 相当于是被观察者中的状态变量,Controller 层相当于是被观察者,View 相当于是观察者。

随着前后端的分离,MVC 架构中的 View 层已经基本从后端中剥离出去了,对于后端来说,接口就是 View。因此,在前后端分离的场景下,感官上后端的一个 Model 其实只对应一个 View。但是站在全局的角度来说,后端通过接口返回给前端的数据,在被前端利用时,可能会影响到前端意义下的多个 View 组件。

知乎上看到过一句可能不是很准确的话:前后端通过接口通信,接口中传递的 json 数据可以看做是后端 MVC 中的 V,前端 MVC 中的 M。

至于 Servlet Listener,就是观察者模式的经典应用了。

ServletContextListener 接口为例,该接口的实现类,就是观察者。
ServletContext 就是被观察者中的状态变量。
至于被观察者,需要深入了解 Servlet 容器的初始化过程了。

应该是存在一个类,管理着所有 ServletContextListener 实现类的注册,注册应该是在读取解析 配置文件时候就完成的;另外该类应该也持有 ServletContext 对象,管理着 ServletContext 对象的创建与销毁(状态变化),从而便于通知所有观察者,即所有 ServletContextListener 实现类。

可以阅读:

观察者模式

模板方法模式

在 Servlet 中学过。建议阅读:

模板模式(上):剖析模板模式在JDK、Servlet、JUnit等中的应用

责任链模式

在 Servlet Filter 中学过。建议阅读:

职责链模式(上):如何实现可灵活扩展算法的敏感信息过滤框架?

职责链模式(下):框架中常用的过滤器、拦截器是如何实现的?:主要看 Servlet Filter 小节

迭代器模式

在 JavaSE 的 Iterator 里学过。建议阅读:

迭代器模式

学习设计模式——迭代器模式

迭代器模式:《深入设计模式》的图书网站