创建型设计模式(Creational Patterns)

这类模式主要关注对象的创建过程

✨单例模式

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

为什么要使用单例模式?

  1. 表示全局唯一

    有些数据在系统中应该有且仅有只能保存一份,那就应该设计为单例类,如:配置类,全局计数器。

  2. 处理资源访问冲突

    日志输出功能就是用单例模式实现的

如何实现一个单例?

在编写单例代码的时候需要注意以下4点:

  1. 构造器需要私有化
  2. 暴露一个公共的获取单例对象的接口
  3. 是否支持懒加载(延迟加载)
  4. 是否线程安全

常见的单例设计模式,有以下5种写法:

饿汉式

饿汉式的实现方法较为简单,在类加载的时候,instance静态实例就已经创建并初始化好了,所以,instance实例的创建过程是线程安全的。

1
2
3
4
5
6
7
8
9
10
public class EagerSingleton{
//持有一个jvm全局唯一的实例
private final static EagerSingleton INSTANCE = new EagerSingleton();
//为了避免被随意的创建,需要私有化构造器
private EagerSingleton(){}
//对外需要暴露一个方法,用来获取实例
public static EagerSingleton getInstance(){
return INSTANCE;
}
}

懒汉式

1
2
3
4
5
6
7
8
9
10
public class LazySingleton{
private static LazySingleton instance;
private LazySingleton(){}
public static LazySingleton getInstance(){
if(null == instance){
instance = new LazySingleton();
}
return instance;
}
}

懒汉式相对于饿汉式的优势是支持延迟加载,但是上面这个写法,线程不安全,很有可能会有超过一个线程同时执行了new LazySingleton(),由此引出双重检查锁

双重检查锁

第一次创建需要上锁,一旦创建好了,就不需要再上锁,这种写法既可以满足延迟加载,也可以满足线程安全,推荐使用这种单例设计模式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class DclSingleton{
//volatile如果不加可能会出现半初始化的对象,对于高版本的 Java 已经在 JDK 内部实现中解决了这个问题,但为了兼容性,还是选择加上volatile
private volatile static DclSingleton instance;
private DclSingleton(){}
public static DclSingleton getInstance(){
//第一次判断,实例没创建,很多线程进入synchronized代码块
if(null == instance){
//上锁,让线程排队进入代码块执行代码
synchronized(DclSingleton.class){
//第二次判断,假如没这个判断,线程会排队依次创建实例
if(null == instance){
instance = new DclSingleton();
}
}
}
return instance;
}
}

静态内部类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class InnerSingleton{
//私有化构造器
private InnerSingleton(){}
//对外提供公共的访问方法
public static InnerSingleton getInstance(){
return InnerSingletonHolder.INSTANCE;
}
//定义内部类来持有实例,内部类加载的时机就是外部调用了getInstance(),然后实例就会在内部类加载的时候初始化。
private static class InnerSingletonHolder{
//jvm保证类创建的时候,线程是安全的,并且类的静态属性只会在第一次加载类的时候初始化
private final static InnerSingleton INSTANCE = new InnerSingleton();
}
}

InnerSingletonHolder是一个静态内部类,当外部类 InnerSingleton被加载的时候,并不会创建 InnerSingletonHolder实例对象。只有当调用 getInstance() 方法时,InnerSingletonHolder才会被加载,这个时候才会创建 instance。insance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。所以,这种实现方法既保证了线程安全,又能做到延迟加载。

枚举

基于枚举类型的单例是一种最简单的实现方式,这种实现方式通过 Java 枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性,还能防止反序列化重新创建新的对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Singleton{
private Singleton(){}

public static enum EnumSingleton{
INSTANCE;
private Singleton singleton = null;
private EnumSingleton(){
singleton = new Singleton();
}
public Singleton getSingleton(){
return singleton;
}
}
}

反射入侵

事实上,我们想要阻止其他人构造实例仅仅私有化构造器还是不够的,因为我们还可以使用反射获取私有构造器进行构造,当然使用枚举的方式是可以解决这个问题的,对于其他的书写方案,可以在类的构造方法中添加个if判断

1
2
3
4
5
6
7
//通过反射创建的实例并没有经过同步处理,相当于绕过了双重检查锁的机制
//为了避免反射创建实例破坏单例模式,可以在构造函数中增加一个判断逻辑,如果已经存在实例,则抛出异常或者返回已有实例。
if(instance != null){
throw new RuntimeException("该对象是单里的,无法创建多个");
//或者
return instance;
}

序列化与反序列化安全

到目前为止,其实还没实现真正意义上的单例,看下面例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Test
public void testSerialize() throws IllegalAccessException,NoSuchMethodException,IOException,ClassNotFoundException {
// 获取单例并序列化
Singleton singleton = Singleton.getInstance();
FileOutputStream fout = new FileOutputStream("D://singleton.txt");
ObjectOutputStream out = new ObjectOutputStream(fout);
out.writeObject(singleton);
// 将实例反序列化出来
FileInputStream fin = new FileInputStream("D://singleton.txt");
ObjectInputStream in = new ObjectInputStream(fin);
Object o = in.readObject();
System.out.println("他们是同一个实例吗?{}",o == singleton)//结果是false
}

解决方法:readResolve()方法可以用于替换从流中读取的对象,在进行反序列化时,会尝试执行readResolve方法,并将返回值作为反序列化的结果,而不会克隆一个新的实例,保证jvm中仅仅有一个实例存在,需要在类中添加这么一个方法:

1
2
3
4
// 直接返回单例
public Object readResolve(){
return singleton;
}

✨工厂方法模式

简单工厂

简单工厂模式(也称为静态工厂模式)是最简单的工厂模式,它由一个工厂类负责创建其他类的对象。调用者只需传递给工厂类一个参数,工厂类便可根据这个参数动态实例化一个对象。这种模式可以有效地隐藏类之间的关系,提高代码的可维护性和可扩展性。

