本篇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 缓存是两回事。我们平时所讲的缓存,主要是为了提高访问效率,而非复用。
享元模式跟对象池的区别
对象池、连接池(比如数据库连接池)、线程池等也是为了复用,那它们跟享元模式有什么区别呢?虽然对象池、连接池、线程池、享元模式都是为了复用,但是对象池、连接池、线程池等池化技术中的“复用”和享元模式中的“复用”实际上是不同的概念。
- 池化技术中的复用可以理解为重复使用,主要目的是节省时间(比如从数据库池中取一个连接,不需要重新创建)。在任意时刻,每一个对象、连接、线程,并不会被多处使用,而是被一个使用者独占,当使用完成之后,放回到池中,再由其他使用者重复利用。
- 享元模式中的复用可以理解为共享使用,在整个生命周期中,都是被所有使用者共享的,主要目的是节省空间。
所以说设计结构类似但是设计意图不同
总结一下
享元模式从概念上看非常简单,意图就是复用相似对象,结构上和多例模式、缓存、池化技术等类似都用到了工厂,但是设计意图却大相径庭,例如多例模式主要用于限制对象个数,缓存时提高对象访问效率,池化技术主要目的是节省时间。从这里也能看出,有些模型设计虽然结构非常类似,但是出发点却不同,这也印证了学习设计模式的出发点应为设计意图而非死记结构,最终结构只是设计的落地方式而已。