享元模式
介绍
享元模式
,主要在于共享通用对象,减少内存的使用,提升系统的访问效率。而这部分共享对象通常比较耗费内存或者需要查询大量接口或者使用数据库资源,因此统一抽离作为共享对象使用
。
另外享元模式可以分为在服务端和客户端,一般互联网H5和Web场景下大部分数据都需要服务端进行处理,比如数据库连接池的使用、多线程线程池的使用,除了这些功能外,还有些需要服务端进行包装后的处理下发给客户端,因为服务端需要做享元处理。但在一些游戏场景下,很多都是客户端需要进行渲染地图效果,比如;树木、花草、鱼虫,通过设置不同元素描述使用享元公用对象,减少内存的占用,让客户端的游戏更加流畅。
在享元模型的实现中需要使用到享元工厂来进行管理这部分独立的对象和共享的对象,避免出现线程安全的问题。
Java中的String类型
在JAVA语言中,String类型就是使用了享元模式
。String对象是final类型,对象一旦创建就不可改变。在JAVA中字符串常量都是存在常量池中的,JAVA会确保一个字符串常量在常量池中只有一个拷贝。String a=“abc”,其中"abc"就是一个字符串常量。
String a = "abc";
String b = "abc";
System.out.println(a==b);
上面的例子中结果为:true ,这就说明a和b两个引用都指向了常量池中的同一个字符串常量"abc"。这样的设计避免了在创建N多相同对象时所产生的不必要的大量的资源消耗。
享元模式的结构
享元模式采用一个共享来避免大量拥有相同内容对象的开销
。这种开销最常见、最直观的就是内存的损耗。享元对象能做到共享的关键是区分内蕴状态(Internal State)
和外蕴状态(External State)
。
一个内蕴状态是存储在享元对象内部的,并且是不会随环境的改变而有所不同
。因此,一个享元可以具有内蕴状态并可以共享。
一个外蕴状态是随环境的改变而改变的、不可以共享的
。享元对象的外蕴状态必须由客户端保存,并在享元对象被创建之后,在需要使用的时候再传入到享元对象内部。外蕴状态不可以影响享元对象的内蕴状态,它们是相互独立的。
实例
通过对围棋软件进行分析,发现在围棋棋盘中包含大量的黑子和白子,它们的形状、大小都一模一样,只是出现的位置不同而已。如果将每一个棋子都作为一个独立的对象存储在内存中,将导致该围棋软件在运行时所需内存空间较大,如何降低运行代价、提高系统性能是需要解决的一个问题。为了解决这个问题,决定使用享元模式来设计该围棋软件的棋子对象
内蕴状态(Internal State)
//围棋棋子类:抽象享元类
public abstract class IgoChessman {
public abstract String getColor();
public void display() {
System.out.println("棋子颜色:" + this.getColor());
}
}
//白色棋子类:具体享元类
public class WhiteIgoChessman extends IgoChessman {
public String getColor() {
return "白色";
}
}
//黑色棋子类:具体享元类
public class BlackIgoChessman extends IgoChessman {
public String getColor() {
return "黑色";
}
}
//围棋棋子工厂类:享元工厂类,使用单例模式进行设计
public class IgoChessmanFactory {
private static IgoChessmanFactory instance = new IgoChessmanFactory();
private static Hashtable ht; //使用Hashtable来存储享元对象,充当享元池
private IgoChessmanFactory() {
ht = new Hashtable();
IgoChessman black,white;
black = new BlackIgoChessman();
ht.put("b",black);
white = new WhiteIgoChessman();
ht.put("w",white);
}
//返回享元工厂类的唯一实例
public static IgoChessmanFactory getInstance() {
return instance;
}
//通过key来获取存储在Hashtable中的享元对象
public static IgoChessman getIgoChessman(String color) {
return (IgoChessman)ht.get(color);
}
}
外蕴状态(External State)
通过对围棋棋子进行进一步分析,发现虽然黑色棋子和白色棋子可以共享,但是它们将显示在棋盘的不同位置,如何让相同的黑子或者白子能够多次重复显示且位于一个棋盘的不同地方?解决方法就是将棋子的位置定义为棋子的一个外部状态,在需要时再进行设置。因此,我们增加了一个新的类Coordinates(坐标类),用于存储每一个棋子的位置
//围棋棋子类:抽象享元类
public abstract class ExternalIgoChessman {
public abstract String getColor();
public void display(Coordinates coord){
System.out.println("棋子颜色:" + this.getColor() + ",棋子位置:" + coord.getX() + "," + coord.getY() );
}
}
//白色棋子类:具体享元类
public class ExternalWhiteIgoChessman extends ExternalIgoChessman {
public String getColor() {
return "白色";
}
}
//黑色棋子类:具体享元类
public class ExternalBlackIgoChessman extends ExternalIgoChessman {
public String getColor() {
return "黑色";
}
}
public class Coordinates {
private int x;
private int y;
public Coordinates(int x,int y) {
this.x = x;
this.y = y;
}
public int getX() {
return this.x;
}
public void setX(int x) {
this.x = x;
}
public int getY() {
return this.y;
}
public void setY(int y) {
this.y = y;
}
}
//围棋棋子工厂类:享元工厂类,使用单例模式进行设计
public class ExternalIgoChessmanFactory {
private static ExternalIgoChessmanFactory instance = new ExternalIgoChessmanFactory();
private static Hashtable ht; //使用Hashtable来存储享元对象,充当享元池
private ExternalIgoChessmanFactory() {
ht = new Hashtable();
ExternalIgoChessman black, white;
black = new ExternalBlackIgoChessman();
ht.put("b", black);
white = new ExternalWhiteIgoChessman();
ht.put("w", white);
}
//返回享元工厂类的唯一实例
public static ExternalIgoChessmanFactory getInstance() {
return instance;
}
//通过key来获取存储在Hashtable中的享元对象
public static ExternalIgoChessman getIgoChessman(String color) {
return (ExternalIgoChessman) ht.get(color);
}
}
测试类
@Slf4j
public class ApiTest {
@Test
public void testCommand() {
log.info("--------------------- 内部状态 享元模式 ---------------------");
IgoChessman black1, black2, black3, white1, white2;
IgoChessmanFactory factory;
//获取享元工厂对象
factory = IgoChessmanFactory.getInstance();
//通过享元工厂获取三颗黑子
black1 = factory.getIgoChessman("b");
black2 = factory.getIgoChessman("b");
black3 = factory.getIgoChessman("b");
System.out.println("判断两颗黑子是否相同:" + (black1 == black2));
//通过享元工厂获取两颗白子
white1 = factory.getIgoChessman("w");
white2 = factory.getIgoChessman("w");
System.out.println("判断两颗白子是否相同:" + (white1 == white2));
//显示棋子
black1.display();
black2.display();
black3.display();
white1.display();
white2.display();
log.info("--------------------- 外部状态 享元模式 ---------------------");
ExternalIgoChessman externalBlack1,externalBlack2,externalBlack3,externalWhite1,externalWhite2;
ExternalIgoChessmanFactory externalFactory;
//获取享元工厂对象
externalFactory = ExternalIgoChessmanFactory.getInstance();
//通过享元工厂获取三颗黑子
externalBlack1 = externalFactory.getIgoChessman("b");
externalBlack2 = externalFactory.getIgoChessman("b");
externalBlack3 = externalFactory.getIgoChessman("b");
System.out.println("判断两颗黑子是否相同:" + (externalBlack1==externalBlack2));
//通过享元工厂获取两颗白子
externalWhite1 = externalFactory.getIgoChessman("w");
externalWhite2 = externalFactory.getIgoChessman("w");
System.out.println("判断两颗白子是否相同:" + (externalWhite1==externalWhite2));
//显示棋子,同时设置棋子的坐标位置
externalBlack1.display(new Coordinates(1,2));
externalBlack2.display(new Coordinates(3,4));
externalBlack3.display(new Coordinates(1,3));
externalWhite1.display(new Coordinates(2,5));
externalWhite2.display(new Coordinates(2,4));
}
}
小结
享元模式的优点在于它大幅度地降低内存中对象的数量。但是,它做到这一点所付出的代价也是很高的:
- 享元模式使得系统更加复杂。为了使对象可以共享,需要将一些状态外部化,这使得程序的逻辑复杂化。
- 享元模式将享元对象的状态外部化,而读取外部状态使得运行时间稍微变长。