理解设计模式之单例模式、享元模式

单例模式和享元模式是两个看上去毫无关系的模式,而且一个属于创建型模式,一个属于结构型模式。
但是,当把享元模式限制在只有一个元素且没有状态时,两个模式就变的扑朔迷离,安能辨我是雌雄了!
当然事情不是表面上这么的简单,不能就这么随意的把单例模式理解为享元模式的特例了。
下面我们就来聊聊这两个设计模式。

单例

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式屏蔽了大量的new()操作,解耦了与其它大部分类的直接引用,更重要的是节省系统资源。
单例模式的难点完全不在与其思想,而在于技术实现,这甚至涉及到语言层面。

单例的那些技术实现,比如饿汉式,懒汉式,线程安全,Synchronized,DCL(双重检锁),volatile,静态内部类实现,枚举实现等等,网上文章很多,在此不做赘述。
关于双重检查锁定,可以参考这篇文章:双重检查锁定与延迟初始化

这里提供懒汉式的一个基本写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class Singleton {
// 私有内部实例,volatile结合DCL保证单例
private volatile static Singleton instance;
// 私有构造函数,防止外部实例化
private Singleton() {
}
// 对外提供一个访问点得到单例的私有内部实例
public static Singleton getInstance() {
if (instance == null) {
// 同步保护new操作,结合volatile保证单例
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

单例模式的UML示意图如下:
单例模式UML示意图

状态化单例

当你改变关注点时事情就会发生变化。
如果把侧重点从单例上转移到状态上,比如,不同状态返回对应的不同单例,会如何?
同时,单例也是一个特殊的工厂,一个只生产自己且只生产一份的工厂。
想到这两点,我不禁想扩展一下用工厂实现这个状态化的单例(也许这里再叫单例已经不合适了),看看是什么样子。

先把Singleton中添加状态属性state;
有了状态就要管理对应状态的实例,需要一个Map容器存储;
因为已经不关注单例的保障,清除相关的代码。

最后得出下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class SingletonFactory {
private static Map<String, SingletonFactory> instances
= new HashMap<>();
private String state;
public SingletonFactory(String state) {
this.state = state;
}
public static SingletonFactory getInstance(String state) {
if (instances.get(state) == null) {
instances.put(state, new SingletonFactory(state));
}
return instances.get(state);
}
}

享元模式

运用共享技术有效地支持大量细粒度的对象。

享元模式的核心思想在于大量细颗粒对象的重用,以节省内存资源,实现思路类似于上面的状态化单例,但是稍微要复杂一点。
系统中什么样的对象属于大量细颗粒对象?
答案是,大量相同或者相似的那些对象。比如:表情包,一些素材,字符等等。
最终效果,就是实现一个对象池。

我们先定义可重用的对象结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
public interface IFlyweight {
}
public class Flyweight implements IFlyweight {
private String intrinsicState1;
private String intrinsicState2;
public Flyweight(String intrinsicState1, String intrinsicState2) {
this.intrinsicState1 = intrinsicState1;
this.intrinsicState2 = intrinsicState2;
}
}

接下来是实现Flyweight工厂类,集中管理和缓存这些可重用的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class FlyweightFactory {
private Map<String, IFlyweight> flyweightMap = new HashMap<>();
// key应该根据内部状态生成
public IFlyweight get(String intrinsicState1, String intrinsicState2) {
String key = intrinsicState1 + "-" + intrinsicState2;
if (flyweightMap.get(key) == null) {
IFlyweight flyweight = new Flyweight(intrinsicState1, intrinsicState2);
flyweightMap.put(key, flyweight);
}
return flyweightMap.get(key);
}
}

现在根据intrinsicState1,intrinsicState2两个参数我们就能决定是否重用已有Flyweight对象:如果相同则重用,否则创建新对象。
当系统中存在大量相同的intrinsicState1,intrinsicState2属性对象,这种重用必将大大减少重复对象的创建。

分离内部和外部状态

虽然上面看似已经达到了享元模式的意图,但是享元有一个容易被人忽略的关键:内部和外部状态的分离。
这很重要吗?非常重要。
因为内部和外部状态的分离包含了一个重要思想:应变,其本质就是在抽象不变和变化,而这正是软件架构的核心。
OK,回归代码,把外部状态注入到IFlyweight:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public interface IFlyweight {
void operation(String extrinsicState);
}
public class Flyweight implements IFlyweight {
private String intrinsicState1; // 内部状态,可供全局共享
private String intrinsicState2; // 内部状态,可供全局共享
private String extrinsicState; // 外部状态,由外部负责
public Flyweight(String intrinsicState1, String intrinsicState2) {
this.intrinsicState1 = intrinsicState1;
this.intrinsicState2 = intrinsicState2;
}
@Override
public void operation(String extrinsicState) {
this.extrinsicState = extrinsicState;
}
}

extrinsicState用于存储不被共享的那些信息,把这些信息剥离出来才能使Flyweight共享最大化。
extrinsicState可以是任何类型,取决于非共享的信息量。
最终,享元模式的UML示意图如下:
享元模式UML示意图

思考:享元模式 = 单例模式 + 工厂模式?

小结

单例和享元模式都一定程度避免了对象的膨胀,大大节省了系统资源,甚至如果没有它们这个思想有的场景都无法进行。
通过两个模式的对比,它们是不属于同一颗粒级别的解决方案,导致应用场景也大不同,这也是学习设计模式要考虑的地方。
单例和享元模式都是使用门槛低(不需要特别大的场景)但却非常有用的模式。