设计模式七大原则

  • 单一职责原则:一个类只负责一个功能,降低耦合性,方便迭代维护。
  • 开放封闭原则:类、方法可以进行功能扩展,但不能修改。对扩展开放,对修改封闭。
  • 依赖倒置原则:高级模块不能依赖低级模块,都应该依赖于接口。先将类进行抽象,先设计功能接口,再对接口进行功能细节的实现。
  • 接口隔离原则:不同接口定义不同的功能。
  • 里氏代换原则:子类可替换其父类。
  • 迪米特原则:每个模块间要尽可能少的相互调用,减少依赖,降低耦合性。

设计模式讲解链接:

http://www.cyc2018.xyz/%E5%85%B6%E5%AE%83/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F/%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F%20-%20%E7%9B%AE%E5%BD%95.html#%E4%B8%80%E3%80%81%E5%89%8D%E8%A8%80

单例模式实现

懒汉式(线程不安全)

1
2
3
4
5
6
7
8
9
10
11
12
public class Lazy {
private static Lazy lazyInstance;

private Lazy(){}

public static Lazy getLazyInstance(){
if(lazyInstance == null){
lazyInstance = new Lazy();
}
return lazyInstance;
}
}

懒汉式是指先不创建实例,当首次调用后再创建。

私有化构造方法,不能使用 new 进行实例化,使用方法判断当前实例是否已存在,不存在则创建,保证实例的单一性。

线程不安全,在多线程情况下,可能首次会有多个线程一起调用 getLazyInstance方法 ,那么此时实例为空,会同时创建多个实例。

饿汉式(线程安全)

1
2
3
4
5
6
7
8
9
public class Hungry {
private static Hungry hungryInstance = new Hungry();

private Hungry(){}

public static Hungry getHungryInstance(){
return hungryInstance;
}
}

饿汉式指无论是否要使用实例,先进行一次实例化,随后需要实例时直接调用方法返回即可;

线程安全,因为初始化类时已经进行了实例化。但若长时间不使用该实例会造成资源浪费。

懒汉式(线程安全)

1
2
3
4
5
6
7
8
9
10
11
12
public class LazyConcurrent {
private static LazyConcurrent lazyInstance;

private LazyConcurrent(){}

public static synchronized LazyConcurrent getLazyInstance(){
if(lazyInstance == null){
lazyInstance = new LazyConcurrent();
}
return lazyInstance;
}
}

在实例化执行方法上加锁保证线程安全。

线程安全,但效率低,多线程争夺锁会造成线程阻塞。

双重检查锁实现(线程安全)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class DoubleCheck {
private static volatile DoubleCheck doubleCheckInstance;

private DoubleCheck(){}

public static DoubleCheck getInstance(){
if(doubleCheckInstance == null){
synchronized (DoubleCheck.class){
if(doubleCheckInstance == null){
doubleCheckInstance = new DoubleCheck();
}
}
}
return doubleCheckInstance;
}
}

双重检查锁相当于是线程安全懒汉式的优化版。

首先我们不是在执行方法上加锁,而是在方法内部加锁。先进行一次实例判断,如果为空则获得锁然后再进行一次判断,因为第一次判断可能有多个线程同时通过,而获取锁后需要第二次判断再实例对象,以防同时通过第一次判断线程进行多次实例化。

实例变量使用volatile修饰。我们正常代码实例化执行步骤如下:

1
2
3
4
doubleCheckInstance = new DoubleCheck();
1、为对象分配内存空间
2、初始化对象
3、将对象指向分配好的内存空间

由于JVM的指令重排机制,指令执行顺序可能会由原来的123变为132。而在多线程的影响下,会导致线程得到一个尚未初始化的实例。为了解决该问题可以在声明实例变量时加上 volatile,禁止JVM进行指令重排,保证多线程的安全。

静态内部类实现(线程安全)

1
2
3
4
5
6
7
8
9
10
11
public class Single {
private Single(){}

private static class SingleInner{
private static final Single instance = new Single();
}

public static Single getInstance(){
return SingleInner.instance;
}
}

当外部类Single加载时,其静态内部类SingleInner并未加载,当调用实例获取方法走到返回值时,才会去加载静态内部类,并进行实例化。

使用静态内部类可以延迟实例化,节省资源,并保证线程安全。

枚举实现(线程安全)

1
2
3
4
5
6
public enum Unique {
INTANCE;

public void doSomething(){
}
}

枚举默认即线程安全 + 单例,还可防范一系列反射破解单例的操作。

工厂模式(简单工厂、工厂、抽象工厂)

简单工厂

一个产品接口,有多种具体实现,我们创建一个工厂类,声明一个实例获取的方法,根据参数和if - else判断生成哪一个具体的产品实例。然后可以使用该实例。

工厂类使用if - else判断,扩展性差。

1.jpg

工厂模式

将工厂抽象化,通过工厂的具体子类创建具体产品实例,工厂抽象类中将实例产品的方法抽象化,交由具体子类实现,而其他的功能方法则可以直接写。

这样我们有新产品加入时,只需要创建对应的子工厂继承抽象工厂,然后使用子工厂进行操作即可,不用修改其他类。

但如果我们产品种类很多时,不可能为每一个产品都创建一个子工厂,这会大大增加代码的复杂度。

2.jpg

抽象工厂

为了缩减工厂子类的数量,我们不必为每一个产品分配一个工厂类,可将产品分组,比如有产品键盘和鼠标,然后我们将工厂分类,工厂接口声明可创建很多产品(键盘、鼠标等),然后创建具体品牌工厂实现接口,声明各品牌的键盘鼠标。比如戴尔工厂有戴尔的键盘和鼠标,联想工厂有联想的键盘和鼠标。此时需要哪个品牌的哪种产品直接去对应品牌工厂获取即可。

但是抽象工厂每次新增一个产品需要进行很多扩展,对产品和工厂都要新增。

3.jpg

总结

  • 简单工厂:单一工厂类,单一产品接口,创建工厂根据方法的入参判断需要创建的具体产品实例。
  • 工厂:多个工厂类,单一产品接口,通过子工厂实现多态,完成不同产品实例的创建,避免入参判断。
  • 抽象工厂:多个工厂类,多个产品抽象类,产品会根据子类分组,同一个产品工厂实现同种组件,减少了工厂子类的数量。