0%

设计模式之创建型模式

创建型模式关注点是如何创建对象,其核心思想是要把对象的创建和使用相分离,这样使得两者能相对独立地变换。

单例模式

一个类只允许创建一个对象,那这个类就是一个单例类,这种设计模式就叫作单例模式。这个概念理解起来比较简单,单例模式主要用来表示全局唯一类,例如配置信息类。在系统中,应该只有一个配置文件,当配置文件被加载进入内存之中以后,也应该只有一个配置类的对象,这种情况下就可以使用单例模式进行设计。

单例的Java实现

饿汉式

1
2
3
4
5
6
7
//java 11
public class Instance{
private static Instance instance=new Instance();
public static Instance getInstance(){
return instance;
}
}

饿汉式的实现方式比较简单,在类加载的时候就已经创建并初始化好了instance实例对象,不过这一实现方式的缺点是不能够实现延迟加载。

双重检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//java 11
public class Instance{
private volatile static Instance instance;
public static Instance getInstance(){
if (instance==null){
synchronized (Instance.class){
if (instance==null){
instance=new Instance();
}
}
}
return instance;
}
}

在这种实现方式中,在instance被创建之前,会进入synchronized锁逻辑之中,只要instance被创建之后,就再也不会进入到加锁逻辑之中了。同时因为指令重排序的问题,可能会导致Instance对象被new出来,并且赋值给instance之后,还没来得及初始化,就被使用了,需要给instance成员变量添加上volatile关键字,禁止指令重排序。

静态内部类

1
2
3
4
5
6
7
8
9
//java 11
public class Instance{
private static class InstanceHolder{
private static final Instance instance=new Instance();
}
public static Instance getInstance(){
return InstanceHolder.instance;
}
}

利用Java的静态内部类也可以实现单例模式,InstanceHolder是一个静态内部类,当外部类Instance被加载的时候不会创建InstanceHolder对象,只有当调用getInstance时才会被加载创建instance。这种方式既保证了线程安全又做到了延迟加载。

枚举

1
2
3
4
//java 11
public enum Instance{
INSTANCE
}

枚举是实现最简单的实现方法,通过枚举自身的特性,保证了线程安全性以及实例的唯一性。

工厂模式

工厂模式也是比较常见的一种设计模式,一般分为三种:简单工厂、工厂方法、抽象工厂。

简单工厂

下面这段代码模拟的是根据配置文件的不同的后缀,获取到不同的解析器,解析存储在文件中的配置信息,这用的就是简单工厂模式。

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
//kotlin 1.4.0
object ParserFactory {
//简单工厂
fun createParser(fileExtension: String) =
when (fileExtension.toLowerCase()) {
"xml" -> XmlParser()
"json" -> JsonParser()
"properties" -> PropertiesParser()
else -> throw IllegalArgumentException("未知的类型")
}
}

abstract class Parser {
abstract fun parse(path: String)
}

class XmlParser : Parser() {
override fun parse(path: String) = TODO("省略逻辑")
}

class JsonParser : Parser() {
override fun parse(path: String) = TODO("省略逻辑")
}

class PropertiesParser : Parser() {
override fun parse(path: String) = TODO("省略逻辑")
}
fun main() {
val parser = ParserFactory.createParser("xml")
parser.parse("test.xml")
}

工厂方法

如果为了更好地扩展性,可以使用工厂方法模式,代码如下:

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
//kotlin 1.4.0
interface IParseFactory {
companion object {
private val map = mapOf(
"xml" to XmlParserFactory(),
"json" to JsonParserFactory(),
"properties" to PropertiesParserFactory()
)
fun createParser(fileExtension: String) =
map[fileExtension.toLowerCase()]
?: throw IllegalArgumentException("未知的类型")
}
fun createParser(): Parser
}

class XmlParserFactory : IParseFactory {
override fun createParser() = XmlParser()
}

class JsonParserFactory : IParseFactory {
override fun createParser() = JsonParser()
}

class PropertiesParserFactory : IParseFactory {
override fun createParser() = PropertiesParser()
}

abstract class Parser {
abstract fun parse(path: String)
}

class XmlParser : Parser() {
override fun parse(path: String) = TODO("省略逻辑")
}

class JsonParser : Parser() {
override fun parse(path: String) = TODO("省略逻辑")
}

class PropertiesParser : Parser() {
override fun parse(path: String) = TODO("省略逻辑")
}

fun main() {
val parserFactory = IParseFactory.createParser("xml")
val parser = parserFactory.createParser()
parser.parse("test.xml")
}

工厂方法需要额外创造诸多的Factory类,会增加代码的复杂性。每个Factory类只是做简单的new操作,没有必要设计成独立的类,所以,在这个应用场景下,简单工厂模式简单好用,比工厂方法模式更加合适。但是如果当对象的创建逻辑比较复杂,需要很多操作时,则可以将复杂的创建逻辑添加到多个工厂类之中,让每个工厂类不过于复杂。而如果使用简单工厂模式,将所有的创建逻辑放到工厂类之中,会导致这个工厂类十分复杂。

抽象工厂

在简单工厂和工厂方法之中,类只有一个分类方式。但是如果有两种分类方式,比如不仅按照文件格式来分类(例如Json,Xml,Properties),还可以通过解析的对象来分类(例如规则配置,系统配置,业务配置等等),就会有3*3个类,会让系统难以维护。抽象工厂就是为了这一场景而诞生的。

