创建型模式关注点是如何创建对象,其核心思想是要把对象的创建和使用相分离,这样使得两者能相对独立地变换。
单例模式
一个类只允许创建一个对象,那这个类就是一个单例类,这种设计模式就叫作单例模式。这个概念理解起来比较简单,单例模式主要用来表示全局唯一类,例如配置信息类。在系统中,应该只有一个配置文件,当配置文件被加载进入内存之中以后,也应该只有一个配置类的对象,这种情况下就可以使用单例模式进行设计。
单例的Java实现
饿汉式
1 | //java 11 |
饿汉式的实现方式比较简单,在类加载的时候就已经创建并初始化好了instance实例对象,不过这一实现方式的缺点是不能够实现延迟加载。
双重检查
1 | //java 11 |
在这种实现方式中,在instance被创建之前,会进入synchronized锁逻辑之中,只要instance被创建之后,就再也不会进入到加锁逻辑之中了。同时因为指令重排序的问题,可能会导致Instance对象被new出来,并且赋值给instance之后,还没来得及初始化,就被使用了,需要给instance成员变量添加上volatile关键字,禁止指令重排序。
静态内部类
1 | //java 11 |
利用Java的静态内部类也可以实现单例模式,InstanceHolder是一个静态内部类,当外部类Instance被加载的时候不会创建InstanceHolder对象,只有当调用getInstance时才会被加载创建instance。这种方式既保证了线程安全又做到了延迟加载。
枚举
1 | //java 11 |
枚举是实现最简单的实现方法,通过枚举自身的特性,保证了线程安全性以及实例的唯一性。 ## 工厂模式
工厂模式也是比较常见的一种设计模式,一般分为三种:简单工厂、工厂方法、抽象工厂。
简单工厂
下面这段代码模拟的是根据配置文件的不同的后缀,获取到不同的解析器,解析存储在文件中的配置信息,这用的就是简单工厂模式。
1 | //kotlin 1.4.0 |
工厂方法
如果为了更好地扩展性,可以使用工厂方法模式,代码如下:
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
20interface 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 | //java 11 |
原型模式
原型模式指的是在创建新对象的时候,通过现有的一个原型来创建。原型模式的使用如下所示。
1 | //kotlin 1.4.0 |
深拷贝与浅拷贝
原型模式有两种实现方法,深拷贝和浅拷贝。浅拷贝和深拷贝的区别在于,浅拷贝只会复制对象中基本数据类型数据和引用对象的内存地址。而深拷贝不仅仅会递归的赋值本身,得到完全独立的对象。kotlin的copy函数在进行拷贝的时候默认使用的是浅拷贝,在上面的代码运行完成之后,向user1中permission的列表添加admin属性,可以看到在user2的permission也出现了admin。
1 | //kotlin 1.4.0 |
为了解决这个问题,可以在copy调用的时候将一个新的permission列表传递给user2,这样就可以做到深拷贝。
1 | //kotlin 1.4.0 |
或者也可以通过先序列化后反序列的方式进行深拷贝。例如在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))