设计模式与范式:创建型

单例设计模式(Singleton Design Pattern)

单例的定义

单例设计模式(Singleton Design Pattern)理解起来⾮常简单。⼀个类只允许创建⼀个对象(或者叫实例),那这个类就是⼀个单例类,这种设计模式就叫作单例设计模式,简称单例模式。

单例的⽤处

从业务概念上,有些数据在系统中只应该保存⼀份,就⽐较适合设计为单例类。⽐如,系统的配置信息类。除此之外,我们还可以使⽤单例解决资源访问冲突的问题。

单例存在哪些问题

  • 单例对 OOP 特性的⽀持不友好
  • 单例会隐藏类之间的依赖关系
  • 单例对代码的扩展性不友好
  • 单例对代码的可测试性不友好
  • 单例不⽀持有参数的构造函数

单例有什么替代解决⽅案?

为了保证全局唯⼀,除了使⽤单例,我们还可以⽤静态⽅法来实现。不过,静态⽅法这种实现思路,并不能解决我们之前提到的问题。如果要完全解决这些问题,我们可能要从根上,寻找其他⽅式来实现全局唯⼀类了。⽐如,通过⼯⼚模式、IOC 容器(⽐如 Spring IOC 容器)来保证,由程序员⾃⼰来保证(⾃⼰在编写代码的时候⾃⼰保证不要创建两个类对象)。

有⼈把单例当作反模式,主张杜绝在项⽬中使⽤。我个⼈觉得这有点极端。模式没有对错,关键看你怎么⽤。如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太⼤问题。对于⼀些全局的类,我们在其他地⽅ new 的话,还要在类之间传来传去,不如直接做成单例类,使⽤起来简洁⽅便。

⼯⼚模式(Factory Design Pattern)

简单⼯场(Simple Factory)

实现⼀

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
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";
}
}

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();
} else if ("properties".equalsIgnoreCase(configFormat)) {
parser = new PropertiesRuleConfigParser();
}
return parser;
}
}

实现⼆

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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());
cachedParsers.put("properties", new PropertiesRuleConfigParser());
}
public static IRuleConfigParser createParser(String configFormat) {
if (configFormat == null || configFormat.isEmpty()) {
return null;//返回null还是IllegalArgumentException全凭你⾃⼰说了算
}
IRuleConfigParser parser = cachedParsers.get(configFormat.toLowerCase());
return parser;
}
}

⼯⼚⽅法模式

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
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();
}
}
public class PropertiesRuleConfigParserFactory implements IRuleConfigParserFactory {
@Override
public IRuleConfigParser createParser() {
return new PropertiesRuleConfigParser();
}
}
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;
}
}

那什么时候该⽤⼯⼚⽅法模式,⽽⾮简单⼯⼚模式呢?

我们前⾯提到,之所以将某个代码块剥离出来,独⽴为函数或者类,原因是这个代码块的逻辑过于复杂,剥离之后能让代码更加清晰,更加可读、可维护。但是,如果代码块本身并不复杂,就⼏⾏代码⽽已,我们完全没必要将它拆分成单独的函数或者类。

基于这个设计思想,当对象的创建逻辑⽐较复杂,不只是简单的 new ⼀下就可以,⽽是要组合其他类对象,做各种初始化操作的时候,我们推荐使⽤⼯⼚⽅法模式,将复杂的创建逻辑拆分到多个⼯⼚类中,让每个⼯⼚类都不⾄于过于复杂。⽽使⽤简单⼯⼚模式,将所有的创建逻辑都放到⼀个⼯⼚类中,会导致这个⼯⼚类变得很复杂。

抽象⼯⼚(Abstract Factory)

在简单⼯⼚和⼯⼚⽅法中,类只有⼀种分类⽅式。⽐如,在规则配置解析那个例⼦中,解析器类只会根据配置⽂件格式(Json、Xml、Yaml……)来分类。但是,如果类有两种分类⽅式,⽐如,我们既可以按照配置⽂件格式来分类,也可以按照解析的对象(Rule 规则配置还是 System 系统配置)来分类,那就会对应下⾯这 8 个 parser 类。

1
2
3
4
5
6
7
8
9
10
针对规则配置的解析器:基于接⼝IRuleConfigParser
JsonRuleConfigParser
XmlRuleConfigParser
YamlRuleConfigParser
PropertiesRuleConfigParser
针对系统配置的解析器:基于接⼝ISystemConfigParser
JsonSystemConfigParser
XmlSystemConfigParser
YamlSystemConfigParser
PropertiesSystemConfigParser

针对这种特殊的场景,如果还是继续⽤⼯⼚⽅法来实现的话,我们要针对每个 parser 都编写⼀个⼯⼚类,也就是要编写 8 个⼯⼚类。如果我们未来还需要增加针对业务配置的解析器(⽐如 IBizConfigParser),那就要再对应地增加 4 个⼯⼚类。⽽我们知道,过多的类也会让系统难维护。这个问题该怎么解决呢?

抽象⼯⼚就是针对这种⾮常特殊的场景⽽诞⽣的。我们可以让⼀个⼯⼚负责创建多个不同类型的对象(IRuleConfigParser、ISystemConfigParser 等),⽽不是只创建⼀种 parser 对象。这样就可以有效地减少⼯⼚类的个数。具体的代码实现如下所示:

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
public interface IConfigParserFactory {
IRuleConfigParser createRuleParser();
ISystemConfigParser createSystemParser();
//此处可以扩展新的parser类型,⽐如IBizConfigParser
}
public class JsonConfigParserFactory implements IConfigParserFactory {
@Override
public IRuleConfigParser createRuleParser() {
return new JsonRuleConfigParser();
}
@Override
public ISystemConfigParser createSystemParser() {
return new JsonSystemConfigParser();
}
}
public class XmlConfigParserFactory implements IConfigParserFactory {
@Override
public IRuleConfigParser createRuleParser() {
return new XmlRuleConfigParser();
}
@Override
public ISystemConfigParser createSystemParser() {
return new XmlSystemConfigParser();
}
}
// 省略YamlConfigParserFactory和PropertiesConfigParserFactory代码