抽象工厂可以让一个工厂负责创建多个不同类型的对象,而不是之创建一种对象,可以有效减少工厂类的个数,具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
interface IParseFactory {
companion object {
private val map = mapOf(
"xml" to XmlParserFactory(),
"json" to JsonParserFactory(),
"properties" to PropertiesParserFactory()
)
fun createParser(fileExtension: String) =
map[fileExtension.toLowerCase()]
?: throw IllegalArgumentException("未知的类型")
}
fun createParser1(): Parser
fun createParser2(): Parser
}

class XmlParserFactory : IParseFactory {
override fun createParser1() = XmlParser1()
override fun createParser2() = XmlParser2()
}
//...其他Factory代码

创建者模式

最常见创建一个对象的方法是调用类的构造函数来完成,但是在有些情况下这种方式就不适用了,就需要使用创建者模式来创建对象。例如对于一个线程池配置类,有以下几个成员变量

成员变量 解释 是否必填 默认值
name 名称
maxPoolSize 最大连接数 10
corePoolSize 线程池大小 5
keepAliveTime 存活时间 1min

如果使用构造函数,则代码如下所示

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
//java 11
public class ThreadPoolConfig {
public static void main(String[] args) {
var config = new ThreadPoolConfig("test", 10, null, null);
}

private String name;
private int maxPoolSize = 10;
private int corePoolSize = 5;
private int keepAliveTime = 1;

public ThreadPoolConfig(String name, Integer maxPoolSize, Integer corePoolSize, Integer keepAliveTime) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("名称不能为空");
}
this.name = name;
if (maxPoolSize != null) {
if (maxPoolSize <= 0) {
throw new IllegalArgumentException("maxPoolSize 必须为正数");
}
this.maxPoolSize = maxPoolSize;
}
if (corePoolSize != null) {
if (corePoolSize < 0) {
throw new IllegalArgumentException("corePoolSize 必须为正数");
}
this.corePoolSize = corePoolSize;
}
if (keepAliveTime != null) {
if (keepAliveTime < 0) {
throw new IllegalArgumentException("keepAliveTime 必须为正数");
}
this.keepAliveTime = keepAliveTime;
}
}
}

如果按照上面的代码,那么随着可配置项的增多,构造函数的列表会变得很长,容易搞错传递参数的顺序。为了解决这一个问题,就需要使用到建造者模式。在建造者模式中,先通过set方法设置建造者的变量值,所有逻辑检验的部分被放到了build方法之中。使用建造者模式重新实现的代码如下所示:

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
//java 11
public class ThreadPoolConfig {
public static void main(String[] args) {
var config = new ThreadPoolConfig.Builder()
.setName("test")
.setKeepAliveTime(100).build();
}

private String name;
private int maxPoolSize;
private int corePoolSize;
private int keepAliveTime;

private ThreadPoolConfig(Builder builder) {
this.name = builder.name;
this.maxPoolSize = builder.maxPoolSize;
this.corePoolSize = builder.corePoolSize;
this.keepAliveTime = builder.keepAliveTime;
}

public static class Builder {
private String name;
private int maxPoolSize = 10;
private int corePoolSize = 5;
private int keepAliveTime = 1;

public ThreadPoolConfig build() {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("名称不能为空");
}
if (maxPoolSize <= 0) {
throw new IllegalArgumentException("maxPoolSize 必须为正数");
}
if (corePoolSize < 0) {
throw new IllegalArgumentException("corePoolSize 必须为正数");
}
if (keepAliveTime < 0) {
throw new IllegalArgumentException("keepAliveTime 必须为正数");
}
return new ThreadPoolConfig(this);
}


public Builder setName(String name) {
this.name = name;
return this;
}

public Builder setMaxPoolSize(int maxPoolSize) {
this.maxPoolSize = maxPoolSize;
return this;
}

public Builder setCorePoolSize(int corePoolSize) {
this.corePoolSize = corePoolSize;
return this;
}

public Builder setKeepAliveTime(int keepAliveTime) {
this.keepAliveTime = keepAliveTime;
return this;
}
}
}

原型模式

原型模式指的是在创建新对象的时候,通过现有的一个原型来创建。原型模式的使用如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
//kotlin 1.4.0
data class User(
val id: Int,
val name: String,
val permission: MutableList<String>
)

fun main() {
val permissionList = arrayListOf("user")
val user1 = User(1, "test", permissionList)
val user2 = user1.copy()
}

深拷贝与浅拷贝

原型模式有两种实现方法,深拷贝和浅拷贝。浅拷贝和深拷贝的区别在于,浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址。而深拷贝不仅仅会递归的赋值本身,得到完全独立的对象。kotlin的copy函数在进行拷贝的时候默认使用的是浅拷贝,在上面的代码运行完成之后,向user1中permission的列表添加admin属性,可以看到在user2的permission也出现了admin。

1
2
3
4
5
6
7
//kotlin 1.4.0
//...
fun main() {
//...
user1.permission.add("admin")
println(user2.permission)//[user, admin]
}

为了解决这个问题,可以在copy调用的时候将一个新的permission列表传递给user2,这样就可以做到深拷贝。

1
2
3
4
5
6
7
//kotlin 1.4.0
fun main() {
//...
val user2 = user1.copy(permission = user1.permission.toMutableList())
user1.permission.add("admin")
println(user2.permission)//[user]
}

或者也可以通过先序列化后反序列的方式进行深拷贝。例如在javascript中先通过JSON.stringify来进行序列化,后通过JSON.parse来反序列化进行深拷贝。

1
2
3
4
5
6
7
//javascript
var user1 = {
id:1,
name:"test",
permission:["admin"]
}
var user2 = JSON.parse(JSON.stringify(user1))