本篇Blog继续学习结构型模式,了解如何更优雅的布局类和对象。结构型模式描述如何将类或对象按某种布局组合以便获得更好、更灵活的结构。虽然面向对象的继承机制提供了最基本的子类扩展父类的功能,但结构型模式不仅仅简单地使用继承,而更多地通过组合与运行期的动态组合来实现更灵活的功能。它分为类结构型模式和对象结构型模式,前者采用继承机制来组织接口和类,后者釆用组合或聚合来组合对象。本篇学习的是享元模式。由于学习的都是设计模式,所有系列文章都遵循如下的目录:
- 模式档案:包含模式的定义、模式的特点、解决什么问题、优缺点、使用场景等
- 模式结构:包含模式的角色定义及调用关系以及其模版代码
- 模式示例:包含模式的实现方式代码举例,生活中的简单问题映射
- 模式实践:如果工作中或开源项目用到了该模式,就将使用过程贴到这里,并且客观讨论使用的是否恰当
- 模式对比:如果模式相似,有必要体现其相似点及不同点,区分使用,说明哪些场景下使用哪种模式比较好
- 模式扩展:如果模式有与标准结构定义不同的变体形式,一并体现出其变体结构
接下来所有设计模式的介绍都暂且遵循此基本行文逻辑吗,如果某一条目没有则无需体现,但条目顺序遵循此结构
模式档案
在面向对象程序设计过程中,有时会面临要创建大量相同或相似对象实例的问题。创建那么多的对象将会耗费很多的系统资源,它是系统性能提高的一个瓶颈。例如,围棋和五子棋中的黑白棋子、图像中的坐标点或颜色,这些对象有很多相似的地方,如果能把它们相同的部分提取出来共享,则能节省大量的系统资源,这就是享元模式
模式定义:享元模式运用共享技术来有效地支持大量细粒度对象的复用。它通过共享已经存在的不变对象来大幅度减少需要创建的对象数量、避免大量相似类的开销,从而提高系统资源的利用率。
解决什么问题:享元模式的意图是复用对象,节省内存,前提是享元对象是不可变对象,具体来讲,当一个系统中存在大量重复对象的时候,如果这些重复的对象是不可变对象,我们就可以利用享元模式将对象设计成享元,在内存中只保留一份实例,供多处代码引用。这样可以减少内存中对象的数量,起到节省内存的目的。实际上,不仅仅相同对象可以设计成享元,对于相似对象,我们也可以将这些对象中相同的部分(字段)提取出来,设计成享元,让这些大量相似对象引用这些享元。
优点:相同对象只要保存一份,这降低了系统中对象的数量,从而降低了系统中细粒度对象给内存带来的压力
缺点:享元模式 使得系统更加复杂,需要分离出内部状态和外部状态,这使得程序的逻辑复杂化;
使用场景:当满足如下场景时可以使用享元模式:
- 系统中存在大量相同或相似的对象,这些对象耗费大量的内存资源。
- 由于享元模式需要额外维护一个保存享元的数据结构,所以应当在有足够多的享元实例时才值得使用享元模式。
需要注意的是:享元对象应该为不可变对象,不可变对象指一旦通过构造函数初始化完成之后,它的状态(对象的成员变量或者属性)就不会再被修改了。所以,不可变对象不能暴露任何 set()
等修改内部状态的方法。之所以要求享元是不可变对象,那是因为它会被多处代码共享使用,避免一处代码对享元进行了修改,影响到其他使用它的代码
模式结构
享元模式的主要角色有如下。
- 抽象享元角色(Flyweight):是所有的具体享元类的基类,为具体享元规范需要实现的公共接口,非享元的外部状态以参数的形式通过方法传入。
- 具体享元角色(Concrete Flyweight):实现抽象享元角色中所规定的接口。
- 非享元角色(Unsharable Flyweight):是不可以共享的外部状态,它以参数的形式注入具体享元的相关方法中。
- 享元工厂角色(Flyweight Factory):负责创建和管理享元角色。当客户对象请求一个享元对象时,享元工厂检査系统中是否存在符合求的享元对象,如果存在则提供给客户;如果不存在的话,则创建一个新的享元对象。
角色间的关系UML如下:
模式实现
按照如上UML图角色代码如下:
1 抽象享元角色
// 抽象享元角色
interface IFlyweight {
void operation(UnsharedConcreteFlyweight unsharedConcreteFlyweight);
}
2 具体享元角色
// 具体享元角色
class ConcreteFlyweight implements IFlyweight {
private String intrinsicState;
public ConcreteFlyweight(String intrinsicState) {
this.intrinsicState = intrinsicState;
}
@Override
public void operation(UnsharedConcreteFlyweight unsharedConcreteFlyweight) {
System.out.println("Object address: " + System.identityHashCode(this));
System.out.println("IntrinsicState: " + this.intrinsicState);
System.out.println("ExtrinsicState: " + unsharedConcreteFlyweight.getExtrinsicState());
}
}
3 非享元角色
// 非享元角色
class UnsharedConcreteFlyweight {
private String extrinsicState;
UnsharedConcreteFlyweight(String extrinsicState) {
this.extrinsicState = extrinsicState;
}
public String getExtrinsicState() {
return extrinsicState;
}
public void setExtrinsicState(String extrinsicState) {
this.extrinsicState = extrinsicState;
}
}
4 享元工厂角色
// 享元工厂
class FlyweightFactory {
private static Map<String, IFlyweight> pool = new HashMap<>();
// 因为内部状态具备不变性,因此作为缓存的键
public static IFlyweight getFlyweight(String intrinsicState) {
if (!pool.containsKey(intrinsicState)) {
IFlyweight flyweight = new ConcreteFlyweight(intrinsicState);
pool.put(intrinsicState, flyweight);
}
return pool.get(intrinsicState);
}
}
客户端调用代码如下:
public class FlyweightPattern {
public static void main(String[] args) {
IFlyweight flyweight1 = FlyweightFactory.getFlyweight("intrinsicState-a");
IFlyweight flyweight2 = FlyweightFactory.getFlyweight("intrinsicState-b");
IFlyweight flyweight3 = FlyweightFactory.getFlyweight("intrinsicState-a");
UnsharedConcreteFlyweight unsharedConcreteFlyweight_1 = new UnsharedConcreteFlyweight("extrinsicState-1");
UnsharedConcreteFlyweight unsharedConcreteFlyweight_2 = new UnsharedConcreteFlyweight("extrinsicState-2");
UnsharedConcreteFlyweight unsharedConcreteFlyweight_3 = new UnsharedConcreteFlyweight("extrinsicState-3");
flyweight1.operation(unsharedConcreteFlyweight_1);
flyweight2.operation(unsharedConcreteFlyweight_2);
flyweight3.operation(unsharedConcreteFlyweight_3);
}
}
打印结果如下:
内部状态一致时,可以看到打印的是同一个享元对象。
模式实践
就以下棋为例进行实践举例:设计一个在线棋牌游戏大厅
设计一个在线棋牌游戏大厅
一个游戏厅中有成千上万个房间,每个房间对应一个棋局。棋局要保存每个棋子的数据,比如:棋子类型(将、相、士、炮等)、棋子颜色(红方、黑方)、棋子在棋局中的位置。利用这些数据,我们就能显示一个完整的棋盘给玩家。具体的代码如下所示。其中,ChessPiece 类表示棋子,ChessBoard 类表示一个棋局,里面保存了象棋中 32 个棋子的信息
public class ChessPiece {//棋子
private String id;
private String text;
private String color;
private int positionX;
private int positionY;
public ChessPiece(String id, String text, Color color, int positionX, int positionY) {
this.id = id;
this.text = text;
this.color = color;
this.positionX = positionX;
this.positionY = positionX;
}
// ...省略其他属性和getter/setter方法...
}
public class ChessBoard {//棋局
private Map<String , ChessPiece> chessPieces = new HashMap<>();
public ChessBoard() {
init();
}
private void init() {
chessPieces.put("black-che-0-0", new ChessPiece("black-che-0-0", "車", ChessPiece.Color.BLACK, 0, 0));
chessPieces.put("red-che-0-1", new ChessPiece("red-che-0-0","馬", ChessPiece.Color.BLACK, 0, 1));
//...省略摆放其他棋子的代码...
}
public void move(String chessPieceId, int toPositionX, int toPositionY) {
//...省略...
}
}
为了记录每个房间当前的棋局情况,我们需要给每个房间都创建一个 ChessBoard 棋局对象。因为游戏大厅中有成千上万的房间(实际上,百万人同时在线的游戏大厅也有很多),那保存这么多棋局对象就会消耗大量的内存。有没有什么办法来节省内存呢?这个时候,享元模式就可以派上用场了。像刚刚的实现方式,在内存中会有大量的相似对象。这些相似对象的 id、text、color 都是相同的,唯独 positionX、positionY 不同。实际上,我们可以将棋子的 id、text、color 属性拆分出来,设计成独立的类,并且作为享元供多个棋盘复用。这样,棋盘只需要记录每个棋子的位置信息就可以了。具体的代码实现如下所示:
package com.example.designpattern.flyweight;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
public class ChessBoard {
public static void main(String[] args) {
ChessPieceUnit chessPieceUnit1 = ChessPieceUnitFactory.getChessPiece("black-che");
PiecePos piecePos1 = new PiecePos(0, 1);
ChessPieceUnit chessPieceUnit2 = ChessPieceUnitFactory.getChessPiece("black-che");
PiecePos piecePos2 = new PiecePos(5, 1);
ChessPieceUnit chessPieceUnit3 = ChessPieceUnitFactory.getChessPiece("red-che");
PiecePos piecePos3 = new PiecePos(0, 4);
chessPieceUnit1.move(piecePos1);
chessPieceUnit2.move(piecePos2);
chessPieceUnit3.move(piecePos3);
}
}
// 抽象享元角色
interface ChessPieceUnit {
void move(PiecePos piecePos);
}
// 非享元角色
@Data
@AllArgsConstructor
class PiecePos {
int toPositionX;
int toPositionY;
}
// 具体享元角色
@AllArgsConstructor
@Getter
class ChessPieceUnitImpl implements ChessPieceUnit {
private String name;
private String text;
private String color;
@Override
public void move(PiecePos piecePos) {
System.out.println("Object address: " + System.identityHashCode(this));
System.out.println(this.name + "棋子所在棋盘位置为: " + piecePos.toPositionX + "," + piecePos.toPositionY);
}
}
// 享元工厂
class ChessPieceUnitFactory {
private static final Map<String, ChessPieceUnitImpl> pieces = new HashMap<>();
static {
ChessPieceUnitImpl chessPieceUnit_black_che = new ChessPieceUnitImpl("black-che", "車", "black");
ChessPieceUnitImpl chessPieceUnit_red_che = new ChessPieceUnitImpl("red-che", "車", "red");
pieces.put(chessPieceUnit_black_che.getName(), chessPieceUnit_black_che);
pieces.put(chessPieceUnit_red_che.getName(), chessPieceUnit_red_che);
//...省略其他棋子的代码...
}
public static ChessPieceUnitImpl getChessPiece(String chessPieceId) {
return pieces.get(chessPieceId);
}
}
上面的代码中,我们利用工厂类来缓存 ChessPieceUnitImpl 信息(也就是 id、text、color)。通过工厂类获取到的 ChessPieceUnitImpl 就是享元。所有的 ChessBoard 对象共享这 32 个 ChessPieceUnitImpl 对象(因为象棋中只有 32 个棋子)。在使用享元模式之前,记录 1 万个棋局,我们要创建 32 万(32*1 万)个棋子的 ChessPieceUnitImpl 对象。利用享元模式,我们只需要创建 32 个享元对象供所有棋局共享使用即可,大大节省了内存。实际上,它的代码实现非常简单,主要是通过工厂模式,在工厂类中,通过一个 Map 来缓存已经创建过的享元对象,来达到复用的目的
享元模式在JDK中应用
享元模式应用于Java中的基本类型Cache和String类型的缓存。
IntegerCache实现
Java中的IntegerCache就是享元模式的一个应用:当我们通过自动装箱,也就是调用 valueOf()
来创建 Integer 对象的时候
Integer i = 59;底层执行了:Integer i = Integer.valueOf(59);
如果要创建的 Integer 对象的值在 -128 到 127 之间,会从 IntegerCache 类中直接返回,否则才调用 new 方法创建
/**
* Cache to support the object identity semantics of autoboxing for values between
* -128 and 127 (inclusive) as required by JLS.
*
* The cache is initialized on first usage. The size of the cache
* may be controlled by the {@code -XX:AutoBoxCacheMax=<size>} option.
* During VM initialization, java.lang.Integer.IntegerCache.high property
* may be set and saved in the private system properties in the
* sun.misc.VM class.
*/
private static class IntegerCache {
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
private IntegerCache() {}
}
以下的例子中:
Integer a = new Integer(123);
Integer a = 123;
Integer a = Integer.valueOf(123);
第一种创建方式并不会使用到 IntegerCache,而后面两种创建方法可以利用 IntegerCache 缓存,返回共享的对象,以达到节省内存的目的。举一个极端一点的例子,假设程序需要创建 1 万个 -128 到 127 之间的 Integer 对象。使用第一种创建方式,我们需要分配 1 万个 Integer 对象的内存空间;使用后两种创建方式,我们最多只需要分配 256 个 Integer 对象的内存空间
String字符串常量池
Java的Sting也用到了享元模式,JVM 会专门开辟一块存储区来存储字符串常量,这块存储区叫作字符串常量池,细节参照我这篇Blog:【深入理解JVM 三】类Class文件结构。
String s1 = "tml";
String s2 = "tml";
String s3 = new String("tml");
System.out.println(s1 == s2);
System.out.println(s1 == s3);
上面代码的运行结果是:一个 true,一个 false。跟 Integer 类的设计思路相似,String 类利用享元模式来复用相同的字符串常量(也就是代码中的tml
)。
模式对比
分别看看享元模式和几个相似概念的对比:
享元模式跟单例的区别
在单例模式中,一个类只能创建一个对象,而在享元模式中,一个类可以创建多个对象,每个对象被多处代码引用共享。实际上,享元模式有点类似于之前讲到的单例的变体:多例。但是区别两种设计模式,不能光看代码实现,而是要看设计意图,也就是要解决的问题。尽管从代码实现上来看,享元模式和多例有很多相似之处,但从设计意图上来看,它们是完全不同的。应用享元模式是为了对象复用,节省内存,而应用多例模式是为了限制对象的个数。
享元模式跟缓存的区别
在享元模式的实现中,我们通过工厂类来缓存已经创建好的对象。这里的缓存实际上是存储的意思,跟我们平时所说的数据库缓存、CPU 缓存、MemCache 缓存是两回事。我们平时所讲的缓存,主要是为了提高访问效率,而非复用。
享元模式跟对象池的区别
对象池、连接池(比如数据库连接池)、线程池等也是为了复用,那它们跟享元模式有什么区别呢?虽然对象池、连接池、线程池、享元模式都是为了复用,但是对象池、连接池、线程池等池化技术中的“复用”和享元模式中的“复用”实际上是不同的概念。
- 池化技术中的复用可以理解为重复使用,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。
- 享元模式中的复用可以理解为共享使用,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。
所以说设计结构类似但是设计意图不同
总结一下
享元模式从概念上看非常简单,意图就是复用相似对象,结构上和多例模式、缓存、池化技术等类似都用到了工厂,但是设计意图却大相径庭,例如多例模式主要用于限制对象个数,缓存时提高对象访问效率,池化技术主要目的是节省时间。从这里也能看出,有些模型设计虽然结构非常类似,但是出发点却不同,这也印证了学习设计模式的出发点应为设计意图而非死记结构,最终结构只是设计的落地方式而已。