⼯⼚模式和 DI 容器有何区别?

实际上,DI 容器底层最基本的设计思路就是基于⼯⼚模式的。DI 容器相当于⼀个⼤的⼯⼚类,负责在程序启动的时候,根据配置(要创建哪些类对象,每个类对象的创建需要依赖哪些其他类对象)事先创建好对象。当应⽤程序需要使⽤某个类对象的时候,直接从容器中获取即可。正是因为它持有⼀堆对象,所以这个框架才被称为“容器”。

DI 容器相对于我们上节课讲的⼯⼚模式的例⼦来说,它处理的是更⼤的对象创建⼯程。上节课讲的⼯⼚模式中,⼀个⼯⼚类只负责某个类对象或者某⼀组相关类对象(继承⾃同⼀抽象类或者接⼝的⼦类)的创建,⽽ DI 容器负责的是整个应⽤中所有类对象的创建。

除此之外,DI 容器负责的事情要⽐单纯的⼯⼚模式要多。⽐如,它还包括配置的解析、对象⽣命周期的管理。接下来,我们就详细讲讲,⼀个简单的 DI 容器应该包含哪些核⼼功能。

DI 容器的核⼼功能有哪些?

总结⼀下,⼀个简单的 DI 容器的核⼼功能⼀般有三个:配置解析、对象创建和对象⽣命周期管理。

构建者模式(Builder pattern)

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
public class ResourcePoolConfig {
private String name;
private int maxTotal;
private int maxIdle;
private int minIdle;
private ResourcePoolConfig(Builder builder) {
this.name = builder.name;
this.maxTotal = builder.maxTotal;
this.maxIdle = builder.maxIdle;
this.minIdle = builder.minIdle;
}
//...省略getter⽅法...
//我们将Builder类设计成了ResourcePoolConfig的内部类。
//我们也可以将Builder类设计成ᇿ⽴的⾮内部类ResourcePoolConfigBuilder。
public static class Builder {
private static final int DEFAULT_MAX_TOTAL = 8;
private static final int DEFAULT_MAX_IDLE = 8;
private static final int DEFAULT_MIN_IDLE = 0;
private String name;
private int maxTotal = DEFAULT_MAX_TOTAL;
private int maxIdle = DEFAULT_MAX_IDLE;
private int minIdle = DEFAULT_MIN_IDLE;
public ResourcePoolConfig build() {
// 校验逻辑放到这⾥来做,包括必填项校验、依赖关系校验、约束条件校验等
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("...");
}
if (maxIdle > maxTotal) {
throw new IllegalArgumentException("...");
}
if (minIdle > maxTotal || minIdle > maxIdle) {
throw new IllegalArgumentException("...");
}
return new ResourcePoolConfig(this);
}
public Builder setName(String name) {
if (StringUtils.isBlank(name)) {
throw new IllegalArgumentException("...");
}
this.name = name;
return this;
}
public Builder setMaxTotal(int maxTotal) {
if (maxTotal <= 0) {
throw new IllegalArgumentException("...");
}
this.maxTotal = maxTotal;
return this;
}
public Builder setMaxIdle(int maxIdle) {
if (maxIdle < 0) {
throw new IllegalArgumentException("...");
}
this.maxIdle = maxIdle;
return this;
}
public Builder setMinIdle(int minIdle) {
if (minIdle < 0) {
throw new IllegalArgumentException("...");
}
this.minIdle = minIdle;
return this;
}
}
}
// 这段代码会抛出IllegalArgumentException,因为minIdle>maxIdle
ResourcePoolConfig config = new ResourcePoolConfig.Builder()
.setName("dbconnectionpool")
.setMaxTotal(16)
.setMaxIdle(10)
.setMinIdle(12)
.build();

原型模式(Prototype Pattern)

什么是原型模式?

如果对象的创建成本⽐较⼤,⽽同⼀个类的不同对象之间差别不⼤(⼤部分字段都相同),在这种情况下,我们可以利⽤对已有对象(原型)进⾏复制(或者叫拷⻉)的⽅式,来创建新对象,以达到节省创建时间的⽬的。这种基于原型来创建对象的⽅式就叫作原型设计模式,简称原型模式。

原型模式的两种实现⽅法

原型模式有两种实现⽅法,深拷⻉和浅拷⻉。浅拷⻉只会复制对象中基本数据类型数据和引⽤对象的内存地址,不会递归地复制引⽤对象,以及引⽤对象的引⽤对象……⽽深拷⻉得到的是⼀份完完全全独⽴的对象。所以,深拷⻉⽐起浅拷⻉来说,更加耗时,更加耗内存空间。

如果要拷⻉的对象是不可变对象,浅拷⻉共享不可变对象是没问题的,但对于可变对象来说,浅拷⻉得到的对象和原始对象会共享部分数据,就有可能出现数据被修改的⻛险,也就变得复杂多了。除⾮像我们今天实战中举的那个例⼦,需要从数据库中加载 10 万条数据并构建散列表索引,操作⾮常耗时,这种情况下⽐较推荐使⽤浅拷⻉,否则,没有充分的理由,不要为了⼀点点的性能提升⽽使⽤浅拷⻉。