在看工厂方法模式之前,先来看下简单工厂模式,现在有一个场景,我们需要一个资源加载器,他要根据不用的url进行资源加载

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
//工厂类
public class ResourceFactory {
public static Resource create(String type,String url){
if("http".equals(type)){
// ..发起请求下载资源... 可能很复杂
return new Resource(url);
} else if ("file".equals(type)) {
// ..建立流,做异常处理等等
return new Resource(url);
} else if ("classpath".equals(type)) {
// ...
return new Resource(url);
} else {
return new Resource("default");
}
}
}

//资源加载器
public class ResourceLoader {
public Resource load(String url){
// 1、根据url获取前缀
String type = getPrefix(url);
// 2、根据前缀处理不同的资源
return ResourceFactory.create(type,url);
}

private String getPrefix(String url) {
if(url == null || "".equals(url) || !url.contains(":")){
throw new ResourceLoadException("此资源url不合法.");
}
String[] split = url.split(":");
return split[0];
}
}

将创建对象的过程交给工厂类、其他业务需要某个产品时,直接使用create(方法名字不重要)创建即可这样的好处是:

  1. 工厂将创建的过程进行封装,不需要关系创建的细节,更加符合面向对象思想
  2. 这样主要的业务逻辑不会被创建对象的代码干扰,代码更易阅读
  3. 产品的创建可以独立测试,更将容易测试
  4. 独立的工厂类只负责创建产品,更加符合单一原则

工厂方法

工厂方法模式(也称为多态性工厂模式)更具灵活性和可扩展性。工厂方法模式将对象的创建过程分散到各个子类中,每个子类只负责创建特定的对象,从而实现了开放封闭原则。此外,由于将实例化逻辑分散到子类中,工厂方法模式更加符合面向对象设计的原则,可以更好地支持单元测试和扩展。

接上面例子,如果有一天,我们的if分支逻辑不断膨胀,有变为肿瘤代码的可能,就有必要将 if 分支逻辑去掉,那又该怎么办呢?比较经典的处理方法就是利用多态。按照多态的实现思路,对上面的代码进行重构。我们会为每一个Resource 创建一个独立的工厂类,形成一个个小作坊,将每一个实例的创建过程交给工厂类完成,重构之后的代码如下所示:

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
//将生产资源的工厂类进行抽象
public interface IResourceLoader{
Resource load(String url);
}

//并为每一种资源创建与之相匹配的实现:
public class HttpResourceLoader implements IResourceLoader {
@Override
public Resource load(String url) {
// 中间省略复杂的创建过程
return new Resource(url);
}
}

public class FileResourceLoader implements IResourceLoader {
@Override
public Resource load(String url) {
// 中间省略复杂的创建过程
return new Resource(url);
}
}

public class ClassPathResourceLoader implements IResourceLoader {
@Override
public Resource load(String url) {
// 中间省略复杂的创建过程
return new Resource(url);
}
}

public class DefaultResourceLoader implements IResourceLoader {
@Override
public Resource load(String url) {
// 中间省略复杂的创建过程
return new Resource(url);
}
}

//在ResourceLoader中创建一个工厂的缓存来统一管理工厂的实例,去掉if,使代码看起来更简洁。去掉ResourceFactory,重构一下ResourceLoader
public class ResourceLoader {
private static Map<String,IResourceLoader> resourceLoaderCache = new HashMap<>(8);
//静态代码块来缓存
static {
resourceLoaderCache.put("http",new HttpResourceLoader());
resourceLoaderCache.put("file",new FileResourceLoader());
resourceLoaderCache.put("classpath",new ClassPathResourceLoader());
resourceLoaderCache.put("default",new DefaultResourceLoader());
}
public Resource load(String url){
String prefix = getPrefix(url);
return resourceLoaderCache.get(prefix).load(url);
}
private String getPrefix(String url) {
if(url == null || "".equals(url) || !url.contains(":")){
throw new ResourceLoadException("此资源url不合法.");
}
String[] split = url.split(":");
return split[0];
}
}

这样子修改完,代码看起来比第一个版本舒服多了,但是以后如果要新增一种产品,那是不是就要在static代码块里修改代码了,这又不符合开闭原则了,为此,可以搞一个配置文件,将我们的工厂类进行配置,如下:

1
2
3
4
5
//新建一个配置文件,叫resourceLoader.properties吧
http=HttpResourceLoader的路径
file=FileResourceLoader的路径
classpath=ClassPathResourceLoader的路径
default=DefaultResourceLoader的路径

修改静态代码块的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
static {
InputStream inputStream = Thread.currentThread().getContextClassLoader()
.getResourceAsStream("resourceLoader.properties");
Properties properties = new Properties();
try {
properties.load(inputStream);
for (Map.Entry<Object,Object> entry : properties.entrySet()){
String key = entry.getKey().toString();
Class<?> clazz = Class.forName(entry.getValue().toString());
IResourceLoader loader = (IResourceLoader)
clazz.getConstructor().newInstance();
resourceLoaderCache.put(key,loader);
}
} catch (IOException | ClassNotFoundException | NoSuchMethodException |
InstantiationException |
IllegalAccessException | InvocationTargetException e) {
throw new RuntimeException(e);
}
}

以后我们想新增或删除一个resourceLoader只需要写一个类实现IResourceLoader接口,并在配置文件中进行配置即可。此时此刻我们已经看不到if-else的影子了且完全满足开闭原则。

总结

  1. 简单工厂模式适用于创建数量较少的对象,工厂方法模式适用于创建更多的对象
  2. 工厂方法模式比起简单工厂模式更加符合开闭原则