享元模式,一种使用概率比较低的设计模式,享元表述的是共享元数据的意思,使用的初衷是为了节约内存空间,并在一定程度上可以提高系统性能。
我们来举个电商中的场景,某个大学在为学生宿舍安装购置了5000台空调。在代码中实现,需要在内存中创建5000个空调对象,对象中包含空调的内机信息、外机信息、用户的信息、说明书信息、厂商的信息等等,属于一个大对象。而对象的实例化是个很复杂、消耗资源的操作,所以通过new
来操作必然是很消耗性能的。这个时候可以借助原型模式来实现,可以在很大程度上减少实例化的次数,确实可以提效,但是依然在内存中需要保留这5000个大对象,仍然无法解决资源占用的问题。这个时候,享元模式就可以解决这个问题。
1. 概述
享元模式 运用共享技术实现大量相似对象的复用。系统可以只创建少量的对象,对象中某些属性是相同的不变的,这些不变的属性称为内部状态,同时依靠客户端传入一些外部属性,来区分不同的对象,这些变化的属性成为外部状态。享元模式是一种结构性设计模式。
享元模式的原理很简单,其实就是在堆中创建少量对象,通过改变对象的内部属性,而不改变对象的内存地址,实现对象的复用。
在享元模式中,存储这些共享实例的地方称为享元池,比如26个英文字母在内存中会被无数次使用到,这个时候每个字母就可以记录到特定享元池中。享元模式通常和工厂模式搭配使用,享元池就存储在工厂模式中。
享元模式的实现需要3个角色:
- 抽象享元类:是一个接口或者抽象类,在抽象享元类中声明了公共方法,这些方法可以向外界提供对象的内部状态数据,也可以通过这些方法来设置外部状态。
- 享元实现类:抽象享元类的实现或者子类,维护自身的内部状态,可以结合单例模式来设计具体享元类,以此来为每个具体享元类提供唯一的享元对象。
- 享元工厂类:用来维护享元对象的工厂类,负责对享元对象实例的创建与管理,并且可以给外部提供获取享元对象的服务。
2. 代码实现
在下面这个代码案例中,我们来写一个下围棋的Demo,其中白色棋子的位置从[0, 1] 到 [179, 180],黑色棋子的位置从[180, 181]到[359, 360],实现如下:
- 抽象享元类
// 享元接口 - 围棋棋子
public abstract class GoChess {
// 围棋颜色(内部状态)
private String color;
// 围棋材质(内部状态)
private String texture;
// 围棋尺寸(内部状态)
private String size;
// 坐标位置(外部状态)
private int x, y;
GoChess(String color) {
this.color = color;
this.texture = "大理石";
this.size = "半径1cm";
}
// 设置位置坐标
public void setCoordinate(int x, int y) {
System.out.println(String.format("【%s】的坐标位置是[%s, %s]", color, x, y));
}
}
- 具体享元实现
// 具体享元实现-白色围棋棋子
public class WhiteGoChess extends GoChess{
// 设置单例模式
private static volatile WhiteGoChess instance;
// 设置围棋的颜色
private WhiteGoChess(String color) {
super(color);
}
public static WhiteGoChess getInstance() {
if (instance == null) {
synchronized (WhiteGoChess.class) {
if (instance == null) {
instance = new WhiteGoChess("白色");
}
}
}
return instance;
}
}
// 具体享元实现 - 黑色围棋棋子
public class BlackGoChess extends GoChess {
// 设置单例模式
private static volatile BlackGoChess instance;
// 设置围棋的颜色
private BlackGoChess(String color) {
super(color);
}
// 获取享元对象的方法
public static BlackGoChess getInstance() {
if (instance == null) {
synchronized (WhiteGoChess.class) {
if (instance == null) {
instance = new BlackGoChess("黑色");
}
}
}
return instance;
}
}
- 享元工厂类
// 享元对象工厂 - 这里可以设置为单例模式
public class GoChessFactory {
public GoChessFactory() {}
// 静态内部类,用作单例实现
private static class Pool {
static final Map<String, GoChess> INSTANCE = new HashMap<>();
// 静态代码块中复制
static {
INSTANCE.put("白色", WhiteGoChess.getInstance());
INSTANCE.put("黑色", BlackGoChess.getInstance());
}
}
// 获取享元对象的方法
public static GoChess getInstance(String color) {
// 享元池中获取
return Pool.INSTANCE.get(color);
}
}
- 客户端
// 客户端的实现
public class Client {
public static void main(String[] args) {
// 设置180个白棋
for (int i = 0; i < 180; i++) {
GoChessFactory.getInstance("白色").setCoordinate(i, i + 1);
}
// 设置180个黑色黑棋
for (int i = 180; i < 360; i++) {
GoChessFactory.getInstance("黑色").setCoordinate(i, i + 1);
}
}
}
执行结果,部分结果打印如下:
3. UML类图
基于我们的代码示例,画一个UML类图,如下所示:
4. 总结
享元模式在开发中实际使用频率并不多,但是作为一种节约内存的设计模式,还是有一些应用场景的。
- 首先可以极大的减少内存中对象的竖向,从而可以节约资源,提高系统性能。
- 享元模式使用时需要注意外部状态的变化,尤其是多线程场景下注意并发的问题。