0%

设计模式学习记录

记录在学习设计模式过程中的点滴

1 单例模式

  • 优点:

    • 单例模式可以保证内存中只有一个实例对象,从而会减少内存的开销;
    • 单例模式可以避免对资源的多重占用;
    • 单例模式设置全局访问点,可以起到优化和共享资源的访问的作用;
  • 缺点:

    • 扩展难, 因为单例模式通常是没有接口的啊,如果想要扩展,那么你唯一途径就是修改之前的代码,所以说单例模式违背了开闭原则;
    • 调试难,因为在并发测试中,单例模式是不利于代码的调试的,单例中的代码没有执行完,也不能模拟生成一个新对象;
    • 违背单一职责原则,因为单例模式的业务代码通常写在一个类中,如果功能设计不合理,就很容易违背单一职责原则;

1.1 饿汉式标准写法

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
/**
* 单例模式的通用写法
*/
public class Singleton {
/**
* 内部初始化一次
*/
private static final Singleton instance = new Singleton();

/**
* 隐藏构造方法
*/
private Singleton() {
}

/**
* 提供一个全局访问点
*
* @return Singleton
*/
public static Singleton getInstance() {
return instance;
}

}

饿汉式单例写法在类的初始化的时候就会进行初始化操作,并且创建对象,绝对的线程安全,因为此时线程还没有出现就已经实例化了,故不会存在访问安全的问题。

1.2 饿汉式静态块机制写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class HungryStaticSingleton {

private static final HungryStaticSingleton hungrySingleton;
//静态代码块 类加载的时候就初始化
static {
hungrySingleton=new HungryStaticSingleton();
}
/**
* 私有化构造函数
*/
private HungryStaticSingleton(){}

/**
* 提供一个全局访问点
* @return
*/
public static HungryStaticSingleton getInstance() {
return hungrySingleton;
}
}

这种写法可以明显的看到所以对象是类在加载的时候就进行实例化了,那么这样一来,会导致单例对象的数量不确定,从而会导致系统初始化的时候就造成大量内存浪费。

1.3 懒汉式单例写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class LazySimpleSingleton {

private static LazySimpleSingleton lazySingleton = null;

/**
* 私有化构造函数
*/
private LazySimpleSingleton() {

}
/**
* 提供一个全局访问点
*
* @return
*/
public static LazySimpleSingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySimpleSingleton();
}
return lazySingleton;
}
}

这样实现的好处就是只有对象被使用的时候才会进行初始化,不会存在内存浪费的问题,但是它会在多线程环境下,存在线程安全问题。

1.4 synchronized修饰的懒汉式单例实现

我们可以利用synchronized关键字将全局访问点方法变成一个同步方法,这样就可以解决线程安全的问题,代码如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySingleton = null;
/**
* 私有化构造函数
*/
private LazySimpleSingleton() {}
/**
* 提供一个全局访问点
*
* @return
*/
public synchronized static LazySimpleSingleton getInstance() {
if (lazySingleton == null) {
lazySingleton = new LazySimpleSingleton();
}
return lazySingleton;
}
}

但是,这样虽然解决了线程安全的问题,可是如果在线程数量剧增的情况下,用synchronized加锁,则会导致大批线程阻塞,从而骤减系统性能。

1.5 懒汉式-双重检测单例实现

在上述代码上进一步优化,代码如下所示:

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
public class LazyDoubleCheckSingleton {
// volatile 关键字修饰
private volatile static LazyDoubleCheckSingleton lazySingleton ;
/**
* 私有化构造函数
*/
private LazyDoubleCheckSingleton() {}
/**
* 提供一个全局访问点
*
* @return
*/
public static LazyDoubleCheckSingleton getInstance() {
// 这里先判断一下是否阻塞
if (lazySingleton == null) {
synchronized (LazyDoubleCheckSingleton.class){
// 判断是否需要重新创建实例
if (lazySingleton == null) {
lazySingleton = new LazyDoubleCheckSingleton();
}
}
}
return lazySingleton;
}
}

当第一个线程调用getInstance()方法时,第二个线程也可以调用,但是第一个线程执行synchronized时候,第二个线程就会发现阻塞,但是此时的阻塞是getInstance()内部的阻塞。

1.6 静态内部类单例实现

虽然双重检测锁的单例模式解决了线程安全和性能问题,但是毕竟涉及加锁的操作,多多少少就会到了性能的影响,下面我们分享一下更加优雅的单例模式实现,如下代码所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class LazyStaticInnerClassSingleton {
// 在构造方法里面抛出异常真的合适?
private LazyStaticInnerClassSingleton(){
if(LazyHolder.INSTANCE != null){
throw new RuntimeException("不允许创建多个实例");
}
}
// static 保证这个方法不会被重写 覆盖
private static LazyStaticInnerClassSingleton getInstance(){
return LazyHolder.INSTANCE;
}
// Java 默认不会加载内部类
private static class LazyHolder{
private static final LazyStaticInnerClassSingleton INSTANCE=new LazyStaticInnerClassSingleton();
}
}

2 创建者模式

2.1 工厂方法模式

工厂模式又称工厂方法模式,是一种创建者模式,主要是在父类中提供一个创建对象的方法,允许子类决定实例化对象的类型。

这种设计模式也是 Java 开发中最常见的一种模式,它的主要意图是定义一个创建对象的接口,让其子类自己决定实例化哪一个工厂类,工厂模式使其创建过程延迟到子类进行。

简单来说,就是为了提供代码结构的扩展性,屏蔽每一个功能类中的具体实现逻辑。让外部可以更简单的只是知道调用即可,同时这也是去掉众多 ifelse 的方式。当然这也可能有一些缺点,比如需要实现的类非常多,如何去维护,怎样去降低成本。但这些问题都可以在后续的设计模式结合使用中,逐步降低。

https://raw.githubusercontent.com/littlefxc/littlefxc.github.io/images/images/工厂模式类图.png

2.2 抽象工厂方法模式

https://raw.githubusercontent.com/littlefxc/littlefxc.github.io/images/images/抽象工厂模式.png

2.3 建造者模式

https://raw.githubusercontent.com/littlefxc/littlefxc.github.io/images/images/建造者模式.png

2.4 原型模式

原型模式主要解决的是创建重复对象,而这部分对象内容本身比较复杂,生成过程可能从库或者RPC接口中获取数据的耗时较长,因此采用克隆的方式节省时间。

案例场景:在线生成试卷,考题从题库中抽选随机成立一份试卷,考题类型有选择题、问答题

https://raw.githubusercontent.com/littlefxc/littlefxc.github.io/images/images/原型模式-0.png

3 结构型模式

3.1 适配器模式

本示例来自于https://www.runoob.com/design-pattern/adapter-pattern.html

适配器模式(Adapter Pattern) :将一个接口转换成客户希望的另一个接口,适配器模式使接口不兼容的那些类可以一起工作,其别名为包装器(Wrapper)。适配器模式既可以作为类结构型模式,也可以作为对象结构型模式。

适配器模式包含如下角色:

  • Target:目标抽象类
  • Adapter:适配器类
  • Adaptee:适配者类
  • Client:客户类

案例场景:音频播放器设备只能播放 mp3 文件,通过使用一个更高级的音频播放器来播放 vlc 和 mp4 文件。

实现思路:我们想要让 AudioPlayer 播放其他格式的音频文件。为了实现这个功能,我们需要创建一个实现了 MediaPlayer 接口的适配器类 MediaAdapter,并使用 AdvancedMediaPlayer 对象来播放所需的格式。

类图如下:

适配器模式

3.2 桥接模式

桥接模式

参考资源