本文中涉及到的完整代码存放于以下 GitHub 仓库中 LearningCode
1. 理论部分
享元模式(Flyweight Pattern):运用共享技术高效地支持大量细粒度对象的复用。
1.1 结构与实现
享元模式能做到共享的关键是对状态进行了区分:
- 内部状态:存储在享元对象内部并且不会随环境改变而改变的状态,内部状态可以共享。
- 外部状态:是随着环境改变而改变的、不可以共享的状态。享元对象的外部状态通常由客户端保存,并在享元对象被创建之后需要使用的时候再传入到享元对象内部。一个外部状态与另一个外部状态之间是相互独立的。
享元模式包括以下 4 个角色:
- Flyweight(抽象享元类):
- 职责:声明了具体享元类公共的方法,这些方法可以向外界提供享元对象的内部数据(内部状态),同时也可以通过这些方法来设置外部数据(外部状态)。
- 实现:通常声明为接口或抽象类。
- ConcreteFlyweight(具体享元类):
- 职责:实现了抽象享元类种定义的方法,其实例称为享元对象;在具体享元类中为内部状态提供了存储空间。
- 实现:通常声明为具体类。
- UnsharedConcreteFlyweight(非共享具体享元类):
- 职责:并不是所有的抽象享元类的子类都需要被共享,不能被共享的子类可设计为非共享具体享元类;当需要一个非共享具体享元类的对象时可以直接通过实例化创建。
- 实现:通常声明为具体类。
- FlyweightFactory(享元工厂类):
- 职责:用于创建并管理享元对象。当用户请求一个具体享元对象时,享元工厂提供一个存储在享元池中已创建的实例或者创建一个新的实例(如果不存在),返回新创建的实例并将其存储在享元池中。
- 实现:通常声明为具体类。
享元模式的 UML 类图如下所示:
1.2 扩展:单纯享元模式
在单纯享元模式中,所有的享元类都是可以共享的,不存在非共享具体享元类。单纯享元模式的 UML 类图如下所示:
1.3 扩展:复合享元模式
复合享元模式是享元模式与组合模式的结合使用。在复合享元模式中,复合享元对象由一些单纯享元对象组成,但其本身不能被共享。通过使用复合享元模式可以让复合享元类 CompositeConcreteFlyweight 中所包含的每个单纯享元类 ConcreteFlyweight 都具有相同的外部状态,而这些单纯享元的内部状态往往可以不同。如果希望为多个内部状态不同的享元对象设置相同的外部状态,可以考虑使用复合享元模式。复合享元模式的 UML 类图如下所示:
1.4 优缺点与适用场景
享元模式具有以下优点:享元模式可以减少内存中对象的数量,使得相同或者相似对象在内存中只保存一份,从而可以节约系统资源,提高系统性能。
享元模式存在以下缺点:
- 享元模式使系统变得复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化。
- 为了使对象可以共享,享元模式需要将享元对象的部分状态外部化,而读取外部状态将使运行时间变长。
享元模式适用于以下场景:
- 一个系统有大量相同或者相似的对象,造成内存的大量耗费。
- 对象的大部分状态都可以外部化,可以将这些外部状态传入对象中。
- 应用程序不应当依赖于对象标识。
2. 实现部分
以 Java 代码为例,演示单纯享元模式的实现。
标准享元模式只是在单纯享元模式的基础上多了一个非共享具体享元类,而复合享元模式是享元模式与组合模式结合后的产物,代码比较简单,这里不进行演示。
案例介绍:
围棋中每个棋子的颜色分为两种,所以可以作为内部状态,而棋子的位置与其它棋子均不相同,因此将位置作为外部状态。
将围棋的坐标抽象为一个类Coordinates
public class Coordinates {
private int x;
private int y;
// 省略get set
}
定义抽象享元类——IgoChessman
public abstract class IgoChessman {
public abstract String getColor();
public void display(Coordinates coordinates) {
System.out.println("棋子颜色: " + getColor() + ", 棋子位置:" + coordinates.getX()
+ "," + coordinates.getY());
}
}
定义共享具体享元类,以黑棋为例
public class BlackIgoChessman extends IgoChessman{
@Override
public String getColor() {
return "黑色";
}
}
定义享元工厂
public class IgoChessmanFactory {
private static final HashMap<String, IgoChessman> map = new HashMap<>();
static {
map.put("b", new BlackIgoChessman());
map.put("w", new WhiteIgoChessman());
}
public static IgoChessman getIgoChessman(String color) {
return map.get(color);
}
}
客户端调用,将围棋的外部状态——位置传入到享元对象中。
public class Main {
public static void main(String[] args) {
// 获取 3 颗黑棋
IgoChessman black1 = IgoChessmanFactory.getIgoChessman("b");
IgoChessman black2 = IgoChessmanFactory.getIgoChessman("b");
IgoChessman black3 = IgoChessmanFactory.getIgoChessman("b");
// 获取 2 颗白棋
IgoChessman white1 = IgoChessmanFactory.getIgoChessman("w");
IgoChessman white2 = IgoChessmanFactory.getIgoChessman("w");
// 显示棋子
black1.display(new Coordinates(1,2));
black2.display(new Coordinates(3, 4));
black3.display(new Coordinates(1,3));
white1.display(new Coordinates(2, 5));
white2.display(new Coordinates(2, 4));
}
}
完整的 UML 类图如下所示:
3. 参考资料
学习视频:
- 设计模式快速入门 —— 图灵星球TuringPlanet —— 享元模式
- Java设计模式详解 —— 黑马程序员 —— 享元模式(P87 ~ P90)
- Java设计模式 —— 尚硅谷 —— 享元模式(P86 ~ P90)
学习读物:
- 《设计模式:可复用面向对象软件的基础》—— Erich Gamma 著 —— 李英军 译 —— 第 4.6 节(P146)
- 《Java 设计模式》 —— 刘伟 著 —— 第 14 章(P188)
- 《设计模式之美》—— 王争 著 —— 第 7.7 节(P239)
- 《设计模式之禅》 —— 第 2 版 —— 秦小波 著 —— 第 28 章(P356)
- 《图解设计模式》—— 结城浩 著 —— 杨文轩 译 —— 第 20 章(P237)
电子文献: