亨元模式(结构型)
一、概念引入
1). 案例问题
Sunny软件公司计划开发一个围棋人间,开发人员通过对围棋软件进行分析,发现在围棋棋盘中包含大量的黑子和白子,它们的形状、大小都一模一样,只是出现的位置不同而已。如果将每一个棋子都作为一个独立的对象存储在内存中,将导致该围棋软件在运行时所需内存空间较大,那么该如何降低运行代价、提高系统性能?享元模式正为解决这一类问题而诞生。享元模式通过共享技术实现相同或相似对象的重用,逻辑上是不同的对象,物理上却是同一个对象,这些实例对象放在一个叫做享元池(Flyweight Pool)的容器里面。
2). 定义
享元模式(Flyweight Pattern):运用共享技术有效地支持大量细粒度对象的复用。系统只使用少量的对象,而这些对象都很相似,状态变化很小,可以实现对象的多次复用。(由于享元模式要求能够共享的对象必须是细粒度对象,因此它又称为轻量级模式,它是一种对象结构型模式。)
3). 享元模式的内部外部状态
享元模式以共享的方式高效地支持大量细粒度对象的重用,享元对象能做到共享的关键是区分了内部状态(Intrinsic State)和外部状态(Extrinsic State)
内部状态是存储在享元对象内部并且不会随环境改变而改变的状态,内部状态可以共享。
外部状态是随环境改变而改变的、不可以共享的状态。享元对象的外部状态通常由客户端保存,并在享元对象被创建之后,需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的。如字符的颜色,可以在不同的地方有不同的颜色。
正因为区分了内部状态和外部状态,我们可以将具有相同内部状态的对象存储在享元池中,享元池中的对象是可以实现共享的,需要的时候就将对象从享元池中取出,实现对象的复用。通过向取出的对象注入不同的外部状态,可以得到一系列相似的对象,而这些对象在内存中实际上只存储一份。
4). 亨元模式典型结构图
5). 亨元模式中的角色
- Flyweight(抽象享元类):通常是一个接口或抽象类,在抽象享元类中声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
- ConcreteFlyweight(具体享元类):它实现了抽象享元类,其实例称为享元对象;在具体享元类中为内部状态提供了存储空间。通常我们可以结合单例模式来设计具体享元类,为每一个具体享元类提供唯一的享元对象。
- UnsharedConcreteFlyweight(非共享具体享元类):并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
- FlyweightFactory(享元工厂类):享元工厂类用于创建并管理享元对象,它针对抽象享元类编程,将各种类型的具体享元对象存储在一个享元池中,享元池一般设计为一个存储“键值对”的集合(也可以是其他类型的集合),可以结合工厂模式进行设计;当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在的话),返回新创建的实例并将其存储在享元池中。
二、案例设计
开篇引入的围棋设计案例
- 亨元类(内部状态类 <这里为共享类>)
/**
* 抽象亨元接口
*/
interface IgoChessman {
public abstract String getColor();
public default void display(Coordinates coordinates) {
System.out.println("棋子颜色:" + this.getColor() + ",棋子位置:" + coordinates);
}
}
/**
* 具体亨元类:内部状态类(共享) 黑色棋子
*/
class BlackIgoChessman implements IgoChessman {
@Override
public String getColor() {
return "黑色";
}
}
/**
* 具体亨元类: 内部状态类(共享) 白色棋子
*/
class WhiteIgoChessman implements IgoChessman {
@Override
public String getColor() {
return "白色";
}
}
- 外部类(非共享)
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.ToString;
/**
* 外部类:外部状态类(独有)棋子坐标
*/
@Data
@AllArgsConstructor
@ToString
class Coordinates {
private Integer x;
private Integer y;
}
享元类中要将内部状态和外部状态分开处理,通常将内部状态作为享元类的成员变量,而外部状态通过注入的方式添加到享元类中
- 亨元工厂类
import java.util.concurrent.ConcurrentHashMap;
/**
* 亨元工厂类(简单工厂),使用单例模式设计
*/
class IgoChessmanFactory {
// 单例模式设计
private static IgoChessmanFactory igoChessmanFactory = new IgoChessmanFactory();
// 使用ConcurrentHashMap 来存储亨元对象
private static ConcurrentHashMap<String, IgoChessman> chessMap;
// 构造方法私有化,单例模式设计
private IgoChessmanFactory() {
chessMap = new ConcurrentHashMap<String, IgoChessman>();
chessMap.put("b", new BlackIgoChessman());
chessMap.put("w", new WhiteIgoChessman());
}
// 返回亨元工厂类的唯一实例
public static IgoChessmanFactory getInstance() {
return igoChessmanFactory;
}
// 通过key来获取在map中的亨元对象
public IgoChessman getIgoChessman(String color) {
return IgoChessmanFactory.chessMap.get(color);
}
}
享元工厂类的作用在于提供一个用于存储享元对象的享元池,当用户需要对象时,首先从享元池中获取,如果享元池中不存在,则创建一个新的享元对象返回给用户,并在享元池中保存该新增对象
- 客户端测试代码
/**
* 客户端代码
* @author Liucheng
* @since 2019-08-31
*/
public class Client {
public static void main(String[] args) {
// 亨元工厂
IgoChessmanFactory igoChessmanFactory = IgoChessmanFactory.getInstance();
// 取两颗黑子
IgoChessman blackOne = igoChessmanFactory.getIgoChessman("b");
IgoChessman blackTwo = igoChessmanFactory.getIgoChessman("b");
System.out.println("黑子是否相同:" + (blackOne == blackTwo));
// 取两颗白子
IgoChessman whiteOne = igoChessmanFactory.getIgoChessman("w");
IgoChessman whiteTwo = igoChessmanFactory.getIgoChessman("w");
System.out.println("白子是否相同:" + (whiteOne == whiteTwo));
System.out.println("\n");
// 显示棋子,同时设置棋子的坐标位置
blackOne.display(new Coordinates(1, 2));
blackTwo.display(new Coordinates(2, 3));
whiteOne.display(new Coordinates(1, 3));
whiteTwo.display(new Coordinates(2, 2));
}
}
三、单纯亨元模式与复合亨元模式
标准的享元模式结构图中既包含可以共享的具体享元类,也包含不可以共享的非共享具体享元类。但实际生产中还有两种特殊的亨元模式。
1). 单纯亨元模式
在单纯享元模式中,所有的具体享元类都是可以共享的,不存在非共享具体享元类
2). 复合亨元模式
将一些单纯享元对象使用组合模式加以组合,还可以形成复合享元对象,这样的复合享元对象本身不能共享,但是它们可以分解成单纯享元对象,而后者则可以共享
通过复合享元模式,可以确保复合享元类CompositeConcreteFlyweight中所包含的每个单纯享元类ConcreteFlyweight都具有相同的外部状态,而这些单纯享元的内部状态往往可以不同。如果希望为多个内部状态不同的享元对象设置相同的外部状态,可以考虑使用复合享元模式。
四、亨元模式补充
1). 与其他模式联用
亨元模式通常会与其他模式联用
- 在享元模式的享元工厂类中通常提供一个静态的工厂方法用于返回享元对象,使用简单工厂模式来生成享元对象。
- 在一个系统中,通常只有唯一一个享元工厂,因此可以使用单例模式进行享元工厂类的设计。
- 享元模式可以结合 组合模式 形成复合享元模式,统一对多个享元对象设置外部状态。
2). 亨元模式与String类
JDK类库中的String类使用了享元模式
public class ClientOne {
@Test
public void testString() {
String str1 = "abcd";
String str2 = "abcd";
String str3 = "ab" + "cd";
String str4 = "ab";
str4 += "cd";
System.out.println(str1 == str2); // true
System.out.println(str1 == str3); // true
System.out.println(str1 == str4); // false
str2 += "e";
System.out.println(str1 == str2); // false
}
}
在Java语言中,如果每次执行类似String str1="abcd"的操作时都创建一个新的字符串对象将导致内存开销很大,因此如果第一次创建了内容为"abcd"的字符串对象str1,下一次再创建内容相同的字符串对象str2时会将它的引用指向"abcd",不会重新分配内存空间,从而实现了"abcd"在内存中的共享。可以看出,前两个输出语句均为true,说明str1、str2、str3在内存中引用了相同的对象;如果有一个字符串str4,其初值为"ab",再对它进行操作str4 += "cd",此时虽然str4的内容与str1相同,但是由于str4的初始值不同,在创建str4时重新分配了内存,所以第三个输出语句结果为false;最后一个输出语句结果也为false,说明当对str2进行修改时将创建一个新的对象,修改工作在新对象上完成,而原来引用的对象并没有发生任何改变,str1仍然引用原有对象,而str2引用新对象,str1与str2引用了两个完全不同的对象。
关于Java String类这种在修改享元对象时,先将原有对象复制一份,然后在新对象上再实施修改操作的机制称为Copy On Write
五、总结
当系统中存在大量相同或者相似的对象时,享元模式是一种较好的解决方案,它通过共享技术实现相同或相似的细粒度对象的复用,从而节约了内存空间,提高了系统性能
1). 优点
- 可以极大减少内存中对象的数量,使得相同或相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能。
- 享元模式的外部状态相对独立,而且不会影响其内部状态,从而使得享元对象可以在不同的环境中被共享。
2). 缺点
- 享元模式使得系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
- 为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使得运行时间变长。
3). 适用场景
- 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
- 在使用享元模式时需要维护一个存储享元对象的享元池,而这需要耗费一定的系统资源,因此,应当在需要多次重复使用享元对象时才值得使用享元模式。