设计模式学习笔记(十四):享元模式
1 概述
1.1 引言
当一个系统中运行时的产生的对象太多,会带来性能下降等问题,比如一个文本字符串存在大量重复字符,如果每一个字符都用一个单独的对象表示,将会占用较多内存空间。
那么该如何避免出现大量相同或相似的对象,同时又不影响客户端以面向对象的方式操作呢?
享元模式正为解决这一问题而生,通过共享技术实现相同或相似对象的重用,在逻辑上每一个出现的字符都有一个对象与之对
应,但是物理上却共享一个享元对象。
在享元模式中,存储共享实例的地方称为享元池,可以针对每一个不同的字符创建一个享元对象,放置于享元池中,需要时取
出,示意图如下:
1.2 内部状态与外部状态
享元模式以共享的方式高效地支持大量细粒度对象的重用,能做到共享的关键是区分了内部状态以及外部状态。
- 内部状态:存储在享元对象内部并且不会随环境改变而改变,内部状态可以共享,例如字符的内容,字符a永远是字符a,不会变为字符b
- 外部状态:能够随环境改变而改变,不可以共享的状态,通常由客户端保存,并在享元对象被创建之后,需要使用的时候再传入到享元对象内部。外部状态之间通常是相互独立的,比如字符的颜色,字号,字体等,可以独立变化,没有影响,客户端在使用时将外部状态注入到享元对象中
正因为区分了内部状态以及外部状态,可以将具有相同内部状态的对象存储在享元池中,享元池的对象是可以实现共享的,需要的时候从中取出,实现对象的复用。通过向取出的对象注入不同的外部状态,可以得到一系列相似的对象,而这些对象实际上只存储一份。
1.3 定义
享元模式:运用共享技术有效地支持大量细粒度对象的复用。
系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。由于享元模式要求能够共享的对象必须是细粒度对象,因此又叫轻量级模式,是一种对象结构型模式。
1.4 结构图
享元模式一般结合工厂模式一起使用,结构图如下:
1.5 角色
Flyweights
(抽象享元类):通常是一个接口或者抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)ConcreteFlyweight
(具体享元类):实现/继承抽象共享类,实例称为共享对象,在具体享元类中为内部状态提供了存储空间,通常可以结合单例模式来设计具体享元类UnsharedConcreteFlyweight
(非共享具体享元类):并不是所有的抽象享元子类都需要被共享,不能被共享的子类可设计为非共享具体享元类,当需要一个非具体享元对象时可以直接实例化创建FlyweightFactory
(享元工厂类):享元工厂类用于创建并管理享元对象,针对抽象享元类编程,将具体享元对象存储于享元池中。一般使用键值对集合(比如Java中的HashMap
)作为享元池,当客户端获取享元对象时,首先判断是否存在,存在则从集合中取出并返回,不存在则创建新具体享元的实例,存储于享元池中并返回新实例
2 典型实现
2.1 步骤
- 定义抽象享元类:将抽象享元类定义为接口或者抽象类,声明业务方法
- 定义具体享元类:继承或实现抽象享元,实现其中的业务方法,同时使用单例模式设计,确保每个具体享元类提供唯一的享元对象
- (可选)定义非共享具体享元类:继承或实现抽象享元类,不使用单例模式设计,每次客户端获取都会返回一个新实例
- 定义享元工厂类:通常使用一个键值对集合作为享元池,根据键值返回对应的具体享元对象或非共享具体享元对象
2.2 抽象享元类
这里使用接口实现,包含一个opeartion
业务方法:
interface Flyweight
{
void operation(String extrinsicState);
}
2.3 具体享元类
简单设计两个枚举单例的具体享元类:
enum ConcreteFlyweight1 implements Flyweight
{
INSTANCE("INTRINSIC STATE 1");
private String intrinsicState;
private ConcreteFlyweight1(String intrinsicState)
{
this.intrinsicState = intrinsicState;
}
@Override
public void operation(String extrinsicState)
{
System.out.println("具体享元操作");
System.out.println("内部状态:"+intrinsicState);
System.out.println("外部状态:"+extrinsicState);
}
}
enum ConcreteFlyweight2 implements Flyweight
{
INSTANCE("INTRINSIC STATE 2");
private String intrinsicState;
private ConcreteFlyweight2(String intrinsicState)
{
this.intrinsicState = intrinsicState;
}
@Override
public void operation(String extrinsicState)
{
System.out.println("具体享元操作");
System.out.println("内部状态:"+intrinsicState);
System.out.println("外部状态:"+extrinsicState);
}
}
2.4 非共享具体享元类
两个简单的非共享具体享元类,不是枚举单例类:
class UnsharedConcreteFlyweight1 implements Flyweight
{
@Override
public void operation(String extrinsicState)
{
System.out.println("非共享具体享元操作");
System.out.println("外部状态:"+extrinsicState);
}
}
class UnsharedConcreteFlyweight2 implements Flyweight
{
@Override
public void operation(String extrinsicState)
{
System.out.println("非共享具体享元操作");
System.out.println("外部状态:"+extrinsicState);
}
}
2.5 享元工厂类
为了方便客户端以及工厂管理具体享元以及非共享具体享元,首先建立两个枚举类作为享元池的键:
enum Key { KEY1,KEY2 }
enum UnsharedKey { KEY1,KEY2 }
这里的工厂类使用了枚举单例:
enum Factory
{
INSTANCE;
private Map<Key,Flyweight> map = new HashMap<>();
public Flyweight get(Key key)
{
if(map.containsKey(key))
return map.get(key);
switch(key)
{
case KEY1:
map.put(key, ConcreteFlyweight1.INSTANCE);
return ConcreteFlyweight1.INSTANCE;
case KEY2:
map.put(key, ConcreteFlyweight2.INSTANCE);
return ConcreteFlyweight2.INSTANCE;
default:
return null;
}
}
public Flyweight get(UnsharedKey key)
{
switch(key)
{
case KEY1:
return new UnsharedConcreteFlyweight1();
case KEY2:
return new UnsharedConcreteFlyweight2();
default:
return null;
}
}
}
使用HashMap<String,Flyweight>
作为享元池:
- 对于具体享元类,根据键值判断享元池中是否存在具体享元对象,如果存在直接返回,如果不存在把具体享元的单例存入享元池,并返回该单例
- 对于非共享具体享元类,由于是“非共享”,不需要把实例对象存储于享元池中,每次调用直接返回新实例
2.6 客户端
客户端针对抽象享元进行编程,首先获取享元工厂单例,接着利用工厂方法,传入对应的枚举参数获取对应的具体享元或者非共享具体享元:
public static void main(String[] args)
{
Factory factory = Factory.INSTANCE;
Flyweight flyweight1 = factory.get(Key.KEY1);
Flyweight flyweight2 = factory.get(Key.KEY1);
System.out.println(flyweight1 == flyweight2);
flyweight1 = factory.get(UnsharedKey.KEY1);
flyweight2 = factory.get(UnsharedKey.KEY1);
System.out.println(flyweight1 == flyweight2);
}
2.7 反射简化
如果具体享元对象变多,工厂类的get()
中的switch
会变得很长,这时候可以将键值类以及工厂类的get()
改进以简化代码,例如在上面的基础上又增加了两个具体享元类:
enum ConcreteFlyweight3 implements Flyweight {...}
enum ConcreteFlyweight4 implements Flyweight {...}
这样工厂类的switch
需要增加两个Key
:
switch(key)
{
case KEY1:
map.put(key, ConcreteFlyweight1.INSTANCE);
return ConcreteFlyweight1.INSTANCE;
case KEY2:
map.put(key, ConcreteFlyweight2.INSTANCE);
return ConcreteFlyweight2.INSTANCE;
case KEY3:
map.put(key, ConcreteFlyweight3.INSTANCE);
return ConcreteFlyweight3.INSTANCE;
case KEY4:
map.put(key, ConcreteFlyweight4.INSTANCE);
return ConcreteFlyweight4.INSTANCE;
default:
return null;
}
可以利用具体享元类的命名方式进行简化,这里使用了顺序编号1,2,3,4...
的方式,因此,利用反射获取对应的类后直接获取其中的单例对象:
public Flyweight get(Key key)
{
if(map.containsKey(key))
return map.get(key);
try
{
Class<?> cls = Class.forName("ConcreteFlyweight"+key.code());
Flyweight flyweight = (Flyweight)(cls.getField("INSTANCE").get(null));
map.put(key,flyweight);
return flyweight;
}
catch(Exception e)
{
e.printStackTrace();
return null;
}
}
在此之前需要修改一下Key
类:
enum Key
{
KEY1(1),KEY2(2),KEY3(3),KEY4(4);
private int code;
private Key(int code)
{
this.code = code;
}
public int code()
{
return code;
}
}
增加一个code
字段,作为区分每一个具体享元的标志。
对于非共享具体享元类似,首先修改UnsharedKey
,同理添加code
字段:
enum UnsharedKey
{
KEY1(1),KEY2(2),KEY3(3),KEY4(4);
private int code;
private UnsharedKey(int code)
{
this.code = code;
}
public int code()
{
return code;
}
}
接着修改get
方法:
public Flyweight get(UnsharedKey key)
{
try
{
Class<?> cls = Class.forName("UnsharedConcreteFlyweight"+key.code());
return (Flyweight)(cls.newInstance());
}
catch(Exception e)
{
e.printStackTrace();
return null;
}
}
由于笔者使用的是OpenJDK11,其中newInstance
被标记为过时了:
因此使用如下方式代替直接使用newInstance()
:
return (Flyweight)(cls.getDeclaredConstructor().newInstance());
区别如下:
newInstance
:直接调用无参构造方法getDeclaredConstructor().newInstance()
:getDeclaredConstructor()
会根据传入的参数搜索该类的构造方法并返回,没有参数就返回该类的无参构造方法,接着调用newInstance
进行实例化
3 实例
围棋棋子的设计:一个棋盘中含有大量相同的黑白棋子,只是出现的位置不一样,使用享元模式对棋子进行设计。
设计如下:
- 抽象享元类:
IgoChessman
接口(如果想要具体享元类为枚举单例的话必须是接口,使用其他方式实现单例可以为抽象类),包含getColor
以及display
方法 - 具体享元类:
BlackChessman
+WhiteChessman
,枚举单例类 - 非共享具体享元类:无
- 享元工厂类:
Factory
,枚举单例类,包含简单的get
作为获取具体享元的方法,加上了white
以及balck
简单封装,在构造方法中初始化享元池
代码如下:
//抽象享元接口
interface IgoChessman
{
Color getColor();
void display();
}
//具体享元枚举单例类
enum BlackChessman implements IgoChessman
{
INSTANCE;
@Override
public Color getColor()
{
return Color.BLACK;
}
@Override
public void display()
{
System.out.println("棋子颜色"+getColor().color());
}
}
//具体享元枚举单例类
enum WhiteChessman implements IgoChessman
{
INSTANCE;
@Override
public Color getColor()
{
return Color.WHITE;
}
@Override
public void display()
{
System.out.println("棋子颜色"+getColor().color());
}
}
//享元工厂枚举单例类
enum Factory
{
INSTANCE;
//HashMap<Color,IgoChessman>作为享元池
private Map<Color,IgoChessman> map = new HashMap<>();
private Factory()
{
//构造方法中直接初始化享元池
map.put(Color.WHITE, WhiteChessman.INSTANCE);
map.put(Color.BLACK, BlackChessman.INSTANCE);
}
public IgoChessman get(Color color)
{
//由于在构造方法中已经初始化,如果不存在可以返回null或者添加新实例到享元池并返回,这里选择了返回null
if(!map.containsKey(color))
return null;
return (IgoChessman)map.get(color);
}
//简单封装
public IgoChessman white()
{
return get(Color.WHITE);
}
public IgoChessman black()
{
return get(Color.BLACK);
}
}
enum Color
{
WHITE("白色"),BLACK("黑色");
private String color;
private Color(String color)
{
this.color = color;
}
public String color()
{
return color;
}
}
在初始化享元池时,如果具体享元类过多可以使用反射简化,不需要手动逐个put
:
private Factory()
{
map.put(Color.WHITE, WhiteChessman.INSTANCE);
map.put(Color.BLACK, BlackChessman.INSTANCE);
}
根据枚举值数组,结合List
与forEach
,逐个利用数组中的值获取对应的类,进而获取实例:
private Factory()
{
List.of(Color.values()).forEach(t->
{
String className = t.name().substring(0,1)+t.name().substring(1).toLowerCase()+"Chessman";
try
{
map.put(t,(IgoChessman)(Class.forName(className).getField("INSTANCE").get(null)));
}
catch(Exception e)
{
e.printStackTrace();
map.put(t,null);
}
});
}
测试:
public static void main(String[] args)
{
Factory factory = Factory.INSTANCE;
IgoChessman white1 = factory.white();
IgoChessman white2 = factory.white();
white1.display();
white2.display();
System.out.println(white1 == white2);
IgoChessman black1 = factory.black();
IgoChessman black2 = factory.black();
black1.display();
black2.display();
System.out.println(black1 == black2);
}
4 加入外部状态
通过上面的方式已经能够实现黑白棋子的共享了,但是还有一个问题没有解决,就是如何将相同的黑白棋子放置于不同的棋盘位置上?
解决办法也不难,增加一个坐标类Coordinates
,调用display
时作为要放置的坐标参数传入函数。
首先增加一个坐标类:
class Coordinates
{
private int x;
private int y;
public Coordinates(int x,int y)
{
this.x = x;
this.y = y;
}
//setter+getter...
}
接着需要修改抽象享元接口,在display
中加入Coordinates
参数:
interface IgoChessman
{
Color getColor();
void display(Coordinates coordinates);
}
然后修改具体享元类即可:
enum BlackChessman implements IgoChessman
{
INSTANCE;
@Override
public Color getColor()
{
return Color.BLACK;
}
@Override
public void display(Coordinates coordinates)
{
System.out.println("棋子颜色"+getColor().color());
System.out.println("显示坐标:");
System.out.println("横坐标"+coordinates.getX());
System.out.println("纵坐标"+coordinates.getY());
}
}
对于客户端,创建享元对象的代码无须修改,只需修改调用了display
的地方,传入Coordinates
参数即可:
IgoChessman white1 = factory.white();
IgoChessman white2 = factory.white();
white1.display(new Coordinates(1, 2));
white2.display(new Coordinates(2, 3));
5 单纯享元模式与复合享元模式
5.1 单纯享元模式
标准的享元模式既可以包含具体享元类,也包含非共享具体享元类。
但是在单纯享元模式中,所有的具体享元类都是共享的,也就是不存在非共享具体享元类。
比如上面棋子的例子,黑白棋子作为具体享元类都是共享的,不存在非共享具体享元类。
5.2 复合享元模式
将一些单纯享元对象进行使用组合模式加以组合还可以形成复合享元对象,这样的复合享元对象本身不能共享,但是它们可以分解为单纯享元对象,而后者可以共享。
通过复合享元模式可以确保复合享元类所包含的每个单纯享元类都具有相同的外部状态,而这些单纯享元的内部状态可以不一样,比如,上面棋子的例子中:
- 黑棋子是单纯享元
- 白棋子也是单纯享元
- 这两个单纯享元的内部状态不同(颜色不同)
- 但是可以设置相同的外部状态(比如设置为棋盘上同一位置,但是这样没有什么实际意义,或者设置显示为同一大小)
例子如下,首先在抽象享元中添加一个以int
为参数的display
:
interface IgoChessman
{
Color getColor();
void display(int size);
}
在具体享元实现即可:
enum BlackChessman implements IgoChessman
{
INSTANCE;
@Override
public Color getColor()
{
return Color.BLACK;
}
@Override
public void display(int size)
{
System.out.println("棋子颜色"+getColor().color());
System.out.println("棋子大小"+size);
}
}
接着添加复合享元类,里面包含一个HashMap
存储所有具体享元:
enum Chessmans implements IgoChessman
{
INSTANCE;
private Map<Color,IgoChessman> map = new HashMap<>();
public void add(IgoChessman chessman)
{
map.put(chessman.getColor(),chessman);
}
@Override
public Color getColor()
{
return null;
}
@Override
public void display(int size)
{
map.forEach((k,v)->v.display(size));
}
}
在display
中,实际上是遍历了HashMap
,给每一个具体享元的display
传入相同的参数。
测试:
public static void main(String[] args) {
Factory factory = Factory.INSTANCE;
IgoChessman white = factory.white();
IgoChessman black = factory.black();
Chessmans chessmans = Chessmans.INSTANCE;
chessmans.add(white);
chessmans.add(black);
chessmans.display(30);
}
输出:
这样内部状态不同(颜色不同)的两个具体享元类(黑白棋)就被复合享元类(Chessmans
)设置为具有相同的外部状态(显示大小30)。
6 补充说明
- 与其他模式联用:享元模式通常需要与其他模式联用,比如工厂模式(享元工厂),单例模式(具体享元枚举单例),组合模式(复合享元模式)
- JDK中的享元模式:JDK中的
String
使用了享元模式。大家都知道String
是不可变类,对于类似String a = "123"
这种声明方式,会创建一个值为"123"
的享元对象,下次使用"123"
时从享元池获取,在修改享元对象时,比如a += "1"
,先将原有对象复制一份,然后在新对象上进行修改,这种机制叫做"Copy On Write"。基本思路是,一开始大家都在共享内容,当某人需要修改时,把内容复制出去形成一个新内容并修改
7 主要优点
- 降低内存消耗:享元模式可以极大地减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而节约系统资源,提供系统性能
- 外部状态独立:享元模式外部状态相对独立,不会影响到内部状态,从而使得享元对象可以在不同环境中被共享
8 主要缺点
- 增加复杂度:享元模式使得系统变复杂,需要分离出内部状态以及外部状态,使得程序逻辑复杂化
- 运行时间变长:为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态使得运行时间变长
9 适用场景
- 一个系统有大量相似或相同对象,造成大量内存浪费
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中
- 由于需要维护享元池,造成一定的资源开销,因此在需要真正多次重复使用享元对象时才值得使用享元模式